diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..e27c7eec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,39 @@ +name: 🐞 Bug report +description: Create a report to help us improve. +labels: ["bug"] +body: + - type: input + id: ucupaintversion + attributes: + label: UCUPAINT Version + description: Type the version from [Edit > Preferences > Get Extensions > Ucupaint] here. + placeholder: ex. 2.2.0 + validations: + required: true + - type: textarea + id: describebug + attributes: + label: Describe The Bug + description: A clear and concise description of what the bug is. + placeholder: ex. This should happen, but that happens instead. + validations: + required: true + - type: textarea + id: reproducebug + attributes: + label: To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + id: incaseofcrash + attributes: + label: In the case of a crash or when relevant include the logs + description: Please include the logs (https://docs.blender.org/manual/en/latest/troubleshooting/crash.html#crash-log) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000..ff67c4c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,26 @@ +name: 🚀 Feature Request +description: Suggest an idea for this project. +labels: ["enhancement"] +body: + - type: checkboxes + id: terms + attributes: + label: Pre-Flight checklist + description: Make sure these are correct before submitting + options: + - label: Did you check to see if this issue already exists? + required: true + - label: This is a single feature request. (Do not put multiple feature requests in one issue) + required: true + - type: textarea + id: describefeaturerequest + attributes: + label: Describe The Feature Request Below + description: A clear and concise description of what the feature request is. + placeholder: | + EXAMPLES: + - Normally this happens, but that could happen instead + - This functionality should be added + - This functionality should be changed + validations: + required: true diff --git a/.gitignore b/.gitignore index 3b7b559d..bcbdd529 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ *__pycache__ -ucupaint_updater/ucupaint_updater_status.json \ No newline at end of file +ucupaint_updater/ucupaint_updater_status.json diff --git a/Bake.py b/Bake.py index 607e2f50..f2c25b51 100644 --- a/Bake.py +++ b/Bake.py @@ -115,6 +115,7 @@ def transfer_uv(objs, mat, entity, uv_map, is_entity_baked=False): temp_image.colorspace_settings.name = image.colorspace_settings.name temp_image.generated_color = col + temp_image.alpha_mode = image.alpha_mode # Create bake nodes tex = mat.node_tree.nodes.new('ShaderNodeTexImage') @@ -216,6 +217,10 @@ def transfer_uv(objs, mat, entity, uv_map, is_entity_baked=False): # Copy the result to original temp image copy_image_channel_pixels(temp_image1, temp_image, 0, 3) + # Premultiplied float image need more process + if temp_image.is_float and temp_image.alpha_mode == 'PREMUL': + multiply_image_rgb_by_alpha(temp_image) + # Swap tile again to recover if tilenum != 1001: UDIM.swap_tile(temp_image, 1001, tilenum) @@ -257,7 +262,7 @@ def transfer_uv(objs, mat, entity, uv_map, is_entity_baked=False): # HACK: Pack and refresh to update image in Blender 2.77 and lower if not is_bl_newer_than(2, 78) and (image.packed_file or image.filepath == ''): if image.is_float: - image_ops.pack_float_image(image) + image_ops.pack_float_image_27x(image) else: image.pack(as_png=True) image.reload() @@ -378,7 +383,7 @@ def get_entities_to_transfer(yp, from_uv_map, to_uv_map): return entities class YTransferSomeLayerUV(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_transfer_some_layer_uv" + bl_idname = "wm.y_transfer_some_layer_uv" bl_label = "Transfer Some Layer UV" bl_description = "Transfer some layers/masks UV by baking it to other uv (this will take quite some time to finish)" bl_options = {'REGISTER', 'UNDO'} @@ -542,16 +547,12 @@ def execute(self, context): # Refresh mapping and stuff yp.active_layer_index = yp.active_layer_index - print( - 'INFO: All layers and masks using', self.from_uv_map, - 'are transferred to', self.uv_map, - 'in', '{:0.2f}'.format(time.time() - T), 'seconds!' - ) + self.report({'INFO'}, 'All layers and masks using '+self.from_uv_map+' are transferred to '+self.uv_map+' in '+'{:0.2f}'.format(time.time() - T)+' seconds!') return {'FINISHED'} class YTransferLayerUV(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_transfer_layer_uv" + bl_idname = "wm.y_transfer_layer_uv" bl_label = "Transfer Layer UV" bl_description = "Transfer Layer UV by baking it to other uv (this will take quite some time to finish)" bl_options = {'REGISTER', 'UNDO'} @@ -634,6 +635,7 @@ def execute(self, context): mat = get_active_material() yp = self.entity.id_data.yp objs = get_all_objects_with_same_materials(mat) + ori_uv_name = self.entity.uv_name # Prepare bake settings book = remember_before_bake(yp) @@ -659,12 +661,7 @@ def execute(self, context): # Refresh mapping and stuff yp.active_layer_index = yp.active_layer_index - print( - 'INFO:', self.entity.name, - 'UV is transferred from', self.entity.uv_name, - 'to', self.uv_map, - 'in', '{:0.2f}'.format(time.time() - T), 'seconds!' - ) + self.report({'INFO'}, self.entity.name+' UV is transferred from '+ori_uv_name+' to '+self.uv_map+' in '+'{:0.2f}'.format(time.time() - T)+' seconds!') return {'FINISHED'} @@ -691,7 +688,7 @@ def update_resize_image_tile_number(self, context): self.height = tile.size[1] class YResizeImage(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_resize_image" + bl_idname = "wm.y_resize_image" bl_label = "Resize Image Layer/Mask" bl_description = "Resize image of layer or mask" bl_options = {'REGISTER', 'UNDO'} @@ -855,7 +852,7 @@ def execute(self, context): class YBakeChannelToVcol(bpy.types.Operator, BaseBakeOperator): """Bake Channel to Vertex Color""" - bl_idname = "node.y_bake_channel_to_vcol" + bl_idname = "wm.y_bake_channel_to_vcol" bl_label = "Bake channel to vertex color" bl_options = {'REGISTER', 'UNDO'} @@ -1091,7 +1088,7 @@ def execute(self, context): return {'FINISHED'} class YDeleteBakedChannelImages(bpy.types.Operator): - bl_idname = "node.y_delete_baked_channel_images" + bl_idname = "wm.y_delete_baked_channel_images" bl_label = "Delete All Baked Channel Images" bl_description = "Delete all baked channel images" bl_options = {'UNDO'} @@ -1202,7 +1199,7 @@ def bake_vcol_channel_items(self, context): class YBakeChannels(bpy.types.Operator, BaseBakeOperator): """Bake Channels to Image(s)""" - bl_idname = "node.y_bake_channels" + bl_idname = "wm.y_bake_channels" bl_label = "Bake channels to Image" bl_options = {'REGISTER', 'UNDO'} @@ -1283,6 +1280,24 @@ class YBakeChannels(bpy.types.Operator, BaseBakeOperator): description = 'Use float image for baked displacement', default = False ) + + use_osl : BoolProperty( + name = 'Use OSL', + description = 'Use Open Shading Language (slower but can handle more complex layer setup)', + default = False + ) + + use_dithering : BoolProperty( + name = 'Use Dithering', + description = 'Use dithering for less banding color', + default = False + ) + + dither_intensity : FloatProperty( + name = 'Dither Intensity', + description = 'Amount of dithering noise added to the rendered image to break up banding', + default=1.0, min=0.0, max=2.0, subtype='FACTOR' + ) bake_disabled_layers : BoolProperty( name = 'Bake Disabled Layers', @@ -1323,12 +1338,25 @@ def invoke(self, context, event): self.uv_map_coll.add().name = uv.name # List of channels that will be baked - if self.only_active_channel and yp.active_channel_index < len(yp.channels): - self.channels = [yp.channels[yp.active_channel_index]] + self.channels = [] + if self.only_active_channel: + if yp.active_channel_index < len(yp.channels): + self.channels = [yp.channels[yp.active_channel_index]] else: self.channels = yp.channels + self.no_layer_using = False self.enable_bake_as_vcol = False if len(self.channels) > 0: + + # Check if any layer is using the channels + layer_found = False + for ch in self.channels: + if is_any_layer_using_channel(ch, node): + layer_found = True + break + if not layer_found: + self.no_layer_using = True + bi = None for ch in self.channels: baked = node.node_tree.nodes.get(ch.baked) @@ -1358,7 +1386,7 @@ def invoke(self, context, event): if self.vcol_force_first_ch_idx == '': self.vcol_force_first_ch_idx = 'Do Nothing' - if get_user_preferences().skip_property_popups and not event.shift: + if (get_user_preferences().skip_property_popups and not event.shift) or len(self.channels) == 0 or self.no_layer_using: return self.execute(context) return context.window_manager.invoke_props_dialog(self, width=320) @@ -1451,7 +1479,9 @@ def draw(self, context): col.separator() if is_bl_newer_than(2, 80): - col.prop(self, 'bake_device', text='') + if self.use_osl: + col.label(text='CPU (OSL)') + else: col.prop(self, 'bake_device', text='') col.prop(self, 'interpolation', text='') col.prop_search(self, "uv_map", self, "uv_map_coll", text='', icon='GROUP_UVS') @@ -1472,6 +1502,18 @@ def draw(self, context): ccol.prop(self, 'fxaa', text='Use FXAA') if is_bl_newer_than(2, 81): ccol.prop(self, 'denoise', text='Use Denoise') + + any_color_channel = any([c for c in self.channels if c.type == 'RGB' and c.colorspace == 'SRGB' and c.use_clamp]) + if any_color_channel: + if not self.use_dithering: + ccol.prop(self, 'use_dithering', text='Use Dithering') + if self.use_dithering: + row = split_layout(ccol, 0.55) + row.prop(self, 'use_dithering', text='Use Dithering') + row.prop(self, 'dither_intensity', text='') + + ccol.prop(self, 'use_osl') + ccol.prop(self, 'force_bake_all_polygons') ccol.prop(self, 'bake_disabled_layers') @@ -1487,6 +1529,18 @@ def execute(self, context): obj = context.object mat = obj.active_material + if len(self.channels) == 0: + self.report({'ERROR'}, "This node has no channel!") + return {'CANCELLED'} + + if self.only_active_channel and self.no_layer_using: + self.report({'ERROR'}, "No layer is using '"+self.channels[0].name+"' channel!") + return {'CANCELLED'} + + if self.no_layer_using: + self.report({'ERROR'}, "No layer is using any channel!") + return {'CANCELLED'} + if is_bl_newer_than(2, 80) and (obj.hide_viewport or obj.hide_render): self.report({'ERROR'}, "Please unhide render and viewport of the active object!") return {'CANCELLED'} @@ -1597,6 +1651,10 @@ def execute(self, context): temp_objs = [] ori_objs = [] if (len(objs) > 1 or any_uv_geonodes) and not is_join_objects_problematic(yp, mat): + + # Make sure there's no missing vertex color on any objects + fix_missing_object_vcols(yp, objs, enabled_only=True) + ori_objs = objs objs = temp_objs = [get_merged_mesh_objects(scene, objs)] @@ -1609,9 +1667,12 @@ def execute(self, context): # Prepare bake settings prepare_bake_settings( book, objs, yp, self.samples, margin, self.uv_map, disable_problematic_modifiers=True, - bake_device=self.bake_device, margin_type=self.margin_type + bake_device=self.bake_device, margin_type=self.margin_type, use_osl=self.use_osl ) + # Get bake properties + bprops = get_bake_properties_from_self(self) + # Get tilenums tilenums = UDIM.get_tile_numbers(objs, self.uv_map) if self.use_udim else [1001] @@ -1623,24 +1684,40 @@ def execute(self, context): layer.enable = True # Bake channels + baked_exists = [] for ch in self.channels: + + # Check if baked node exists + baked = tree.nodes.get(ch.baked) + if baked: baked_exists.append(True) + else: baked_exists.append(False) + ch.no_layer_using = not is_any_layer_using_channel(ch, node) if not ch.no_layer_using: - use_hdr = not ch.use_clamp + use_hdr = not ch.use_clamp or (self.use_dithering and ch.type == 'RGB' and ch.colorspace == 'SRGB') bake_channel( self.uv_map, mat, node, ch, width, height, use_hdr=use_hdr, force_use_udim=self.use_udim, tilenums=tilenums, interpolation=self.interpolation, use_float_for_displacement=self.use_float_for_displacement, - use_float_for_normal=self.use_float_for_normal + use_float_for_normal=self.use_float_for_normal, bprops=bprops ) # Process baked images baked_images = [] - for ch in self.channels: + for i, ch in enumerate(self.channels): + if ch.no_layer_using: continue baked = tree.nodes.get(ch.baked) if baked and baked.image: + # Only expand baked data when baked is just created + if not baked_exists[i]: + ch.expand_baked_data = True + + # Dithering + if ch.type == 'RGB' and ch.colorspace == 'SRGB' and self.use_dithering and ch.use_clamp: + dither_image(baked.image, dither_intensity=self.dither_intensity, alpha_aware=ch.enable_alpha) + # Denoise if self.denoise and is_bl_newer_than(2, 81) and ch.type != 'NORMAL': denoise_image(baked.image) @@ -1912,10 +1989,14 @@ def execute(self, context): ) for ch in self.channels: if ch.enable_bake_to_vcol and ch.type != 'NORMAL': + + # Get vcol name + vcol_name = 'Baked ' + ch.name if ch.bake_to_vcol_name == '' else ch.bake_to_vcol_name + # Check vertex color for ob in objs: vcols = get_vertex_colors(ob) - vcol = vcols.get(ch.bake_to_vcol_name) + vcol = vcols.get(vcol_name) # Set index to first so new vcol will copy their value if len(vcols) > 0: @@ -1924,7 +2005,7 @@ def execute(self, context): if not vcol: try: - vcol = new_vertex_color(ob, ch.bake_to_vcol_name) + vcol = new_vertex_color(ob, vcol_name) except Exception as e: print(e) # Get newly created vcol name @@ -1938,11 +2019,14 @@ def execute(self, context): # Get the newly created vcol to avoid pointer error vcol = vcols.get(vcol_name) set_active_vertex_color(ob, vcol) - bake_to_vcol(mat, node, ch, objs, None, 1, ch.bake_to_vcol_alpha or ch.enable_alpha, ch.bake_to_vcol_name) + bake_to_vcol(mat, node, ch, objs, None, 1, ch.bake_to_vcol_alpha or ch.enable_alpha, vcol_name) baked = tree.nodes.get(ch.baked_vcol) if not baked or not is_root_ch_prop_node_unique(ch, 'baked_vcol'): baked = new_node(tree, ch, 'baked_vcol', get_vcol_bl_idname(), 'Baked Vcol ' + ch.name) - set_source_vcol_name(baked, ch.bake_to_vcol_name) + # Set channel to use baked vertex color only when baked_vcol is just created + ch.use_baked_vcol = True + + set_source_vcol_name(baked, vcol_name) for ob in objs: # Recover material index if ori_mat_ids[ob.name]: @@ -1951,6 +2035,11 @@ def execute(self, context): p.material_index = ori_mat_ids[ob.name][i] if is_sort_by_channel: current_vcol_order += 1 + + # Set back vcol name to channel baked vcol name + if ch.bake_to_vcol_name != vcol_name: + ch.bake_to_vcol_name = vcol_name + else: # If has baked vcol node, remove it baked = tree.nodes.get(ch.baked_vcol) @@ -1992,9 +2081,12 @@ def execute(self, context): # Refresh active channel index yp.active_channel_index = yp.active_channel_index + # Update UI + ypui = context.window_manager.ypui + ypui.need_update = True + # If bake target ui is visible, refresh bake target index to show up the image result if len(yp.bake_targets) > 0: - ypui = context.window_manager.ypui if ypui.show_bake_targets: yp.active_bake_target_index = yp.active_bake_target_index @@ -2006,7 +2098,9 @@ def execute(self, context): for o in temp_objs: remove_mesh_obj(o) - print('INFO:', tree.name, 'channels are baked in', '{:0.2f}'.format(time.time() - T), 'seconds!') + if self.only_active_channel: + self.report({'INFO'}, yp.channels[yp.active_channel_index].name+' channel is baked in '+'{:0.2f}'.format(time.time() - T)+' seconds!') + else: self.report({'INFO'}, tree.name+' channels are baked in '+'{:0.2f}'.format(time.time() - T)+' seconds!') return {'FINISHED'} @@ -2142,7 +2236,7 @@ def remove_layer_modifiers_and_transforms(layer): Mask.remove_mask(layer, m, bpy.context.object) class YMergeLayer(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_merge_layer" + bl_idname = "wm.y_merge_layer" bl_label = "Merge layer" bl_description = "Merge Layer" bl_options = {'REGISTER', 'UNDO'} @@ -2174,10 +2268,11 @@ class YMergeLayer(bpy.types.Operator, BaseBakeOperator): default = True ) - #height_aware : BoolProperty( - # name = 'Height Aware', - # description = 'Height will take account for merge', - # default = True) + force_mix_blending : BoolProperty( + name = 'Force Mix Blending', + description = 'Force to use mix blending while merging so there\'s no missing parts on the merge result', + default = True + ) @classmethod def poll(cls, context): @@ -2192,7 +2287,7 @@ def invoke(self, context, event): self.invoke_operator(context) node = get_active_ypaint_node() - yp = node.node_tree.yp + yp = self.yp = node.node_tree.yp # Get active layer layer_idx = self.layer_idx = yp.active_layer_index @@ -2262,18 +2357,30 @@ def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=320) + def check(self, context): + return True + def draw(self, context): row = split_layout(self.layout, 0.5) + main_ch = self.yp.channels[int(self.channel_idx)] + ch = self.layer.channels[int(self.channel_idx)] + blend_type = ch.blend_type if main_ch.type != 'NORMAL' else ch.normal_blend_type + col = row.column(align=False) col.label(text='Main Channel:') col.label(text='Apply Modifiers:') col.label(text='Apply Neighbor Modifiers:') + + if blend_type != 'MIX': + col.label(text='Force Mix Blending:') col = row.column(align=False) col.prop(self, 'channel_idx', text='') col.prop(self, 'apply_modifiers', text='') col.prop(self, 'apply_neighbor_modifiers', text='') + if blend_type != 'MIX': + col.prop(self, 'force_mix_blending', text='') if self.legacy_on_non_object_mode: col = self.layout.column(align=True) @@ -2324,10 +2431,13 @@ def execute(self, context): merge_success = False + if (layer.type == 'IMAGE' and main_ch.type == 'NORMAL' and ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP'): + self.report({'ERROR'}, "Merging VDM layers is not supported yet!") + return {'CANCELLED'} + # Merge image layers if (layer.type == 'IMAGE' and layer.texcoord_type == 'UV'): # and neighbor_layer.type == 'IMAGE'): - book = remember_before_bake(yp) prepare_bake_settings( book, objs, yp, samples=1, margin=5, @@ -2358,13 +2468,14 @@ def execute(self, context): if not self.apply_neighbor_modifiers: neighbor_oris = remember_and_disable_layer_modifiers_and_transforms(neighbor_layer, False) - # Make sure to Use mix on layer channel - if main_ch.type != 'NORMAL': - ori_blend_type = ch.blend_type - ch.blend_type = 'MIX' - else: - ori_blend_type = ch.normal_blend_type - ch.normal_blend_type = 'MIX' + # Force to use mix on layer channel + if self.force_mix_blending: + if main_ch.type != 'NORMAL': + ori_blend_type = ch.blend_type + ch.blend_type = 'MIX' + else: + ori_blend_type = ch.normal_blend_type + ch.normal_blend_type = 'MIX' # Enable alpha on main channel (will also update all the nodes) ori_enable_alpha = main_ch.enable_alpha @@ -2397,9 +2508,10 @@ def execute(self, context): # Recover original props main_ch.enable_alpha = ori_enable_alpha yp.alpha_auto_setup = True - if main_ch.type != 'NORMAL': - ch.blend_type = ori_blend_type - else: ch.normal_blend_type = ori_blend_type + if self.force_mix_blending: + if main_ch.type != 'NORMAL': + ch.blend_type = ori_blend_type + else: ch.normal_blend_type = ori_blend_type # Set all channel intensity value to 1.0 for c in layer.channels: @@ -2492,25 +2604,33 @@ def execute(self, context): for lay in yp.layers: lay.parent_idx = get_layer_index_by_name(yp, parent_dict[lay.name]) - if height_ch and main_ch.type == 'NORMAL' and height_ch.normal_map_type == 'BUMP_MAP': - height_ch.bump_distance = max_height - reconnect_yp_nodes(tree) rearrange_yp_nodes(tree) # Refresh index routine yp.active_layer_index = min(layer_idx, neighbor_idx) + # HACK: To make the result correct, multiply rgb by alpha if image alpha mode is premultiplied + layer = yp.layers[yp.active_layer_index] + if layer.type == 'IMAGE': + source = get_layer_source(layer) + if source and source.image: + image = source.image + if image.is_float and image.alpha_mode == 'PREMUL': + multiply_image_rgb_by_alpha(image) + # Update list items ListItem.refresh_list_items(yp, repoint_active=True) else: self.report({'ERROR'}, "Merge failed for some reason!") return {'CANCELLED'} + self.report({'INFO'}, 'Merging layers is done in '+'{:0.2f}'.format(time.time() - T)+' seconds!') + return {'FINISHED'} class YMergeMask(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_merge_mask" + bl_idname = "wm.y_merge_mask" bl_label = "Merge mask" bl_description = "Merge Mask" bl_options = {'UNDO'} @@ -2577,6 +2697,8 @@ def check(self, context): def execute(self, context): if not self.is_cycles_exist(context): return {'CANCELLED'} + T = time.time() + mask = self.mask layer = self.layer yp = layer.id_data.yp @@ -2693,8 +2815,6 @@ def execute(self, context): mat.node_tree.links.new(node.outputs[LAYER_ALPHA_VIEWER], emit.inputs[0]) mat.node_tree.links.new(emit.outputs[0], output.inputs[0]) - #return {'FINISHED'} - # Bake bake_object_op() @@ -2704,7 +2824,7 @@ def execute(self, context): # HACK: Pack and refresh to update image in Blender 2.77 and lower if not is_bl_newer_than(2, 78) and (source.image.packed_file or source.image.filepath == ''): if source.image.is_float: - image_ops.pack_float_image(source.image) + image_ops.pack_float_image_27x(source.image) else: source.image.pack(as_png=True) source.image.reload() @@ -2727,7 +2847,7 @@ def execute(self, context): mask.modifiers.remove(i) # Remove neighbor mask - Mask.remove_mask(layer, neighbor_mask, obj) + Mask.remove_mask(layer, neighbor_mask, obj, refresh_list=False) # Remove bake nodes simple_remove_node(mat.node_tree, tex) @@ -2755,128 +2875,10 @@ def execute(self, context): mask.active_edit = True yp.active_layer_index = yp.active_layer_index - return {'FINISHED'} + # Refresh list items + ListItem.refresh_list_items(yp, repoint_active=True) -class YBakeTempImage(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_bake_temp_image" - bl_label = "Bake temporary image of layer" - bl_description = "Bake temporary image of layer, can be useful to prefent glitching with cycles" - bl_options = {'REGISTER', 'UNDO'} - - uv_map : StringProperty(default='') - uv_map_coll : CollectionProperty(type=bpy.types.PropertyGroup) - - hdr : BoolProperty(name='32 bit Float', default=True) - - @classmethod - def poll(cls, context): - return get_active_ypaint_node() #and hasattr(context, 'parent') - - def invoke(self, context, event): - self.invoke_operator(context) - - obj = context.object - ypup = get_user_preferences() - - self.auto_cancel = False - if not hasattr(context, 'parent'): - self.auto_cancel = True - return self.execute(context) - - self.parent = context.parent - - if self.parent.type not in {'HEMI'}: - self.auto_cancel = True - return self.execute(context) - - # Use active uv layer name by default - uv_layers = get_uv_layers(obj) - - # UV Map collections update - self.uv_map_coll.clear() - for uv in uv_layers: - if not uv.name.startswith(TEMP_UV): - self.uv_map_coll.add().name = uv.name - - if len(self.uv_map_coll) > 0: - self.uv_map = self.uv_map_coll[0].name - - if get_user_preferences().skip_property_popups and not event.shift: - return self.execute(context) - - return context.window_manager.invoke_props_dialog(self, width=320) - - def draw(self, context): - node = get_active_ypaint_node() - yp = node.node_tree.yp - - row = split_layout(self.layout, 0.4) - - col = row.column(align=False) - - #col.label(text='') - col.label(text='Width:') - col.label(text='Height:') - col.label(text='') - col.label(text='UV Map:') - col.label(text='Samples:') - - col.label(text='Margin:') - - col = row.column(align=False) - - #col.prop(self, 'hdr') - col.prop(self, 'width', text='') - col.prop(self, 'height', text='') - col.prop(self, 'hdr') - col.prop_search(self, "uv_map", self, "uv_map_coll", text='', icon='GROUP_UVS') - col.prop(self, 'samples', text='') - - if is_bl_newer_than(3, 1): - split = split_layout(col, 0.4, align=True) - split.prop(self, 'margin', text='') - split.prop(self, 'margin_type', text='') - else: - col.prop(self, 'margin', text='') - - def execute(self, context): - if not self.is_cycles_exist(context): return {'CANCELLED'} - - if not hasattr(self, 'parent'): - self.report({'ERROR'}, "Context is incorrect!") - return {'CANCELLED'} - - entity = self.parent - if entity.type not in {'HEMI'}: - self.report({'ERROR'}, "This layer type is not supported (yet)!") - return {'CANCELLED'} - - # Bake temp image - image = temp_bake( - context, entity, self.width, self.height, self.hdr, self.samples, - self.margin, self.uv_map, margin_type=self.margin_type, - bake_device=self.bake_device - ) - - return {'FINISHED'} - -class YDisableTempImage(bpy.types.Operator): - bl_idname = "node.y_disable_temp_image" - bl_label = "Disable Baked temporary image of layer" - bl_description = "Disable bake temporary image of layer" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - return get_active_ypaint_node() and hasattr(context, 'parent') - - def execute(self, context): - entity = context.parent - if not entity.use_temp_bake: - self.report({'ERROR'}, "This layer is not temporarily baked!") - return {'CANCELLED'} - - disable_temp_bake(entity) + self.report({'INFO'}, 'Merging masks is done in '+'{:0.2f}'.format(time.time() - T)+' seconds!') return {'FINISHED'} @@ -3034,7 +3036,7 @@ def update_enable_baked_outside(self, context): mtree.links.new(tex.outputs[0], norm.inputs[1]) baked_normal_overlay = None - if not is_overlay_normal_empty(yp): + if is_baked_normal_without_bump_needed(ch): baked_normal_overlay = tree.nodes.get(ch.baked_normal_overlay) if baked_normal_overlay and baked_normal_overlay.image: loc_y -= 300 @@ -3048,10 +3050,10 @@ def update_enable_baked_outside(self, context): if not is_bl_newer_than(2, 80) and baked_normal_overlay.image.colorspace_settings.name != get_srgb_name(): tex_normal_overlay.color_space = 'NONE' - if ch.enable_subdiv_setup: - mtree.links.new(tex_normal_overlay.outputs[0], norm.inputs[1]) + # NOTE: Use combined normal since displacement map is not exportable using GLTF + #if ch.enable_subdiv_setup: + # mtree.links.new(tex_normal_overlay.outputs[0], norm.inputs[1]) - #if not ch.enable_subdiv_setup or baked_normal_overlay: for l in outp.links: mtree.links.new(norm.outputs[0], l.to_socket) @@ -3422,6 +3424,10 @@ def create_vector_displacement_node(tree, connect_to=None): if is_bl_newer_than(2, 80): vdisp = tree.nodes.new('ShaderNodeVectorDisplacement') + # Make sure vector displacement node has 1.0 scale + if 'Scale' in vdisp.inputs: + vdisp.inputs['Scale'].default_value = 1.0 + if vdisp and connect_to: create_link(tree, vdisp.outputs[0], connect_to) @@ -3515,24 +3521,32 @@ def check_displacement_node(mat, node, set_one=False, unset_one=False, set_outsi if set_one: # Create links - if vdisp: create_link(mat.node_tree, vdisp_outp, vdisp.inputs['Vector']) + if vdisp and vdisp_outp: create_link(mat.node_tree, vdisp_outp, vdisp.inputs['Vector']) if disp: create_link(mat.node_tree, height_outp, disp.inputs['Height']) create_link(mat.node_tree, max_height_outp, disp.inputs['Scale']) - if disp and unset_one: - height_inp = node.inputs.get(height_ch.name + io_suffix['HEIGHT']) - max_height_inp = node.inputs.get(height_ch.name + io_suffix['MAX_HEIGHT']) + if unset_one: + if disp: + height_inp = node.inputs.get(height_ch.name + io_suffix['HEIGHT']) + max_height_inp = node.inputs.get(height_ch.name + io_suffix['MAX_HEIGHT']) - if height_inp and len(height_inp.links) > 0: - soc = height_inp.links[0].from_socket - create_link(mat.node_tree, soc, disp.inputs['Height']) - break_input_link(mat.node_tree, height_inp) + if height_inp and len(height_inp.links) > 0: + soc = height_inp.links[0].from_socket + create_link(mat.node_tree, soc, disp.inputs['Height']) + break_input_link(mat.node_tree, height_inp) - if max_height_inp and len(max_height_inp.links) > 0: - soc = max_height_inp.links[0].from_socket - create_link(mat.node_tree, soc, disp.inputs['Scale']) - break_input_link(mat.node_tree, max_height_inp) + if max_height_inp and len(max_height_inp.links) > 0: + soc = max_height_inp.links[0].from_socket + create_link(mat.node_tree, soc, disp.inputs['Scale']) + break_input_link(mat.node_tree, max_height_inp) + + if vdisp: + vdisp_inp = node.inputs.get(height_ch.name + io_suffix['VDISP']) + if vdisp_inp and len(vdisp_inp.links) > 0: + soc = vdisp_inp.links[0].from_socket + create_link(mat.node_tree, soc, vdisp.inputs['Vector']) + break_input_link(mat.node_tree, height_inp) return disp @@ -3569,13 +3583,13 @@ def check_subdiv_setup(height_ch): # Set displacement mode if hasattr(mat, 'displacement_method'): - #mat.displacement_method = 'BOTH' - mat.displacement_method = 'DISPLACEMENT' + mat.displacement_method = 'BOTH' - if is_bl_newer_than(2, 80): - #mat.cycles.displacement_method = 'BOTH' - mat.cycles.displacement_method = 'DISPLACEMENT' - else: mat.cycles.displacement_method = 'TRUE' + # Set cycles displacement mode + if hasattr(mat.cycles, 'displacement_method'): + if is_bl_newer_than(2, 80): + mat.cycles.displacement_method = 'BOTH' + else: mat.cycles.displacement_method = 'TRUE' # Displacement method is inside object data for Blender 2.77 and below if not is_bl_newer_than(2, 78): @@ -3837,8 +3851,6 @@ def register(): bpy.utils.register_class(YBakeChannelToVcol) bpy.utils.register_class(YMergeLayer) bpy.utils.register_class(YMergeMask) - bpy.utils.register_class(YBakeTempImage) - bpy.utils.register_class(YDisableTempImage) bpy.utils.register_class(YDeleteBakedChannelImages) def unregister(): @@ -3849,6 +3861,4 @@ def unregister(): bpy.utils.unregister_class(YBakeChannelToVcol) bpy.utils.unregister_class(YMergeLayer) bpy.utils.unregister_class(YMergeMask) - bpy.utils.unregister_class(YBakeTempImage) - bpy.utils.unregister_class(YDisableTempImage) bpy.utils.unregister_class(YDeleteBakedChannelImages) diff --git a/BakeInfo.py b/BakeInfo.py index c1d50fbf..193a129e 100644 --- a/BakeInfo.py +++ b/BakeInfo.py @@ -20,6 +20,7 @@ class YBakeInfoSelectedObject(bpy.types.PropertyGroup): class YBakeInfoProps(bpy.types.PropertyGroup): is_baked : BoolProperty(default=False) # Flag to mark if the image is from baking or not is_baked_channel : BoolProperty(default=False) # Flag to mark if the image baked from main channel + is_baked_entity : BoolProperty(default=False) # Flag to mark if the image baked from entity bake_type : EnumProperty( name = 'Bake Type', @@ -28,6 +29,12 @@ class YBakeInfoProps(bpy.types.PropertyGroup): default = 'AO' ) + baked_entity_type : StringProperty( + name = 'Baked Entity Type', + description = 'Baked entity type', + default = '' + ) + samples : IntProperty( name = 'Bake Samples', description = 'Bake Samples, more means less jagged on generated textures', @@ -99,12 +106,35 @@ class YBakeInfoProps(bpy.types.PropertyGroup): default = False ) + blur_type : EnumProperty( + name = 'Blur Type', + description = 'Blur type for the baked image', + items = ( + ('NOISE', 'Noise Blur', 'Noisy and need more samples but has matching value to the blur vector option'), + ('FLAT', 'Flat', 'Flat blur'), + ('TENT', 'Tent', 'Tent blur'), + ('QUAD', 'Quadratic', 'Quadratic blur'), + ('CUBIC', 'Cubic', 'Cubic blur'), + ('GAUSS', 'Gaussian', 'Gausssian blur'), + ('FAST_GAUSS', 'Fast Gaussian', 'Fast gausssian blur'), + ('CATROM', 'Catrom', 'Catrom blur'), + ('MITCH', 'Mitch', 'Mitch blur') + ), + default='GAUSS' + ) + blur_factor : FloatProperty( name = 'Blur Factor', description = "Blur factor to baked image", default=1.0, min=0.0, max=100.0 ) + blur_size : FloatProperty( + name = 'Blur Size', + description = 'Blur size (in pixels) to the baked image', + default=10.0, min=0.0 + ) + use_baked_disp : BoolProperty( name = 'Use Baked Displacement Map', description = 'Use baked displacement map, this will also apply subdiv setup on object', @@ -121,6 +151,12 @@ class YBakeInfoProps(bpy.types.PropertyGroup): default = 'CPU' ) + use_cage : BoolProperty( + name = 'Cage Object', + description = 'Cast rays to active material objects from a cage', + default = False + ) + cage_object_name : StringProperty( name = 'Cage Object', description = 'Object to use as cage instead of calculating the cage from the active object with cage extrusion', @@ -170,12 +206,36 @@ class YBakeInfoProps(bpy.types.PropertyGroup): default = False ) + use_osl : BoolProperty( + name = 'Use OSL', + description = 'Use Open Shading Language (slower but can handle more complex layer setup)', + default = False + ) + + use_dithering : BoolProperty( + name = 'Use Dithering', + description = 'Use dithering for less banding color', + default = False + ) + + dither_intensity : FloatProperty( + name = 'Dither Intensity', + description = 'Amount of dithering noise added to the rendered image to break up banding', + default=1.0, min=0.0, max=2.0, subtype='FACTOR' + ) + bake_disabled_layers : BoolProperty( name = 'Bake Disabled Layers', description = 'Take disabled layers into account when baking', default = False ) + normalize : BoolProperty( + name = 'Normalize Bake Result', + description = 'Normalize the bake result', + default = False, + ) + # To store other objects info other_objects : CollectionProperty(type=YBakeInfoOtherObject) diff --git a/BakeTarget.py b/BakeTarget.py index b2344012..bf6c7443 100644 --- a/BakeTarget.py +++ b/BakeTarget.py @@ -119,7 +119,7 @@ def update_new_bake_target_preset(self, context): self.name = get_unique_name(tree_name + suffix, bpy.data.images) class YNewBakeTarget(bpy.types.Operator): - bl_idname = "node.y_new_bake_target" + bl_idname = "wm.y_new_bake_target" bl_label = "New Bake Target" bl_description = "New bake target" bl_options = {'REGISTER', 'UNDO'} @@ -221,7 +221,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveBakeTarget(bpy.types.Operator): - bl_idname = "node.y_remove_bake_target" + bl_idname = "wm.y_remove_bake_target" bl_label = "Remove Bake Target" bl_description = "Remove bake target" bl_options = {'REGISTER', 'UNDO'} @@ -253,14 +253,141 @@ def execute(self, context): return {'FINISHED'} +class YCopyBakeTarget(bpy.types.Operator): + bl_idname = "wm.y_copy_bake_target" + bl_label = "Copy Bake Target" + bl_description = "Copy Bake Target" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + node = get_active_ypaint_node() + if not node: return False + + group_tree = node.node_tree + yp = group_tree.yp + + return context.object and len(yp.bake_targets) > 0 and yp.active_bake_target_index >= 0 + + def execute(self, context): + node = get_active_ypaint_node() + yp = node.node_tree.yp + wmp = context.window_manager.ypprops + + bt = yp.bake_targets[yp.active_bake_target_index] + + wmp.clipboard_bake_target.clear() + cbt = wmp.clipboard_bake_target.add() + + cbt.name = bt.name + cbt.use_float = bt.use_float + cbt.data_type = bt.data_type + + cbt.r.channel_name = bt.r.channel_name + cbt.r.subchannel_index = bt.r.subchannel_index + cbt.r.default_value = bt.r.default_value + cbt.r.normal_type = bt.r.normal_type + cbt.r.invert_value = bt.r.invert_value + + cbt.g.channel_name = bt.g.channel_name + cbt.g.subchannel_index = bt.g.subchannel_index + cbt.g.default_value = bt.g.default_value + cbt.g.normal_type = bt.g.normal_type + cbt.g.invert_value = bt.g.invert_value + + cbt.b.channel_name = bt.b.channel_name + cbt.b.subchannel_index = bt.b.subchannel_index + cbt.b.default_value = bt.b.default_value + cbt.b.normal_type = bt.b.normal_type + cbt.b.invert_value = bt.b.invert_value + + cbt.a.channel_name = bt.a.channel_name + cbt.a.subchannel_index = bt.a.subchannel_index + cbt.a.default_value = bt.a.default_value + cbt.a.normal_type = bt.a.normal_type + cbt.a.invert_value = bt.a.invert_value + + return {'FINISHED'} + +class YPasteBakeTarget(bpy.types.Operator): + bl_idname = "wm.y_paste_bake_target" + bl_label = "Paste Bake Target As New" + bl_description = "Paste Bake Target" + bl_options = {'UNDO'} + + paste_as_new : BoolProperty( + name = 'Paste As New Bake Target', + default = True + ) + + @classmethod + def poll(cls, context): + node = get_active_ypaint_node() + + wmp = context.window_manager.ypprops + has_clipboard = len(wmp.clipboard_bake_target) > 0 + + return context.object and node and has_clipboard + + def execute(self, context): + node = get_active_ypaint_node() + yp = node.node_tree.yp + wmp = context.window_manager.ypprops + + if not self.paste_as_new and (yp.active_bake_target_index < 0 or yp.active_bake_target_index >= len(yp.bake_targets) or len(yp.bake_targets) == 0): + self.report({'ERROR'}, "Cannot paste values, no bake target selected") + return {'CANCELLED'} + + cbt = wmp.clipboard_bake_target[0] + + if self.paste_as_new: + name = get_unique_name(cbt.name, yp.bake_targets) + bt = yp.bake_targets.add() + bt.name = name + else: + bt = yp.bake_targets[yp.active_bake_target_index] + + bt.use_float = cbt.use_float + bt.data_type = cbt.data_type + + bt.r.channel_name = cbt.r.channel_name + bt.r.subchannel_index = cbt.r.subchannel_index + bt.r.default_value = cbt.r.default_value + bt.r.normal_type = cbt.r.normal_type + bt.r.invert_value = cbt.r.invert_value + + bt.g.channel_name = cbt.g.channel_name + bt.g.subchannel_index = cbt.g.subchannel_index + bt.g.default_value = cbt.g.default_value + bt.g.normal_type = cbt.g.normal_type + bt.g.invert_value = cbt.g.invert_value + + bt.b.channel_name = cbt.b.channel_name + bt.b.subchannel_index = cbt.b.subchannel_index + bt.b.default_value = cbt.b.default_value + bt.b.normal_type = cbt.b.normal_type + bt.b.invert_value = cbt.b.invert_value + + bt.a.channel_name = cbt.a.channel_name + bt.a.subchannel_index = cbt.a.subchannel_index + bt.a.default_value = cbt.a.default_value + bt.a.normal_type = cbt.a.normal_type + bt.a.invert_value = cbt.a.invert_value + + return {'FINISHED'} + def register(): bpy.utils.register_class(YNewBakeTarget) bpy.utils.register_class(YRemoveBakeTarget) bpy.utils.register_class(YBakeTargetChannel) bpy.utils.register_class(YBakeTarget) - + bpy.utils.register_class(YCopyBakeTarget) + bpy.utils.register_class(YPasteBakeTarget) + def unregister(): bpy.utils.unregister_class(YNewBakeTarget) bpy.utils.unregister_class(YRemoveBakeTarget) bpy.utils.unregister_class(YBakeTargetChannel) bpy.utils.unregister_class(YBakeTarget) + bpy.utils.unregister_class(YCopyBakeTarget) + bpy.utils.unregister_class(YPasteBakeTarget) diff --git a/BakeToLayer.py b/BakeToLayer.py index 2b4590f8..e7309a98 100644 --- a/BakeToLayer.py +++ b/BakeToLayer.py @@ -9,11 +9,8 @@ from .node_arrangements import * from . import lib, Layer, Mask, ImageAtlas, UDIM, vector_displacement_lib, vector_displacement -TEMP_VCOL = '__temp__vcol__' -TEMP_EMISSION = '_TEMP_EMI_' - class YTryToSelectBakedVertexSelect(bpy.types.Operator): - bl_idname = "node.y_try_to_select_baked_vertex" + bl_idname = "wm.y_try_to_select_baked_vertex" bl_label = "Try to reselect baked selected vertex" bl_description = "Try to reselect baked selected vertex. It might give you wrong results if mesh number of vertex changed" bl_options = {'REGISTER', 'UNDO'} @@ -108,7 +105,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveBakeInfoOtherObject(bpy.types.Operator): - bl_idname = "node.y_remove_bake_info_other_object" + bl_idname = "wm.y_remove_bake_info_other_object" bl_label = "Remove other object info" bl_description = "Remove other object bake info, so it won't be automatically baked anymore if you choose to rebake" bl_options = {'REGISTER', 'UNDO'} @@ -141,7 +138,7 @@ def update_bake_to_layer_uv_map(self, context): self.use_udim = UDIM.is_uvmap_udim(objs, self.uv_map) class YBakeToLayer(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_bake_to_layer" + bl_idname = "wm.y_bake_to_layer" bl_label = "Bake To Layer" bl_description = "Bake something as layer/mask" bl_options = {'REGISTER', 'UNDO'} @@ -184,6 +181,13 @@ class YBakeToLayer(bpy.types.Operator, BaseBakeOperator): ) # Other objects props + + use_cage : BoolProperty( + name = 'Cage Object', + description = 'Cast rays to active material objects from a cage', + default = False + ) + cage_object_name : StringProperty( name = 'Cage Object', description = 'Object to use as cage instead of calculating the cage from the active object with cage extrusion', @@ -203,6 +207,12 @@ class YBakeToLayer(bpy.types.Operator, BaseBakeOperator): description = 'The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit', default=0.2, min=0.0, max=1.0 ) + + normalize : BoolProperty( + name = 'Normalize Bake Result', + description = 'Normalize the bake result', + default = True, + ) # AO Props ao_distance : FloatProperty(default=1.0) @@ -349,7 +359,7 @@ def invoke(self, context, event): height_root_ch = get_root_height_channel(yp) # Set default float image - if self.type in {'POINTINESS', 'MULTIRES_DISPLACEMENT', 'BEVEL_MASK'}: + if self.type in {'POINTINESS', 'MULTIRES_DISPLACEMENT'}: self.hdr = True else: self.hdr = False @@ -407,6 +417,7 @@ def invoke(self, context, event): self.normal_blend_type = 'OVERLAY' elif self.type == 'OTHER_OBJECT_EMISSION': + self.blend_type = 'MIX' self.subsurf_influence = False self.margin = 0 @@ -423,6 +434,7 @@ def invoke(self, context, event): self.margin = 0 elif self.type == 'OTHER_OBJECT_CHANNELS': + self.blend_type = 'MIX' self.subsurf_influence = False self.use_image_atlas = False self.margin = 0 @@ -616,7 +628,8 @@ def draw(self, context): else: col.label(text='Name:') - if self.target_type == 'LAYER': + # Other object channels always bakes all channels + if self.target_type == 'LAYER' and self.type != 'OTHER_OBJECT_CHANNELS': col.label(text='Channel:') if channel and channel.type == 'NORMAL': col.label(text='Type:') @@ -624,10 +637,14 @@ def draw(self, context): col.label(text='Name:') if self.type.startswith('OTHER_OBJECT_'): - col.label(text='Cage Object:') - col.label(text='Cage Extrusion:') - if hasattr(bpy.context.scene.render.bake, 'max_ray_distance'): - col.label(text='Max Ray Distance:') + col.label(text='') + if self.use_cage: + col.label(text='Cage Object:') + col.label(text='Cage Extrusion:') + if hasattr(bpy.context.scene.render.bake, 'max_ray_distance'): + col.label(text='Max Ray Distance:') + elif self.type == 'POINTINESS' and is_bl_newer_than(2, 83): + col.label(text='') elif self.type == 'AO': col.label(text='AO Distance:') col.label(text='') @@ -655,6 +672,8 @@ def draw(self, context): col.separator() col.label(text='Bake Device:') col.label(text='Interpolation:') + if self.target_type == 'MASK': + col.label(text='Blend:') col.separator() col.label(text='') #col.label(text='') @@ -686,7 +705,8 @@ def draw(self, context): else: col.prop(self, 'name', text='') - if self.target_type == 'LAYER': + # Other object channels always bakes all channels + if self.target_type == 'LAYER' and self.type != 'OTHER_OBJECT_CHANNELS': rrow = col.row(align=True) rrow.prop(self, 'channel_idx', text='') if channel: @@ -699,12 +719,14 @@ def draw(self, context): col.label(text=self.overwrite_name) if self.type.startswith('OTHER_OBJECT_'): - col.prop_search(self, "cage_object_name", self, "cage_object_coll", text='', icon='OBJECT_DATA') - rrow = col.row(align=True) - rrow.active = self.cage_object_name == '' - rrow.prop(self, 'cage_extrusion', text='') - if hasattr(bpy.context.scene.render.bake, 'max_ray_distance'): - col.prop(self, 'max_ray_distance', text='') + col.prop(self, 'use_cage') + if self.use_cage: + col.prop_search(self, "cage_object_name", self, "cage_object_coll", text='', icon='OBJECT_DATA') + col.prop(self, 'cage_extrusion', text='') + if hasattr(bpy.context.scene.render.bake, 'max_ray_distance'): + col.prop(self, 'max_ray_distance', text='') + elif self.type == 'POINTINESS' and is_bl_newer_than(2, 83): + col.prop(self, 'normalize', text='Normalize Pointiness') elif self.type == 'AO': col.prop(self, 'ao_distance', text='') col.prop(self, 'only_local') @@ -738,12 +760,15 @@ def draw(self, context): col.prop(self, 'bake_device', text='') col.prop(self, 'interpolation', text='') + if self.target_type == 'MASK': + col.prop(self, 'blend_type', text='') + col.separator() if self.type.startswith('OTHER_OBJECT_'): col.prop(self, 'ssaa') else: col.prop(self, 'fxaa') - if self.type in {'AO', 'BEVEL_MASK'} and is_bl_newer_than(2, 81): + if self.type in {'AO', 'BEVEL_MASK', 'BEVEL_NORMAL'} and is_bl_newer_than(2, 81): col.prop(self, 'denoise') col.separator() @@ -776,1286 +801,36 @@ def draw(self, context): def execute(self, context): if not self.is_cycles_exist(context): return {'CANCELLED'} - T = time.time() - mat = get_active_material() node = get_active_ypaint_node() yp = node.node_tree.yp - tree = node.node_tree - ypui = context.window_manager.ypui - scene = context.scene - obj = context.object - ypup = get_user_preferences() - channel_idx = int(self.channel_idx) if len(yp.channels) > 0 else -1 - - active_layer = None - if len(yp.layers) > 0: - active_layer = yp.layers[yp.active_layer_index] - - if self.type == 'SELECTED_VERTICES' and obj.mode != 'EDIT': - self.report({'ERROR'}, "Should be in edit mode!") - return {'CANCELLED'} - - if self.target_type == 'MASK' and not active_layer: - self.report({'ERROR'}, "Mask need active layer!") - return {'CANCELLED'} if (self.overwrite_choice or self.overwrite_current) and self.overwrite_name == '': self.report({'ERROR'}, "Overwrite layer/mask cannot be empty!") return {'CANCELLED'} - if self.type in {'BEVEL_NORMAL', 'BEVEL_MASK'} and not is_bl_newer_than(2, 80): - self.report({'ERROR'}, "Blender 2.80+ is needed to use this feature!") - return {'CANCELLED'} - - if self.type in {'MULTIRES_NORMAL', 'MULTIRES_DISPLACEMENT'} and not is_bl_newer_than(2, 80): - #self.report({'ERROR'}, "This feature is not implemented yet in Blender 2.79!") - self.report({'ERROR'}, "Blender 2.80+ is needed to use this feature!") - return {'CANCELLED'} - - if (hasattr(context.object, 'hide_viewport') and context.object.hide_viewport) or context.object.hide_render: - self.report({'ERROR'}, "Please unhide render and viewport of active object!") - return {'CANCELLED'} - - if self.type == 'FLOW' and (self.uv_map == '' or self.uv_map_1 == '' or self.uv_map == self.uv_map_1): - self.report({'ERROR'}, "UVMap and Straight UVMap are cannot be the same or empty!") - return {'CANCELLED'} - - cage_object = None - if self.type.startswith('OTHER_OBJECT_') and self.cage_object_name != '': - cage_object = bpy.data.objects.get(self.cage_object_name) - if cage_object: - - if any([mod for mod in cage_object.modifiers if mod.type not in {'ARMATURE'}]) or any([mod for mod in obj.modifiers if mod.type not in {'ARMATURE'}]): - self.report({'ERROR'}, "Mesh modifiers is not working with cage object for now!") - return {'CANCELLED'} - - if len(cage_object.data.polygons) != len(obj.data.polygons): - self.report({'ERROR'}, "Invalid cage object, the cage mesh must have the same number of faces as the active object!") - return {'CANCELLED'} - - # Get all objects using material - if self.type.startswith('MULTIRES_') and not get_multires_modifier(context.object): - objs = [] - meshes = [] - multires_count = 0 - else: - objs = [context.object] - meshes = [context.object.data] - multires_count = 1 - - if mat.users > 1: - # Emptying the lists again in case active object is problematic - objs = [] - meshes = [] - for ob in get_scene_objects(): - if ob.type != 'MESH': continue - if hasattr(ob, 'hide_viewport') and ob.hide_viewport: continue - if len(get_uv_layers(ob)) == 0: continue - if len(ob.data.polygons) == 0: continue - if cage_object and cage_object == ob: continue - - # Do not bake objects with hide_render on - if ob.hide_render: continue - if not in_renderable_layer_collection(ob): continue - - if self.type.startswith('MULTIRES_') and get_multires_modifier(ob): - multires_count += 1 - - for i, m in enumerate(ob.data.materials): - if m == mat: - ob.active_material_index = i - if ob not in objs and ob.data not in meshes: - objs.append(ob) - meshes.append(ob.data) - - if not objs or (self.type.startswith('MULTIRES_') and multires_count == 0): - self.report({'ERROR'}, "No valid objects found to bake!") - return {'CANCELLED'} - - do_overwrite = False + # Get overwrite image overwrite_img = None + segment = None if (self.overwrite_choice or self.overwrite_current) and self.overwrite_image_name != '': overwrite_img = bpy.data.images.get(self.overwrite_image_name) - do_overwrite = True - segment = None - if overwrite_img: if overwrite_img.yia.is_image_atlas: segment = overwrite_img.yia.segments.get(self.overwrite_segment_name) elif overwrite_img.yua.is_udim_atlas: segment = overwrite_img.yua.segments.get(self.overwrite_segment_name) - # Get other objects for other object baking - other_objs = [] - - if self.type.startswith('OTHER_OBJECT_'): - - # Get other objects based on selected objects with different material - for o in context.selected_objects: - if o in objs or not o.data or not hasattr(o.data, 'materials'): continue - if mat.name not in o.data.materials: - other_objs.append(o) - - # Try to get other_objects from bake info - if overwrite_img: - - bi = segment.bake_info if segment else overwrite_img.y_bake_info - - scene_objs = get_scene_objects() - for oo in bi.other_objects: - if is_bl_newer_than(2, 79): - ooo = oo.object - else: ooo = scene_objs.get(oo.object_name) - - if ooo: - if is_bl_newer_than(2, 80): - # Check if object is on current view layer - layer_cols = get_object_parent_layer_collections([], bpy.context.view_layer.layer_collection, ooo) - if ooo not in other_objs and any(layer_cols): - other_objs.append(ooo) - else: - o = scene_objs.get(ooo.name) - if o and o not in other_objs: - other_objs.append(o) - - if self.type == 'OTHER_OBJECT_CHANNELS': - ch_other_objects, ch_other_mats, ch_other_sockets, ch_other_defaults, ch_other_alpha_sockets, ch_other_alpha_defaults, ori_mat_no_nodes = prepare_other_objs_channels(yp, other_objs) - - if not other_objs: - if overwrite_img: - self.report({'ERROR'}, "No source objects found! They're probably deleted!") - else: self.report({'ERROR'}, "Source objects must be selected and it should have different material!") - return {'CANCELLED'} - - # Get tile numbers - tilenums = [1001] - if self.use_udim: - tilenums = UDIM.get_tile_numbers(objs, self.uv_map) - - # Remember things - book = remember_before_bake(yp, mat=mat) - - # FXAA doesn't work with hdr image - # FXAA also does not works well with baked image with alpha, so other object bake will use SSAA instead - use_fxaa = not self.hdr and self.fxaa and not self.type.startswith('OTHER_OBJECT_') - - # For now SSAA only works with other object baking - use_ssaa = self.ssaa and self.type.startswith('OTHER_OBJECT_') - - # Denoising only available for AO bake for now - use_denoise = self.denoise and self.type in {'AO', 'BEVEL_MASK'} and is_bl_newer_than(2, 81) - - # SSAA will multiply size by 2 then resize it back - if use_ssaa: - width = self.width * 2 - height = self.height * 2 - else: - width = self.width - height = self.height - - # If use baked disp, need to bake normal and height map first - subdiv_setup_changes = False - height_root_ch = get_root_height_channel(yp) - if height_root_ch and self.use_baked_disp and not self.type.startswith('MULTIRES_'): - - if not height_root_ch.enable_subdiv_setup: - height_root_ch.enable_subdiv_setup = True - subdiv_setup_changes = True - - # To hold temporary objects - temp_objs = [] - - # Sometimes Cavity bake will create temporary objects - if (self.type == 'CAVITY' and (self.subsurf_influence or self.use_baked_disp)): - - # NOTE: Baking cavity with subdiv setup can only happen if there's only one object and no UDIM - if is_bl_newer_than(4, 2) and len(objs) == 1 and not self.use_udim and height_root_ch and height_root_ch.enable_subdiv_setup: - - # Check if there's VDM layer - vdm_layer = get_first_vdm_layer(yp) - vdm_uv_name = vdm_layer.uv_name if vdm_layer else self.uv_map - - # Get baked combined vdm image - combined_vdm_image = vector_displacement.get_combined_vdm_image(objs[0], vdm_uv_name, width=self.width, height=self.height) - - # Bake tangent and bitangent - # NOTE: Only bake the first object tangent since baking combined mesh can cause memory leak at the moment - tanimage, bitimage = vector_displacement.get_tangent_bitangent_images(objs[0], self.uv_map) - - # Duplicate object - objs = temp_objs = [get_merged_mesh_objects(scene, objs, True, disable_problematic_modifiers=False)] - - # Use VDM loader geometry nodes - # NOTE: Geometry nodes currently does not support UDIM, so using UDIM will cause wrong bake result - set_active_object(objs[0]) - vdm_loader = vector_displacement_lib.get_vdm_loader_geotree(self.uv_map, combined_vdm_image, tanimage, bitimage, 1.0) - bpy.ops.object.modifier_add(type='NODES') - geomod = objs[0].modifiers[-1] - geomod.node_group = vdm_loader - bpy.ops.object.modifier_apply(modifier=geomod.name) - - # Remove temporary datas - remove_datablock(bpy.data.node_groups, vdm_loader) - remove_datablock(bpy.data.images, combined_vdm_image) - - else: - objs = temp_objs = get_duplicated_mesh_objects(scene, objs, True) - - # Join objects then extend with other objects - elif self.type.startswith('OTHER_OBJECT_'): - if len(objs) > 1: - objs = [get_merged_mesh_objects(scene, objs)] - temp_objs = objs.copy() - - objs.extend(other_objs) - - # Join objects if the number of objects is higher than one - elif not self.type.startswith('MULTIRES_') and len(objs) > 1 and not is_join_objects_problematic(yp): - objs = temp_objs = [get_merged_mesh_objects(scene, objs, True)] - - fill_mode = 'FACE' - obj_vertex_indices = {} - if self.type == 'SELECTED_VERTICES': - if context.tool_settings.mesh_select_mode[0] or context.tool_settings.mesh_select_mode[1]: - fill_mode = 'VERTEX' - - if is_bl_newer_than(2, 80): - edit_objs = [o for o in objs if o.mode == 'EDIT'] - else: edit_objs = [context.object] - - for obj in edit_objs: - mesh = obj.data - bm = bmesh.from_edit_mesh(mesh) - - bm.verts.ensure_lookup_table() - #bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - v_indices = [] - if fill_mode == 'FACE': - for face in bm.faces: - if face.select: - v_indices.append(face.index) - #for loop in face.loops: - # v_indices.append(loop.index) - - else: - for vert in bm.verts: - if vert.select: - v_indices.append(vert.index) - - obj_vertex_indices[obj.name] = v_indices - - bpy.ops.object.mode_set(mode = 'OBJECT') - for obj in objs: - try: - vcol = new_vertex_color(obj, TEMP_VCOL) - set_obj_vertex_colors(obj, vcol.name, (0.0, 0.0, 0.0, 1.0)) - set_active_vertex_color(obj, vcol) - except: pass - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.mesh.y_vcol_fill(color_option ='WHITE') - #return {'FINISHED'} - bpy.ops.object.mode_set(mode = 'OBJECT') - - #return {'FINISHED'} - - # Check if there's channel using alpha - alpha_outp = None - for c in yp.channels: - if c.enable_alpha: - alpha_outp = node.outputs.get(c.name + io_suffix['ALPHA']) - if alpha_outp: break - - # Prepare bake settings - if self.type == 'AO': - if alpha_outp: - # If there's alpha channel use standard AO bake, which has lesser quality denoising - bake_type = 'AO' - else: - # When there is no alpha channel use combined render bake, which has better denoising - bake_type = 'COMBINED' - elif self.type == 'MULTIRES_NORMAL': - bake_type = 'NORMALS' - elif self.type == 'MULTIRES_DISPLACEMENT': - bake_type = 'DISPLACEMENT' - elif self.type in {'OTHER_OBJECT_NORMAL', 'OBJECT_SPACE_NORMAL'}: - bake_type = 'NORMAL' - else: - bake_type = 'EMIT' - - # If use only local, hide other objects - hide_other_objs = self.type != 'AO' or self.only_local - - # Fit tilesize to bake resolution if samples is equal 1 - if self.samples <= 1: - tile_x = width - tile_y = height - else: - tile_x = 256 - tile_y = 256 - - # Cage object only used for other object baking - cage_object_name = self.cage_object_name if self.type.startswith('OTHER_OBJECT_') else '' - - prepare_bake_settings( - book, objs, yp, samples=self.samples, margin=self.margin, - uv_map=self.uv_map, bake_type=bake_type, #disable_problematic_modifiers=True, - bake_device=self.bake_device, hide_other_objs=hide_other_objs, - bake_from_multires=self.type.startswith('MULTIRES_'), tile_x = tile_x, tile_y = tile_y, - use_selected_to_active=self.type.startswith('OTHER_OBJECT_'), - max_ray_distance=self.max_ray_distance, cage_extrusion=self.cage_extrusion, - source_objs=other_objs, use_denoising=False, margin_type=self.margin_type, - cage_object_name = cage_object_name, - normal_space = 'OBJECT' if self.type == 'OBJECT_SPACE_NORMAL' else 'TANGENT' - ) - - # Set multires level - #ori_multires_levels = {} - if self.type.startswith('MULTIRES_'): #or self.type == 'AO': - for ob in objs: - mod = get_multires_modifier(ob) - - #mod.render_levels = mod.total_levels - if mod and self.type.startswith('MULTIRES_'): - mod.render_levels = self.multires_base - mod.levels = self.multires_base - - #ori_multires_levels[ob.name] = mod.render_levels - - # Setup for cavity - if self.type == 'CAVITY': - - tt = time.time() - print('BAKE TO LAYER: Applying subsurf/multires for Cavity bake...') - - # Set vertex color for cavity - for obj in objs: - - set_active_object(obj) - - if self.subsurf_influence or self.use_baked_disp: - need_to_be_applied_modifiers = [] - for m in obj.modifiers: - if m.type in {'SUBSURF', 'MULTIRES'} and m.levels > 0 and m.show_viewport: - - # Set multires to the highest level - if m.type == 'MULTIRES': - m.levels = m.total_levels - - need_to_be_applied_modifiers.append(m) - - # Also apply displace - if m.type == 'DISPLACE' and m.show_viewport: - need_to_be_applied_modifiers.append(m) - - # Apply shape keys and modifiers - if any(need_to_be_applied_modifiers): - if obj.data.shape_keys: - if is_bl_newer_than(3, 3): - bpy.ops.object.shape_key_remove(all=True, apply_mix=True) - else: bpy.ops.object.shape_key_remove(all=True) - - for m in need_to_be_applied_modifiers: - bpy.ops.object.modifier_apply(modifier=m.name) - - # Create new vertex color for dirt - try: - vcol = new_vertex_color(obj, TEMP_VCOL) - set_obj_vertex_colors(obj, vcol.name, (1.0, 1.0, 1.0, 1.0)) - set_active_vertex_color(obj, vcol) - except: pass - - bpy.ops.paint.vertex_color_dirt(dirt_angle=math.pi / 2) - - print('BAKE TO LAYER: Applying subsurf/multires is done in', '{:0.2f}'.format(time.time() - tt), 'seconds!') - - # Setup for flow - if self.type == 'FLOW': - bpy.ops.object.mode_set(mode = 'OBJECT') - for obj in objs: - uv_layers = get_uv_layers(obj) - main_uv = uv_layers.get(self.uv_map) - straight_uv = uv_layers.get(self.uv_map_1) - - if main_uv and straight_uv: - flow_vcol = get_flow_vcol(obj, main_uv, straight_uv) - - # Flip normals setup - if self.flip_normals: - #ori_mode[obj.name] = obj.mode - if is_bl_newer_than(2, 80): - # Deselect other objects first - for o in other_objs: - o.select_set(False) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.reveal() - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.flip_normals() - bpy.ops.object.mode_set(mode='OBJECT') - # Reselect other objects - for o in other_objs: - o.select_set(True) - else: - for obj in objs: - if obj in other_objs: continue - context.scene.objects.active = obj - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.reveal() - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.flip_normals() - bpy.ops.object.mode_set(mode='OBJECT') - - # More setup - ori_mods = {} - ori_viewport_mods = {} - ori_mat_ids = {} - ori_loop_locs = {} - ori_multires_levels = {} - - # Do not disable modifiers for surface based bake types - disable_problematic_modifiers = self.type not in {'CAVITY', 'POINTINESS', 'BEVEL_NORMAL', 'BEVEL_MASK'} - - for obj in objs: - - # Disable few modifiers - ori_mods[obj.name] = [m.show_render for m in obj.modifiers] - ori_viewport_mods[obj.name] = [m.show_viewport for m in obj.modifiers] - if self.type.startswith('MULTIRES_'): - mul = get_multires_modifier(obj) - multires_index = 99 - if mul: - for i, m in enumerate(obj.modifiers): - if m == mul: multires_index = i - if i > multires_index: - m.show_render = False - m.show_viewport = False - elif disable_problematic_modifiers and obj not in other_objs: - for m in get_problematic_modifiers(obj): - m.show_render = False - - ori_mat_ids[obj.name] = [] - ori_loop_locs[obj.name] = [] - - if self.subsurf_influence and not self.use_baked_disp and not self.type.startswith('MULTIRES_'): - for m in obj.modifiers: - if m.type == 'MULTIRES': - ori_multires_levels[obj.name] = m.render_levels - m.render_levels = m.total_levels - break - - if len(obj.data.materials) > 1: - active_mat_id = [i for i, m in enumerate(obj.data.materials) if m == mat] - if active_mat_id: active_mat_id = active_mat_id[0] - else: continue - - uv_layers = get_uv_layers(obj) - uvl = uv_layers.get(self.uv_map) - - for p in obj.data.polygons: - - # Set uv location to (0,0) if not using current material - if uvl and not self.force_bake_all_polygons: - uv_locs = [] - for li in p.loop_indices: - uv_locs.append(uvl.data[li].uv.copy()) - if p.material_index != active_mat_id: - uvl.data[li].uv = Vector((0.0, 0.0)) - - ori_loop_locs[obj.name].append(uv_locs) - - # Need to assign all polygon to active material if there are multiple materials - ori_mat_ids[obj.name].append(p.material_index) - p.material_index = active_mat_id - - #return {'FINISHED'} - - # Create bake nodes - tex = mat.node_tree.nodes.new('ShaderNodeTexImage') - bsdf = mat.node_tree.nodes.new('ShaderNodeEmission') - normal_bake = None - geometry = None - vector_math = None - vector_math_1 = None - if self.type == 'BEVEL_NORMAL': - #bsdf = mat.node_tree.nodes.new('ShaderNodeBsdfDiffuse') - normal_bake = mat.node_tree.nodes.new('ShaderNodeGroup') - normal_bake.node_tree = get_node_tree_lib(lib.BAKE_NORMAL_ACTIVE_UV) - elif self.type == 'BEVEL_MASK': - geometry = mat.node_tree.nodes.new('ShaderNodeNewGeometry') - vector_math = mat.node_tree.nodes.new('ShaderNodeVectorMath') - vector_math.operation = 'CROSS_PRODUCT' - if is_bl_newer_than(2, 81): - vector_math_1 = mat.node_tree.nodes.new('ShaderNodeVectorMath') - vector_math_1.operation = 'LENGTH' - - # Get output node and remember original bsdf input - output = get_active_mat_output_node(mat.node_tree) - ori_bsdf = output.inputs[0].links[0].from_socket - - if self.type == 'AO': - # If there's alpha channel use standard AO bake, which has lesser quality denoising - if alpha_outp: - src = None - - if hasattr(context.scene.cycles, 'use_fast_gi'): - context.scene.cycles.use_fast_gi = True - - if context.scene.world: - context.scene.world.light_settings.distance = self.ao_distance - # When there is no alpha channel use combined render bake, which has better denoising - else: - src = mat.node_tree.nodes.new('ShaderNodeAmbientOcclusion') - - if 'Distance' in src.inputs: - src.inputs['Distance'].default_value = self.ao_distance - - # Links - if not is_bl_newer_than(2, 80): - mat.node_tree.links.new(src.outputs[0], output.inputs[0]) - else: - mat.node_tree.links.new(src.outputs['AO'], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'POINTINESS': - src = mat.node_tree.nodes.new('ShaderNodeNewGeometry') - - # Links - mat.node_tree.links.new(src.outputs['Pointiness'], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'CAVITY': - src = mat.node_tree.nodes.new('ShaderNodeGroup') - src.node_tree = get_node_tree_lib(lib.CAVITY) - - # Set vcol - vcol_node = src.node_tree.nodes.get('vcol') - vcol_node.attribute_name = TEMP_VCOL - - mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'DUST': - src = mat.node_tree.nodes.new('ShaderNodeGroup') - src.node_tree = get_node_tree_lib(lib.DUST) - - mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'PAINT_BASE': - src = mat.node_tree.nodes.new('ShaderNodeGroup') - src.node_tree = get_node_tree_lib(lib.PAINT_BASE) - - mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'BEVEL_NORMAL': - src = mat.node_tree.nodes.new('ShaderNodeBevel') - - src.samples = self.bevel_samples - src.inputs[0].default_value = self.bevel_radius - - #mat.node_tree.links.new(src.outputs[0], bsdf.inputs['Normal']) - mat.node_tree.links.new(src.outputs[0], normal_bake.inputs[0]) - mat.node_tree.links.new(normal_bake.outputs[0], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'BEVEL_MASK': - src = mat.node_tree.nodes.new('ShaderNodeBevel') - - src.samples = self.bevel_samples - src.inputs[0].default_value = self.bevel_radius - - mat.node_tree.links.new(geometry.outputs['Normal'], vector_math.inputs[0]) - mat.node_tree.links.new(src.outputs[0], vector_math.inputs[1]) - #mat.node_tree.links.new(src.outputs[0], bsdf.inputs['Normal']) - if is_bl_newer_than(2, 81): - mat.node_tree.links.new(vector_math.outputs[0], vector_math_1.inputs[0]) - mat.node_tree.links.new(vector_math_1.outputs[1], bsdf.inputs[0]) - else: - mat.node_tree.links.new(vector_math.outputs[1], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'SELECTED_VERTICES': - if is_bl_newer_than(2, 80): - src = mat.node_tree.nodes.new('ShaderNodeVertexColor') - src.layer_name = TEMP_VCOL - else: - src = mat.node_tree.nodes.new('ShaderNodeAttribute') - src.attribute_name = TEMP_VCOL - mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - elif self.type == 'FLOW': - # Set vcol - src = mat.node_tree.nodes.new('ShaderNodeAttribute') - src.attribute_name = FLOW_VCOL - - mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - else: - src = None - mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) - - # Get number of target images - ch_ids = [0] - - # Other object channels related - all_other_mats = [] - ori_from_nodes = {} - ori_from_sockets = {} - - if self.type == 'OTHER_OBJECT_CHANNELS': - ch_ids = [i for i, coo in enumerate(ch_other_objects) if len(coo) > 0] - - # Get all other materials - for oo in other_objs: - for m in oo.data.materials: - if m == None or not m.use_nodes: continue - if m not in all_other_mats: - all_other_mats.append(m) - - # Remember original socket connected to outputs - for m in all_other_mats: - soc = None - from_node = '' - from_socket = '' - mout = get_material_output(m) - if mout: - for l in mout.inputs[0].links: - soc = l.from_socket - from_node = l.from_node.name - from_socket = l.from_socket.name - - # Create temporary emission - temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) - if not temp_emi: - temp_emi = m.node_tree.nodes.new('ShaderNodeEmission') - temp_emi.name = TEMP_EMISSION - m.node_tree.links.new(temp_emi.outputs[0], mout.inputs[0]) - - ori_from_nodes[m.name] = from_node - ori_from_sockets[m.name] = from_socket - - # Newly created layer index and image - active_id = None - image = None - - for idx in ch_ids: - - # Image name and colorspace - image_name = self.name - colorspace = get_srgb_name() - - if self.type == 'OTHER_OBJECT_CHANNELS': - - root_ch = yp.channels[idx] - image_name += ' ' + yp.channels[idx].name - - # Hide irrelevant objects - for oo in other_objs: - if oo not in ch_other_objects[idx]: - oo.hide_render = True - else: oo.hide_render = False - - if root_ch.type == 'NORMAL': - bake_type = 'NORMAL' - - # Set back original socket - for m in all_other_mats: - mout = get_material_output(m) - if mout: - nod = m.node_tree.nodes.get(ori_from_nodes[m.name]) - if nod: - soc = nod.outputs.get(ori_from_sockets[m.name]) - if soc: m.node_tree.links.new(soc, mout.inputs[0]) - - else: - bake_type = 'EMIT' - - # Set emission connection - for j, m in enumerate(ch_other_mats[idx]): - default = ch_other_defaults[idx][j] - socket = ch_other_sockets[idx][j] - - temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) - if not temp_emi: continue - - if default != None: - # Set default - if type(default) == float: - temp_emi.inputs[0].default_value = (default, default, default, 1.0) - else: temp_emi.inputs[0].default_value = (default[0], default[1], default[2], 1.0) - - # Break link - for l in temp_emi.inputs[0].links: - m.node_tree.links.remove(l) - elif socket: - m.node_tree.links.new(socket, temp_emi.inputs[0]) - - colorspace = get_noncolor_name() if root_ch.colorspace == 'LINEAR' else get_srgb_name() - - elif self.type in {'BEVEL_NORMAL', 'MULTIRES_NORMAL', 'OTHER_OBJECT_NORMAL', 'OBJECT_SPACE_NORMAL'}: - colorspace = get_noncolor_name() - - # Using float image will always make the image linear/non-color - if self.hdr: - colorspace = get_noncolor_name() - - # Base color of baked image - if self.type == 'AO': - color = [1.0, 1.0, 1.0, 1.0] - elif self.type in {'BEVEL_NORMAL', 'MULTIRES_NORMAL', 'OTHER_OBJECT_NORMAL', 'OBJECT_SPACE_NORMAL'}: - if self.hdr: - color = [0.7354, 0.7354, 1.0, 1.0] - else: - color = [0.5, 0.5, 1.0, 1.0] - elif self.type == 'FLOW': - color = [0.5, 0.5, 0.0, 1.0] - else: - if self.hdr: - color = [0.7354, 0.7354, 0.7354, 1.0] - else: color = [0.5, 0.5, 0.5, 1.0] - - # Make image transparent if its baked from other objects - if self.type.startswith('OTHER_OBJECT_'): - color[3] = 0.0 - - # New target image - if self.use_udim: - image = bpy.data.images.new( - name=image_name, width=width, height=height, - alpha=True, float_buffer=self.hdr, tiled=True - ) - - # Fill tiles - for tilenum in tilenums: - UDIM.fill_tile(image, tilenum, color, width, height) - UDIM.initial_pack_udim(image, color) - - # Remember base color - image.yui.base_color = color - else: - image = bpy.data.images.new( - name=image_name, width=width, height=height, - alpha=True, float_buffer=self.hdr - ) - - image.generated_color = color - image.colorspace_settings.name = colorspace - - # Set image filepath if overwrite image is found - if do_overwrite: - # Get overwrite image again to avoid pointer error - overwrite_img = bpy.data.images.get(self.overwrite_image_name) - #if idx == 0: - if idx == min(ch_ids): - if not overwrite_img.packed_file and overwrite_img.filepath != '': - image.filepath = overwrite_img.filepath - else: - layer = yp.layers[yp.active_layer_index] - root_ch = yp.channels[idx] - ch = layer.channels[idx] - - if root_ch.type == 'NORMAL': - source = get_channel_source_1(ch, layer) - else: source = get_channel_source(ch, layer) - - if source and hasattr(source, 'image') and source.image and not source.image.packed_file and source.image.filepath != '': - image.filepath = source.image.filepath - - # Set bake image - tex.image = image - mat.node_tree.nodes.active = tex - - # Bake! - try: - if self.type.startswith('MULTIRES_'): - bpy.ops.object.bake_image() - else: - if bake_type != 'EMIT': - bpy.ops.object.bake(type=bake_type) - else: bpy.ops.object.bake() - except Exception as e: - - # Try to use CPU if GPU baking is failed - if self.bake_device == 'GPU': - print('EXCEPTIION: GPU baking failed! Trying to use CPU...') - self.bake_device = 'CPU' - scene.cycles.device = 'CPU' - - if self.type.startswith('MULTIRES_'): - bpy.ops.object.bake_image() - else: - if bake_type != 'EMIT': - bpy.ops.object.bake(type=bake_type) - else: bpy.ops.object.bake() - else: - print('EXCEPTIION:', e) - - if use_fxaa: fxaa_image(image, False, bake_device=self.bake_device) - - # Bake other object alpha - if self.type in {'OTHER_OBJECT_NORMAL', 'OTHER_OBJECT_CHANNELS'}: - - alpha_found = False - if self.type == 'OTHER_OBJECT_CHANNELS': - - # Set emission connection - for j, m in enumerate(ch_other_mats[idx]): - alpha_default = ch_other_alpha_defaults[idx][j] - alpha_socket = ch_other_alpha_sockets[idx][j] - - temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) - if not temp_emi: continue - - if alpha_default != 1.0: - alpha_found = True - # Set alpha_default - if type(alpha_default) == float: - temp_emi.inputs[0].default_value = (alpha_default, alpha_default, alpha_default, 1.0) - else: temp_emi.inputs[0].default_value = (alpha_default[0], alpha_default[1], alpha_default[2], 1.0) - - # Break link - for l in temp_emi.inputs[0].links: - m.node_tree.links.remove(l) - elif alpha_socket: - alpha_found = True - m.node_tree.links.new(alpha_socket, temp_emi.inputs[0]) - else: - alpha_found = True - - if alpha_found: - - temp_img = image.copy() - temp_img.colorspace_settings.name = get_noncolor_name() - tex.image = temp_img - - # Set temp filepath - if image.source == 'TILED': - temp_img.name = '__TEMP__' - UDIM.initial_pack_udim(temp_img) - - # Need to use clear so there's alpha on the baked image - scene.render.bake.use_clear = True - - # Bake emit can will create alpha image - bpy.ops.object.bake(type='EMIT') - - # Set tile pixels - for tilenum in tilenums: - - # Swap tile - if tilenum != 1001: - UDIM.swap_tile(image, 1001, tilenum) - UDIM.swap_tile(temp_img, 1001, tilenum) - - # Copy alpha to RGB channel, so it can be fxaa-ed - if self.type == 'OTHER_OBJECT_NORMAL': - copy_image_channel_pixels(temp_img, temp_img, 3, 0) - - # FXAA alpha - fxaa_image(temp_img, False, self.bake_device, first_tile_only=True) - - # Copy alpha to actual image - copy_image_channel_pixels(temp_img, image, 0, 3) - - # Swap tile again to recover - if tilenum != 1001: - UDIM.swap_tile(image, 1001, tilenum) - UDIM.swap_tile(temp_img, 1001, tilenum) - - # Remove temp image - remove_datablock(bpy.data.images, temp_img, user=tex, user_prop='image') - - # Back to original size if using SSA - if use_ssaa: - image, temp_segment = resize_image( - image, self.width, self.height, image.colorspace_settings.name, - alpha_aware=True, bake_device=self.bake_device - ) - - # Denoise AO image - if use_denoise: - image = denoise_image(image) - - new_segment_created = False - - if self.use_image_atlas: - - need_to_create_new_segment = False - if segment: - ia_image = segment.id_data - if self.use_udim: - need_to_create_new_segment = ia_image.is_float != self.hdr - if need_to_create_new_segment: - UDIM.remove_udim_atlas_segment_by_name(ia_image, segment.name, yp) - else: - need_to_create_new_segment = self.width != segment.width or self.height != segment.height or ia_image.is_float != self.hdr - if need_to_create_new_segment: - segment.unused = True - - if not segment or need_to_create_new_segment: - - if self.use_udim: - segment = UDIM.get_set_udim_atlas_segment( - tilenums, color=(0, 0, 0, 0), colorspace=get_srgb_name(), hdr=self.hdr, yp=yp - ) - else: - # Clearing unused image atlas segments - img_atlas = ImageAtlas.check_need_of_erasing_segments(yp, 'TRANSPARENT', self.width, self.height, self.hdr) - if img_atlas: ImageAtlas.clear_unused_segments(img_atlas.yia) - - segment = ImageAtlas.get_set_image_atlas_segment( - self.width, self.height, 'TRANSPARENT', self.hdr, yp=yp - ) - - new_segment_created = True - - ia_image = segment.id_data - - # Set baked image to segment - if self.use_udim: - offset = get_udim_segment_mapping_offset(segment) * 10 - copy_dict = {} - for tilenum in tilenums: - copy_dict[tilenum] = tilenum + offset - UDIM.copy_tiles(image, ia_image, copy_dict) - else: copy_image_pixels(image, ia_image, segment) - temp_img = image - image = ia_image - - # Remove original baked image - remove_datablock(bpy.data.images, temp_img) - - # Index 0 is the main image - if idx == min(ch_ids): - if do_overwrite: - - # Get overwrite image again to avoid pointer error - overwrite_img = bpy.data.images.get(self.overwrite_image_name) - - active_id = yp.active_layer_index - - if overwrite_img != image: - if segment and not self.use_image_atlas: - entities = ImageAtlas.replace_segment_with_image(yp, segment, image) - segment = None - else: entities = replace_image(overwrite_img, image, yp, self.uv_map) - elif segment: entities = ImageAtlas.get_entities_with_specific_segment(yp, segment) - else: entities = get_entities_with_specific_image(yp, image) - - for entity in entities: - if new_segment_created: - entity.segment_name = segment.name - ImageAtlas.set_segment_mapping(entity, segment, image) - - if entity.uv_name != self.uv_map: - entity.uv_name = self.uv_map - - if self.target_type == 'LAYER': - layer_ids = [i for i, l in enumerate(yp.layers) if l in entities] - if entities and yp.active_layer_index not in layer_ids: - active_id = layer_ids[0] - - # Refresh uv - refresh_temp_uv(context.object, yp.layers[active_id]) - - # Refresh Neighbor UV resolution - set_uv_neighbor_resolution(yp.layers[active_id]) - - elif self.target_type == 'MASK': - masks = [] - for l in yp.layers: - masks.extend([m for m in l.masks if m in entities]) - if masks: - masks[0].active_edit = True - - # Refresh uv - refresh_temp_uv(context.object, masks[0]) - - # Refresh Neighbor UV resolution - set_uv_neighbor_resolution(masks[0]) - - elif self.target_type == 'LAYER': - - layer_name = image.name if not self.use_image_atlas else self.name - - if self.use_image_atlas: - layer_name = get_unique_name(layer_name, yp.layers) - - yp.halt_update = True - layer = Layer.add_new_layer( - node.node_tree, layer_name, 'IMAGE', channel_idx, self.blend_type, - self.normal_blend_type, self.normal_map_type, 'UV', self.uv_map, image, None, segment, - interpolation = self.interpolation, - normal_space = 'OBJECT' if self.type == 'OBJECT_SPACE_NORMAL' else 'TANGENT' - ) - yp.halt_update = False - active_id = yp.active_layer_index - - if segment: - ImageAtlas.set_segment_mapping(layer, segment, image) - - # Refresh uv - refresh_temp_uv(context.object, layer) - - # Refresh Neighbor UV resolution - set_uv_neighbor_resolution(layer) - - - else: - mask_name = image.name if not self.use_image_atlas else self.name - - if self.use_image_atlas: - mask_name = get_unique_name(mask_name, active_layer.masks) - - mask = Mask.add_new_mask( - active_layer, mask_name, 'IMAGE', 'UV', self.uv_map, - image, None, segment - ) - mask.active_edit = True - - reconnect_layer_nodes(active_layer) - rearrange_layer_nodes(active_layer) - - active_id = yp.active_layer_index - - if segment: - ImageAtlas.set_segment_mapping(mask, segment, image) - - # Refresh uv - refresh_temp_uv(context.object, mask) - - # Refresh Neighbor UV resolution - set_uv_neighbor_resolution(mask) - - # Indices > 0 are for channel override images - else: - # Set images to channel override - layer = yp.layers[yp.active_layer_index] - root_ch = yp.channels[idx] - ch = layer.channels[idx] - if not ch.enable: ch.enable = True - - # Normal channel will use second override - if root_ch.type == 'NORMAL': - if ch.normal_map_type != 'NORMAL_MAP': ch.normal_map_type = 'NORMAL_MAP' - if not ch.override_1: ch.override_1 = True - if ch.override_1_type != 'IMAGE': ch.override_1_type = 'IMAGE' - source = get_channel_source_1(ch, layer) - else: - if not ch.override: ch.override = True - if ch.override_type != 'IMAGE': ch.override_type = 'IMAGE' - source = get_channel_source(ch, layer) - - # If image already exists on source - old_image = None - if source.image and image != source.image: - old_image = source.image - source_name = old_image.name - current_name = image.name - - old_image.name = '_____temp' - image.name = source_name - old_image.name = current_name - - # Set image to source - source.image = image - source.interpolation = self.interpolation - - # Remove image if it's not used anymore - if old_image: safe_remove_image(old_image) - - # Set bake info to image/segment - bi = segment.bake_info if segment else image.y_bake_info - - if not bi.is_baked: bi.is_baked = True - if bi.bake_type != self.type: bi.bake_type = self.type - for attr in dir(bi): - #if attr in dir(self): - if attr.startswith('__'): continue - if attr.startswith('bl_'): continue - if attr in {'rna_type'}: continue - try: setattr(bi, attr, getattr(self, attr)) - except: pass - - if other_objs: - - # Remember other objects to bake info - for o in other_objs: - if is_bl_newer_than(2, 79): - oo_recorded = any([oo for oo in bi.other_objects if oo.object == o]) - else: oo_recorded = any([oo for oo in bi.other_objects if oo.object_name == o.name]) - - if not oo_recorded: - oo = bi.other_objects.add() - if is_bl_newer_than(2, 79): - oo.object = o - oo.object_name = o.name - - # Remove unused other objects on bake info - for i, oo in reversed(list(enumerate(bi.other_objects))): - if is_bl_newer_than(2, 79): - ooo = oo.object - else: ooo = bpy.data.objects.get(oo.object_name) - - if ooo not in other_objs: - bi.other_objects.remove(i) - - if self.type == 'SELECTED_VERTICES': - #fill_mode = 'FACE' - #obj_vertex_indices = {} - bi.selected_face_mode = True if fill_mode == 'FACE' else False - - # Clear selected objects first - bi.selected_objects.clear() - - # Collect object to bake info - for obj_name, v_indices in obj_vertex_indices.items(): - obj = bpy.data.objects.get(obj_name) - bso = bi.selected_objects.add() - if is_bl_newer_than(2, 79): - bso.object = obj - bso.object_name = obj.name - - # Collect selected vertex data to bake info - for vi in v_indices: - bvi = bso.selected_vertex_indices.add() - bvi.index = vi - - # Recover other yps - if self.type == 'OTHER_OBJECT_CHANNELS': - for m in all_other_mats: - # Set back original socket - mout = get_material_output(m) - if mout: - nod = m.node_tree.nodes.get(ori_from_nodes[m.name]) - if nod: - soc = nod.outputs.get(ori_from_sockets[m.name]) - if soc: m.node_tree.links.new(soc, mout.inputs[0]) - - # Remove temp emission - temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) - if temp_emi: m.node_tree.nodes.remove(temp_emi) - - # Recover other objects material settings - recover_other_objs_channels(other_objs, ori_mat_no_nodes) - - # Remove temp bake nodes - simple_remove_node(mat.node_tree, tex) - #simple_remove_node(mat.node_tree, srgb2lin) - simple_remove_node(mat.node_tree, bsdf) - if src: simple_remove_node(mat.node_tree, src) - if normal_bake: simple_remove_node(mat.node_tree, normal_bake) - if geometry: simple_remove_node(mat.node_tree, geometry) - if vector_math: simple_remove_node(mat.node_tree, vector_math) - if vector_math_1: simple_remove_node(mat.node_tree, vector_math_1) - - # Recover original bsdf - mat.node_tree.links.new(ori_bsdf, output.inputs[0]) - - #return {'FINISHED'} - - for obj in objs: - # Recover modifiers - for i, m in enumerate(obj.modifiers): - #print(obj.name, i) - if i >= len(ori_mods[obj.name]): break - if ori_mods[obj.name][i] != m.show_render: - m.show_render = ori_mods[obj.name][i] - if i >= len(ori_viewport_mods[obj.name]): break - if ori_viewport_mods[obj.name][i] != m.show_render: - m.show_viewport = ori_viewport_mods[obj.name][i] - - # Recover multires levels - for m in obj.modifiers: - if m.type == 'MULTIRES' and obj.name in ori_multires_levels: - m.render_levels = ori_multires_levels[obj.name] - break - - # Recover material index - if ori_mat_ids[obj.name]: - for i, p in enumerate(obj.data.polygons): - if ori_mat_ids[obj.name][i] != p.material_index: - p.material_index = ori_mat_ids[obj.name][i] - - if ori_loop_locs[obj.name]: - - # Get uv map - uv_layers = get_uv_layers(obj) - uvl = uv_layers.get(self.uv_map) - - # Recover uv locations - if uvl: - for i, p in enumerate(obj.data.polygons): - for j, li in enumerate(p.loop_indices): - uvl.data[li].uv = ori_loop_locs[obj.name][i][j] - - # Delete temp vcol - vcols = get_vertex_colors(obj) - if vcols: - vcol = vcols.get(TEMP_VCOL) - if vcol: vcols.remove(vcol) - - # Recover flip normals setup - if self.flip_normals: - #bpy.ops.object.mode_set(mode = 'EDIT') - #bpy.ops.mesh.flip_normals() - #bpy.ops.mesh.select_all(action='DESELECT') - #bpy.ops.object.mode_set(mode = ori_mode) - if is_bl_newer_than(2, 80): - # Deselect other objects first - for o in other_objs: - o.select_set(False) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.reveal() - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.flip_normals() - bpy.ops.object.mode_set(mode='OBJECT') - # Reselect other objects - for o in other_objs: - o.select_set(True) - else: - for obj in objs: - if obj in other_objs: continue - context.scene.objects.active = obj - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.reveal() - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.flip_normals() - bpy.ops.object.mode_set(mode='OBJECT') - - # Recover subdiv setup - if height_root_ch and subdiv_setup_changes: - height_root_ch.enable_subdiv_setup = not height_root_ch.enable_subdiv_setup - - # Remove flow vcols - if self.type == 'FLOW': - for obj in objs: - vcols = get_vertex_colors(obj) - flow_vcol = vcols.get(FLOW_VCOL) - if flow_vcol: - vcols.remove(flow_vcol) - - # Recover bake settings - recover_bake_settings(book, yp, mat=mat) - # Remove temporary objects - if temp_objs: - for o in temp_objs: - remove_mesh_obj(o) + # Get bake properties + bprops = get_bake_properties_from_self(self) - #return {'FINISHED'} + rdict = bake_to_entity(bprops, overwrite_img, segment) - # Check linear nodes becuse sometimes bake results can be linear or srgb - check_yp_linear_nodes(yp, reconnect=True) + if rdict['message'] != '': + self.report({'ERROR'}, rdict['message']) + return {'CANCELLED'} - # Reconnect and rearrange nodes - #reconnect_yp_layer_nodes(node.node_tree) - reconnect_yp_nodes(node.node_tree) - rearrange_yp_nodes(node.node_tree) + active_id = rdict['active_id'] + image = rdict['image'] # Refresh active index (only when not overwriting current entity) #if active_id != yp.active_layer_index: @@ -2065,6 +840,7 @@ def execute(self, context): update_image_editor_image(context, image) # Expand image source to show rebake button + ypui = context.window_manager.ypui if self.target_type == 'MASK': ypui.layer_ui.expand_masks = True else: @@ -2072,195 +848,13 @@ def execute(self, context): ypui.layer_ui.expand_source = True ypui.need_update = True - # Refresh mapping and stuff - #yp.active_layer_index = yp.active_layer_index - - if image: print('BAKE TO LAYER: Baking', image.name, 'is done in', '{:0.2f}'.format(time.time() - T), 'seconds!') - else: print('BAKE TO LAYER: No image created! Executed in', '{:0.2f}'.format(time.time() - T), 'seconds!') + if image: + self.report({'INFO'}, 'Baking '+bake_type_labels[self.type]+' is done in '+'{:0.2f}'.format(rdict['time_elapsed'])+' seconds!') return {'FINISHED'} -def bake_as_image( - objs, mat, entity, name, width=1024, height=1024, hdr=False, - samples=1, margin=5, uv_name='', bake_device='CPU', - use_udim=False, tilenums=[1001], fxaa=True, blur=False, blur_factor=0.5, - denoise=False, disable_modifiers=True, margin_type='ADJACENT_FACES' - ): - - yp = entity.id_data.yp - - m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) - m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) - - ori_use_baked = False - ori_enabled_mods = [] - #ori_enable_blur = False - if m1: - layer = yp.layers[int(m1.group(1))] - mask = None - elif m2: - layer = yp.layers[int(m2.group(1))] - mask = layer.masks[int(m2.group(2))] - - # Disable use baked first - if mask.use_baked: - ori_use_baked = True - mask.use_baked = False - - # Disable modifiers - if disable_modifiers: - for mod in mask.modifiers: - if mod.enable: - ori_enabled_mods.append(mod) - mod.enable = False - #ori_enable_blur = mask.enable_blur_vector - #mask.enable_blur_vector = False - - else: - return None - - # Remember things - book = remember_before_bake(yp) - - # FXAA doesn't work with hdr image - # FXAA also does not works well with baked image with alpha, so other object bake will use SSAA instead - use_fxaa = not hdr and fxaa - - # Preview setup - ori_channel_index = yp.active_channel_index - ori_preview_mode = yp.preview_mode - ori_layer_preview_mode = yp.layer_preview_mode - ori_layer_preview_mode_type = yp.layer_preview_mode_type - - layer.enable = True - - if mask: - mask.enable = True - mask.active_edit = True - - yp.layer_preview_mode_type = 'SPECIFIC_MASK' if mask else 'LAYER' - yp.layer_preview_mode = True - - # Set active channel so preview will output right value - for i, ch in enumerate(layer.channels): - if mask: - if ch.enable and mask.channels[i].enable: - yp.active_channel_index = i - break - else: - if ch.enable: - yp.active_channel_index = i - break - - # Modifier setups - ori_mods = {} - ori_viewport_mods = {} - - for obj in objs: - - # Disable few modifiers - ori_mods[obj.name] = [m.show_render for m in obj.modifiers] - ori_viewport_mods[obj.name] = [m.show_viewport for m in obj.modifiers] - - for m in get_problematic_modifiers(obj): - m.show_render = False - - prepare_bake_settings( - book, objs, yp, samples=samples, margin=margin, - uv_map=uv_name, bake_type='EMIT', bake_device=bake_device, - margin_type = margin_type - ) - - # Create bake nodes - tex = mat.node_tree.nodes.new('ShaderNodeTexImage') - - if mask: - color = (0, 0, 0, 1) - color_str = 'BLACK' - colorspace = get_noncolor_name() - else: - color = (0, 0, 0, 0) - color_str = 'TRANSPARENT' - colorspace = get_srgb_name() - - # Create image - if use_udim: - image = bpy.data.images.new( - name=name, width=width, height=height, - alpha=True, float_buffer=hdr, tiled=True - ) - - # Fill tiles - for tilenum in tilenums: - UDIM.fill_tile(image, tilenum, color, width, height) - UDIM.initial_pack_udim(image, color) - - # Remember base color - image.yia.color = color_str - else: - image = bpy.data.images.new( - name=name, width=width, height=height, - alpha=True, float_buffer=hdr - ) - - image.generated_color = color - image.colorspace_settings.name = colorspace - - # Set bake image - tex.image = image - mat.node_tree.nodes.active = tex - - # Bake! - bpy.ops.object.bake() - - if blur: - samples = 4096 if is_bl_newer_than(3) else 128 - blur_image(image, False, bake_device=bake_device, factor=blur_factor, samples=samples) - if denoise: - denoise_image(image) - if use_fxaa: fxaa_image(image, False, bake_device=bake_device) - - # Remove temp bake nodes - simple_remove_node(mat.node_tree, tex, remove_data=False) - - # Recover bake settings - recover_bake_settings(book, yp) - - # Recover modifiers - for obj in objs: - # Recover modifiers - for i, m in enumerate(obj.modifiers): - #print(obj.name, i) - if i >= len(ori_mods[obj.name]): break - if ori_mods[obj.name][i] != m.show_render: - m.show_render = ori_mods[obj.name][i] - if i >= len(ori_viewport_mods[obj.name]): break - if ori_viewport_mods[obj.name][i] != m.show_render: - m.show_viewport = ori_viewport_mods[obj.name][i] - - # Recover preview - yp.active_channel_index = ori_channel_index - if yp.preview_mode != ori_preview_mode: - yp.preview_mode = ori_preview_mode - if yp.layer_preview_mode != ori_layer_preview_mode: - yp.layer_preview_mode = ori_layer_preview_mode - if yp.layer_preview_mode_type != ori_layer_preview_mode_type: - yp.layer_preview_mode_type = ori_layer_preview_mode_type - - if disable_modifiers: - for mod in ori_enabled_mods: - mod.enable = True - - #if ori_enable_blur: - # mask.enable_blur_vector = True - - if mask and ori_use_baked: - mask.use_baked = True - - return image - class YBakeEntityToImage(bpy.types.Operator, BaseBakeOperator): - bl_idname = "node.y_bake_entity_to_image" + bl_idname = "wm.y_bake_entity_to_image" bl_label = "Bake Layer/Mask To Image" bl_description = "Bake Layer/Mask to an image" bl_options = {'UNDO'} @@ -2289,19 +883,42 @@ class YBakeEntityToImage(bpy.types.Operator, BaseBakeOperator): description = 'Use Image Atlas', default = False ) + + blur_type : EnumProperty( + name = 'Blur Type', + description = 'Blur type for the baked image', + items = ( + ('NOISE', 'Noise', 'Noisy and need more samples but has matching value to the blur vector option'), + ('FLAT', 'Flat', 'Flat blur'), + ('TENT', 'Tent', 'Tent blur'), + ('QUAD', 'Quadratic', 'Quadratic blur'), + ('CUBIC', 'Cubic', 'Cubic blur'), + ('GAUSS', 'Gaussian', 'Gausssian blur'), + ('FAST_GAUSS', 'Fast Gaussian', 'Fast gausssian blur'), + ('CATROM', 'Catrom', 'Catrom blur'), + ('MITCH', 'Mitch', 'Mitch blur') + ), + default='GAUSS' + ) blur : BoolProperty( name = 'Use Blur', - description = 'Use blur to baked image', + description = 'Use blur to the baked image', default = False ) blur_factor : FloatProperty( name = 'Blur Factor', - description = 'Blur factor to baked image', + description = 'Blur factor to the baked image', default=1.0, min=0.0, max=100.0 ) + blur_size : FloatProperty( + name = 'Blur Size', + description = 'Blur size (in pixels) to the baked image', + default=10.0, min=0.0 + ) + duplicate_entity : BoolProperty( name = 'Duplicate Entity', description = 'Duplicate entity', @@ -2352,6 +969,9 @@ def invoke(self, context, event): self.mask = None self.index = int(m1.group(1)) self.entities = yp.layers + + # NOTE: Duplicate entity currently doesn't work on layer so make sure to disable it + self.duplicate_entity = False elif m2: self.layer = yp.layers[int(m2.group(1))] self.mask = self.layer.masks[int(m2.group(2))] @@ -2360,25 +980,23 @@ def invoke(self, context, event): else: return self.execute(context) + if self.mask: source_tree = get_mask_tree(self.mask) + else: source_tree = get_source_tree(self.layer) + baked_source = source_tree.nodes.get(self.entity.baked_source) + overwrite_image = None - if self.mask: - mask_tree = get_mask_tree(self.mask) - baked_source = mask_tree.nodes.get(self.mask.baked_source) - if baked_source and baked_source.image: - overwrite_image = baked_source.image + if baked_source and baked_source.image: + overwrite_image = baked_source.image - if overwrite_image: - self.name = overwrite_image.name - else: - self.name = self.mask.name - if not self.name.endswith(' Image'): - self.name += ' Image' - self.name = get_unique_name(self.name, self.layer.masks) + if overwrite_image and not overwrite_image.yia.is_image_atlas and not overwrite_image.yua.is_udim_atlas: + self.name = overwrite_image.name else: - self.name = self.layer.name - if not self.name.endswith(' Image'): - self.name += ' Image' - self.name = get_unique_name(self.name, yp.layers) + name = node.node_tree.name.replace(get_addon_title()+' ', '') + self.name = name + ' ' + self.entity.name + #if not self.name.endswith(' Image'): + # self.name += ' Image' + + self.name = get_unique_name(self.name, bpy.data.images) # Use active uv layer name by default uv_layers = get_uv_layers(obj) @@ -2429,16 +1047,19 @@ def invoke(self, context, event): if len(self.uv_map_coll) > 0: self.uv_map = self.uv_map_coll[0].name + if self.entity.type in {'EDGE_DETECT', 'HEMI', 'AO'}: + #self.hdr = True + self.fxaa = False + else: + #self.hdr = False + self.fxaa = True + # Auto set some props for some types - if self.entity.type == 'EDGE_DETECT': + if self.entity.type in {'EDGE_DETECT', 'AO'}: self.samples = 32 - self.hdr = True - self.fxaa = False self.denoise = True else: self.samples = 1 - self.hdr = False - self.fxaa = True self.denoise = False if get_user_preferences().skip_property_popups and not event.shift: @@ -2482,6 +1103,14 @@ def draw(self, context): col.label(text='Bake Device:') col.separator() col.label(text='') + + if self.blur: + if self.blur_type == 'NOISE': + col.label(text='Blur Factor:') + else: col.label(text='Blur Size:') + col.separator() + + col.label(text='') if is_bl_newer_than(2, 81): col.label(text='') col.label(text='') @@ -2513,19 +1142,27 @@ def draw(self, context): col.separator() col.prop(self, 'bake_device', text='') col.separator() - col.prop(self, 'fxaa') - if is_bl_newer_than(2, 81): - col.prop(self, 'denoise', text='Use Denoise') - ccol = col.column(align=True) rrow = col.row(align=True) rrow.prop(self, 'blur') if self.blur: - rrow.prop(self, 'blur_factor', text='') + rrow.prop(self, 'blur_type', text='') + + rrow = col.row(align=True) + if self.blur_type == 'NOISE': + rrow.prop(self, 'blur_factor', text='') + else: rrow.prop(self, 'blur_size', text='') + + col.separator() + + col.prop(self, 'fxaa') + if is_bl_newer_than(2, 81): + col.prop(self, 'denoise', text='Use Denoise') + ccol = col.column(align=True) if self.mask: col.prop(self, 'duplicate_entity', text='Duplicate Mask') - else: col.prop(self, 'duplicate_entity', text='Disable Layer') + #else: col.prop(self, 'duplicate_entity', text='Disable Layer') if self.duplicate_entity: if self.mask: col.prop(self, 'disable_current', text='Disable Current Mask') @@ -2533,45 +1170,6 @@ def draw(self, context): col.prop(self, 'use_image_atlas') - def get_image_atlas_segment(self, context): - yp = self.yp - - segment = None - - # Create new segment - if self.use_image_atlas: - - if self.use_udim: - segment = UDIM.get_set_udim_atlas_segment( - self.tilenums, color=(0, 0, 0, 1), colorspace=get_noncolor_name(), hdr=self.hdr, yp=yp - ) - else: - # Clearing unused image atlas segments - img_atlas = ImageAtlas.check_need_of_erasing_segments(yp, 'BLACK', self.width, self.height, self.hdr) - if img_atlas: ImageAtlas.clear_unused_segments(img_atlas.yia) - - segment = ImageAtlas.get_set_image_atlas_segment( - self.width, self.height, 'BLACK', self.hdr, yp=yp - ) - - ia_image = segment.id_data - - # Set baked image to segment - if self.use_udim: - offset = get_udim_segment_mapping_offset(segment) * 10 - copy_dict = {} - for tilenum in self.tilenums: - copy_dict[tilenum] = tilenum + offset - UDIM.copy_tiles(self.image, ia_image, copy_dict) - else: copy_image_pixels(self.image, ia_image, segment) - temp_img = self.image - self.image = ia_image - - # Remove original baked image - remove_datablock(bpy.data.images, temp_img) - - return segment - def execute(self, context): if not self.is_cycles_exist(context): return {'CANCELLED'} @@ -2579,65 +1177,38 @@ def execute(self, context): self.report({'ERROR'}, "Invalid context!") return {'CANCELLED'} - if self.layer and not self.mask: - self.report({'ERROR'}, "This feature is not implemented yet!") - return {'CANCELLED'} + #if self.layer and not self.mask: + # self.report({'ERROR'}, "This feature is not implemented yet!") + # return {'CANCELLED'} if self.uv_map == '': self.report({'ERROR'}, "UV Map cannot be empty!") return {'CANCELLED'} T = time.time() - mat = get_active_material() node = get_active_ypaint_node() self.yp = yp = node.node_tree.yp tree = node.node_tree layer_tree = get_tree(self.layer) - objs = [context.object] - meshes = [context.object.data] - - if mat.users > 1: - for ob in get_scene_objects(): - if ob.type != 'MESH': continue - if hasattr(ob, 'hide_viewport') and ob.hide_viewport: continue - if len(get_uv_layers(ob)) == 0: continue - if len(ob.data.polygons) == 0: continue - - # Do not bake objects with hide_render on - if ob.hide_render: continue - if not in_renderable_layer_collection(ob): continue - - for i, m in enumerate(ob.data.materials): - if m == mat: - ob.active_material_index = i - if ob not in objs and ob.data not in meshes: - objs.append(ob) - meshes.append(ob.data) - - if not objs: - self.report({'ERROR'}, "No valid objects found to bake!") - return {'CANCELLED'} - # Entity checking entity = self.mask if self.mask else self.layer + entity_label = mask_type_labels[entity.type] if self.mask else layer_type_labels[entity.type] + + # Get bake properties + bprops = get_bake_properties_from_self(self) - # Get tile numbers - self.tilenums = [1001] - if self.use_udim: - self.tilenums = UDIM.get_tile_numbers(objs, self.uv_map) + # Bake entity to image + rdict = bake_entity_as_image(entity, bprops, set_image_to_entity=not self.duplicate_entity) - self.image = bake_as_image( - objs, mat, entity, self.name, width=self.width, height=self.height, hdr=self.hdr, - samples=self.samples, margin=self.margin, uv_name=self.uv_map, bake_device=self.bake_device, - use_udim=self.use_udim, tilenums=self.tilenums, fxaa=self.fxaa, - blur=self.blur, blur_factor=self.blur_factor, denoise=self.denoise, - disable_modifiers=not self.duplicate_entity, margin_type=self.margin_type - ) + if rdict['message'] != '': + self.report({'ERROR'}, rdict['message']) + return {'CANCELLED'} - # Get segment - segment = self.get_image_atlas_segment(context) + image = rdict['image'] + segment = rdict['segment'] + # Duplicate entity if self.duplicate_entity: if self.mask: @@ -2647,10 +1218,10 @@ def execute(self, context): self.mask.enable = False # New entity name - new_entity_name = get_unique_name(self.name, self.entities) if self.use_image_atlas else self.image.name + new_entity_name = get_unique_name(self.name, self.entities) if self.use_image_atlas else image.name # Create new mask - mask = Mask.add_new_mask(self.layer, new_entity_name, 'IMAGE', 'UV', self.uv_map, self.image, None, segment) + mask = Mask.add_new_mask(self.layer, new_entity_name, 'IMAGE', 'UV', self.uv_map, image, '', segment) # Set mask properties mask.intensity_value = self.mask.intensity_value @@ -2665,7 +1236,7 @@ def execute(self, context): mask = self.layer.masks[self.index+1] if segment: - ImageAtlas.set_segment_mapping(mask, segment, self.image) + ImageAtlas.set_segment_mapping(mask, segment, image) mask.segment_name = segment.name # Refresh uv @@ -2680,92 +1251,12 @@ def execute(self, context): else: # TODO: Duplicate layer as image(s) pass - else: - if self.mask: - yp.halt_update = True - - mask = self.mask - mask_tree = get_mask_tree(mask) - - baked_source = mask_tree.nodes.get(mask.baked_source) - if baked_source: - overwrite_image = baked_source.image - overwrite_image_name = overwrite_image.name - - # Remove old segment - if mask.baked_segment_name != '': - if overwrite_image.yia.is_image_atlas: - old_segment = overwrite_image.yia.segments.get(mask.baked_segment_name) - old_segment.unused = True - elif overwrite_image.yua.is_udim_atlas: - UDIM.remove_udim_atlas_segment_by_name(overwrite_image, mask.baked_segment_name, yp=yp) - - # Remove node first to also remove its data - remove_node(mask_tree, mask, 'baked_source') - - # Rename image if it's not image atlas - if mask.baked_segment_name == '' and overwrite_image_name == self.name: - self.image.name = self.name - - # Remove baked segment name since the data is removed - if mask.baked_segment_name != '': - entity.baked_segment_name = '' - - # Set bake info to image/segment - bi = segment.bake_info if segment else self.image.y_bake_info - - bi.is_baked = True - for attr in dir(bi): - #if attr in dir(self): - if attr.startswith('__'): continue - if attr.startswith('bl_'): continue - if attr in {'rna_type'}: continue - try: setattr(bi, attr, getattr(self, attr)) - except: pass - - # Create new node - baked_source = new_node(mask_tree, mask, 'baked_source', 'ShaderNodeTexImage', 'Baked Mask Source') - - # Set image to baked node - baked_source.image = self.image - - # Set mask props - mask.baked_uv_name = self.uv_map - mask.use_baked = True - - yp.halt_update = False - - else: - # TODO: Bake layer as image(s) - pass - - if segment: - # Set up baked mapping - mapping = check_new_node(layer_tree, entity, 'baked_mapping', 'ShaderNodeMapping', 'Baked Mapping') - clear_mapping(entity, use_baked=True) - ImageAtlas.set_segment_mapping(entity, segment, self.image, use_baked=True) - - # Set baked segment name to entity - entity.baked_segment_name = segment.name - else: - remove_node(layer_tree, entity, 'baked_mapping') - - # Refresh uv - refresh_temp_uv(context.object, entity) - - # Refresh Neighbor UV resolution - set_uv_neighbor_resolution(entity) - - # Update global uv - check_uv_nodes(yp) - # Update layer tree inputs - check_all_layer_channel_io_and_nodes(self.layer) - check_start_end_root_ch_nodes(node.node_tree) - - # Make current mask active + # Make current entity active to update image if self.mask: self.mask.active_edit = True + elif get_layer_index(self.layer) == yp.active_layer_index: + yp.active_layer_index = yp.active_layer_index reconnect_layer_nodes(self.layer) rearrange_layer_nodes(self.layer) @@ -2773,10 +1264,20 @@ def execute(self, context): reconnect_yp_nodes(node.node_tree) rearrange_yp_nodes(node.node_tree) + # Expand entity source to show rebake button + ypui = context.window_manager.ypui + if self.mask and not self.duplicate_entity: + self.mask.expand_content = True + self.mask.expand_source = True + ypui.need_update = True + + if image: + self.report({'INFO'}, 'Baking '+entity_label+' is done in '+'{:0.2f}'.format(time.time() - T)+' seconds!') + return {"FINISHED"} class YRemoveBakedEntity(bpy.types.Operator): - bl_idname = "node.y_remove_baked_entity" + bl_idname = "wm.y_remove_baked_entity" bl_label = "Remove Baked Layer/Mask" bl_description = "Remove baked layer/mask" bl_options = {'REGISTER', 'UNDO'} @@ -2843,12 +1344,134 @@ def execute(self, context): return {'FINISHED'} +class YRebakeBakedImages(bpy.types.Operator, BaseBakeOperator): + bl_idname = "wm.y_rebake_baked_images" + bl_label = "Rebake All Baked Images" + bl_description = "Rebake all baked images used by all layers and masks" + bl_options = {'REGISTER'} + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() and context.object.type == 'MESH' + + def invoke(self, context, event): + if get_user_preferences().skip_property_popups and not event.shift: + return self.execute(context) + + return context.window_manager.invoke_props_dialog(self, width=320) + + def draw(self, context): + self.layout.label(text='Rebaking all baked images can take a while to process', icon='ERROR') + + def execute(self, context): + T = time.time() + node = get_active_ypaint_node() + yp = node.node_tree.yp + + baked_counts = rebake_baked_images(yp) + + if baked_counts == 0: + self.report({'ERROR'}, 'No baked layer/mask used!') + return {'CANCELLED'} + + self.report({'INFO'}, 'Rebaking all baked layers & masks is done in '+'{:0.2f}'.format(time.time() - T)+' seconds!') + return {'FINISHED'} + +class YRebakeSpecificLayers(bpy.types.Operator, BaseBakeOperator): + bl_idname = "wm.y_rebake_specific_layers" + bl_label = "Rebake Specific Layers" + bl_description = "Rebake specific layers (FOR INTERNAL USE ONLY)" + bl_options = {'REGISTER'} + + layer_ids : StringProperty( + name = 'Layer Indices', + description = 'Layer indices in form of list of integer converted to string', + default='' + ) + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() and context.object.type == 'MESH' + + def execute(self, context): + T = time.time() + node = get_active_ypaint_node() + yp = node.node_tree.yp + + layers = [] + if self.layer_ids != '': + import ast + ids = ast.literal_eval(self.layer_ids) + layers = [l for i, l in enumerate(yp.layers) if i in ids] + + rebake_baked_images(yp, specific_layers=layers) + + return {'FINISHED'} + +class YSelectAllOtherObjects(bpy.types.Operator): + bl_idname = "wm.y_select_all_other_objects" + bl_label = "Select All Other Objects" + bl_description = "Select all objects in the other objects list" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() and hasattr(context, 'bake_info') + + def execute(self, context): + bpy.ops.object.select_all(action='DESELECT') + + for i in range(len(context.bake_info.other_objects)): + so = context.bake_info.other_objects[i] + if so.object: + so_object = so.object + so_object.hide_viewport = False + set_object_select(so_object, True) + if so_object: + set_active_object(so_object) + + return {'FINISHED'} + +class YToggleOtherObjectsVisibility(bpy.types.Operator): + bl_idname = "wm.y_toggle_other_objects_visibility" + bl_label = "Toggle Other Objects Visibility" + bl_description = "Toggle visibility of all objects in the other objects list" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() and hasattr(context, 'bake_info') + + def execute(self, context): + bi = context.bake_info + + reference_obj = None + for oo in bi.other_objects: + if oo.object: + reference_obj = oo.object + break + + if not reference_obj: + return {'CANCELLED'} + + current_hidden_state = reference_obj.hide_viewport + + for oo in bi.other_objects: + if oo.object: + oo.object.hide_viewport = not current_hidden_state + + return {'FINISHED'} + def register(): bpy.utils.register_class(YBakeToLayer) bpy.utils.register_class(YBakeEntityToImage) bpy.utils.register_class(YRemoveBakeInfoOtherObject) bpy.utils.register_class(YTryToSelectBakedVertexSelect) bpy.utils.register_class(YRemoveBakedEntity) + bpy.utils.register_class(YRebakeBakedImages) + bpy.utils.register_class(YRebakeSpecificLayers) + bpy.utils.register_class(YSelectAllOtherObjects) + bpy.utils.register_class(YToggleOtherObjectsVisibility) def unregister(): bpy.utils.unregister_class(YBakeToLayer) @@ -2856,3 +1479,7 @@ def unregister(): bpy.utils.unregister_class(YRemoveBakeInfoOtherObject) bpy.utils.unregister_class(YTryToSelectBakedVertexSelect) bpy.utils.unregister_class(YRemoveBakedEntity) + bpy.utils.unregister_class(YRebakeBakedImages) + bpy.utils.unregister_class(YRebakeSpecificLayers) + bpy.utils.unregister_class(YSelectAllOtherObjects) + bpy.utils.unregister_class(YToggleOtherObjectsVisibility) diff --git a/BaseOperator.py b/BaseOperator.py new file mode 100644 index 00000000..4a3c4a20 --- /dev/null +++ b/BaseOperator.py @@ -0,0 +1,43 @@ +import bpy +from bpy.props import * +from .common import * + +class FileSelectOptions(): + # File browser filter + filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) + filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) + + display_type : EnumProperty( + items = ( + ('FILE_DEFAULTDISPLAY', 'Default', ''), + ('FILE_SHORTDISLPAY', 'Short List', ''), + ('FILE_LONGDISPLAY', 'Long List', ''), + ('FILE_IMGDISPLAY', 'Thumbnails', '') + ), + default = 'FILE_IMGDISPLAY', + options = {'HIDDEN', 'SKIP_SAVE'} + ) + +class OpenImage(FileSelectOptions): + + # File related + files : CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) + directory : StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) + + relative : BoolProperty(name="Relative Path", default=True, description="Apply relative paths") + + def running_fileselect_modal(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + + def check(self, context): + return True + + def generate_paths(self): + return (fn.name for fn in self.files), self.directory + + def get_loaded_images(self): + import_list, directory = self.generate_paths() + loaded_images = tuple(load_image(path, directory) for path in import_list) + + return loaded_images diff --git a/Decal.py b/Decal.py new file mode 100644 index 00000000..ac9cc550 --- /dev/null +++ b/Decal.py @@ -0,0 +1,353 @@ +import bpy, re +from . import lib +from bpy.props import * +from bpy.app.handlers import persistent +from .common import * + +def get_decal_object(entity): + m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + + if m1: tree = get_tree(entity) + elif m2: tree = get_mask_tree(entity) + else: return None + + decal_obj = None + texcoord = tree.nodes.get(entity.texcoord) + if texcoord and hasattr(texcoord, 'object'): decal_obj = texcoord.object + + return decal_obj + +def get_decal_shrinkwrap_constraint(decal_obj): + cs = [c for c in decal_obj.constraints if c.type == 'SHRINKWRAP'] + if len(cs) > 0: return cs[0] + return None + +def any_decal_inside_layer(layer): + if layer.texcoord_type == 'Decal': + return True + + for mask in layer.masks: + if mask.texcoord_type == 'Decal': + return True + + return False + +def remove_decal_object(tree, entity): + if not tree: return + # NOTE: This will remove the texcoord object even if the entity is not using decal + #if entity.texcoord_type == 'Decal': + texcoord = tree.nodes.get(entity.texcoord) + if texcoord and hasattr(texcoord, 'object') and texcoord.object: + decal_obj = texcoord.object + if decal_obj.type == 'EMPTY' and decal_obj.users <= 2: + texcoord.object = None + remove_datablock(bpy.data.objects, decal_obj) + +def update_enable_decal_object_constraint(self, context): + obj = context.object + decal_obj = self.id_data + decal_const = get_decal_shrinkwrap_constraint(decal_obj) + + if self.enable_shrinkwrap: + if not decal_const and obj: + c = decal_obj.constraints.new('SHRINKWRAP') + c.target = obj + if is_bl_newer_than(2, 80): + c.use_track_normal = True + c.track_axis = 'TRACK_Z' + else: + if decal_const: + decal_obj.constraints.remove(decal_const) + +def create_decal_empty(): + obj = bpy.context.object + scene = bpy.context.scene + empty_name = get_unique_name('Decal', bpy.data.objects) + empty = bpy.data.objects.new(empty_name, None) + if is_bl_newer_than(2, 80): + empty.empty_display_type = 'SINGLE_ARROW' + else: empty.empty_draw_type = 'SINGLE_ARROW' + custom_collection = obj.users_collection[0] if is_bl_newer_than(2, 80) and len(obj.users_collection) > 0 else None + link_object(scene, empty, custom_collection) + if is_bl_newer_than(2, 80): + empty.location = scene.cursor.location.copy() + empty.rotation_euler = scene.cursor.rotation_euler.copy() + else: + empty.location = scene.cursor_location.copy() + + # Parent empty to active object + empty.parent = obj + empty.matrix_parent_inverse = obj.matrix_world.inverted() + + return empty + +def check_entity_decal_nodes(entity, tree=None): + yp = entity.id_data.yp + m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + + if m1: + entity_enabled = get_layer_enabled(entity) + source = get_layer_source(entity) + if not tree: tree = get_tree(entity) + layer = entity + mask = None + elif m2: + entity_enabled = get_mask_enabled(entity) + source = get_mask_source(entity) + layer = yp.layers[int(m2.group(1))] + if not tree: tree = get_tree(entity) + mask = entity + else: return + + # Get height channel + height_ch = get_height_channel(layer) + + # Create texcoord node if decal is used + texcoord = tree.nodes.get(entity.texcoord) + if entity_enabled and entity.texcoord_type == 'Decal' and is_mapping_possible(entity.type): + + # Set image extension type to clip + image = None + if entity.type == 'IMAGE' and source: + image = source.image + + # Create new empty object if there's no texcoord yet + if not texcoord: + empty = create_decal_empty() + texcoord = new_node(tree, entity, 'texcoord', 'ShaderNodeTexCoord', 'TexCoord') + texcoord.object = empty + + decal_process = tree.nodes.get(entity.decal_process) + if not decal_process: + decal_process = new_node(tree, entity, 'decal_process', 'ShaderNodeGroup', 'Decal Process') + decal_process.node_tree = get_node_tree_lib(lib.DECAL_PROCESS) + + # Set image extension only after decal process node is initialized + if image and source: + entity.original_image_extension = source.extension + source.extension = 'CLIP' + + # Set decal aspect ratio + if image and image.size[0] > 0 and image.size[1] > 0: + if image.size[0] > image.size[1]: + decal_process.inputs['Scale'].default_value = (image.size[1] / image.size[0], 1.0, 1.0) + else: decal_process.inputs['Scale'].default_value = (1.0, image.size[0] / image.size[1], 1.0) + + # Create decal alpha nodes + if mask: + + # Check if height channel is enabled + height_root_ch = get_root_height_channel(yp) + height_ch_enabled = get_channel_enabled(height_ch) if height_ch else False + + decal_alpha = check_new_node(tree, mask, 'decal_alpha', 'ShaderNodeMath', 'Decal Alpha') + if decal_alpha.operation != 'MULTIPLY': + decal_alpha.operation = 'MULTIPLY' + + if height_ch and height_ch_enabled and height_root_ch.enable_smooth_bump: + for letter in nsew_letters: + decal_alpha = check_new_node(tree, mask, 'decal_alpha_' + letter, 'ShaderNodeMath', 'Decal Alpha ' + letter.upper()) + if decal_alpha.operation != 'MULTIPLY': + decal_alpha.operation = 'MULTIPLY' + else: + for letter in nsew_letters: + remove_node(tree, mask, 'decal_alpha_' + letter) + + else: + + for i, ch in enumerate(layer.channels): + root_ch = yp.channels[i] + ch_enabled = get_channel_enabled(ch) + if ch_enabled: + decal_alpha = check_new_node(tree, ch, 'decal_alpha', 'ShaderNodeMath', 'Decal Alpha') + if decal_alpha.operation != 'MULTIPLY': + decal_alpha.operation = 'MULTIPLY' + else: + remove_node(tree, ch, 'decal_alpha') + + if root_ch.type == 'NORMAL': + if ch_enabled and root_ch.enable_smooth_bump: + for letter in nsew_letters: + decal_alpha = check_new_node(tree, ch, 'decal_alpha_' + letter, 'ShaderNodeMath', 'Decal Alpha ' + letter.upper()) + if decal_alpha.operation != 'MULTIPLY': + decal_alpha.operation = 'MULTIPLY' + else: + for letter in nsew_letters: + remove_node(tree, ch, 'decal_alpha_' + letter) + else: + + if not texcoord or not hasattr(texcoord, 'object') or not texcoord.object: + remove_node(tree, entity, 'texcoord') + remove_node(tree, entity, 'decal_process') + + if mask: + remove_node(tree, mask, 'decal_alpha') + + if height_ch: + for letter in nsew_letters: + remove_node(tree, mask, 'decal_alpha_' + letter) + else: + for i, ch in enumerate(layer.channels): + root_ch = yp.channels[i] + remove_node(tree, ch, 'decal_alpha') + + if root_ch.type == 'NORMAL': + for letter in nsew_letters: + remove_node(tree, ch, 'decal_alpha_' + letter) + + # Recover image extension type + if entity.type == 'IMAGE' and entity.original_texcoord == 'Decal' and entity.original_image_extension != '': + source = get_mask_source(mask) if mask else get_layer_source(layer) + if source: + source.extension = entity.original_image_extension + entity.original_image_extension = '' + + # Save original texcoord type + if entity.original_texcoord != entity.texcoord_type: + entity.original_texcoord = entity.texcoord_type + +class YSelectDecalObject(bpy.types.Operator): + bl_idname = "wm.y_select_decal_object" + bl_label = "Select Decal Object" + bl_description = "Select Decal Object" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + group_node = get_active_ypaint_node() + return group_node and hasattr(context, 'entity') + + def execute(self, context): + scene = context.scene + + decal_obj = get_decal_object(context.entity) + if decal_obj: + try: bpy.ops.object.mode_set(mode='OBJECT') + except: pass + bpy.ops.object.select_all(action='DESELECT') + if decal_obj.name not in get_scene_objects(): + parent = decal_obj.parent + custom_collection = parent.users_collection[0] if is_bl_newer_than(2, 80) and parent and len(parent.users_collection) > 0 else None + link_object(scene, decal_obj, custom_collection) + set_active_object(decal_obj) + set_object_select(decal_obj, True) + else: return {'CANCELLED'} + + return {'FINISHED'} + +class YSetDecalObjectPositionToCursor(bpy.types.Operator): + bl_idname = "wm.y_set_decal_object_position_to_sursor" + bl_label = "Set Decal Position to Cursor" + bl_description = "Set the position of the decal object to the 3D cursor" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + group_node = get_active_ypaint_node() + return group_node and hasattr(context, 'entity') + + def execute(self, context): + scene = bpy.context.scene + entity = context.entity + + m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + + if m1: tree = get_tree(entity) + elif m2: tree = get_mask_tree(entity) + else: return {'CANCELLED'} + + texcoord = tree.nodes.get(entity.texcoord) + + if texcoord and hasattr(texcoord, 'object') and texcoord.object: + # Move decal object to 3D cursor + if is_bl_newer_than(2, 80): + texcoord.object.location = scene.cursor.location.copy() + texcoord.object.rotation_euler = scene.cursor.rotation_euler.copy() + else: + texcoord.object.location = scene.cursor_location.copy() + + else: return {'CANCELLED'} + + return {'FINISHED'} + +class BaseDecal(): + + decal_distance_value : FloatProperty( + name = 'Decal Distance', + description = 'Distance between surface and the decal object', + min=0.0, max=100.0, default=0.5, precision=3 + ) + + original_texcoord : EnumProperty( + name = 'Original Texture Coordinate Type', + items = texcoord_type_items, + default = 'UV' + ) + + original_image_extension : StringProperty( + name = 'Original Image Extension Type', + default = '' + ) + +class YPaintDecalObjectProps(bpy.types.PropertyGroup): + enable_shrinkwrap : BoolProperty( + name = 'Enable Decal Shrinkwrap Constraint', + description = 'Enable shrinkwrap constraint, so decal object always follow the target object', + default = False, + update = update_enable_decal_object_constraint + ) + + last_operator : StringProperty(default='') + last_operator_pointer : StringProperty(default='') + +@persistent +def ypaint_decal_constraint_update(scene): + wm = bpy.context.window_manager + if len(wm.operators) == 0: return + op = wm.operators[-1] + if not op.bl_idname.startswith('TRANSFORM_OT_'): return + + for obj in bpy.context.selected_objects: + if not obj.yp_decal.enable_shrinkwrap: continue + + # Get constraint + c = get_decal_shrinkwrap_constraint(obj) + if not c or c.mute: continue + + if obj.yp_decal.last_operator != op.bl_idname or obj.yp_decal.last_operator_pointer != str(op.as_pointer()): + obj.yp_decal.last_operator = op.bl_idname + obj.yp_decal.last_operator_pointer = str(op.as_pointer()) + + # Apply the constraint after transforming + mat = obj.matrix_world.copy() + try: + c.mute = True + obj.matrix_world = mat + c.mute = False + except Exception as e: print('EXCEPTIION:', e) + +def register(): + bpy.utils.register_class(YSelectDecalObject) + bpy.utils.register_class(YSetDecalObjectPositionToCursor) + bpy.utils.register_class(YPaintDecalObjectProps) + + # YPaint Props + bpy.types.Object.yp_decal = PointerProperty(type=YPaintDecalObjectProps) + + # Handlers + if is_bl_newer_than(2, 80): + bpy.app.handlers.depsgraph_update_post.append(ypaint_decal_constraint_update) + else: bpy.app.handlers.scene_update_pre.append(ypaint_decal_constraint_update) + +def unregister(): + bpy.utils.unregister_class(YSelectDecalObject) + bpy.utils.unregister_class(YSetDecalObjectPositionToCursor) + bpy.utils.unregister_class(YPaintDecalObjectProps) + + # Handlers + if is_bl_newer_than(2, 80): + bpy.app.handlers.depsgraph_update_post.remove(ypaint_decal_constraint_update) + else: bpy.app.handlers.scene_update_pre.remove(ypaint_decal_constraint_update) diff --git a/ImageAtlas.py b/ImageAtlas.py index fa451856..8a9f5a4f 100644 --- a/ImageAtlas.py +++ b/ImageAtlas.py @@ -71,8 +71,11 @@ def create_image_atlas(color='BLACK', size=8192, hdr=False, name=''): img.yia.is_image_atlas = True img.yia.color = color #img.yia.float_buffer = hdr - #if hdr: - #img.colorspace_settings.name = get_noncolor_name() + + # Float image atlas always use premultiplied alpha + if hdr and is_bl_newer_than(2, 80): + #img.colorspace_settings.name = get_noncolor_name() + img.alpha_mode = 'PREMUL' return img @@ -216,7 +219,7 @@ def replace_segment_with_image(yp, segment, image, uv_name=''): return entities #class YUVTransformTest(bpy.types.Operator): -# bl_idname = "node.y_uv_transform_test" +# bl_idname = "wm.y_uv_transform_test" # bl_label = "UV Transform Test" # bl_description = "UV Transform Test" # bl_options = {'REGISTER', 'UNDO'} @@ -285,7 +288,7 @@ def set_segment_mapping(entity, segment, image, use_baked=False): mapping.translation[1] = offset_y class YNewImageAtlasSegmentTest(bpy.types.Operator): - bl_idname = "node.y_new_image_atlas_segment_test" + bl_idname = "wm.y_new_image_atlas_segment_test" bl_label = "New Image Atlas Segment Test" bl_description = "New Image Atlas segment test" bl_options = {'REGISTER', 'UNDO'} @@ -389,7 +392,7 @@ def execute(self, context): return {'FINISHED'} class YRefreshTransformedLayerUV(bpy.types.Operator): - bl_idname = "node.y_refresh_transformed_uv" + bl_idname = "wm.y_refresh_transformed_uv" bl_label = "Refresh Layer UV with Custom Transformation" bl_description = "Refresh layer UV with custom transformation" bl_options = {'REGISTER', 'UNDO'} @@ -429,7 +432,7 @@ def execute(self, context): return {'FINISHED'} class YBackToOriginalUV(bpy.types.Operator): - bl_idname = "node.y_back_to_original_uv" + bl_idname = "wm.y_back_to_original_uv" bl_label = "Back to Original UV" bl_description = "Transformed UV detected, your changes will be lost if you edit on this UV.\nClick this button to go back to original UV" bl_options = {'REGISTER', 'UNDO'} @@ -461,10 +464,10 @@ def execute(self, context): uv_layers = get_uv_layers(ob) for uv in uv_layers: - if uv.name == src_of_img.uv_name: + if uv.name == uv_name: - if uv_layers.active != uv_layers.get(src_of_img.uv_name): - uv_layers.active = uv_layers.get(src_of_img.uv_name) + if uv_layers.active != uv_layers.get(uv_name): + uv_layers.active = uv_layers.get(uv_name) if uv.name == TEMP_UV: uv_layers.remove(uv) @@ -485,7 +488,7 @@ def execute(self, context): return {'FINISHED'} class YConvertToImageAtlas(bpy.types.Operator): - bl_idname = "node.y_convert_to_image_atlas" + bl_idname = "wm.y_convert_to_image_atlas" bl_label = "Convert Image to Image Atlas" bl_description = "Convert image to image atlas (useful to avoid material texture limit)" bl_options = {'REGISTER', 'UNDO'} @@ -509,7 +512,7 @@ def execute(self, context): entities, images, segment_names, segment_name_props = get_yp_entities_images_and_segments(yp) else: mapping = get_entity_mapping(context.entity) - if is_transformed(mapping) and not context.entity.use_baked: + if is_transformed(mapping, context.entity) and not context.entity.use_baked: self.report({'ERROR'}, "Cannot convert transformed image!") return {'CANCELLED'} @@ -536,7 +539,7 @@ def execute(self, context): # Transformed mapping on entity is not valid for conversion mapping = get_entity_mapping(entity) - if use_baked or not is_transformed(mapping): + if use_baked or not is_transformed(mapping, entity): valid_entities.append(entity) if not any(valid_entities): @@ -578,6 +581,10 @@ def execute(self, context): #entity.segment_name = new_segment.name setattr(entity, segment_name_props[i][j], new_segment.name) + # Make sure uniform scaling is not used + if entity.enable_uniform_scale: + entity.enable_uniform_scale = False + # Set image to editor if entity == context.entity: update_image_editor_image(bpy.context, ia_image) @@ -597,7 +604,7 @@ def execute(self, context): return {'FINISHED'} class YConvertToStandardImage(bpy.types.Operator): - bl_idname = "node.y_convert_to_standard_image" + bl_idname = "wm.y_convert_to_standard_image" bl_label = "Convert Image Atlas to standard image" bl_description = "Convert image atlas to standard image" bl_options = {'REGISTER', 'UNDO'} @@ -649,6 +656,8 @@ def execute(self, context): name=entities[i][0].name, width=segment.width, height=segment.height, alpha=True, float_buffer=image.is_float ) + if image.is_float and is_bl_newer_than(2, 80): + new_image.alpha_mode = 'PREMUL' else: new_image = bpy.data.images.new( name=entities[i][0].name, width=image.size[0], height=image.size[1], @@ -733,7 +742,7 @@ def execute(self, context): return {'FINISHED'} -class YImageAtlasSegments(bpy.types.PropertyGroup): +class YImageAtlasSegment(bpy.types.PropertyGroup): name : StringProperty( name = 'Name', @@ -772,7 +781,7 @@ class YImageAtlas(bpy.types.PropertyGroup): #float_buffer : BoolProperty(default=False) - segments : CollectionProperty(type=YImageAtlasSegments) + segments : CollectionProperty(type=YImageAtlasSegment) def register(): #bpy.utils.register_class(YUVTransformTest) @@ -783,7 +792,7 @@ def register(): bpy.utils.register_class(YConvertToStandardImage) #bpy.utils.register_class(YImageSegmentOtherObject) #bpy.utils.register_class(YImageSegmentBakeInfoProps) - bpy.utils.register_class(YImageAtlasSegments) + bpy.utils.register_class(YImageAtlasSegment) bpy.utils.register_class(YImageAtlas) bpy.types.Image.yia = PointerProperty(type=YImageAtlas) @@ -797,5 +806,5 @@ def unregister(): bpy.utils.unregister_class(YConvertToStandardImage) #bpy.utils.unregister_class(YImageSegmentOtherObject) #bpy.utils.unregister_class(YImageSegmentBakeInfoProps) - bpy.utils.unregister_class(YImageAtlasSegments) + bpy.utils.unregister_class(YImageAtlasSegment) bpy.utils.unregister_class(YImageAtlas) diff --git a/Layer.py b/Layer.py index 3b36d23e..cf0432f2 100644 --- a/Layer.py +++ b/Layer.py @@ -1,9 +1,9 @@ import bpy, time, re, os, random, numpy from bpy.props import * from bpy_extras.io_utils import ImportHelper -from . import Modifier, lib, Mask, transition, ImageAtlas, UDIM, NormalMapModifier, ListItem +from . import Modifier, lib, Mask, transition, ImageAtlas, UDIM, NormalMapModifier, ListItem, BaseOperator, Decal from .common import * -#from .bake_common import * +#from . import bake_common from .node_arrangements import * from .node_connections import * from .subtree import * @@ -14,16 +14,16 @@ DEFAULT_NEW_VDM_SUFFIX = ' VDM' def channel_items(self, context): - node = get_active_ypaint_node() - yp = node.node_tree.yp - items = [] - for i, ch in enumerate(yp.channels): - # Add two spaces to prevent text from being translated - text_ch_name = ch.name + ' ' - icon_name = lib.channel_custom_icon_dict[ch.type] - items.append((str(i), text_ch_name, '', lib.get_icon(icon_name), i)) + node = get_active_ypaint_node() + if node: + yp = node.node_tree.yp + for i, ch in enumerate(yp.channels): + # Add two spaces to prevent text from being translated + text_ch_name = ch.name + ' ' + icon_name = lib.channel_custom_icon_dict[ch.type] + items.append((str(i), text_ch_name, '', lib.get_icon(icon_name), i)) items.append(('-1', 'All Channels', '', lib.get_icon('channels'), len(items))) @@ -54,10 +54,12 @@ def add_new_layer( mask_texcoord_type='UV', mask_color='BLACK', mask_use_hdr=False, mask_uv_name='', mask_width=1024, mask_height=1024, use_image_atlas_for_mask=False, hemi_space='WORLD', hemi_use_prev_normal=True, - mask_color_id=(1, 0, 1), mask_vcol_data_type='BYTE_COLOR', mask_vcol_domain='CORNER', + mask_color_id=(1, 0, 1), mask_vcol_fill=True, + mask_vcol_data_type='BYTE_COLOR', mask_vcol_domain='CORNER', use_divider_alpha=False, use_udim_for_mask=False, interpolation='Linear', mask_interpolation='Linear', mask_edge_detect_radius=0.05, - normal_space = 'TANGENT' + normal_space = 'TANGENT', edge_detect_radius=0.05, mask_use_prev_normal=True, + ao_distance=1.0 ): yp = group_tree.yp @@ -79,9 +81,11 @@ def add_new_layer( # Get a possible parent layer group parent_layer = None + active_layer_is_group = False if active_layer: if active_layer.type == 'GROUP': parent_layer = active_layer + active_layer_is_group = True elif active_layer.parent_idx != -1: parent_layer = yp.layers[active_layer.parent_idx] @@ -97,6 +101,11 @@ def add_new_layer( layer = yp.layers.add() layer.type = layer_type layer.name = get_unique_name(layer_name, yp.layers) + + # Set default uv name if it's an empty string + if uv_name == '': + uv_name = get_default_uv_name() + layer.uv_name = uv_name check_uvmap_on_other_objects_with_same_mat(mat, uv_name) @@ -108,7 +117,7 @@ def add_new_layer( # Move new layer to current index last_index = len(yp.layers)-1 - if active_layer and active_layer.type == 'GROUP': + if active_layer_is_group: index = yp.active_layer_index + 1 else: index = yp.active_layer_index @@ -151,8 +160,8 @@ def add_new_layer( if layer_type == 'IMAGE': # Always set non color to image node because of linear pipeline - if hasattr(source, 'color_space'): - source.color_space = 'NONE' + #if hasattr(source, 'color_space'): + # source.color_space = 'NONE' # Add new image if it's image layer source.image = image @@ -176,6 +185,15 @@ def add_new_layer( layer.hemi_space = hemi_space layer.hemi_use_prev_normal = hemi_use_prev_normal + elif layer_type == 'EDGE_DETECT': + layer.hemi_use_prev_normal = hemi_use_prev_normal + Mask.setup_edge_detect_source(layer, source, edge_detect_radius) + + elif layer_type == 'AO': + layer.hemi_use_prev_normal = hemi_use_prev_normal + layer.ao_distance = ao_distance + enable_eevee_ao() + # Add texcoord node #texcoord = new_node(tree, layer, 'texcoord', 'NodeGroupInput', 'TexCoord Inputs') @@ -202,7 +220,7 @@ def add_new_layer( #mask_name = 'Mask ' + layer.name mask_name = Mask.get_new_mask_name(obj, layer, mask_type) mask_image = None - mask_vcol = None + mask_vcol_name = '' mask_segment = None if mask_type == 'IMAGE': @@ -280,21 +298,32 @@ def add_new_layer( for o in objs: if mask_name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue - mask_vcol = new_vertex_color(o, mask_name, mask_vcol_data_type, mask_vcol_domain) - if mask_color == 'WHITE': - set_obj_vertex_colors(o, mask_vcol.name, (1.0, 1.0, 1.0, 1.0)) - elif mask_color == 'BLACK': - set_obj_vertex_colors(o, mask_vcol.name, (0.0, 0.0, 0.0, 1.0)) + + color = () + if mask_color == 'WHITE': color = (1.0, 1.0, 1.0, 1.0) + elif mask_color == 'BLACK': color = (0.0, 0.0, 0.0, 1.0) + + mask_vcol = new_vertex_color(o, mask_name, mask_vcol_data_type, mask_vcol_domain, color_fill=color) set_active_vertex_color(o, mask_vcol) + mask_vcol_name = mask_vcol.name + + # Fill selected geometry if in edit mode + if mask_vcol_fill and bpy.context.mode == 'EDIT_MESH': + bpy.ops.mesh.y_vcol_fill(color_option='WHITE') elif mask_type == 'COLOR_ID': - check_colorid_vcol(objs) + check_colorid_vcol(objs, set_as_active=True) + + # Fill selected geometry if in edit mode + if mask_vcol_fill and bpy.context.mode == 'EDIT_MESH': + bpy.ops.mesh.y_vcol_fill_face_custom(color=(mask_color_id[0], mask_color_id[1], mask_color_id[2], 1.0)) mask = Mask.add_new_mask( layer, mask_name, mask_type, mask_texcoord_type, - mask_uv_name, mask_image, mask_vcol, mask_segment, + mask_uv_name, mask_image, mask_vcol_name, mask_segment, interpolation=mask_interpolation, color_id=mask_color_id, - edge_detect_radius=mask_edge_detect_radius + edge_detect_radius=mask_edge_detect_radius, + hemi_use_prev_normal=mask_use_prev_normal ) mask.active_edit = True @@ -362,7 +391,7 @@ def add_new_layer( class YRefreshNeighborUV(bpy.types.Operator): """Refresh Neighbor UV""" - bl_idname = "node.y_refresh_neighbor_uv" + bl_idname = "wm.y_refresh_neighbor_uv" bl_label = "Refresh Neighbor UV" bl_options = {'REGISTER', 'UNDO'} @@ -376,7 +405,7 @@ def execute(self, context): class YUseLinearColorSpace(bpy.types.Operator): """This addon need to linear color space image to works properly""" - bl_idname = "node.y_use_linear_color_space" + bl_idname = "wm.y_use_linear_color_space" bl_label = "Use Linear Color Space" bl_options = {'REGISTER', 'UNDO'} @@ -391,9 +420,9 @@ def execute(self, context): return {'FINISHED'} class YNewVcolToOverrideChannel(bpy.types.Operator): - bl_idname = "node.y_new_vcol_to_override_channel" - bl_label = "New Vertex Color To Override Channel Layer" - bl_description = "New Vertex Color To Override Channel Layer" + bl_idname = "wm.y_new_vcol_to_override_channel" + bl_label = "New Vertex Color to Layer Channel Source" + bl_description = "New Vertex Color to custom layer channel source" bl_options = {'UNDO'} name : StringProperty(default='') @@ -489,8 +518,7 @@ def execute(self, context): for o in objs: if self.name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue - vcol = new_vertex_color(o, self.name, self.data_type, self.domain) - set_obj_vertex_colors(o, vcol.name, (1.0, 1.0, 1.0, 1.0)) + vcol = new_vertex_color(o, self.name, self.data_type, self.domain, color_fill=(1.0, 1.0, 1.0, 1.0)) set_active_vertex_color(o, vcol) # Update vcol cache @@ -553,7 +581,7 @@ def update_channel_idx_new_layer(self, context): self.interpolation = 'Cubic' class YNewVDMLayer(bpy.types.Operator): - bl_idname = "node.y_new_vdm_layer" + bl_idname = "wm.y_new_vdm_layer" bl_label = "New VDM Layer" bl_description = "New Vector Displacement Layer" bl_options = {'UNDO'} @@ -740,9 +768,11 @@ def execute(self, context): yp.halt_update = True layer = add_new_layer( - node.node_tree, self.name, 'IMAGE', - channel_idx, 'MIX', self.blend_type, - 'VECTOR_DISPLACEMENT_MAP', 'UV', self.uv_map, img, + group_tree=node.node_tree, layer_name=self.name, + layer_type='IMAGE', channel_idx=channel_idx, + blend_type='MIX', normal_blend_type=self.blend_type, + normal_map_type='VECTOR_DISPLACEMENT_MAP', + texcoord_type='UV', uv_name=self.uv_map, image=img, interpolation = 'Cubic' ) @@ -777,7 +807,7 @@ def execute(self, context): return {'FINISHED'} class YNewLayer(bpy.types.Operator): - bl_idname = "node.y_new_layer" + bl_idname = "wm.y_new_layer" bl_label = "New Layer" bl_description = "New Layer" bl_options = {'REGISTER', 'UNDO'} @@ -903,6 +933,12 @@ class YNewLayer(bpy.types.Operator): subtype = 'COLOR', default=(1.0, 0.0, 1.0), min=0.0, max=1.0 ) + + mask_vcol_fill : BoolProperty( + name = 'Fill Selected Geometry with Vertex Color / Color ID Mask', + description = 'Fill selected geometry with vertex color or color ID mask', + default = True + ) mask_image_filepath : StringProperty( name = 'Mask Image Path', @@ -994,12 +1030,30 @@ class YNewLayer(bpy.types.Operator): ) # For edge detection + edge_detect_radius : FloatProperty( + name = 'Detect Mask Radius', + description = 'Edge detect radius', + default=0.05, min=0.0, max=10.0 + ) + mask_edge_detect_radius : FloatProperty( name = 'Edge Detect Mask Radius', description = 'Edge detect mask radius', default=0.05, min=0.0, max=10.0 ) + mask_use_prev_normal : BoolProperty( + name = 'Use previous Normal for Mask', + description = 'Take account previous Normal for mask', + default = True + ) + + ao_distance : FloatProperty( + name = 'Ambient Occlusion Distance', + description = 'Ambient occlusion distance', + default=1.0, min=0.0, max=10.0 + ) + uv_map_coll : CollectionProperty(type=bpy.types.PropertyGroup) image_resolution : EnumProperty( @@ -1067,10 +1121,19 @@ def invoke(self, context, event): self.normal_map_type = 'BUMP_MAP' # Fake lighting default blend type is add - if self.type == 'HEMI': + if self.type in {'HEMI', 'EDGE_DETECT'}: self.blend_type = 'ADD' + if self.type == 'AO': + self.blend_type = 'MULTIPLY' else: self.blend_type = 'MIX' + # Disable use previous normal for edge detect since it has very little effect + if self.type == 'EDGE_DETECT': + self.hemi_use_prev_normal = False + + if self.add_mask and self.mask_type == 'EDGE_DETECT': + self.mask_use_prev_normal = False + # Layer name self.name = get_unique_name(name, items) @@ -1081,6 +1144,7 @@ def invoke(self, context, event): # Make sure decal is off when adding non mappable layer if not is_mapping_possible(self.type) and self.texcoord_type == 'Decal': self.texcoord_type = 'UV' + self.mask_texcoord_type = 'UV' # Check if color id already being used while True: @@ -1088,19 +1152,20 @@ def invoke(self, context, event): self.mask_color_id = (random.uniform(COLORID_TOLERANCE, 1.0), random.uniform(COLORID_TOLERANCE, 1.0), random.uniform(COLORID_TOLERANCE, 1.0)) if not is_colorid_already_being_used(yp, self.mask_color_id): break - if obj.type != 'MESH': + if not is_object_work_with_uv(obj): self.texcoord_type = 'Generated' - else: - if obj.type == 'MESH': - uv_name = get_default_uv_name(obj, yp) - self.uv_map = uv_name - if self.add_mask: self.mask_uv_name = uv_name + self.mask_texcoord_type = 'Generated' + + if obj.type == 'MESH': + uv_name = get_default_uv_name(obj, yp) + self.uv_map = uv_name + if self.add_mask: self.mask_uv_name = uv_name - # UV Map collections update - self.uv_map_coll.clear() - for uv in get_uv_layers(obj): - if not uv.name.startswith(TEMP_UV): - self.uv_map_coll.add().name = uv.name + # UV Map collections update + self.uv_map_coll.clear() + for uv in get_uv_layers(obj): + if not uv.name.startswith(TEMP_UV): + self.uv_map_coll.add().name = uv.name if get_user_preferences().skip_property_popups and not event.shift: return self.execute(context) @@ -1195,6 +1260,14 @@ def draw(self, context): if self.type == 'HEMI': col.label(text='Space:') + + if self.type == 'EDGE_DETECT': + col.label(text='Edge Detect Radius:') + + if self.type == 'AO': + col.label(text='AO Distance:') + + if self.type in {'HEMI', 'EDGE_DETECT', 'AO'}: col.label(text='') if self.type == 'IMAGE' and self.use_custom_resolution == False: @@ -1209,7 +1282,7 @@ def draw(self, context): col.label(text='') col.label(text='Interpolation:') - if self.type not in {'VCOL', 'GROUP', 'COLOR', 'BACKGROUND', 'HEMI'}: + if self.type not in {'VCOL', 'GROUP', 'COLOR', 'BACKGROUND', 'HEMI', 'EDGE_DETECT', 'AO'}: col.label(text='Vector:') if self.type in {'VCOL'}: @@ -1221,6 +1294,8 @@ def draw(self, context): col.label(text='Mask Type:') if self.mask_type == 'COLOR_ID': col.label(text='Mask Color ID:') + if obj.mode == 'EDIT': + col.label(text='') elif self.mask_type == 'EDGE_DETECT': col.label(text='Edge Detect Radius:') else: @@ -1278,7 +1353,16 @@ def draw(self, context): if self.type == 'HEMI': col.prop(self, 'hemi_space', text='') + + if self.type == 'EDGE_DETECT': + col.prop(self, 'edge_detect_radius', text='') + + if self.type == 'AO': + col.prop(self, 'ao_distance', text='') + + if self.type in {'HEMI', 'EDGE_DETECT', 'AO'}: col.prop(self, 'hemi_use_prev_normal') + if self.type == 'IMAGE' and self.use_custom_resolution == False: crow = col.row(align=True) crow.prop(self, 'use_custom_resolution') @@ -1294,7 +1378,7 @@ def draw(self, context): col.prop(self, 'hdr') col.prop(self, 'interpolation', text='') - if self.type not in {'VCOL', 'GROUP', 'COLOR', 'BACKGROUND', 'HEMI'}: + if self.type not in {'VCOL', 'GROUP', 'COLOR', 'BACKGROUND', 'HEMI', 'EDGE_DETECT', 'AO'}: crow = col.row(align=True) crow.prop(self, 'texcoord_type', text='') if obj.type == 'MESH' and self.texcoord_type == 'UV': @@ -1316,8 +1400,11 @@ def draw(self, context): col.prop(self, 'mask_type', text='') if self.mask_type == 'COLOR_ID': col.prop(self, 'mask_color_id', text='') + if obj.mode == 'EDIT': + col.prop(self, 'mask_vcol_fill', text='Fill Selected Faces') elif self.mask_type == 'EDGE_DETECT': col.prop(self, 'mask_edge_detect_radius', text='') + col.prop(self, 'mask_use_prev_normal', text='Use Previous Normal') else: if self.mask_type == 'IMAGE': if self.mask_image_filepath: @@ -1338,7 +1425,7 @@ def draw(self, context): crow = col.row(align=True) crow.prop(self, 'mask_texcoord_type', text='') - if self.mask_texcoord_type == 'UV': + if self.mask_texcoord_type == 'UV' and obj.type == 'MESH': crow.prop_search(self, "mask_uv_name", self, "uv_map_coll", text='', icon='GROUP_UVS') if not self.mask_image_filepath: @@ -1347,11 +1434,15 @@ def draw(self, context): ccol = col.column() ccol.prop(self, 'use_image_atlas_for_mask', text='Use Image Atlas') - if is_bl_newer_than(3, 2) and self.mask_type == 'VCOL': - crow = col.row(align=True) - crow.prop(self, 'mask_vcol_domain', expand=True) - crow = col.row(align=True) - crow.prop(self, 'mask_vcol_data_type', expand=True) + if self.mask_type == 'VCOL': + if is_bl_newer_than(3, 2): + crow = col.row(align=True) + crow.prop(self, 'mask_vcol_domain', expand=True) + crow = col.row(align=True) + crow.prop(self, 'mask_vcol_data_type', expand=True) + + if obj.mode == 'EDIT': + col.prop(self, 'mask_vcol_fill', text='Fill Selected Faces') if self.get_to_be_cleared_image_atlas(context, yp): col = self.layout.column(align=True) @@ -1405,14 +1496,18 @@ def execute(self, context): return {'CANCELLED'} # Edge Detect mask is only possible in Blender 2.93 or above - if not is_bl_newer_than(2, 93) and self.add_mask and self.mask_type == 'EDGE_DETECT': - self.report({'ERROR'}, "Edge detect mask is only supported in Blender 2.93 or above!") + if not is_bl_newer_than(2, 93) and (self.type in {'EDGE_DETECT', 'AO'} or self.add_mask and self.mask_type == 'EDGE_DETECT'): + self.report({'ERROR'}, "Realtime layer/mask mask is only supported in Blender 2.93 or above!") return {'CANCELLED'} # Clearing unused image atlas segments img_atlas = self.get_to_be_cleared_image_atlas(context, yp) if img_atlas: ImageAtlas.clear_unused_segments(img_atlas.yia) + # Get channel index + try: channel_idx = int(self.channel_idx) + except: channel_idx = 0 + img = None segment = None if self.type == 'IMAGE': @@ -1455,8 +1550,9 @@ def execute(self, context): if hasattr(img, 'use_alpha'): img.use_alpha = True - #if img.colorspace_settings.name != get_noncolor_name(): - # img.colorspace_settings.name = get_noncolor_name() + # Set alpha mode to premultiplied to make sure alpha will be packed correctly + if img.is_float and is_bl_newer_than(2, 80): + img.alpha_mode = 'PREMUL' update_image_editor_image(context, img) @@ -1473,32 +1569,32 @@ def execute(self, context): for o in objs: if self.name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue - vcol = new_vertex_color(o, self.name, self.vcol_data_type, self.vcol_domain) - if is_bl_newer_than(2, 92): - set_obj_vertex_colors(o, vcol.name, (0.0, 0.0, 0.0, 0.0)) - else: set_obj_vertex_colors(o, vcol.name, (1.0, 1.0, 1.0, 1.0)) + color = (0.0, 0.0, 0.0, 0.0) + if not is_bl_newer_than(2, 92): + color = (1.0, 1.0, 1.0, 1.0) + vcol = new_vertex_color(o, self.name, self.vcol_data_type, self.vcol_domain, color_fill=color) set_active_vertex_color(o, vcol) yp.halt_update = True - try: channel_idx = int(self.channel_idx) - except: channel_idx = 0 - layer = add_new_layer( node.node_tree, self.name, self.type, channel_idx, self.blend_type, self.normal_blend_type, - self.normal_map_type, self.texcoord_type, self.uv_map, img, vcol, segment, - self.solid_color, - self.add_mask, self.mask_type, self.mask_image_filepath, self.mask_relative, - self.mask_texcoord_type, self.mask_color, self.mask_use_hdr, self.mask_uv_name, - self.mask_width, self.mask_height, self.use_image_atlas_for_mask, - self.hemi_space, self.hemi_use_prev_normal, self.mask_color_id, - self.mask_vcol_data_type, self.mask_vcol_domain, self.use_divider_alpha, - self.use_udim_for_mask, - self.interpolation, self.mask_interpolation, - self.mask_edge_detect_radius + self.normal_map_type, self.texcoord_type, uv_name=self.uv_map, + image=img, vcol=vcol, segment=segment, solid_color=self.solid_color, + add_mask=self.add_mask, mask_type=self.mask_type, + mask_image_filepath=self.mask_image_filepath, mask_relative=self.mask_relative, + mask_texcoord_type=self.mask_texcoord_type, mask_color=self.mask_color, + mask_use_hdr=self.mask_use_hdr, mask_uv_name=self.mask_uv_name, + mask_width=self.mask_width, mask_height=self.mask_height, use_image_atlas_for_mask=self.use_image_atlas_for_mask, + hemi_space=self.hemi_space, hemi_use_prev_normal=self.hemi_use_prev_normal, + mask_color_id=self.mask_color_id, mask_vcol_fill=self.mask_vcol_fill, + mask_vcol_data_type=self.mask_vcol_data_type, mask_vcol_domain=self.mask_vcol_domain, use_divider_alpha=self.use_divider_alpha, + use_udim_for_mask=self.use_udim_for_mask, interpolation=self.interpolation, mask_interpolation=self.mask_interpolation, + mask_edge_detect_radius=self.mask_edge_detect_radius, edge_detect_radius=self.edge_detect_radius, + mask_use_prev_normal=self.mask_use_prev_normal, ao_distance=self.ao_distance ) if segment: @@ -1548,48 +1644,19 @@ def execute(self, context): return {'FINISHED'} -class YOpenImageToOverrideChannel(bpy.types.Operator, ImportHelper): +class YOpenImageToOverrideChannel(bpy.types.Operator, ImportHelper, BaseOperator.OpenImage): """Open Image to Override Channel""" - bl_idname = "node.y_open_image_to_override_layer_channel" + bl_idname = "wm.y_open_image_to_override_layer_channel" bl_label = "Open Image to Override Channel Layer" bl_options = {'REGISTER', 'UNDO'} - # File related - files : CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory : StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) - - # File browser filter - filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - - display_type : EnumProperty( - items = ( - ('FILE_DEFAULTDISPLAY', 'Default', ''), - ('FILE_SHORTDISLPAY', 'Short List', ''), - ('FILE_LONGDISPLAY', 'Long List', ''), - ('FILE_IMGDISPLAY', 'Thumbnails', '') - ), - default = 'FILE_IMGDISPLAY', - options = {'HIDDEN', 'SKIP_SAVE'} - ) - - relative : BoolProperty(name="Relative Path", default=True, description="Apply relative paths") - - def generate_paths(self): - return (fn.name for fn in self.files), self.directory - @classmethod def poll(cls, context): - #return hasattr(context, 'group_node') and context.group_node return get_active_ypaint_node() def invoke(self, context, event): self.ch = context.parent - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - def check(self, context): - return True + return self.running_fileselect_modal(context, event) def execute(self, context): ch = self.ch @@ -1598,8 +1665,7 @@ def execute(self, context): wm = context.window_manager node = get_active_ypaint_node() - import_list, directory = self.generate_paths() - loaded_images = tuple(load_image(path, directory) for path in import_list) + loaded_images = self.get_loaded_images() images = [] for i, new_img in enumerate(loaded_images): @@ -1655,6 +1721,15 @@ def execute(self, context): if not ch.override: ch.override = True + # Set relative + if self.relative: + try: image.filepath = bpy.path.relpath(image.filepath) + except: pass + + # Set colorspace + if root_ch.colorspace == 'LINEAR' and not image.is_dirty: + image.colorspace_settings.name = get_noncolor_name() + # Update image cache if ch.override_type == 'IMAGE': source_tree = get_channel_source_tree(ch, layer) @@ -1673,6 +1748,15 @@ def execute(self, context): if not ch.override_1: ch.override_1 = True + # Set relative + if self.relative: + try: image_1.filepath = bpy.path.relpath(image_1.filepath) + except: pass + + # Set colorspace + if not image_1.is_dirty: + image_1.colorspace_settings.name = get_noncolor_name() + # Update image 1 cache if ch.override_1_type == 'IMAGE': source_label = root_ch.name + ' Override 1 : ' + ch.override_1_type @@ -1698,6 +1782,9 @@ def execute(self, context): if ch.normal_map_type == 'NORMAL_MAP': ch.normal_map_type = 'BUMP_MAP' + # Update list items + ListItem.refresh_list_items(yp) + # Update UI wm.ypui.need_update = True print('INFO: Image(s) opened in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') @@ -1705,48 +1792,19 @@ def execute(self, context): return {'FINISHED'} -class YOpenImageToOverride1Channel(bpy.types.Operator, ImportHelper): +class YOpenImageToOverride1Channel(bpy.types.Operator, ImportHelper, BaseOperator.OpenImage): """Open Image to Override 1 Channel""" - bl_idname = "node.y_open_image_to_override_1_layer_channel" + bl_idname = "wm.y_open_image_to_override_1_layer_channel" bl_label = "Open Image to Override 1 Channel Layer" bl_options = {'REGISTER', 'UNDO'} - # File related - files : CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory : StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) - - # File browser filter - filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - - display_type : EnumProperty( - items = ( - ('FILE_DEFAULTDISPLAY', 'Default', ''), - ('FILE_SHORTDISLPAY', 'Short List', ''), - ('FILE_LONGDISPLAY', 'Long List', ''), - ('FILE_IMGDISPLAY', 'Thumbnails', '') - ), - default = 'FILE_IMGDISPLAY', - options = {'HIDDEN', 'SKIP_SAVE'} - ) - - relative : BoolProperty(name="Relative Path", default=True, description="Apply relative paths") - - def generate_paths(self): - return (fn.name for fn in self.files), self.directory - @classmethod def poll(cls, context): - #return hasattr(context, 'group_node') and context.group_node return get_active_ypaint_node() def invoke(self, context, event): self.ch = context.parent - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - def check(self, context): - return True + return self.running_fileselect_modal(context, event) def execute(self, context): ch = self.ch @@ -1755,8 +1813,7 @@ def execute(self, context): wm = context.window_manager node = get_active_ypaint_node() - import_list, directory = self.generate_paths() - loaded_images = tuple(load_image(path, directory) for path in import_list) + loaded_images = self.get_loaded_images() images = [] for i, new_img in enumerate(loaded_images): @@ -1809,6 +1866,15 @@ def execute(self, context): if not ch.override_1: ch.override_1 = True + # Set relative + if self.relative: + try: image.filepath = bpy.path.relpath(image.filepath) + except: pass + + # Set colorspace + if not image.is_dirty: + image.colorspace_settings.name = get_noncolor_name() + # Update image cache if ch.override_1_type == 'IMAGE': source_label = root_ch.name + ' Override 1 : ' + ch.override_1_type @@ -1826,6 +1892,15 @@ def execute(self, context): if not ch.override: ch.override = True + # Set relative + if self.relative: + try: image_1.filepath = bpy.path.relpath(image_1.filepath) + except: pass + + # Set colorspace + if not image_1.is_dirty: + image_1.colorspace_settings.name = get_noncolor_name() + # Update image 1 cache if ch.override_type == 'IMAGE': source_tree = get_channel_source_tree(ch, layer) @@ -1850,34 +1925,18 @@ def execute(self, context): if ch.normal_map_type == 'BUMP_MAP': ch.normal_map_type = 'NORMAL_MAP' + # Update list items + ListItem.refresh_list_items(yp) + # Update UI wm.ypui.need_update = True print('INFO: Image(s) opened in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') wm.yptimer.time = str(time.time()) return {'FINISHED'} + -class BaseMultipleImagesLayer(): - # File related - files : CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory : StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) - - # File browser filter - filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - - display_type : EnumProperty( - items = ( - ('FILE_DEFAULTDISPLAY', 'Default', ''), - ('FILE_SHORTDISLPAY', 'Short List', ''), - ('FILE_LONGDISPLAY', 'Long List', ''), - ('FILE_IMGDISPLAY', 'Thumbnails', '') - ), - default = 'FILE_IMGDISPLAY', - options = {'HIDDEN', 'SKIP_SAVE'} - ) - - relative : BoolProperty(name="Relative Path", default=True, description="Apply relative paths") +class BaseMultipleImagesLayer(BaseOperator.OpenImage): texcoord_type : EnumProperty( name = 'Layer Coordinate Type', @@ -1964,9 +2023,6 @@ class BaseMultipleImagesLayer(): default = False ) - def generate_paths(self): - return (fn.name for fn in self.files), self.directory - #def is_mask_using_udim(self): # return self.use_udim_for_mask and UDIM.is_udim_supported() @@ -2020,8 +2076,8 @@ def is_synonym_in_image_name(self, syname, img_name): # return True return False - - def open_images_to_single_layer(self, context:bpy.context, directory:str, import_list, non_import_images=[]) -> bool: + + def open_images_to_single_layer(self, context:bpy.context, directory:str='', import_list=[], non_import_images=[], channel_image_dict={}) -> bool: T = time.time() @@ -2036,6 +2092,13 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import valid_images = [] valid_synonyms = [] + # Filter image to make sure it's not empty + filtered_images = [] + for image in images: + if not image: continue + filtered_images.append(image) + images = filtered_images + # Check for DirectX and OpenGL images dx_image = None gl_image = None @@ -2054,7 +2117,7 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import 'alpha' : ['opacity', 'a'], 'ambient occlusion' : ['ao'], 'metallic' : ['metalness', 'm'], - 'roughness' : ['glossiness', 'r'], + 'roughness' : ['glossiness', 'smoothness', 'r'], 'normal' : ['displacement', 'height', 'bump', 'n'], # Prioritize displacement/bump before actual normal map } @@ -2089,20 +2152,31 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import for image in images: - # One image will only use one channel - if image in valid_images: continue - # DirectX image will be skipped if there's OpenGL image if ch.type == 'NORMAL' and dx_image and gl_image and image == dx_image: continue - # Get filename without extension - if image.filepath != '': - img_name = os.path.splitext(bpy.path.basename(image.filepath))[0].lower() - else: img_name = image.name.lower() + if image in channel_image_dict.values(): + + # Get alias based on channel-image dictionary + for ch_name, img in channel_image_dict.items(): + if img == image: + alias = ch_name.lower() + if syname in ch_name.lower(): + break + + elif image.filepath != '': + # Get alias base on filename without extension + alias = os.path.splitext(bpy.path.basename(image.filepath))[0].lower() + else: + # Get alias base on filename + alias = image.name.lower() + + # Remove trailing digits and spaces from the filename + alias = re.sub(r'[\d\s]+$', '', alias) # Check if synonym is in image name - if self.is_synonym_in_image_name(syname, img_name): + if self.is_synonym_in_image_name(syname, alias): if (ch.type != 'NORMAL' or (syname in bump_synonyms and not bump_image_found) or # Only proceed if bump image is not yet found @@ -2139,6 +2213,7 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import #if valid_channels and valid_channels[0] layer = None + main_image = None for i, image in enumerate(valid_images): root_ch = valid_channels[i] syname = valid_synonyms[i] @@ -2159,9 +2234,12 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import yp.halt_update = True layer = add_new_layer( - node.node_tree, image.name, 'IMAGE', - int(ch_idx), 'MIX', 'MIX', - normal_map_type, self.texcoord_type, self.uv_map, image, None, None, (1, 1, 1), + group_tree=node.node_tree, layer_name=image.name, + layer_type='IMAGE', channel_idx=int(ch_idx), + blend_type='MIX', normal_blend_type='MIX', + normal_map_type=normal_map_type, texcoord_type=self.texcoord_type, + uv_name=self.uv_map, image=image, + vcol=None, segment=None, solid_color=(1, 1, 1), add_mask=self.add_mask, mask_type=self.mask_type, mask_color=self.mask_color, mask_use_hdr=self.mask_use_hdr, mask_uv_name=self.mask_uv_name, mask_width=self.mask_width, mask_height=self.mask_height, use_image_atlas_for_mask=self.use_image_atlas_for_mask, use_udim_for_mask=self.use_udim_for_mask @@ -2169,14 +2247,18 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import yp.halt_update = False tree = get_tree(layer) + main_image = image else: ch = layer.channels[ch_idx] ch.enable = True if root_ch.type == 'NORMAL' and (syname in {'normal', 'n'} or 'normal without bump' in image.name.lower()): - image_node, dirty = check_new_node(tree, ch, 'cache_1_image', 'ShaderNodeTexImage', '', True) - image_node.image = image - ch.override_1 = True - ch.override_1_type = 'IMAGE' + if image == main_image: + ch.override_1 = False + else: + image_node, dirty = check_new_node(tree, ch, 'cache_1_image', 'ShaderNodeTexImage', '', True) + image_node.image = image + ch.override_1 = True + ch.override_1_type = 'IMAGE' if (self.normal_map_flip_y and (not gl_image or gl_image != image)) or (dx_image and dx_image == image): ch.image_flip_y = True else: @@ -2185,11 +2267,14 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import if root_ch.type == 'NORMAL': image_node.interpolation = 'Cubic' # Add invert modifier for glosiness - if syname == 'glossiness': + if syname in {'glossiness', 'smoothness'}: Modifier.add_new_modifier(ch, 'INVERT') - ch.override = True - ch.override_type = 'IMAGE' + if image == main_image: + ch.override = False + else: + ch.override = True + ch.override_type = 'IMAGE' # Check image projections check_layer_projections(layer) @@ -2210,8 +2295,9 @@ def open_images_to_single_layer(self, context:bpy.context, directory:str, import wm.ypui.expand_channels = True # Expand vector transformation because it's highly likely user want to tweak this - wm.ypui.layer_ui.expand_content = True + wm.ypui.layer_ui.expand_content = False wm.ypui.layer_ui.expand_vector = True + wm.ypui.layer_ui.expand_channels = True print('INFO: Image(s) opened in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') wm.yptimer.time = str(time.time()) @@ -2231,8 +2317,8 @@ def invoke_operator(self, context): elif ypup.default_image_resolution != 'DEFAULT': self.mask_image_resolution = ypup.default_image_resolution - if obj.type != 'MESH': - self.texcoord_type = 'Object' + if not is_object_work_with_uv(obj): + self.texcoord_type = 'Generated' # Use active uv layer name by default if obj.type == 'MESH': @@ -2363,8 +2449,36 @@ def search_for_images(tree): return images -class YOpenImagesFromMaterialToLayer(bpy.types.Operator, BaseMultipleImagesLayer): - bl_idname = "node.y_open_images_from_material_to_single_layer" +def search_for_image_node(node, channel_name, channel_image_dict={}): + if node.type == 'TEX_IMAGE' and node.image: + #print("add", node.image.name, "to", channel_name) + channel_image_dict[channel_name] = node.image + elif node.type == "BUMP": + for inp in node.inputs: + if inp.is_linked: + # print("input=", inp.name, " linked=",inp.is_linked," type=" ,inp.type) + for link in inp.links: + # print("link=", link.from_node.name, " type=", link.from_node.type) + ch_name = inp.name + if ch_name == "Height": + ch_name = "Bump" + # Skip if already in dictionary + if ch_name in channel_image_dict: + continue + search_for_image_node(link.from_node, ch_name, channel_image_dict) + else: + #print("else node=", node.name, "type=", node.type) + for inp in node.inputs: + if inp.is_linked: + # print("input=", inp.name, " linked=",inp.is_linked," type=" ,inp.type) + for link in inp.links: + # print("link=", link.from_node.name, " type=", link.from_node.type) + search_for_image_node(link.from_node, channel_name, channel_image_dict) + if channel_name in channel_image_dict: + break + +class YOpenImagesFromMaterialToLayer(bpy.types.Operator, ImportHelper, BaseMultipleImagesLayer): + bl_idname = "wm.y_open_images_from_material_to_single_layer" bl_label = "Open Images from Material to single " + get_addon_title() + " Layer" bl_description = "Open images inside material node tree to single " + get_addon_title() + " layer" bl_options = {'REGISTER', 'UNDO'} @@ -2373,6 +2487,16 @@ class YOpenImagesFromMaterialToLayer(bpy.types.Operator, BaseMultipleImagesLayer mat_coll : CollectionProperty(type=bpy.types.PropertyGroup) asset_library_path : StringProperty(default='') + read_method : EnumProperty( + name = 'Read Method', + description = '', + items = ( + ('IMAGE_NAMES', 'Read Image Names', 'Read all image names in material'), + ('READ_NODE', 'Read Node Connections', 'Read node connections in material'), + ), + default = 'READ_NODE' + ) + @classmethod def poll(cls, context): #return get_active_ypaint_node() @@ -2419,6 +2543,13 @@ def draw(self, context): if self.asset_library_path == '': row.prop_search(self, "mat_name", self, "mat_coll", text='', icon='MATERIAL_DATA') else: row.label(text=self.mat_name, icon='MATERIAL_DATA') + + row = split_layout(self.layout, 0.325) + row.label(text='Read method:') + + col = row.column() + col.prop(self, 'read_method', text="") + self.draw_operator(context, display_relative_toggle=False) def execute(self, context): @@ -2448,13 +2579,40 @@ def execute(self, context): self.report({'ERROR'}, "Source material cannot be found!") return {'CANCELLED'} - # Check material for images + # Images to be processed images = [] - if mat.node_tree: - images = search_for_images(mat.node_tree) - # Check for yp images + # channel name and their image dictionary + channel_image_dict = {} + + # Get active material output output = get_material_output(mat) + + if self.read_method == 'READ_NODE': + # Search images based on node connections + for inp in output.inputs: + if inp.is_linked and inp.name == "Displacement": + for link in inp.links: + search_for_image_node(link.from_node, "Bump", channel_image_dict) + + bsdf_node = get_closest_bsdf_backward(output) + + for inp in bsdf_node.inputs: + if inp.is_linked: + for link in inp.links: + ch_name = inp.name + if ch_name == "Base Color": + ch_name = "Color" + search_for_image_node(link.from_node, ch_name, channel_image_dict) + + for img in channel_image_dict.values(): + if img not in images: images.append(img) + + elif self.read_method == 'IMAGE_NAMES': + + if mat.node_tree: + images = search_for_images(mat.node_tree) + yp_node = get_closest_yp_node_backward(output) if yp_node: otree = yp_node.node_tree @@ -2465,21 +2623,20 @@ def execute(self, context): baked_normal_overlay = None if root_ch.type == 'NORMAL': baked_disp = otree.nodes.get(root_ch.baked_disp) - if baked_disp and baked_disp.image: + if baked_disp and baked_disp.image and 'Bump' not in channel_image_dict.keys(): images.append(baked_disp.image) + channel_image_dict['Bump'] = baked_disp.image baked_normal_overlay = otree.nodes.get(root_ch.baked_normal_overlay) - if baked_normal_overlay and baked_normal_overlay.image: + if baked_normal_overlay and baked_normal_overlay.image and 'Normal' not in channel_image_dict.keys(): images.append(baked_normal_overlay.image) + channel_image_dict['Normal'] = baked_normal_overlay.image if root_ch.type != 'NORMAL' or not (baked_disp and baked_normal_overlay): baked = otree.nodes.get(root_ch.baked) - if baked and baked.image: + if baked and baked.image and root_ch.name not in channel_image_dict.keys(): images.append(baked.image) - - if not images: - self.report({'ERROR'}, "Couldn't find images inside of the material!") - return {'CANCELLED'} + channel_image_dict[root_ch.name] = baked.image # Check for existing images if the image source is from asset library if from_asset_library: @@ -2511,16 +2668,20 @@ def execute(self, context): # Use filtered images images = filtered_images + if len(images) == 0: + self.report({'ERROR'}, "Couldn't find images inside of the material!") + return {'CANCELLED'} + # Use quick setup if yp node is not found node = get_active_ypaint_node() quick_setup_happen = False if not node: - bpy.ops.node.y_quick_ypaint_node_setup() + bpy.ops.wm.y_quick_ypaint_node_setup() quick_setup_happen = True failed = False - if not self.open_images_to_single_layer(context, directory='', import_list=[], non_import_images=images): - self.report({'ERROR'}, "Images should have channel name as suffix!") + if not self.open_images_to_single_layer(context, directory='', import_list=[], non_import_images=images, channel_image_dict=channel_image_dict): + self.report({'ERROR'}, "Cannot find valid images!") failed = True # Remove material if it has only fake users @@ -2529,13 +2690,101 @@ def execute(self, context): if failed: if quick_setup_happen: - bpy.ops.node.y_remove_yp_node() + bpy.ops.wm.y_remove_yp_node() return {'CANCELLED'} return {'FINISHED'} +class YOpenLayersFromMaterial(bpy.types.Operator): + bl_idname = "wm.y_open_layers_from_material" + bl_label = "Open Layers from " + get_addon_title() + " Material" + bl_description = "Open layers from material to current " + get_addon_title() + " material" + bl_options = {'REGISTER', 'UNDO'} + + mat_name : StringProperty(default='') + asset_library_path : StringProperty(default='') + + @classmethod + def poll(cls, context): + return context.object and get_active_ypaint_node() + + @classmethod + def description(self, context, properties): + return get_operator_description(self) + + def remove_mat(self, mat, from_asset_library): + # Remove material if it has only fake users (from asset library) + if from_asset_library and ((mat.use_fake_user and mat.users == 1) or mat.users == 0): + remove_datablock(bpy.data.materials, mat) + + def execute(self, context): + if self.mat_name == '': + self.report({'ERROR'}, "Source material cannot be empty!") + return {'CANCELLED'} + + obj = context.object + if not obj.data or not hasattr(obj.data, 'materials'): + self.report({'ERROR'}, "Cannot use "+get_addon_title()+" with object '"+obj.name+"'!") + return {'CANCELLED'} + + # Get material from local first + mat = bpy.data.materials.get(self.mat_name) + + # If not found get from the asset library + from_asset_library = self.asset_library_path != '' + if not mat and from_asset_library and is_bl_newer_than(3): + with bpy.data.libraries.load(str(self.asset_library_path), assets_only=True) as (data_from, data_to): + for mat_name in data_from.materials: + if mat_name == self.mat_name: + data_to.materials.append(mat_name) + mat = bpy.data.materials.get(self.mat_name) + + if not mat: + self.report({'ERROR'}, "Material cannot be found!") + return {'CANCELLED'} + + if not mat.node_tree: + self.remove_mat(mat, from_asset_library) + self.report({'ERROR'}, "Material has no node tree!") + return {'CANCELLED'} + + # Find yp node in source material + source_yp_node = None + for node in mat.node_tree.nodes: + if node.type == 'GROUP' and hasattr(node.node_tree, 'yp') and node.node_tree.yp.is_ypaint_node: + source_yp_node = node + break + + if not source_yp_node: + self.remove_mat(mat, from_asset_library) + self.report({'ERROR'}, "Material has no "+get_addon_title()+" node!") + return {'CANCELLED'} + + source_tree = source_yp_node.node_tree + source_yp = source_tree.yp + + if len(source_yp.layers) == 0: + self.remove_mat(mat, from_asset_library) + self.report({'ERROR'}, "Material has no layers!") + return {'CANCELLED'} + + # Copy all layers to clipboard + wmp = context.window_manager.ypprops + wmp.clipboard_tree = source_tree.name + wmp.clipboard_layer = '' # Empty means all layers + + try: + bpy.ops.wm.y_paste_layer('INVOKE_DEFAULT') + except Exception as e: + self.report({'ERROR'}, "Failed to paste layers: "+str(e)) + return {'CANCELLED'} + + self.remove_mat(mat, from_asset_library) + + return {'FINISHED'} + class YOpenImagesToSingleLayer(bpy.types.Operator, ImportHelper, BaseMultipleImagesLayer): - bl_idname = "node.y_open_images_to_single_layer" + bl_idname = "wm.y_open_images_to_single_layer" bl_label = "Open Images to single Layer" bl_description = "Open images to single layer" bl_options = {'REGISTER', 'UNDO'} @@ -2547,8 +2796,7 @@ def poll(cls, context): def invoke(self, context, event): self.invoke_operator(context) - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + return self.running_fileselect_modal(context, event) def check(self, context): return self.check_operator(context) @@ -2558,39 +2806,18 @@ def draw(self, context): def execute(self, context): import_list, directory = self.generate_paths() - if not self.open_images_to_single_layer(context, directory, import_list): + if not self.open_images_to_single_layer(context, directory=directory, import_list=import_list): self.report({'ERROR'}, "Images should have channel name as suffix!") return {'CANCELLED'} return {'FINISHED'} -class YOpenImageToLayer(bpy.types.Operator, ImportHelper): +class YOpenImageToLayer(bpy.types.Operator, ImportHelper, BaseOperator.OpenImage): """Open Image to Layer""" - bl_idname = "node.y_open_image_to_layer" + bl_idname = "wm.y_open_image_to_layer" bl_label = "Open Image to Layer" bl_options = {'REGISTER', 'UNDO'} - # File related - files : CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory : StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) - - # File browser filter - filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - - display_type : EnumProperty( - items = ( - ('FILE_DEFAULTDISPLAY', 'Default', ''), - ('FILE_SHORTDISLPAY', 'Short List', ''), - ('FILE_LONGDISPLAY', 'Long List', ''), - ('FILE_IMGDISPLAY', 'Thumbnails', '') - ), - default = 'FILE_IMGDISPLAY', - options = {'HIDDEN', 'SKIP_SAVE'} - ) - - relative : BoolProperty(name="Relative Path", default=True, description="Apply relative paths") - interpolation : EnumProperty( name = 'Image Interpolation Type', description = 'Image interpolation type', @@ -2647,9 +2874,6 @@ class YOpenImageToLayer(bpy.types.Operator, ImportHelper): file_browser_filepath : StringProperty(default='') - def generate_paths(self): - return (fn.name for fn in self.files), self.directory - @classmethod def poll(cls, context): #return hasattr(context, 'group_node') and context.group_node @@ -2664,8 +2888,8 @@ def invoke(self, context, event): node = get_active_ypaint_node() yp = self.yp = node.node_tree.yp - if obj.type != 'MESH': - self.texcoord_type = 'Object' + if not is_object_work_with_uv(obj): + self.texcoord_type = 'Generated' # Use active uv layer name by default if obj.type == 'MESH': @@ -2685,11 +2909,7 @@ def invoke(self, context, event): return self.execute(context) return context.window_manager.invoke_props_dialog(self) - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} - - def check(self, context): - return True + return self.running_fileselect_modal(context, event) def draw(self, context): node = get_active_ypaint_node() @@ -2777,9 +2997,12 @@ def execute(self, context): except: pass add_new_layer( - node.node_tree, image.name, 'IMAGE', int(self.channel_idx), self.blend_type, - self.normal_blend_type, self.normal_map_type, self.texcoord_type, self.uv_map, - image, None, None, interpolation=self.interpolation, normal_space=self.normal_space + group_tree=node.node_tree, layer_name=image.name, + layer_type='IMAGE', channel_idx=int(self.channel_idx), + blend_type=self.blend_type, normal_blend_type=self.normal_blend_type, + normal_map_type=self.normal_map_type, texcoord_type=self.texcoord_type, + uv_name = self.uv_map, image=image, vcol=None, segment=None, + interpolation=self.interpolation, normal_space=self.normal_space ) node.node_tree.yp.halt_update = False @@ -2801,7 +3024,7 @@ def execute(self, context): class YOpenAvailableDataToOverride1Channel(bpy.types.Operator): """Open Available Data to Override 1 Channel Layer""" - bl_idname = "node.y_open_available_data_to_override_1_channel" + bl_idname = "wm.y_open_available_data_to_override_1_channel" bl_label = "Open Available Data to Override 1 Channel Layer" bl_options = {'REGISTER', 'UNDO'} @@ -2824,7 +3047,7 @@ def invoke(self, context, event): imgs = bpy.data.images baked_channel_images = get_all_baked_channel_images(node.node_tree) for img in imgs: - if not img.yia.is_image_atlas and img not in baked_channel_images and img.name not in {'Render Result', 'Viewer Node'}: + if is_image_available_to_open(img) and img not in baked_channel_images: self.image_coll.add().name = img.name return context.window_manager.invoke_props_dialog(self) @@ -2906,6 +3129,9 @@ def execute(self, context): ch.override_1_type = 'IMAGE' ch.active_edit_1 = True + # Update list items + ListItem.refresh_list_items(yp) + # Update UI wm.ypui.need_update = True print('INFO: Data is opened in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') @@ -2915,7 +3141,7 @@ def execute(self, context): class YOpenAvailableDataToOverrideChannel(bpy.types.Operator): """Open Available Data to Override Channel Layer""" - bl_idname = "node.y_open_available_data_to_override_channel" + bl_idname = "wm.y_open_available_data_to_override_channel" bl_label = "Open Available Data to Override Channel Layer" bl_options = {'REGISTER', 'UNDO'} @@ -2951,7 +3177,7 @@ def invoke(self, context, event): imgs = bpy.data.images baked_channel_images = get_all_baked_channel_images(node.node_tree) for img in imgs: - if not img.yia.is_image_atlas and img not in baked_channel_images and img.name not in {'Render Result', 'Viewer Node'}: + if is_image_available_to_open(img) and img not in baked_channel_images: self.image_coll.add().name = img.name elif self.type == 'VCOL': self.vcol_coll.clear() @@ -3061,8 +3287,7 @@ def execute(self, context): if self.vcol_name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue data_type, domain = get_vcol_data_type_and_domain_by_name(o, self.vcol_name, objs) - other_v = new_vertex_color(o, self.vcol_name, data_type, domain) - set_obj_vertex_colors(o, other_v.name, (0.0, 0.0, 0.0, 1.0)) + other_v = new_vertex_color(o, self.vcol_name, data_type, domain, color_fill=(0.0, 0.0, 0.0, 1.0)) set_active_vertex_color(o, other_v) # Update vcol cache @@ -3086,6 +3311,9 @@ def execute(self, context): ch.override_type = self.type ch.active_edit = self.type in {'IMAGE', 'VCOL'} + # Update list items + ListItem.refresh_list_items(yp) + # Update UI wm.ypui.need_update = True print('INFO: Data is opened in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') @@ -3095,7 +3323,7 @@ def execute(self, context): class YOpenAvailableDataToLayer(bpy.types.Operator): """Open Available Data to Layer""" - bl_idname = "node.y_open_available_data_to_layer" + bl_idname = "wm.y_open_available_data_to_layer" bl_label = "Open Available Data to Layer" bl_options = {'REGISTER', 'UNDO'} @@ -3172,8 +3400,8 @@ def invoke(self, context, event): node = get_active_ypaint_node() yp = node.node_tree.yp - if obj.type != 'MESH': - self.texcoord_type = 'Object' + if not is_object_work_with_uv(obj): + self.texcoord_type = 'Generated' # Use active uv layer name by default if obj.type == 'MESH': @@ -3191,7 +3419,7 @@ def invoke(self, context, event): imgs = bpy.data.images baked_channel_images = get_all_baked_channel_images(node.node_tree) for img in imgs: - if not img.yia.is_image_atlas and img not in baked_channel_images and img.name not in {'Render Result', 'Viewer Node'}: + if is_image_available_to_open(img) and img not in baked_channel_images: self.image_coll.add().name = img.name elif self.type == 'VCOL': self.vcol_coll.clear() @@ -3288,16 +3516,21 @@ def execute(self, context): if self.vcol_name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue data_type, domain = get_vcol_data_type_and_domain_by_name(o, self.vcol_name, objs) - other_v = new_vertex_color(o, self.vcol_name, data_type, domain) - if is_bl_newer_than(2, 92): - set_obj_vertex_colors(o, other_v.name, (0.0, 0.0, 0.0, 0.0)) - else: set_obj_vertex_colors(o, other_v.name, (0.0, 0.0, 0.0, 1.0)) + + color = (0.0, 0.0, 0.0, 0.0) + if not is_bl_newer_than(2, 92): + color = (0.0, 0.0, 0.0, 1.0) + other_v = new_vertex_color(o, self.vcol_name, data_type, domain, color_fill=color) + set_active_vertex_color(o, other_v) add_new_layer( - node.node_tree, name, self.type, int(self.channel_idx), self.blend_type, - self.normal_blend_type, self.normal_map_type, self.texcoord_type, self.uv_map, - image, vcol, None, interpolation=self.interpolation, normal_space=self.normal_space + group_tree=node.node_tree, layer_name=name, + layer_type=self.type, channel_idx=int(self.channel_idx), + blend_type=self.blend_type, normal_blend_type=self.normal_blend_type, + normal_map_type=self.normal_map_type, texcoord_type=self.texcoord_type, + uv_name=self.uv_map, image=image, vcol=vcol, segment=None, + interpolation=self.interpolation, normal_space=self.normal_space ) node.node_tree.yp.halt_update = False @@ -3318,7 +3551,7 @@ def execute(self, context): return {'FINISHED'} class YMoveInOutLayerGroup(bpy.types.Operator): - bl_idname = "node.y_move_in_out_layer_group" + bl_idname = "wm.y_move_in_out_layer_group" bl_label = "Move In/Out Layer Group" bl_description = "Move in or out layer group" bl_options = {'REGISTER', 'UNDO'} @@ -3452,7 +3685,7 @@ def execute(self, context): return {'FINISHED'} class YMoveLayer(bpy.types.Operator): - bl_idname = "node.y_move_layer" + bl_idname = "wm.y_move_layer" bl_label = "Move Layer" bl_description = "Move layer" bl_options = {'REGISTER', 'UNDO'} @@ -3611,23 +3844,23 @@ def execute(self, context): def draw_move_up_in_layer_group(self, context): col = self.layout.column() - c = col.operator("node.y_move_layer", text='Move Up (Skip Group)', icon='TRIA_UP') + c = col.operator("wm.y_move_layer", text='Move Up (Skip Group)', icon='TRIA_UP') c.direction = 'UP' - c = col.operator("node.y_move_in_out_layer_group", text='Move Inside Group', icon='TRIA_UP') + c = col.operator("wm.y_move_in_out_layer_group", text='Move Inside Group', icon='TRIA_UP') c.direction = 'UP' def draw_move_down_in_layer_group(self, context): col = self.layout.column() - c = col.operator("node.y_move_layer", text='Move Down (Skip Group)', icon='TRIA_DOWN') + c = col.operator("wm.y_move_layer", text='Move Down (Skip Group)', icon='TRIA_DOWN') c.direction = 'DOWN' - c = col.operator("node.y_move_in_out_layer_group", text='Move Inside Group', icon='TRIA_DOWN') + c = col.operator("wm.y_move_in_out_layer_group", text='Move Inside Group', icon='TRIA_DOWN') c.direction = 'DOWN' class YMoveInOutLayerGroupMenu(bpy.types.Operator): - bl_idname = "node.y_move_in_out_layer_group_menu" + bl_idname = "wm.y_move_in_out_layer_group_menu" bl_label = "Move In/Out Layer Group" bl_description = "Move inside or outside layer group" bl_options = {'UNDO'} @@ -3653,7 +3886,7 @@ def execute(self, context): wm = bpy.context.window_manager if self.move_out: - bpy.ops.node.y_move_in_out_layer_group(direction=self.direction) + bpy.ops.wm.y_move_in_out_layer_group(direction=self.direction) else: if self.direction == 'UP': wm.popup_menu(draw_move_up_in_layer_group, title="Options") @@ -3670,7 +3903,7 @@ def remove_layer(yp, index, remove_on_disk=False): wm = bpy.context.window_manager # Dealing with decal object - remove_decal_object(layer_tree, layer) + Decal.remove_decal_object(layer_tree, layer) # Dealing with image atlas segments if layer.type == 'IMAGE': # and layer.segment_name != '': @@ -3703,7 +3936,7 @@ def remove_layer(yp, index, remove_on_disk=False): for mask in layer.masks: # Dealing with decal object - remove_decal_object(layer_tree, mask) + Decal.remove_decal_object(layer_tree, mask) # Dealing with image atlas segments if mask.type == 'IMAGE': # and mask.segment_name != '': @@ -3753,11 +3986,11 @@ def remove_layer(yp, index, remove_on_disk=False): def draw_remove_group(self, context): col = self.layout.column() - c = col.operator("node.y_remove_layer", text='Remove parent only', icon='PANEL_CLOSE') + c = col.operator("wm.y_remove_layer", text='Remove parent only', icon='PANEL_CLOSE') c.remove_children = False c.remove_on_disk = False - c = col.operator("node.y_remove_layer", text='Remove parent with all of its children', icon='PANEL_CLOSE') + c = col.operator("wm.y_remove_layer", text='Remove parent with all of its children', icon='PANEL_CLOSE') c.remove_children = True c.remove_on_disk = False @@ -3765,12 +3998,12 @@ def draw_remove_group(self, context): col.separator() col.alert = True col.label(text='Danger Zone', icon='ERROR') - c = col.operator("node.y_remove_layer", text='Remove parent with all of its children and files on disk (WARNING: NO PROMPT & NO UNDO!)', icon='PANEL_CLOSE') + c = col.operator("wm.y_remove_layer", text='Remove parent with all of its children and files on disk (WARNING: NO PROMPT & NO UNDO!)', icon='PANEL_CLOSE') c.remove_children = True c.remove_on_disk = True class YRemoveLayerMenu(bpy.types.Operator): - bl_idname = "node.y_remove_layer_menu" + bl_idname = "wm.y_remove_layer_menu" bl_label = "Remove Layer Menu" bl_description = "Remove Layer Menu" #bl_options = {'REGISTER', 'UNDO'} @@ -3786,7 +4019,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveLayer(bpy.types.Operator): - bl_idname = "node.y_remove_layer" + bl_idname = "wm.y_remove_layer" bl_label = "Remove Layer" bl_description = "Remove Layer" bl_options = {'UNDO'} @@ -4014,7 +4247,7 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): source = source_tree.nodes.get(layer.source) # Save source to cache - if layer.type not in {'BACKGROUND', 'GROUP', 'HEMI'} and layer.type != new_type: + if layer.type not in {'BACKGROUND', 'GROUP', 'HEMI', 'EDGE_DETECT', 'AO'} and layer.type != new_type: setattr(layer, 'cache_' + layer.type.lower(), source.name) # Remove uv input link if any(source.inputs) and any(source.inputs[0].links): @@ -4025,7 +4258,7 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): # Try to get available cache cache = None - if new_type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'GROUP', 'HEMI'} or (new_type in {'IMAGE', 'VCOL'} and item_name == ''): + if new_type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'GROUP', 'HEMI', 'EDGE_DETECT', 'AO'} or (new_type in {'IMAGE', 'VCOL'} and item_name == ''): cache = tree.nodes.get(getattr(layer, 'cache_' + new_type.lower())) if cache: @@ -4038,10 +4271,10 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): if new_type == 'IMAGE': image = bpy.data.images.get(item_name) source.image = image - if hasattr(source, 'color_space'): - source.color_space = 'NONE' - #if image.colorspace_settings.name != get_noncolor_name(): - # image.colorspace_settings.name = get_noncolor_name() + + if layer.texcoord_type == 'Decal': + source.extension = 'CLIP' + elif new_type == 'VCOL': set_source_vcol_name(source, item_name) elif new_type == 'HEMI': @@ -4050,6 +4283,12 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): load_hemi_props(layer, source) + elif new_type == 'EDGE_DETECT': + Mask.setup_edge_detect_source(layer, source) + + elif new_type == 'AO': + enable_eevee_ao() + # Change layer type ori_type = layer.type layer.type = new_type @@ -4057,6 +4296,11 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): # Check modifiers tree Modifier.check_modifiers_trees(layer) + # Always remove baked layer when changing type + if layer.use_baked: + layer.use_baked = False + remove_node(tree, layer, 'baked_source') + # Update group ios check_all_layer_channel_io_and_nodes(layer, tree) if layer.type == 'BACKGROUND': @@ -4158,7 +4402,7 @@ def replace_layer_type(layer, new_type, item_name='', remove_data=False): layer.expand_source = layer.type not in {'IMAGE', 'VCOL'} or (image != None and image.y_bake_info.is_baked and not image.y_bake_info.is_baked_channel) class YReplaceLayerChannelOverride(bpy.types.Operator): - bl_idname = "node.y_replace_layer_channel_override" + bl_idname = "wm.y_replace_layer_channel_override" bl_label = "Replace Layer Channel Override" bl_description = "Replace Layer Channel Override" bl_options = {'REGISTER', 'UNDO'} @@ -4178,6 +4422,7 @@ def execute(self, context): ch = context.parent ch.override_type = self.type ch.override = True + if not ch.enable: ch.enable = True # Update list items ListItem.refresh_list_items(ch.id_data.yp, repoint_active=True) @@ -4185,7 +4430,7 @@ def execute(self, context): return {'FINISHED'} class YReplaceLayerChannelOverride1(bpy.types.Operator): - bl_idname = "node.y_replace_layer_channel_override_1" + bl_idname = "wm.y_replace_layer_channel_override_1" bl_label = "Replace Layer Channel Normal Override" bl_description = "Replace Layer Channel Normal Override" bl_options = {'REGISTER', 'UNDO'} @@ -4205,6 +4450,7 @@ def execute(self, context): ch = context.parent ch.override_1_type = self.type ch.override_1 = True + if not ch.enable: ch.enable = True # Update list items ListItem.refresh_list_items(ch.id_data.yp, repoint_active=True) @@ -4212,7 +4458,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveLayerChannelOverrideSource(bpy.types.Operator): - bl_idname = "node.y_remove_channel_override_source" + bl_idname = "wm.y_remove_channel_override_source" bl_label = "Replace Layer Channel Override Source" bl_description = "Replace Layer Channel Override Source" bl_options = {'REGISTER', 'UNDO'} @@ -4232,7 +4478,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveLayerChannelOverride1Source(bpy.types.Operator): - bl_idname = "node.y_remove_channel_override_1_source" + bl_idname = "wm.y_remove_channel_override_1_source" bl_label = "Replace Layer Channel Normal Override Source" bl_description = "Replace Layer Channel Normal Override Source" bl_options = {'REGISTER', 'UNDO'} @@ -4252,7 +4498,7 @@ def execute(self, context): return {'FINISHED'} class YSetLayerChannelNormalBlendType(bpy.types.Operator): - bl_idname = "node.y_set_layer_channel_normal_blend_type" + bl_idname = "wm.y_set_layer_channel_normal_blend_type" bl_label = "Set Layer Channel Normal Blend Type" bl_description = "Set layer channel normal blend type" bl_options = {'UNDO'} @@ -4276,7 +4522,7 @@ def execute(self, context): return {'FINISHED'} class YSetLayerChannelBlendType(bpy.types.Operator): - bl_idname = "node.y_set_layer_channel_blend_type" + bl_idname = "wm.y_set_layer_channel_blend_type" bl_label = "Set Layer Channel Blend Type" bl_description = "Set layer channel blend type" bl_options = {'UNDO'} @@ -4300,7 +4546,7 @@ def execute(self, context): return {'FINISHED'} class YSetLayerChannelInput(bpy.types.Operator): - bl_idname = "node.y_set_layer_channel_input" + bl_idname = "wm.y_set_layer_channel_input" bl_label = "Set Layer Channel Input" bl_description = "Set layer channel input" bl_options = {'UNDO'} @@ -4351,8 +4597,45 @@ def execute(self, context): return {'FINISHED'} +class YOpenImageToReplaceLayer(bpy.types.Operator, ImportHelper, BaseOperator.OpenImage): + """Open Image to Replace Layer""" + bl_idname = "wm.y_open_image_to_replace_layer" + bl_label = "Open Image to Replace Layer" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + group_node = get_active_ypaint_node() + return context.object and group_node and len(group_node.node_tree.yp.layers) > 0 + + def invoke(self, context, event): + self.layer = context.layer + return self.running_fileselect_modal(context, event) + + def execute(self, context): + T = time.time() + + wm = context.window_manager + layer = self.layer + yp = layer.id_data.yp + + loaded_images = self.get_loaded_images() + + if len(loaded_images) == 0 or loaded_images[0] == None: + self.report({'ERROR'}, "No image is selected!") + return {'CANCELLED'} + + image = loaded_images[0] + + replace_layer_type(layer, 'IMAGE', image.name) + + print('INFO: Layer', layer.name, 'is updated in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') + wm.yptimer.time = str(time.time()) + + return {'FINISHED'} + class YReplaceLayerType(bpy.types.Operator): - bl_idname = "node.y_replace_layer_type" + bl_idname = "wm.y_replace_layer_type" bl_label = "Replace Layer Type" bl_description = "Replace Layer Type" bl_options = {'REGISTER', 'UNDO'} @@ -4417,10 +4700,6 @@ def execute(self, context): layer = self.layer yp = layer.id_data.yp - if layer.use_temp_bake: - self.report({'ERROR'}, "Cannot replace temporarily baked layer!") - return {'CANCELLED'} - if self.type == layer.type and self.type not in {'IMAGE', 'VCOL'}: return {'CANCELLED'} #if layer.type == 'GROUP': # self.report({'ERROR'}, "You can't change type of group layer!") @@ -4437,7 +4716,46 @@ def execute(self, context): return {'FINISHED'} -def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate=True, duplicate_blank=False, ondisk_duplicate=False, set_new_decal_position=False): +def update_driver_targets(obj, target_map): + # Update driver target object references based on a given object map. + for fcurve in obj.animation_data.drivers if obj.animation_data else []: + for var in fcurve.driver.variables: + for target in var.targets: + if target.id in target_map: + target.id = target_map[target.id] + +def duplicate_decal_empty_reference(texcoord_name, ttree, set_new_decal_position, duplicated_empties): + texcoord = ttree.nodes.get(texcoord_name) + if not texcoord or not hasattr(texcoord, 'object') or not texcoord.object: + return + + original_empty = texcoord.object + + if set_new_decal_position: + texcoord.object = create_decal_empty() + else: + if original_empty in duplicated_empties: + new_empty = duplicated_empties[original_empty] + else: + nname = get_unique_name(original_empty.name, bpy.data.objects) + custom_collection = ( + original_empty.users_collection[0] + if is_bl_newer_than(2, 80) and len(original_empty.users_collection) > 0 + else None + ) + new_empty = original_empty.copy() + new_empty.name = nname + link_object(bpy.context.scene, new_empty, custom_collection) + + duplicated_empties[original_empty] = new_empty + + # Update drivers on the new empty to point to any other duplicated empties + update_driver_targets(new_empty, duplicated_empties) + + texcoord.object = new_empty + + +def duplicate_layer_nodes_and_images(tree, specific_layers=[], packed_duplicate=True, duplicate_blank=False, ondisk_duplicate=False, set_new_decal_position=False): yp = tree.yp ypup = get_user_preferences() @@ -4446,8 +4764,13 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate img_nodes = [] imgs = [] + vcol_users = [] + vcol_user_types = [] + vcol_nodes = [] + vcol_names = [] + duplicated_empties = {} for layer in yp.layers: - if specific_layer and layer != specific_layer: continue + if specific_layers and layer not in specific_layers: continue oldtree = get_tree(layer) ttree = oldtree.copy() @@ -4485,16 +4808,7 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate # Decal object duplicate if layer.texcoord_type == 'Decal': - texcoord = ttree.nodes.get(layer.texcoord) - if texcoord and hasattr(texcoord, 'object') and texcoord.object: - if set_new_decal_position: - texcoord.object = create_decal_empty() - else: - nname = get_unique_name(texcoord.object.name, bpy.data.objects) - custom_collection = texcoord.object.users_collection[0] if is_bl_newer_than(2, 80) and texcoord.object and len(texcoord.object.users_collection) > 0 else None - texcoord.object = texcoord.object.copy() - texcoord.object.name = nname - link_object(bpy.context.scene, texcoord.object, custom_collection) + duplicate_decal_empty_reference(layer.texcoord, ttree, set_new_decal_position, duplicated_empties) # Duplicate baked layer image baked_layer_source = get_layer_source(layer, get_baked=True) @@ -4512,18 +4826,37 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate img_users.append(layer) img_nodes.append(source) imgs.append(img) + + elif layer.type == 'VCOL': + vcol_name = source.attribute_name + if vcol_name != '': + vcol_users.append(layer) + vcol_user_types.append('LAYER') + vcol_nodes.append(source) + vcol_names.append(vcol_name) + elif layer.type == 'HEMI': duplicate_lib_node_tree(source) # Duplicate override channel for ch in layer.channels: - if ch.override and ch.override_type == 'IMAGE': + if ch.override: ch_source = get_channel_source(ch, layer) - img = ch_source.image - if img: - img_users.append(ch) - img_nodes.append(ch_source) - imgs.append(img) + + if ch.override_type == 'IMAGE': + img = ch_source.image + if img: + img_users.append(ch) + img_nodes.append(ch_source) + imgs.append(img) + + elif ch.override_type == 'VCOL': + vcol_name = ch_source.attribute_name + if vcol_name != '': + vcol_users.append(ch) + vcol_user_types.append('CHANNEL') + vcol_nodes.append(ch_source) + vcol_names.append(vcol_name) if ch.override_1 and ch.override_1_type == 'IMAGE': ch_source = get_channel_source_1(ch, layer) @@ -4534,6 +4867,7 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate imgs.append(img) # Duplicate masks + for mask in layer.masks: if mask.group_node != '': mask_group = ttree.nodes.get(mask.group_node) @@ -4547,17 +4881,8 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate mask_source = ttree.nodes.get(mask.source) # Decal object duplicate if mask.texcoord_type == 'Decal': - texcoord = ttree.nodes.get(mask.texcoord) - if texcoord and hasattr(texcoord, 'object') and texcoord.object: - if set_new_decal_position: - texcoord.object = create_decal_empty() - else: - nname = get_unique_name(texcoord.object.name, bpy.data.objects) - custom_collection = texcoord.object.users_collection[0] if is_bl_newer_than(2, 80) and texcoord.object and len(texcoord.object.users_collection) > 0 else None - texcoord.object = texcoord.object.copy() - texcoord.object.name = nname - link_object(bpy.context.scene, texcoord.object, custom_collection) - + duplicate_decal_empty_reference(mask.texcoord, ttree, set_new_decal_position, duplicated_empties) + # Duplicate baked mask image baked_mask_source = get_mask_source(mask, get_baked=True) if baked_mask_source: @@ -4574,6 +4899,13 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate img_users.append(mask) img_nodes.append(mask_source) imgs.append(img) + elif mask.type == 'VCOL': + vcol_name = mask_source.attribute_name + if vcol_name != '': + vcol_users.append(mask) + vcol_user_types.append('MASK') + vcol_nodes.append(mask_source) + vcol_names.append(vcol_name) elif mask.type == 'HEMI': duplicate_lib_node_tree(mask_source) @@ -4617,6 +4949,42 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate if n.type == 'GROUP' and n != ori: n.node_tree = ori.node_tree + # Copy vertex color on layer and masks + objs = get_all_objects_with_same_materials(get_active_material()) + for i, vcol_name in enumerate(vcol_names): + + # Get all available vcol names across all objects + all_vcol_names = [] + for obj in objs: + vcols = get_vertex_colors(obj) + for vcol in vcols: + if vcol.name not in all_vcol_names: + all_vcol_names.append(vcol.name) + + # Get new name based on already available vcol names + new_vcol_name = get_unique_name(vcol_name, all_vcol_names) + + # Duplicate vertex color + for obj in objs: + vcols = get_vertex_colors(obj) + if vcol_name in vcols: + vcol = vcols.get(vcol_name) + + if vcol_user_types[i] == 'LAYER': + color = (0.0, 0.0, 0.0, 0.0) + else: color = (0.0, 0.0, 0.0, 1.0) + + new_vcol = new_vertex_color(obj, new_vcol_name, vcol.data_type, vcol.domain, color_fill=color) + + if not duplicate_blank: + copy_vertex_color_data(obj, vcol_name, new_vcol_name) + + # Set new vertex color to node and user + vcol_nodes[i].attribute_name = new_vcol_name + yp.halt_update = True + vcol_users[i].name = new_vcol_name + yp.halt_update = False + # Make all images single user #if packed_duplicate: @@ -4644,7 +5012,7 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate img.yia.color, img.is_float, yp=yp) # If using different image atlas per yp, just copy the image (unless specific layer is on) - elif ypup.unique_image_atlas_per_yp and not specific_layer: + elif ypup.unique_image_atlas_per_yp and not specific_layers: if img.name not in copied_image_atlas: copied_image_atlas[img.name] = duplicate_image(img) img_nodes[i].image = copied_image_atlas[img.name] @@ -4680,7 +5048,7 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate ) # If using different image atlas per yp, just copy the image (unless specific layer is on) - elif not specific_layer: + elif not specific_layers: if img.name not in copied_image_atlas: copied_image_atlas[img.name] = duplicate_image(img) img_nodes[i].image = copied_image_atlas[img.name] @@ -4747,7 +5115,7 @@ def duplicate_layer_nodes_and_images(tree, specific_layer=None, packed_duplicate already_copied_ids.append(j) class YDuplicateLayer(bpy.types.Operator): - bl_idname = "node.y_duplicate_layer" + bl_idname = "wm.y_duplicate_layer" bl_label = "Duplicate layer" bl_description = "Duplicate Layer" bl_options = {'UNDO'} @@ -4797,7 +5165,7 @@ def invoke(self, context, event): if not self.duplicate_blank: self.any_packed_image = any(get_layer_images(layer, packed_only=True)) self.any_ondisk_image = any(get_layer_images(layer, ondisk_only=True)) - self.any_decal = any_decal_inside_layer(layer) + self.any_decal = Decal.any_decal_inside_layer(layer) if get_user_preferences().skip_property_popups and not event.shift: return self.execute(context) @@ -4849,10 +5217,10 @@ def execute(self, context): # List of newly created ids created_ids = [] + created_layer_names = [] # Duplicate all relevant layers for i, lname in enumerate(relevant_layer_names): - #idx = relevant_ids[i] # Create new layer new_layer = yp.layers.add() @@ -4875,14 +5243,6 @@ def execute(self, context): source_inp = source_node.inputs.get(inp.name) if source_inp: inp.default_value = source_inp.default_value - duplicate_layer_nodes_and_images( - tree, new_layer, - packed_duplicate = self.packed_duplicate or self.duplicate_blank, - duplicate_blank = self.duplicate_blank, - ondisk_duplicate = self.ondisk_duplicate or self.duplicate_blank, - set_new_decal_position = self.set_new_decal_position - ) - # Rename masks mask_names = [m.name for m in l.masks] for mask in new_layer.masks: @@ -4898,9 +5258,18 @@ def execute(self, context): mask.name = get_unique_name(mask.name, mask_names) mask_names.append(mask.name) - #yp.layers.move(len(yp.layers)-1, idx) + created_layer_names.append(new_layer.name) created_ids.append(len(yp.layers)-1) + # Duplicate data of newly created layers + created_layers = [l for l in yp.layers if l.name in created_layer_names] + duplicate_layer_nodes_and_images( + tree, created_layers, packed_duplicate = self.packed_duplicate or self.duplicate_blank, + duplicate_blank = self.duplicate_blank, + ondisk_duplicate = self.ondisk_duplicate or self.duplicate_blank, + set_new_decal_position = self.set_new_decal_position + ) + # Move duplicated layer to current index for i, idx in enumerate(created_ids): relevant_id = relevant_ids[i] @@ -4933,7 +5302,7 @@ def execute(self, context): return {'FINISHED'} class YCopyLayer(bpy.types.Operator): - bl_idname = "node.y_copy_layer" + bl_idname = "wm.y_copy_layer" bl_label = "Copy Layer" bl_description = "Copy Layer" bl_options = {'REGISTER', 'UNDO'} @@ -4962,7 +5331,7 @@ def execute(self, context): return {'FINISHED'} class YPasteLayer(bpy.types.Operator): - bl_idname = "node.y_paste_layer" + bl_idname = "wm.y_paste_layer" bl_label = "Paste Layer" bl_description = "Paste Layer" bl_options = {'UNDO'} @@ -4985,6 +5354,12 @@ class YPasteLayer(bpy.types.Operator): default = False ) + rebake_bakeds : BoolProperty( + name = 'Rebake Baked Images', + description = 'Rebake baked images', + default = True + ) + @classmethod def poll(cls, context): group_node = get_active_ypaint_node() @@ -4995,24 +5370,37 @@ def description(self, context, properties): return get_operator_description(self) def invoke(self, context, event): + group_node = get_active_ypaint_node() + yp = group_node.node_tree.yp + wm = context.window_manager wmp = wm.ypprops self.any_packed_image = False self.any_ondisk_image = False self.any_decal = False + self.any_baked = False tree_source = bpy.data.node_groups.get(wmp.clipboard_tree) if tree_source: yp_source = tree_source.yp - layer_source = yp_source.layers.get(wmp.clipboard_layer) + source_layers = [] + if wmp.clipboard_layer == '': + source_layers = yp_source.layers + else: + layer = yp_source.layers.get(wmp.clipboard_layer) + source_layers.append(layer) - if layer_source: - self.any_packed_image = any(get_layer_images(layer_source, packed_only=True)) - self.any_ondisk_image = any(get_layer_images(layer_source, ondisk_only=True)) - self.any_decal = any_decal_inside_layer(layer_source) + for layer in source_layers: + if not self.any_packed_image: self.any_packed_image = any(get_layer_images(layer, packed_only=True)) + if not self.any_ondisk_image: self.any_ondisk_image = any(get_layer_images(layer, ondisk_only=True)) + if not self.any_decal: self.any_decal = Decal.any_decal_inside_layer(layer) - if self.any_packed_image or self.any_ondisk_image or self.any_decal: + # Do not check baked if current yp == yp_source + if yp != yp_source: + if not self.any_baked: self.any_baked = any(get_layer_images(layer, baked_only=True)) + + if self.any_packed_image or self.any_ondisk_image or self.any_decal or self.any_baked: if get_user_preferences().skip_property_popups and not event.shift: return self.execute(context) @@ -5020,6 +5408,9 @@ def invoke(self, context, event): return self.execute(context) + def check(self, context): + return True + def draw(self, context): if self.any_packed_image: self.layout.prop(self, 'packed_duplicate') @@ -5030,6 +5421,12 @@ def draw(self, context): if self.any_decal: self.layout.prop(self, 'set_new_decal_position') + if self.any_baked and is_bl_newer_than(2, 80): + self.layout.prop(self, 'rebake_bakeds') + + if self.rebake_bakeds: + self.layout.label(text='Rebaking can take a while', icon='ERROR') + def execute(self, context): T = time.time() @@ -5063,10 +5460,6 @@ def execute(self, context): matching = False break - if not matching: - self.report({'ERROR'}, "Copied tree has different channel names or orders!") - return {'CANCELLED'} - if wmp.clipboard_layer == '': if len(yp_source.layers) == 0: @@ -5097,6 +5490,14 @@ def execute(self, context): for child in children: relevant_layer_names.append(child.name) + # Disable source layers if channel list is not matching + ori_layer_enables = {} + if not matching: + for lname in relevant_layer_names: + l = yp_source.layers.get(lname) + ori_layer_enables[lname] = l.enable + l.enable = False + # Get parent and index dict parent_dict = get_parent_dict(yp) index_dict = get_index_dict(yp) @@ -5123,28 +5524,67 @@ def execute(self, context): new_layer = yp.layers.add() new_layer.name = get_unique_name(ls.name, yp.layers) + # Get original source layer again to avoid pointer error after adding new layer + ls = yp_source.layers.get(lname) + + # Copy layer props copy_id_props(ls, new_layer, ['name']) + if not matching: + # Clear out layer channel props + new_layer.channels.clear() + for mask in new_layer.masks: + mask.channels.clear() + + for root_ch in yp.channels: + + # New layer channel + new_ch = new_layer.channels.add() + new_ch.enable = new_layer.type in {'GROUP', 'BACKGROUND'} # Layer channel default is disabled except for group and background + + # Get matching channel on source yp + source_idx = -1 + if root_ch.name in yp_source.channels: + source_idx = get_channel_index(yp_source.channels.get(root_ch.name)) + + # Copy layer channel props + if source_idx != -1: + copy_id_props(ls.channels[source_idx], new_ch) + + for i, mask in enumerate(new_layer.masks): + + # New mask channel + mch = mask.channels.add() + mch.enable = True # Mask channel default is enabled + + # Copy mask channel props + if source_idx != -1: + copy_id_props(ls.masks[i].channels[source_idx], mch) + + # Reenable new layer + if ls.name in ori_layer_enables: + new_layer.enable = ori_layer_enables[ls.name] + # Duplicate groups new_group_node = new_node(tree, new_layer, 'group_node', 'ShaderNodeGroup', new_layer.name) new_group_node.node_tree = get_tree(ls) - # Duplicate group inputs + # Duplicate group input values source_node = tree_source.nodes.get(ls.group_node) for inp in new_group_node.inputs: source_inp = source_node.inputs.get(inp.name) if source_inp: inp.default_value = source_inp.default_value - # Duplicate images and some nodes inside - duplicate_layer_nodes_and_images( - tree, new_layer, - packed_duplicate = self.packed_duplicate, - ondisk_duplicate = self.ondisk_duplicate, - set_new_decal_position = self.set_new_decal_position - ) - pasted_layer_names.append(new_layer.name) + # Duplicate data of pasted layers + pasted_layers = [l for l in yp.layers if l.name in pasted_layer_names] + duplicate_layer_nodes_and_images( + tree, pasted_layers, packed_duplicate = self.packed_duplicate, + ondisk_duplicate = self.ondisk_duplicate, + set_new_decal_position = self.set_new_decal_position + ) + # Move pasted layer to current index for i, lname in enumerate(pasted_layer_names): nl = yp.layers.get(lname) @@ -5189,88 +5629,34 @@ def execute(self, context): reconnect_yp_nodes(tree) rearrange_yp_nodes(tree) - # Refresh active layer - yp.active_layer_index = yp.active_layer_index - - # Update list items - ListItem.refresh_list_items(yp) + # Revert original layer channel enables + for lname, lenable in ori_layer_enables.items(): + l = yp_source.layers.get(lname) + if l: l.enable = lenable - print('INFO: Layer(s) pasted in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') - wm.yptimer.time = str(time.time()) - - return {'FINISHED'} + # Rebake baked images + # NOTE: Blender versions lower than 2.80 don't copy image's bake info, making rebake process useless + if self.any_baked and self.rebake_bakeds and is_bl_newer_than(2, 80): -class YSelectDecalObject(bpy.types.Operator): - bl_idname = "node.y_select_decal_object" - bl_label = "Select Decal Object" - bl_description = "Select Decal Object" - bl_options = {'REGISTER', 'UNDO'} + # NOTE: Calling rebake function directly is not possible yet due to cyclic file imports + #pasted_layer = [l for l in yp.layers if l.name in pasted_layer_names] + #bake_common.rebake_baked_images(yp, specific_layers=pasted_layers) - @classmethod - def poll(cls, context): - group_node = get_active_ypaint_node() - return group_node and hasattr(context, 'entity') + pasted_layer_ids = [i for i, l in enumerate(yp.layers) if l.name in pasted_layer_names] + bpy.ops.wm.y_rebake_specific_layers(layer_ids=str(pasted_layer_ids)) - def execute(self, context): - scene = context.scene - entity = context.entity - - m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) - m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) - - if m1: tree = get_tree(entity) - elif m2: tree = get_mask_tree(entity) - else: return {'CANCELLED'} - - texcoord = tree.nodes.get(entity.texcoord) - - if texcoord and hasattr(texcoord, 'object') and texcoord.object: - try: bpy.ops.object.mode_set(mode='OBJECT') - except: pass - bpy.ops.object.select_all(action='DESELECT') - if texcoord.object.name not in get_scene_objects(): - parent = texcoord.object.parent - custom_collection = parent.users_collection[0] if is_bl_newer_than(2, 80) and parent and len(parent.users_collection) > 0 else None - link_object(scene, texcoord.object, custom_collection) - set_active_object(texcoord.object) - set_object_select(texcoord.object, True) - else: return {'CANCELLED'} + self.report({'INFO'}, 'Rebaking pasted layers is done in '+'{:0.2f}'.format(time.time() - T)+' seconds!') - return {'FINISHED'} + # TODO: Refactor common functions for adding new data (add_new_layer, add_new_mask, etc) to avoid cyclic imports -class YSetDecalObjectPositionToCursor(bpy.types.Operator): - bl_idname = "node.y_set_decal_object_position_to_sursor" - bl_label = "Set Decal Position to Cursor" - bl_description = "Set the position of the decal object to the 3D cursor" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - group_node = get_active_ypaint_node() - return group_node and hasattr(context, 'entity') - - def execute(self, context): - scene = bpy.context.scene - entity = context.entity - - m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) - m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) - - if m1: tree = get_tree(entity) - elif m2: tree = get_mask_tree(entity) - else: return {'CANCELLED'} - - texcoord = tree.nodes.get(entity.texcoord) + # Refresh active layer + yp.active_layer_index = yp.active_layer_index - if texcoord and hasattr(texcoord, 'object') and texcoord.object: - # Move decal object to 3D cursor - if is_bl_newer_than(2, 80): - texcoord.object.location = scene.cursor.location.copy() - texcoord.object.rotation_euler = scene.cursor.rotation_euler.copy() - else: - texcoord.object.location = scene.cursor_location.copy() + # Update list items + ListItem.refresh_list_items(yp) - else: return {'CANCELLED'} + print('INFO: Layer(s) pasted in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') + wm.yptimer.time = str(time.time()) return {'FINISHED'} @@ -5683,6 +6069,10 @@ def update_hemi_use_prev_normal(self, context): layer = self tree = get_tree(layer) + if layer.type == 'EDGE_DETECT': + source = get_layer_source(layer) + Mask.setup_edge_detect_source(layer, source) + check_layer_tree_ios(layer, tree) check_layer_bump_process(layer, tree) @@ -6071,6 +6461,8 @@ class YLayerChannel(bpy.types.PropertyGroup): # Vector Displacement related vdisp_proc : StringProperty(default='') + vdisp_blend : StringProperty(default='') + vdisp_intensity : StringProperty(default='') # For pack/unpack height io height_group_unpack : StringProperty(default='') @@ -6190,7 +6582,7 @@ class YLayerChannel(bpy.types.PropertyGroup): #description= 'Distance of mask bump', name = 'Transition Bump Height Range', description = 'Transition bump height range.\n(White equals this value, black equals negative of this value)', - default=0.05, min=0.0, max=1.0, precision=3 + default=0.05, min=-1.0, max=1.0, precision=3 ) transition_bump_chain : IntProperty( @@ -6396,15 +6788,15 @@ class YLayerChannel(bpy.types.PropertyGroup): tao : StringProperty(default='') active_edit : BoolProperty( - name = 'Active override channel for editing or preview', - description = 'Active override channel for editing or preview', + name = 'Active Custom Data', + description = 'Active custom data for Blender\'s paint mode and edit mode, or '+get_addon_title()+'\'s Custom Data preview mode', default = False, update = update_channel_active_edit ) active_edit_1 : BoolProperty( - name = 'Active override channel for editing or preview', - description = 'Active override channel for editing or preview', + name = 'Active Custom Normal Data', + description = 'Active custom normal data for Blender\'s paint mode and edit mode, or '+get_addon_title()+'\'s Custom Data preview mode', default = False, update = update_channel_active_edit ) @@ -6489,7 +6881,40 @@ def update_layer_uniform_scale_enabled(self, context): reconnect_layer_nodes(layer) rearrange_layer_nodes(layer) -class YLayer(bpy.types.PropertyGroup): +def update_layer_edge_detect_radius(self, context): + yp = self.id_data.yp + if yp.halt_update: return + layer = self + + source = get_layer_source(layer) + if source: source.inputs[0].default_value = self.edge_detect_radius + +def update_layer_use_baked(self, context): + yp = self.id_data.yp + if yp.halt_update: return + + layer = self + yp = self.id_data.yp + tree = get_tree(layer) + + # Update global uv + check_uv_nodes(yp) + + # Update layer tree inputs + check_all_layer_channel_io_and_nodes(layer) + check_start_end_root_ch_nodes(self.id_data) + + # Refresh active image by setting active layer + if get_layer_index(layer) == yp.active_layer_index: + yp.active_layer_index = yp.active_layer_index + + reconnect_layer_nodes(layer) + rearrange_layer_nodes(layer) + + reconnect_yp_nodes(self.id_data) + rearrange_yp_nodes(self.id_data) + +class YLayer(bpy.types.PropertyGroup, Decal.BaseDecal): name : StringProperty( name = 'Layer Name', description = 'Layer name', @@ -6537,17 +6962,6 @@ class YLayer(bpy.types.PropertyGroup): update = update_texcoord_type ) - original_texcoord : EnumProperty( - name = 'Original Layer Coordinate Type', - items = texcoord_type_items, - default = 'UV' - ) - - original_image_extension : StringProperty( - name = 'Original Image Extension Type', - default = '' - ) - projection_blend : FloatProperty( name = 'Box Projection Blend', description = 'Amount of blend to use between sides', @@ -6556,6 +6970,21 @@ class YLayer(bpy.types.PropertyGroup): update = update_projection_blend ) + # For edge detection + edge_detect_radius : FloatProperty( + name = 'Edge Detect Radius', + description = 'Edge detect radius', + default=0.05, min=0.0, max=10.0, precision=3, + update = update_layer_edge_detect_radius + ) + + # For AO + ao_distance : FloatProperty( + name = 'Ambient Occlusion Distance', + description = 'Ambient occlusion distance', + default=1.0, min=0.0, max=10.0 + ) + # Specific for voronoi voronoi_feature : EnumProperty( name = 'Voronoi Feature', @@ -6565,14 +6994,6 @@ class YLayer(bpy.types.PropertyGroup): update = update_voronoi_feature ) - # For temporary bake - use_temp_bake : BoolProperty( - name = 'Use Temporary Bake', - description = 'Use temporary bake, it can be useful for prevent glitching with cycles', - default = False, - #update=update_layer_temp_bake - ) - original_type : EnumProperty( name = 'Original Layer Type', items = layer_type_items, @@ -6690,16 +7111,11 @@ class YLayer(bpy.types.PropertyGroup): update = update_layer_blur_vector_factor ) - decal_distance_value : FloatProperty( - name = 'Decal Distance', - description = 'Distance between surface and the decal object', - min=0.0, max=100.0, default=0.5, precision=3 - ) - use_baked : BoolProperty( name = 'Use Baked', description = 'Use baked layer image', - default = False + default = False, + update = update_layer_use_baked ) # Sources @@ -6802,6 +7218,8 @@ def register(): bpy.utils.register_class(YOpenImageToLayer) bpy.utils.register_class(YOpenImagesToSingleLayer) bpy.utils.register_class(YOpenImagesFromMaterialToLayer) + bpy.utils.register_class(YOpenLayersFromMaterial) + bpy.utils.register_class(YOpenImageToReplaceLayer) bpy.utils.register_class(YOpenImageToOverrideChannel) bpy.utils.register_class(YOpenImageToOverride1Channel) bpy.utils.register_class(YOpenAvailableDataToLayer) @@ -6823,8 +7241,6 @@ def register(): bpy.utils.register_class(YDuplicateLayer) bpy.utils.register_class(YCopyLayer) bpy.utils.register_class(YPasteLayer) - bpy.utils.register_class(YSelectDecalObject) - bpy.utils.register_class(YSetDecalObjectPositionToCursor) bpy.utils.register_class(YLayerChannel) bpy.utils.register_class(YLayer) @@ -6836,7 +7252,9 @@ def unregister(): bpy.utils.unregister_class(YNewVcolToOverrideChannel) bpy.utils.unregister_class(YOpenImageToLayer) bpy.utils.unregister_class(YOpenImagesToSingleLayer) + bpy.utils.unregister_class(YOpenLayersFromMaterial) bpy.utils.unregister_class(YOpenImagesFromMaterialToLayer) + bpy.utils.unregister_class(YOpenImageToReplaceLayer) bpy.utils.unregister_class(YOpenImageToOverrideChannel) bpy.utils.unregister_class(YOpenImageToOverride1Channel) bpy.utils.unregister_class(YOpenAvailableDataToLayer) @@ -6858,7 +7276,5 @@ def unregister(): bpy.utils.unregister_class(YDuplicateLayer) bpy.utils.unregister_class(YCopyLayer) bpy.utils.unregister_class(YPasteLayer) - bpy.utils.unregister_class(YSelectDecalObject) - bpy.utils.unregister_class(YSetDecalObjectPositionToCursor) bpy.utils.unregister_class(YLayerChannel) bpy.utils.unregister_class(YLayer) diff --git a/ListItem.py b/ListItem.py index e9f82820..78d83829 100644 --- a/ListItem.py +++ b/ListItem.py @@ -53,11 +53,11 @@ def refresh_list_items(yp, repoint_active=False): for i, ch in enumerate(layer.channels): if not ch.enable: continue root_ch = yp.channels[i] - if ch.override and ch.override_type in {'IMAGE', 'VCOL'} and ch.active_edit: + if ch.override and ch.override_type != 'DEFAULT' and ch.active_edit: active_item_name = layer.name + ' ' + root_ch.name active_item_type = 'CHANNEL_OVERRIDE' - if root_ch.type == 'NORMAL' and ch.override_1 and ch.override_1_type == 'IMAGE' and ch.active_edit_1: + if root_ch.type == 'NORMAL' and ch.override_1 and ch.override_1_type != 'DEFAULT' and ch.active_edit_1: active_item_name = layer.name + ' ' + root_ch.name + ' 1' active_item_type = 'CHANNEL_OVERRIDE' active_item_is_second_member = True @@ -110,7 +110,7 @@ def refresh_list_items(yp, repoint_active=False): # Channel Override if (layer.expand_subitems and (root_ch.type != 'NORMAL' or ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}) and - (ch.override and ch.override_type in {'IMAGE', 'VCOL'}) and + (ch.override and ch.override_type != 'DEFAULT') and ch.enable and yp.enable_expandable_subitems ): @@ -136,7 +136,7 @@ def refresh_list_items(yp, repoint_active=False): # Channel Override 1 / Normal if (layer.expand_subitems and (root_ch.type == 'NORMAL' and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}) and - (ch.override_1 and ch.override_1_type in {'IMAGE', 'VCOL'}) and + (ch.override_1 and ch.override_1_type != 'DEFAULT') and ch.enable and yp.enable_expandable_subitems ): @@ -213,7 +213,7 @@ class YListItem(bpy.types.PropertyGroup): ) class YRefreshListItems(bpy.types.Operator): - bl_idname = "node.y_refresh_list_items" + bl_idname = "wm.y_refresh_list_items" bl_label = "Refresh List Items" bl_description = "Refresh List Items" bl_options = {'REGISTER', 'UNDO'} diff --git a/Mask.py b/Mask.py index cee8dea7..aa60bbfe 100644 --- a/Mask.py +++ b/Mask.py @@ -1,7 +1,7 @@ import bpy, re, time, random from bpy.props import * from bpy_extras.io_utils import ImportHelper -from . import lib, ImageAtlas, MaskModifier, UDIM, ListItem +from . import lib, ImageAtlas, MaskModifier, UDIM, ListItem, BaseOperator, Decal from .common import * from .node_connections import * from .node_arrangements import * @@ -32,15 +32,22 @@ def setup_object_idx_source(mask, source, object_index=None): source.inputs[0].default_value = object_index -def setup_edge_detect_source(mask, source, edge_detect_radius=None): - source.node_tree = get_node_tree_lib(lib.EDGE_DETECT) +def setup_edge_detect_source(entity, source, edge_detect_radius=None): + if entity.hemi_use_prev_normal: + lib_name = lib.EDGE_DETECT_CUSTOM_NORMAL + else: lib_name = lib.EDGE_DETECT + + ori_lib = source.node_tree + if not ori_lib or ori_lib.name != lib_name: + source.node_tree = get_node_tree_lib(lib_name) + if ori_lib and ori_lib.users == 0: + remove_datablock(bpy.data.node_groups, ori_lib) + if edge_detect_radius != None: - source.inputs[0].default_value = mask.edge_detect_radius = edge_detect_radius - else: source.inputs[0].default_value = mask.edge_detect_radius + source.inputs[0].default_value = entity.edge_detect_radius = edge_detect_radius + else: source.inputs[0].default_value = entity.edge_detect_radius - # Enable AO to see edge detect mask - scene = bpy.context.scene - if not scene.eevee.use_gtao: scene.eevee.use_gtao = True + enable_eevee_ao() def setup_modifier_mask_source(tree, mask, modifier_type): source = None @@ -55,10 +62,10 @@ def setup_modifier_mask_source(tree, mask, modifier_type): return source def add_new_mask( - layer, name, mask_type, texcoord_type, uv_name, image=None, vcol=None, segment=None, + layer, name, mask_type, texcoord_type, uv_name, image=None, vcol_name='', segment=None, object_index=0, blend_type='MULTIPLY', hemi_space='WORLD', hemi_use_prev_normal=False, color_id=(1, 0, 1), source_input='RGB', edge_detect_radius=0.05, - modifier_type='INVERT', interpolation='Linear' + modifier_type='INVERT', interpolation='Linear', ao_distance=1.0 ): yp = layer.id_data.yp yp.halt_update = True @@ -95,7 +102,7 @@ def add_new_mask( source.color_space = 'NONE' source.interpolation = interpolation elif mask_type == 'VCOL': - if vcol: set_source_vcol_name(source, vcol.name) + if vcol_name != '': set_source_vcol_name(source, vcol_name) else: set_source_vcol_name(source, name) if mask_type == 'HEMI': @@ -104,17 +111,28 @@ def add_new_mask( mask.hemi_space = hemi_space mask.hemi_use_prev_normal = hemi_use_prev_normal - if mask_type == 'OBJECT_INDEX': + elif mask_type == 'OBJECT_INDEX': setup_object_idx_source(mask, source, object_index) - if mask_type == 'COLOR_ID': + elif mask_type == 'COLOR_ID': setup_color_id_source(mask, source, color_id) - if mask_type == 'EDGE_DETECT': + elif mask_type == 'EDGE_DETECT': + mask.hemi_use_prev_normal = hemi_use_prev_normal setup_edge_detect_source(mask, source, edge_detect_radius) + elif mask_type == 'AO': + mask.hemi_use_prev_normal = hemi_use_prev_normal + mask.ao_distance = ao_distance + enable_eevee_ao() + + # Set default uv name if it's an empty string + if uv_name == '': + uv_name = get_default_uv_name() + + mask.uv_name = uv_name + if is_mapping_possible(mask_type): - mask.uv_name = uv_name mapping = new_node(tree, mask, 'mapping', 'ShaderNodeMapping', 'Mask Mapping') mapping.vector_type = 'POINT' if segment else 'TEXTURE' @@ -124,7 +142,6 @@ def add_new_mask( refresh_temp_uv(bpy.context.object, mask) for i, root_ch in enumerate(yp.channels): - ch = layer.channels[i] c = mask.channels.add() mask.blend_type = blend_type @@ -166,6 +183,7 @@ def remove_mask_channel_nodes(tree, c): remove_node(tree, c, 'mix_pure') remove_node(tree, c, 'mix_remains') remove_node(tree, c, 'mix_normal') + remove_node(tree, c, 'mix_vdisp') remove_node(tree, c, 'mix_limit') remove_node(tree, c, 'mix_limit_normal') @@ -185,7 +203,7 @@ def remove_mask_channel(tree, layer, ch_index): for mask in layer.masks: mask.channels.remove(ch_index) -def remove_mask(layer, mask, obj): +def remove_mask(layer, mask, obj, refresh_list=True): tree = get_tree(layer) yp = layer.id_data.yp @@ -195,7 +213,7 @@ def remove_mask(layer, mask, obj): mask_index = [i for i, m in enumerate(layer.masks) if m == mask][0] # Dealing with decal object - remove_decal_object(tree, mask) + Decal.remove_decal_object(tree, mask) # Remove mask fcurves first remove_entity_fcurves(mask) @@ -239,7 +257,8 @@ def remove_mask(layer, mask, obj): layer.masks.remove(mask_index) # Update list items - ListItem.refresh_list_items(yp) + if refresh_list: + ListItem.refresh_list_items(yp) def get_new_mask_name(obj, layer, mask_type, modifier_type=''): surname = '(' + layer.name + ')' @@ -284,7 +303,7 @@ def is_mask_type_cacheable(mask_type, modifier_type=''): if mask_type == 'MODIFIER': return modifier_type in {'RAMP', 'CURVE'} - return mask_type not in {'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'EDGE_DETECT', 'BACKFACE'} + return mask_type not in {'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'EDGE_DETECT', 'BACKFACE', 'AO'} def replace_mask_type(mask, new_type, item_name='', remove_data=False, modifier_type='INVERT'): @@ -365,11 +384,15 @@ def replace_mask_type(mask, new_type, item_name='', remove_data=False, modifier_ if new_type == 'MODIFIER': source = setup_modifier_mask_source(tree, mask, modifier_type) - else: source = new_node(tree, mask, 'source', layer_node_bl_idnames[new_type], 'Source') + elif new_type != 'BACKFACE': source = new_node(tree, mask, 'source', layer_node_bl_idnames[new_type], 'Source') if new_type == 'IMAGE': image = bpy.data.images.get(item_name) source.image = image + + if mask.texcoord_type == 'Decal': + source.extension = 'CLIP' + if hasattr(source, 'color_space'): source.color_space = 'NONE' if image.colorspace_settings.name != get_noncolor_name() and not image.is_dirty: @@ -386,7 +409,7 @@ def replace_mask_type(mask, new_type, item_name='', remove_data=False, modifier_ elif new_type == 'COLOR_ID': mat = get_active_material() objs = get_all_objects_with_same_materials(mat) - check_colorid_vcol(objs) + check_colorid_vcol(objs, set_as_active=True) setup_color_id_source(mask, source) elif new_type == 'OBJECT_INDEX': @@ -395,6 +418,9 @@ def replace_mask_type(mask, new_type, item_name='', remove_data=False, modifier_ elif new_type == 'EDGE_DETECT': setup_edge_detect_source(mask, source) + elif new_type == 'AO': + enable_eevee_ao() + # Change mask type ori_type = mask.type mask.type = new_type @@ -472,6 +498,11 @@ def replace_mask_type(mask, new_type, item_name='', remove_data=False, modifier_ mask.uv_name = get_default_uv_name(obj, yp) yp.halt_update = False + # Always remove baked mask when changing type + if mask.use_baked: + mask.use_baked = False + remove_node(tree, mask, 'baked_source') + # Update group ios check_all_layer_channel_io_and_nodes(layer, tree) @@ -522,7 +553,7 @@ def replace_mask_type(mask, new_type, item_name='', remove_data=False, modifier_ mask.expand_source = mask.type not in {'IMAGE'} or (image != None and image.y_bake_info.is_baked and not image.y_bake_info.is_baked_channel) class YNewLayerMask(bpy.types.Operator): - bl_idname = "node.y_new_layer_mask" + bl_idname = "wm.y_new_layer_mask" bl_label = "New Layer Mask" bl_description = "New Layer Mask" bl_options = {'REGISTER', 'UNDO'} @@ -575,6 +606,12 @@ class YNewLayerMask(bpy.types.Operator): default=(1.0, 0.0, 1.0), min=0.0, max=1.0, ) + vcol_fill : BoolProperty( + name = 'Fill Selected Geometry with Vertex Color / Color ID', + description = 'Fill selected geometry with vertex color / color ID', + default = True + ) + hdr : BoolProperty(name='32 bit Float', default=False) texcoord_type : EnumProperty( @@ -620,7 +657,17 @@ class YNewLayerMask(bpy.types.Operator): default=0, min=0 ) - edge_detect_radius : FloatProperty(default=0.05, min=0.0, max=10.0) + edge_detect_radius : FloatProperty( + name = 'Detect Mask Radius', + description = 'Edge detect radius', + default=0.05, min=0.0, max=10.0 + ) + + ao_distance : FloatProperty( + name = 'Ambient Occlusion Distance', + description = 'Ambient occlusion distance', + default=1.0, min=0.0, max=10.0 + ) vcol_data_type : EnumProperty( name = 'Vertex Color Data Type', @@ -694,13 +741,18 @@ def invoke(self, context, event): self.color_id = (random.uniform(COLORID_TOLERANCE, 1.0), random.uniform(COLORID_TOLERANCE, 1.0), random.uniform(COLORID_TOLERANCE, 1.0)) if not is_colorid_already_being_used(yp, self.color_id): break + # Disable use previous normal for edge detect since it has very little effect + if self.type == 'EDGE_DETECT': + self.hemi_use_prev_normal = False + # Make sure decal is off when adding non mappable mask if not is_mapping_possible(self.type) and self.texcoord_type == 'Decal': self.texcoord_type = 'UV' - if obj.type != 'MESH': + if not is_object_work_with_uv(obj): self.texcoord_type = 'Generated' - elif len(obj.data.uv_layers) > 0: + + if obj.type == 'MESH' and len(obj.data.uv_layers) > 0: self.uv_name = get_default_uv_name(obj, yp) @@ -711,9 +763,7 @@ def invoke(self, context, event): self.uv_map_coll.add().name = uv.name # The default blend type for mask is multiply - if len(layer.masks) == 0: - self.blend_type = 'MULTIPLY' - elif self.type in {'MODIFIER'}: + if self.type in {'MODIFIER'}: self.blend_type = 'MIX' else: self.blend_type = 'MULTIPLY' @@ -772,22 +822,32 @@ def draw(self, context): if self.type == 'COLOR_ID': col.label(text='Color ID:') + if obj.mode == 'EDIT': + col.label(text='') - if is_bl_newer_than(3, 2) and self.type == 'VCOL': - col.label(text='Domain:') - col.label(text='Data Type:') + if self.type == 'VCOL': + if is_bl_newer_than(3, 2): + col.label(text='Domain:') + col.label(text='Data Type:') + if obj.mode == 'EDIT' and self.color_option == 'BLACK': + col.label(text='') if self.type == 'HEMI': col.label(text='Space:') - col.label(text='') if self.type == 'EDGE_DETECT': col.label(text='Radius:') + if self.type == 'AO': + col.label(text='AO Distance:') + + if self.type in {'HEMI', 'EDGE_DETECT', 'AO'}: + col.label(text='') + if self.type == 'IMAGE': col.label(text='') - if self.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}: + if self.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'}: col.label(text='Vector:') if self.type == 'IMAGE': if UDIM.is_udim_supported(): @@ -797,8 +857,7 @@ def draw(self, context): if self.type == 'OBJECT_INDEX': col.label(text='Object Index') - if len(layer.masks) > 0: - col.label(text='Blend:') + col.label(text='Blend:') col = row.column(align=False) col.prop(self, 'name', text='') @@ -821,33 +880,45 @@ def draw(self, context): if self.type == 'COLOR_ID': col.prop(self, 'color_id', text='') + if obj.mode == 'EDIT': + col.prop(self, 'vcol_fill', text='Fill Selected Faces') if self.type == 'HEMI': col.prop(self, 'hemi_space', text='') - col.prop(self, 'hemi_use_prev_normal') if self.type == 'EDGE_DETECT': col.prop(self, 'edge_detect_radius', text='') - if is_bl_newer_than(3, 2) and self.type == 'VCOL': - crow = col.row(align=True) - crow.prop(self, 'vcol_domain', expand=True) - crow = col.row(align=True) - crow.prop(self, 'vcol_data_type', expand=True) + if self.type == 'AO': + col.prop(self, 'ao_distance', text='') + + if self.type in {'HEMI', 'EDGE_DETECT', 'AO'}: + col.prop(self, 'hemi_use_prev_normal') + + if self.type == 'VCOL': + if is_bl_newer_than(3, 2): + crow = col.row(align=True) + crow.prop(self, 'vcol_domain', expand=True) + crow = col.row(align=True) + crow.prop(self, 'vcol_data_type', expand=True) + + if obj.mode == 'EDIT' and self.color_option == 'BLACK': + col.prop(self, 'vcol_fill', text='Fill Selected Faces') if self.type == 'IMAGE': col.prop(self, 'hdr') - if self.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}: + if self.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'}: crow = col.row(align=True) crow.prop(self, 'texcoord_type', text='') if obj.type == 'MESH' and self.texcoord_type == 'UV': crow.prop_search(self, "uv_name", self, "uv_map_coll", text='', icon='GROUP_UVS') - if self.type == 'IMAGE': - if UDIM.is_udim_supported(): - col.prop(self, 'use_udim') - ccol = col.column() - ccol.prop(self, 'use_image_atlas') + + if self.type == 'IMAGE': + if UDIM.is_udim_supported(): + col.prop(self, 'use_udim') + ccol = col.column() + ccol.prop(self, 'use_image_atlas') if self.get_to_be_cleared_image_atlas(context, yp): col = self.layout.column(align=True) @@ -857,8 +928,7 @@ def draw(self, context): if self.type == 'OBJECT_INDEX': col.prop(self, 'object_index', text='') - if len(layer.masks) > 0: - col.prop(self, 'blend_type', text='') + col.prop(self, 'blend_type', text='') def execute(self, context): if hasattr(self, 'auto_cancel') and self.auto_cancel: return {'CANCELLED'} @@ -892,14 +962,20 @@ def execute(self, context): if same_name: if self.type == 'IMAGE': self.report({'ERROR'}, "Image named '" + self.name +"' is already available!") + return {'CANCELLED'} elif self.type == 'VCOL': self.report({'ERROR'}, "Vertex Color named '" + self.name +"' is already available!") - else: self.report({'ERROR'}, "Mask named '" + self.name +"' is already available!") - return {'CANCELLED'} - + return {'CANCELLED'} + elif self.options.is_repeat: + # Remove the mask before re-adding it on operator repeat + remove_mask(layer, same_name[0], obj) + else: + self.report({'ERROR'}, "Mask named '" + self.name +"' is already available!") + return {'CANCELLED'} + alpha = False img = None - vcol = None + vcol_name = '' segment = None # New image @@ -963,24 +1039,36 @@ def execute(self, context): for o in objs: if self.name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue - vcol = new_vertex_color(o, self.name, self.vcol_data_type, self.vcol_domain) + + color = () if self.color_option == 'WHITE': - set_obj_vertex_colors(o, vcol.name, (1.0, 1.0, 1.0, 1.0)) + color = (1.0, 1.0, 1.0, 1.0) elif self.color_option == 'BLACK': - set_obj_vertex_colors(o, vcol.name, (0.0, 0.0, 0.0, 1.0)) + color = (0.0, 0.0, 0.0, 1.0) + + vcol = new_vertex_color(o, self.name, self.vcol_data_type, self.vcol_domain, color_fill=color) set_active_vertex_color(o, vcol) + vcol_name = vcol.name + + # Fill selected geometry if in edit mode + if self.vcol_fill and bpy.context.mode == 'EDIT_MESH' and self.color_option == 'BLACK': + bpy.ops.mesh.y_vcol_fill(color_option='WHITE') elif self.type == 'COLOR_ID': - check_colorid_vcol(objs) + check_colorid_vcol(objs, set_as_active=True) + + # Fill selected geometry if in edit mode + if self.vcol_fill and bpy.context.mode == 'EDIT_MESH': + bpy.ops.mesh.y_vcol_fill_face_custom(color=(self.color_id[0], self.color_id[1], self.color_id[2], 1.0)) # Voronoi and noise mask will use grayscale value by default source_input = 'RGB' if self.type not in {'VORONOI', 'NOISE'} else 'ALPHA' # Add new mask mask = add_new_mask( - layer, self.name, self.type, self.texcoord_type, self.uv_name, img, vcol, segment, self.object_index, self.blend_type, + layer, self.name, self.type, self.texcoord_type, self.uv_name, img, vcol_name, segment, self.object_index, self.blend_type, self.hemi_space, self.hemi_use_prev_normal, self.color_id, source_input=source_input, edge_detect_radius=self.edge_detect_radius, - modifier_type=self.modifier_type, interpolation=self.interpolation + modifier_type=self.modifier_type, interpolation=self.interpolation, ao_distance=self.ao_distance ) # Enable edit mask @@ -1002,33 +1090,12 @@ def execute(self, context): return {'FINISHED'} -class YOpenImageAsMask(bpy.types.Operator, ImportHelper): +class YOpenImageAsMask(bpy.types.Operator, ImportHelper, BaseOperator.OpenImage): """Open Image as Mask""" - bl_idname = "node.y_open_image_as_mask" + bl_idname = "wm.y_open_image_as_mask" bl_label = "Open Image as Mask" bl_options = {'REGISTER', 'UNDO'} - # File related - files : CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory : StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) - - # File browser filter - filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - - display_type : EnumProperty( - items = ( - ('FILE_DEFAULTDISPLAY', 'Default', ''), - ('FILE_SHORTDISLPAY', 'Short List', ''), - ('FILE_LONGDISPLAY', 'Long List', ''), - ('FILE_IMGDISPLAY', 'Thumbnails', '') - ), - default = 'FILE_IMGDISPLAY', - options = {'HIDDEN', 'SKIP_SAVE'} - ) - - relative : BoolProperty(name="Relative Path", default=True, description="Apply relative paths") - interpolation : EnumProperty( name = 'Image Interpolation Type', description = 'image interpolation type', @@ -1071,9 +1138,6 @@ class YOpenImageAsMask(bpy.types.Operator, ImportHelper): file_browser_filepath : StringProperty(default='') - def generate_paths(self): - return (fn.name for fn in self.files), self.directory - @classmethod def poll(cls, context): node = get_active_ypaint_node() @@ -1093,8 +1157,8 @@ def invoke(self, context, event): yp = node.node_tree.yp self.layer = yp.layers[yp.active_layer_index] - if obj.type != 'MESH': - self.texcoord_type = 'Object' + if not is_object_work_with_uv(obj): + self.texcoord_type = 'Generated' # Use active uv layer name by default if obj.type == 'MESH' and len(obj.data.uv_layers) > 0: @@ -1127,8 +1191,7 @@ def invoke(self, context, event): return self.execute(context) return context.window_manager.invoke_props_dialog(self) - context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + return self.running_fileselect_modal(context, event) def check(self, context): return True @@ -1216,7 +1279,7 @@ def execute(self, context): # Add new mask mask = add_new_mask( - layer, image.name, 'IMAGE', self.texcoord_type, self.uv_map, image, None, + layer, image.name, 'IMAGE', self.texcoord_type, self.uv_map, image, '', blend_type=self.blend_type, source_input=self.source_input, interpolation = self.interpolation ) @@ -1238,31 +1301,8 @@ def execute(self, context): return {'FINISHED'} -''' Check if data is used as layer, if so, source input will change to ALPHA ''' -def update_available_data_name_as_mask(self, context): - node = get_active_ypaint_node() - yp = node.node_tree.yp - - if self.type == 'IMAGE': - for layer in yp.layers: - if layer.type == 'IMAGE': - source = get_layer_source(layer) - if source.image and source.image.name == self.image_name: - self.source_input = 'ALPHA' - return - - elif self.type == 'VCOL' and is_bl_newer_than(2, 92): - for layer in yp.layers: - if layer.type == 'VCOL': - source = get_layer_source(layer) - if source.attribute_name == self.vcol_name: - self.source_input = 'ALPHA' - return - - self.source_input = 'RGB' - class YOpenAvailableDataAsMask(bpy.types.Operator): - bl_idname = "node.y_open_available_data_as_mask" + bl_idname = "wm.y_open_available_data_as_mask" bl_label = "Open available data as Layer Mask" bl_description = "Open available data as Layer Mask" bl_options = {'REGISTER', 'UNDO'} @@ -1303,10 +1343,10 @@ class YOpenAvailableDataAsMask(bpy.types.Operator): uv_map : StringProperty(default='') uv_map_coll : CollectionProperty(type=bpy.types.PropertyGroup) - image_name : StringProperty(name="Image", update=update_available_data_name_as_mask) + image_name : StringProperty(name="Image") image_coll : CollectionProperty(type=bpy.types.PropertyGroup) - vcol_name : StringProperty(name="Vertex Color", update=update_available_data_name_as_mask) + vcol_name : StringProperty(name="Vertex Color") vcol_coll : CollectionProperty(type=bpy.types.PropertyGroup) blend_type : EnumProperty( @@ -1367,7 +1407,7 @@ def invoke(self, context, event): imgs = bpy.data.images baked_channel_images = get_all_baked_channel_images(layer.id_data) for img in imgs: - if not img.yia.is_image_atlas and img not in baked_channel_images and img != layer_image and img not in mask_images and img.name not in {'Render Result', 'Viewer Node'}: + if is_image_available_to_open(img) and img not in baked_channel_images and img != layer_image: self.image_coll.add().name = img.name # Make sure default image is available in the collection and update the source input based on the default name @@ -1498,13 +1538,12 @@ def execute(self, context): if self.vcol_name not in get_vertex_colors(o): if not is_bl_newer_than(3, 3) and len(get_vertex_colors(o)) >= 8: continue data_type, domain = get_vcol_data_type_and_domain_by_name(o, self.vcol_name, objs) - other_v = new_vertex_color(o, self.vcol_name, data_type, domain) - set_obj_vertex_colors(o, other_v.name, (1.0, 1.0, 1.0, 1.0)) + other_v = new_vertex_color(o, self.vcol_name, data_type, domain, color_fill=(1.0, 1.0, 1.0, 1.0)) set_active_vertex_color(o, other_v) # Add new mask mask = add_new_mask( - layer, name, self.type, self.texcoord_type, self.uv_map, image, vcol, + layer, name, self.type, self.texcoord_type, self.uv_map, image, self.vcol_name, blend_type=self.blend_type, source_input=self.source_input, interpolation = self.interpolation ) @@ -1532,7 +1571,7 @@ def execute(self, context): return {'FINISHED'} class YMoveLayerMask(bpy.types.Operator): - bl_idname = "node.y_move_layer_mask" + bl_idname = "wm.y_move_layer_mask" bl_label = "Move Layer Mask" bl_description = "Move layer mask" bl_options = {'REGISTER', 'UNDO'} @@ -1605,7 +1644,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveLayerMask(bpy.types.Operator): - bl_idname = "node.y_remove_layer_mask" + bl_idname = "wm.y_remove_layer_mask" bl_label = "Remove Layer Mask" bl_description = "Remove Layer Mask" bl_options = {'REGISTER', 'UNDO'} @@ -1701,8 +1740,46 @@ def execute(self, context): return {'FINISHED'} +class YOpenImageToReplaceMask(bpy.types.Operator, ImportHelper, BaseOperator.OpenImage): + """Open Image to Replace Mask""" + bl_idname = "wm.y_open_image_to_replace_mask" + bl_label = "Open Image to Replace Mask" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + group_node = get_active_ypaint_node() + return context.object and group_node and len(group_node.node_tree.yp.layers) > 0 + + def invoke(self, context, event): + self.mask = context.mask + return self.running_fileselect_modal(context, event) + + def execute(self, context): + + T = time.time() + + wm = context.window_manager + mask = self.mask + yp = mask.id_data.yp + + loaded_images = self.get_loaded_images() + + if len(loaded_images) == 0 or loaded_images[0] == None: + self.report({'ERROR'}, "No image is selected!") + return {'CANCELLED'} + + image = loaded_images[0] + + replace_mask_type(mask, 'IMAGE', image.name) + + print('INFO: Layer', mask.name, 'is updated in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') + wm.yptimer.time = str(time.time()) + + return {'FINISHED'} + class YReplaceMaskType(bpy.types.Operator): - bl_idname = "node.y_replace_mask_type" + bl_idname = "wm.y_replace_mask_type" bl_label = "Replace Mask Type" bl_description = "Replace Mask Type" bl_options = {'REGISTER', 'UNDO'} @@ -1773,10 +1850,6 @@ def execute(self, context): mask = self.mask yp = mask.id_data.yp - if mask.use_temp_bake: - self.report({'ERROR'}, "Cannot replace temporarily baked mask!") - return {'CANCELLED'} - if self.type == mask.type and self.type not in {'IMAGE', 'VCOL', 'MODIFIER'}: return {'CANCELLED'} if self.load_item and self.type in {'VCOL', 'IMAGE'} and self.item_name == '': @@ -1792,7 +1865,7 @@ def execute(self, context): class YFixEdgeDetectAO(bpy.types.Operator): """Eevee Ambient Occlusion must be enabled to make edge detect mask to work""" - bl_idname = "node.y_fix_edge_detect_ao" + bl_idname = "wm.y_fix_edge_detect_ao" bl_label = "Fix Edge Detect Mask AO" bl_options = {'REGISTER', 'UNDO'} @@ -2018,7 +2091,7 @@ def update_mask_uv_name(self, context): tree = get_tree(layer) mask = self - if mask.type in {'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT'} or mask.texcoord_type != 'UV': + if mask.type in {'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'AO'} or mask.texcoord_type != 'UV': return # Cannot use temp uv as standard uv @@ -2068,6 +2141,10 @@ def update_mask_hemi_use_prev_normal(self, context): layer = yp.layers[int(match.group(1))] tree = get_tree(layer) + if self.type == 'EDGE_DETECT': + source = get_mask_source(self) + setup_edge_detect_source(self, source) + check_layer_tree_ios(layer, tree) check_layer_bump_process(layer, tree) @@ -2232,6 +2309,9 @@ class YLayerMaskChannel(bpy.types.PropertyGroup): # Normal and height has its own alpha if using group, this one is for normal mix_normal : StringProperty(default='') + # ... and this one for vector displacement + mix_vdisp : StringProperty(default='') + # To limit mix value to not go above original channel value, useful for group layer mix_limit : StringProperty(default='') mix_limit_normal : StringProperty(default='') @@ -2259,7 +2339,7 @@ def update_mask_uniform_scale_enabled(self, context): reconnect_layer_nodes(layer) rearrange_layer_nodes(layer) -class YLayerMask(bpy.types.PropertyGroup): +class YLayerMask(bpy.types.PropertyGroup, Decal.BaseDecal): name : StringProperty(default='', update=update_mask_name) @@ -2275,8 +2355,8 @@ class YLayerMask(bpy.types.PropertyGroup): ) active_edit : BoolProperty( - name = 'Active mask for editing or preview', - description = 'Active mask for editing or preview', + name = 'Active Mask', + description = 'Active mask for Blender\'s paint mode and edit mode, or '+get_addon_title()+'\'s Mask preview mode', default = False, update = update_mask_active_edit ) @@ -2310,17 +2390,6 @@ class YLayerMask(bpy.types.PropertyGroup): update_mask_texcoord_type(self, context) ) - original_texcoord : EnumProperty( - name = 'Original Layer Coordinate Type', - items = mask_texcoord_type_items, - default = 'UV' - ) - - original_image_extension : StringProperty( - name = 'Original Image Extension Type', - default = '' - ) - modifier_type : EnumProperty( name = 'Mask Modifier Type', items = MaskModifier.mask_modifier_type_items, @@ -2416,12 +2485,6 @@ class YLayerMask(bpy.types.PropertyGroup): default=1.0, min=0.0, max=100.0, precision=3 ) - decal_distance_value : FloatProperty( - name = 'Decal Distance', - description = 'Distance between surface and the decal object', - min=0.0, max=100.0, default=0.5, precision=3 - ) - color_id : FloatVectorProperty( name = 'Color ID', size = 3, @@ -2451,13 +2514,6 @@ class YLayerMask(bpy.types.PropertyGroup): update = update_mask_object_index ) - # For temporary bake - use_temp_bake : BoolProperty( - name = 'Use Temporary Bake', - description = 'Use temporary bake, it can be useful to prevent glitching with cycles', - default = False, - ) - original_type : EnumProperty( name = 'Original Mask Type', items = mask_type_items, @@ -2477,10 +2533,17 @@ class YLayerMask(bpy.types.PropertyGroup): edge_detect_radius : FloatProperty( name = 'Edge Detect Radius', description = 'Edge detect radius', - default=0.05, min=0.0, max=10.0, + default=0.05, min=0.0, max=10.0, precision=3, update = update_mask_edge_detect_radius ) + # For AO + ao_distance : FloatProperty( + name = 'Ambient Occlusion Distance', + description = 'Ambient occlusion distance', + default=1.0, min=0.0, max=10.0 + ) + # Specific for voronoi voronoi_feature : EnumProperty( name = 'Voronoi Feature', @@ -2564,6 +2627,7 @@ def register(): bpy.utils.register_class(YNewLayerMask) bpy.utils.register_class(YOpenImageAsMask) bpy.utils.register_class(YOpenAvailableDataAsMask) + bpy.utils.register_class(YOpenImageToReplaceMask) bpy.utils.register_class(YMoveLayerMask) bpy.utils.register_class(YRemoveLayerMask) bpy.utils.register_class(YReplaceMaskType) @@ -2575,6 +2639,7 @@ def unregister(): bpy.utils.unregister_class(YNewLayerMask) bpy.utils.unregister_class(YOpenImageAsMask) bpy.utils.unregister_class(YOpenAvailableDataAsMask) + bpy.utils.unregister_class(YOpenImageToReplaceMask) bpy.utils.unregister_class(YMoveLayerMask) bpy.utils.unregister_class(YRemoveLayerMask) bpy.utils.unregister_class(YReplaceMaskType) diff --git a/MaskModifier.py b/MaskModifier.py index 228968c8..87574e31 100644 --- a/MaskModifier.py +++ b/MaskModifier.py @@ -126,7 +126,7 @@ def draw_modifier_properties(tree, m, layout): curve.draw_buttons_ext(bpy.context, layout) class YNewMaskModifier(bpy.types.Operator): - bl_idname = "node.y_new_mask_modifier" + bl_idname = "wm.y_new_mask_modifier" bl_label = "New Mask Modifier" bl_description = "New Mask Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -159,7 +159,7 @@ def execute(self, context): return {'FINISHED'} class YMoveMaskModifier(bpy.types.Operator): - bl_idname = "node.y_move_mask_modifier" + bl_idname = "wm.y_move_mask_modifier" bl_label = "Move Mask Modifier" bl_description = "Move Mask Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -214,7 +214,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveMaskModifier(bpy.types.Operator): - bl_idname = "node.y_remove_mask_modifier" + bl_idname = "wm.y_remove_mask_modifier" bl_label = "Remove Mask Modifier" bl_description = "Remove Mask Modifier" bl_options = {'REGISTER', 'UNDO'} diff --git a/Modifier.py b/Modifier.py index c719c584..2adc7075 100644 --- a/Modifier.py +++ b/Modifier.py @@ -112,12 +112,22 @@ def add_new_modifier(parent, modifier_type): m = modifiers[0] m.type = modifier_type + # Color ramp modifier has affect_color and affect_alpha enabled by default + if modifier_type == 'COLOR_RAMP': + ori_halt_update = yp.halt_update + yp.halt_update = True + + m.affect_color = True + m.affect_alpha = True + + yp.halt_update = ori_halt_update + check_modifiers_trees(parent) return m class YNewYPaintModifier(bpy.types.Operator): - bl_idname = "node.y_new_ypaint_modifier" + bl_idname = "wm.y_new_ypaint_modifier" bl_label = "New " + get_addon_title() + " Modifier" bl_description = "New " + get_addon_title() + " Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -179,7 +189,7 @@ def execute(self, context): return {'FINISHED'} class YMoveYPaintModifier(bpy.types.Operator): - bl_idname = "node.y_move_ypaint_modifier" + bl_idname = "wm.y_move_ypaint_modifier" bl_label = "Move " + get_addon_title() + " Modifier" bl_description = "Move " + get_addon_title() + " Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -244,7 +254,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveYPaintModifier(bpy.types.Operator): - bl_idname = "node.y_remove_ypaint_modifier" + bl_idname = "wm.y_remove_ypaint_modifier" bl_label = "Remove " + get_addon_title() + " Modifier" bl_description = "Remove " + get_addon_title() + " Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -304,7 +314,7 @@ def execute(self, context): return {'FINISHED'} -def draw_modifier_properties(context, channel_type, nodes, modifier, layout, is_layer_ch=False): +def draw_modifier_properties(context, channel_type, nodes, modifier, parent, layout, is_root_ch=False): if modifier.type == 'INVERT': row = layout.row(align=True) @@ -326,12 +336,6 @@ def draw_modifier_properties(context, channel_type, nodes, modifier, layout, is_ row.prop(rgb2i.inputs[3], 'default_value', text='') else: row.prop(modifier, 'rgb2i_col', text='') - # Shortcut only available on layer channel - if is_layer_ch: - row = col.row(align=True) - row.label(text='Shortcut on layer list:') - row.prop(modifier, 'shortcut', text='') - elif modifier.type == 'OVERRIDE_COLOR': col = layout.column(align=True) @@ -348,9 +352,21 @@ def draw_modifier_properties(context, channel_type, nodes, modifier, layout, is_ row.prop(modifier, 'shortcut', text='') elif modifier.type == 'COLOR_RAMP': + col = layout.column() color_ramp = nodes.get(modifier.color_ramp) if color_ramp: - layout.template_color_ramp(color_ramp, "color_ramp", expand=True) + ccol = col.column() + ccol.active = modifier.affect_color or modifier.affect_alpha + ccol.template_color_ramp(color_ramp, "color_ramp", expand=True) + + if not is_root_ch or parent.enable_alpha or not modifier.affect_color or not modifier.affect_alpha: + split = split_layout(col, 0.3, align=True) + split.label(text='Affect:') + row = split.row(align=True) + + label = 'Color' if channel_type != 'VALUE' else 'Value' + row.prop(modifier, 'affect_color', text=label, toggle=True) + row.prop(modifier, 'affect_alpha', text='Alpha', toggle=True) elif modifier.type == 'RGB_CURVE': rgb_curve = nodes.get(modifier.rgb_curve) @@ -525,6 +541,14 @@ def update_use_clamp(self, context): math.node_tree.nodes.get('Math.G').use_clamp = self.use_clamp math.node_tree.nodes.get('Math.B').use_clamp = self.use_clamp +def update_affect_color(self, context): + yp = self.id_data.yp + if yp.halt_update or not self.enable: return + tree = get_mod_tree(self) + + if self.type == 'COLOR_RAMP': + update_modifier_enable(self, context) + def update_affect_alpha(self, context): yp = self.id_data.yp if yp.halt_update or not self.enable: return @@ -538,6 +562,9 @@ def update_affect_alpha(self, context): else: alpha.mute = True + elif self.type == 'COLOR_RAMP': + update_modifier_enable(self, context) + def update_math_method(self, context): yp = self.id_data.yp if yp.halt_update or not self.enable: return @@ -638,8 +665,8 @@ class YPaintModifier(bpy.types.PropertyGroup): color_ramp_linear_start : StringProperty(default='') color_ramp_linear : StringProperty(default='') color_ramp_alpha_multiply : StringProperty(default='') - color_ramp_mix_rgb : StringProperty(default='') - color_ramp_mix_alpha : StringProperty(default='') + color_ramp_mix_rgb : StringProperty(default='') # Deprecated + color_ramp_mix_alpha : StringProperty(default='') # Deprecated # RGB Curve nodes rgb_curve : StringProperty(default='') @@ -689,7 +716,8 @@ class YPaintModifier(bpy.types.PropertyGroup): update = update_math_method ) - affect_alpha : BoolProperty(name='Affect Alpha', default=False, update=update_affect_alpha) + affect_color : BoolProperty(name='Affect Color', description='Ramp will affect the color value', default=True, update=update_affect_color) + affect_alpha : BoolProperty(name='Affect Alpha', description='Ramp will affect the alpha value', default=False, update=update_affect_alpha) # Individual modifier node frame frame : StringProperty(default='') @@ -887,6 +915,8 @@ def disable_modifiers_tree(parent, parent_tree=None, rearrange=False): else: return + + if not parent_tree: return # Get modifier group mod_group = parent_tree.nodes.get(parent.mod_group) diff --git a/NormalMapModifier.py b/NormalMapModifier.py index 7e1f2931..8e301aab 100644 --- a/NormalMapModifier.py +++ b/NormalMapModifier.py @@ -25,7 +25,7 @@ def add_new_normalmap_modifier(ch, layer, modifier_type): check_modifier_nodes(m, tree) class YNewNormalmapModifier(bpy.types.Operator): - bl_idname = "node.y_new_normalmap_modifier" + bl_idname = "wm.y_new_normalmap_modifier" bl_label = "New Normal Map Modifier" bl_description = "New Normal Map Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -65,7 +65,7 @@ def execute(self, context): return {'FINISHED'} class YMoveNormalMapModifier(bpy.types.Operator): - bl_idname = "node.y_move_normalmap_modifier" + bl_idname = "wm.y_move_normalmap_modifier" bl_label = "Move " + get_addon_title() + " Modifier" bl_description = "Move " + get_addon_title() + " Modifier" bl_options = {'REGISTER', 'UNDO'} @@ -133,7 +133,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveNormalMapModifier(bpy.types.Operator): - bl_idname = "node.y_remove_normalmap_modifier" + bl_idname = "wm.y_remove_normalmap_modifier" bl_label = "Remove " + get_addon_title() + " Modifier" bl_description = "Remove " + get_addon_title() + " Modifier" bl_options = {'REGISTER', 'UNDO'} diff --git a/Root.py b/Root.py index fc98f52b..b74193f8 100644 --- a/Root.py +++ b/Root.py @@ -74,6 +74,7 @@ def check_yp_channel_nodes(yp, reconnect=False): for layer in yp.layers: layer_tree = get_tree(layer) + # Make sure the number of channels are correct num_difference = len(yp.channels) - len(layer.channels) if num_difference > 0: for i in range(num_difference): @@ -84,7 +85,7 @@ def check_yp_channel_nodes(yp, reconnect=False): last_idx = len(layer.channels)-1 # Remove layer channel layer.channels.remove(channel_idx) - + for mask in layer.masks: num_difference = len(yp.channels) - len(mask.channels) if num_difference > 0: @@ -117,13 +118,12 @@ def check_yp_channel_nodes(yp, reconnect=False): reconnect_yp_nodes(yp.id_data) rearrange_yp_nodes(yp.id_data) -def create_new_group_tree(mat): +def create_new_group_tree(mat, name=None): #ypup = bpy.context.user_preferences.addons[__name__].preferences - # Group name is based from the material - #group_name = mat.name + YP_GROUP_SUFFIX - group_name = YP_GROUP_PREFIX + mat.name + # Group name is based on material unless specified + group_name = name or YP_GROUP_PREFIX + mat.name # Create new group tree group_tree = bpy.data.node_groups.new(group_name, 'ShaderNodeTree') @@ -148,6 +148,7 @@ def create_new_yp_channel(group_tree, name, channel_type, non_color=True, enable # Add new channel channel = yp.channels.add() channel.name = name + channel.original_name = name channel.bake_to_vcol_name = 'Baked ' + name channel.type = channel_type @@ -166,15 +167,16 @@ def create_new_yp_channel(group_tree, name, channel_type, non_color=True, enable channel.colorspace = 'LINEAR' else: channel.colorspace = 'SRGB' else: - # NOTE: Smooth bump is no longer enabled by default in Blender 2.80+ - if is_bl_newer_than(2, 80): channel.enable_smooth_bump = False + # NOTE: Smooth bump is no longer enabled by default at all + #if is_bl_newer_than(2, 80): + channel.enable_smooth_bump = False yp.halt_reconnect = False return channel class YSelectMaterialPolygons(bpy.types.Operator): - bl_idname = "material.y_select_all_material_polygons" + bl_idname = "wm.y_select_all_material_polygons" bl_label = "Select All Material Polygons" bl_description = "Select all polygons using this material" bl_options = {'REGISTER', 'UNDO'} @@ -309,7 +311,7 @@ def execute(self, context): return {'FINISHED'} class YRenameUVMaterial(bpy.types.Operator): - bl_idname = "material.y_rename_uv_using_the_same_material" + bl_idname = "wm.y_rename_uv_using_the_same_material" bl_label = "Rename UV that using same Material" bl_description = "Rename UV on objects that used the same material" bl_options = {'REGISTER', 'UNDO'} @@ -423,11 +425,21 @@ def execute(self, context): return {'FINISHED'} class YQuickYPaintNodeSetup(bpy.types.Operator): - bl_idname = "node.y_quick_ypaint_node_setup" + bl_idname = "wm.y_quick_ypaint_node_setup" bl_label = "Quick " + get_addon_title() + " Node Setup" bl_description = "Quick " + get_addon_title() + " Node Setup" bl_options = {'REGISTER', 'UNDO'} + tree_name : StringProperty( + name = 'Tree Name' + ) + + set_material_name_from_tree_name : BoolProperty( + name = 'Also Set Material Name', + description = 'Also set material name from tree name', + default = False + ) + type : EnumProperty( name = 'Type', items = ( @@ -444,12 +456,6 @@ class YQuickYPaintNodeSetup(bpy.types.Operator): roughness : BoolProperty(name='Roughness', default=True) normal : BoolProperty(name='Normal', default=True) - mute_texture_paint_overlay : BoolProperty( - name = 'Mute Stencil Mask Opacity', - description = 'Set Stencil Mask Opacity found in the 3D Viewport\'s Overlays menu to 0', - default = True - ) - use_linear_blending : BoolProperty( name = 'Use Linear Color Blending', description = 'Use more accurate linear color blending (it will behave differently than Photoshop)', @@ -463,7 +469,6 @@ class YQuickYPaintNodeSetup(bpy.types.Operator): ) target_bsdf_name : StringProperty(default='') - not_muted_paint_opacity : BoolProperty(default=False) not_on_material_view : BoolProperty(default=True) @classmethod @@ -481,6 +486,10 @@ def invoke(self, context, event): valid_bsdf_types = ['BSDF_PRINCIPLED', 'BSDF_DIFFUSE', 'EMISSION'] + # Set the tree name + mat = get_active_material() + self.tree_name = YP_GROUP_PREFIX + (mat.name if mat else obj.name) + # Get target bsdf self.target_bsdf_name = '' output = get_material_output(mat) @@ -490,17 +499,6 @@ def invoke(self, context, event): self.type = bsdf_node.type self.target_bsdf_name = bsdf_node.name - # Normal channel does not works to non mesh object - if obj.type != 'MESH': - self.normal = False - - self.not_muted_paint_opacity = False - if is_bl_newer_than(2, 80): - for area in context.screen.areas: - if area.type == 'VIEW_3D': - self.not_muted_paint_opacity = area.spaces[0].overlay.texture_paint_mode_opacity > 0.0 - break - self.not_on_material_view = space.type == 'VIEW_3D' and ((not is_bl_newer_than(2, 80) and space.viewport_shade not in {'MATERIAL', 'RENDERED'}) or (is_bl_newer_than(2, 80) and space.shading.type not in {'MATERIAL', 'RENDERED'})) if get_user_preferences().skip_property_popups and not event.shift: @@ -515,6 +513,8 @@ def draw(self, context): row = split_layout(self.layout, 0.35) col = row.column() + col.label(text='Tree Name:') + col.separator() col.label(text='Type:') if self.type != 'EMISSION': ccol = col.column(align=True) @@ -527,6 +527,10 @@ def draw(self, context): ccol.label(text='') col = row.column() + rrow = col.row(align=True) + rrow.prop(self, 'tree_name', text='') + rrow.prop(self, 'set_material_name_from_tree_name', text='', icon='MATERIAL_DATA') + col.separator() col.prop(self, 'type', text='') if self.type != 'EMISSION': ccol = col.column(align=True) @@ -539,9 +543,6 @@ def draw(self, context): col.prop(self, 'use_linear_blending') - if is_bl_newer_than(2, 80) and self.not_muted_paint_opacity: - col.prop(self, 'mute_texture_paint_overlay') - if self.not_on_material_view: col.prop(self, 'switch_to_material_view') @@ -556,7 +557,8 @@ def execute(self, context): mat = get_active_material() if not mat: - mat = bpy.data.materials.new(obj.name) + material_name = self.tree_name if self.set_material_name_from_tree_name else obj.name + mat = bpy.data.materials.new(material_name) mat.use_nodes = True if len(obj.material_slots) > 0: @@ -568,6 +570,8 @@ def execute(self, context): # Remove default nodes for n in mat.node_tree.nodes: mat.node_tree.nodes.remove(n) + elif self.set_material_name_from_tree_name: + mat.name = self.tree_name if not mat.node_tree: mat.use_nodes = True @@ -598,7 +602,7 @@ def execute(self, context): loc = Vector((0, 0)) # Create new group node - group_tree = create_new_group_tree(mat) + group_tree = create_new_group_tree(mat, self.tree_name) node = nodes.new(type='ShaderNodeGroup') node.node_tree = group_tree node.select = True @@ -778,10 +782,6 @@ def execute(self, context): # Disable overlay in Blender 2.8 for area in context.screen.areas: if area.type == 'VIEW_3D': - if is_bl_newer_than(2, 80) and self.not_muted_paint_opacity and self.mute_texture_paint_overlay: - area.spaces[0].overlay.texture_paint_mode_opacity = 0.0 - area.spaces[0].overlay.vertex_paint_mode_opacity = 0.0 - if self.not_on_material_view and self.switch_to_material_view: if not is_bl_newer_than(2, 80): area.spaces[0].viewport_shade = 'MATERIAL' @@ -797,7 +797,7 @@ def execute(self, context): return {'FINISHED'} class YNewYPaintNode(bpy.types.Operator): - bl_idname = "node.y_add_new_ypaint_node" + bl_idname = "wm.y_add_new_ypaint_node" bl_label = "Add new " + get_addon_title() + " Node" bl_description = "Add new " + get_addon_title() + " node" bl_options = {'REGISTER', 'UNDO'} @@ -1033,7 +1033,7 @@ def do_alpha_setup(mat, node, channel): tree.links.new(alpha_output, target_socket) class YConnectYPaintChannelAlpha(bpy.types.Operator): - bl_idname = "node.y_connect_ypaint_channel_alpha" + bl_idname = "wm.y_connect_ypaint_channel_alpha" bl_label = "Connect " + get_addon_title() + " Channel Alpha" bl_description = "Connect " + get_addon_title() + " channel alpha to other nodes" bl_options = {'REGISTER', 'UNDO'} @@ -1043,7 +1043,7 @@ def execute(self, context): return {'FINISHED'} class YConnectYPaintChannel(bpy.types.Operator): - bl_idname = "node.y_connect_ypaint_channel" + bl_idname = "wm.y_connect_ypaint_channel" bl_label = "Connect " + get_addon_title() + " Channel" bl_description = "Connect " + get_addon_title() + " channel to other nodes" bl_options = {'REGISTER', 'UNDO'} @@ -1105,7 +1105,7 @@ def execute(self, context): return {'FINISHED'} class YNewYPaintChannel(bpy.types.Operator): - bl_idname = "node.y_add_new_ypaint_channel" + bl_idname = "wm.y_add_new_ypaint_channel" bl_label = "Add new " + get_addon_title() + " Channel" bl_description = "Add new " + get_addon_title() + " channel" bl_options = {'REGISTER', 'UNDO'} @@ -1322,7 +1322,7 @@ def execute(self, context): return {'FINISHED'} class YMoveYPaintChannel(bpy.types.Operator): - bl_idname = "node.y_move_ypaint_channel" + bl_idname = "wm.y_move_ypaint_channel" bl_label = "Move " + get_addon_title() + " Channel" bl_description = "Move " + get_addon_title() + " channel" bl_options = {'REGISTER', 'UNDO'} @@ -1409,7 +1409,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveYPaintChannel(bpy.types.Operator): - bl_idname = "node.y_remove_ypaint_channel" + bl_idname = "wm.y_remove_ypaint_channel" bl_label = "Remove " + get_addon_title() + " Channel" bl_description = "Remove " + get_addon_title() + " channel" bl_options = {'REGISTER', 'UNDO'} @@ -1459,10 +1459,13 @@ def execute(self, context): inputs = get_tree_inputs(group_tree) outputs = get_tree_outputs(group_tree) - # Disable layer preview mode to avoid error + # Disable preview mode to avoid error ori_layer_preview_mode = yp.layer_preview_mode + ori_preview_mode = yp.preview_mode if yp.layer_preview_mode: yp.layer_preview_mode = False + if yp.preview_mode: + yp.preview_mode = False # Get active channel channel_idx = yp.active_channel_index @@ -1641,6 +1644,9 @@ def execute(self, context): if ori_layer_preview_mode: yp.layer_preview_mode = True + if ori_preview_mode: + yp.preview_mode = True + # Repoint channel index #repoint_channel_index(yp) @@ -1652,7 +1658,7 @@ def execute(self, context): return {'FINISHED'} class YAddSimpleUVs(bpy.types.Operator): - bl_idname = "node.y_add_simple_uvs" + bl_idname = "wm.y_add_simple_uvs" bl_label = "Add simple UVs" bl_description = "Add Simple UVs" bl_options = {'REGISTER', 'UNDO'} @@ -1675,7 +1681,7 @@ def execute(self, context): return {'FINISHED'} class YFixChannelMissmatch(bpy.types.Operator): - bl_idname = "node.y_fix_channel_missmatch" + bl_idname = "wm.y_fix_channel_missmatch" bl_label = "Fix Channels Mistmatch" bl_description = "Fix channels missmatch because of error" bl_options = {'REGISTER', 'UNDO'} @@ -1698,7 +1704,7 @@ def execute(self, context): return {'FINISHED'} class YFixMissingUV(bpy.types.Operator): - bl_idname = "node.y_fix_missing_uv" + bl_idname = "wm.y_fix_missing_uv" bl_label = "Fix missing UV" bl_description = "Fix missing UV" bl_options = {'REGISTER', 'UNDO'} @@ -1804,12 +1810,14 @@ def execute(self, context): return {'FINISHED'} class YRenameYPaintTree(bpy.types.Operator): - bl_idname = "node.y_rename_ypaint_tree" - bl_label = "Rename " + get_addon_title() + " Group Name" - bl_description = "Rename " + get_addon_title() + " Group Name" + bl_idname = "wm.y_rename_ypaint_tree" + bl_label = "Rename " + get_addon_title() + " Tree" + bl_description = "Rename " + get_addon_title() + " Tree" bl_options = {'REGISTER', 'UNDO'} + bl_property = 'name' name : StringProperty(name='New Name', description='New Name', default='') + rename_active_material : BoolProperty(name='Also Rename Active Material', description='Also rename active material', default=False) @classmethod def poll(cls, context): @@ -1823,16 +1831,21 @@ def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): - self.layout.prop(self, 'name') + row = self.layout.row(align=True) + row.prop(self, 'name') + row.prop(self, 'rename_active_material', text='', icon='MATERIAL') def execute(self, context): node = get_active_ypaint_node() tree = node.node_tree tree.name = self.name + if self.rename_active_material: + mat = get_active_material() + mat.name = self.name return {'FINISHED'} class YChangeActiveYPaintNode(bpy.types.Operator): - bl_idname = "node.y_change_active_ypaint_node" + bl_idname = "wm.y_change_active_ypaint_node" bl_label = "Change Active " + get_addon_title() + " Node" bl_description = "Change Active " + get_addon_title() + " Node" bl_options = {'REGISTER', 'UNDO'} @@ -1861,7 +1874,7 @@ def execute(self, context): return {'FINISHED'} -def duplicate_mat(mat): +def duplicate_mat(mat, new_name=None): # HACK: mat.copy() on Blender 3.0 and newer will make the yp tree used by 3 users (it should be 2 users) # To get around this issue, use a temporary tree that will replace the yp trees before doing mat.copy() if is_bl_newer_than(3): @@ -1893,17 +1906,31 @@ def duplicate_mat(mat): # Remove temporary trees for temp_tree in reversed(temp_trees): remove_datablock(bpy.data.node_groups, temp_tree) + else: + new_mat = mat.copy() - return new_mat + if new_name: + new_mat.name = new_name - return mat.copy() + return new_mat class YDuplicateYPNodes(bpy.types.Operator): - bl_idname = "node.y_duplicate_yp_nodes" + bl_idname = "wm.y_duplicate_yp_nodes" bl_label = "Duplicate " + get_addon_title() + " Nodes" bl_description = get_addon_title() + " doesn't work with more than one user! Duplicate to make it single user" bl_options = {'REGISTER', 'UNDO'} + new_name : StringProperty( + name = 'New Name', + description = 'New name for duplicated tree', + ) + + set_material_name_from_tree_name : BoolProperty( + name = 'Also Set Material Name', + description = 'Also set duplicated material name from duplicated tree name', + default = False + ) + duplicate_node : BoolProperty( name = 'Duplicate this Node', description = 'Duplicate this node', @@ -1930,7 +1957,6 @@ class YDuplicateYPNodes(bpy.types.Operator): @classmethod def poll(cls, context): - mat = get_active_material() group_node = get_active_ypaint_node() if not group_node: return False @@ -1944,6 +1970,8 @@ def invoke(self, context, event): group_node = get_active_ypaint_node() yp = group_node.node_tree.yp + self.new_name = get_unique_name(group_node.node_tree.name, bpy.data.node_groups) + self.any_ondisk_image = False for layer in yp.layers: @@ -1957,9 +1985,23 @@ def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): - self.layout.prop(self, 'only_active', text='Only Active Object') + row = split_layout(self.layout, 0.35) + + col = row.column() + col.label(text='New Name:') + + col.label(text='') + if self.any_ondisk_image: + col.label(text='') + + col = row.column() + row = col.row(align=True) + row.prop(self, 'new_name', text='') + row.prop(self, 'set_material_name_from_tree_name', text='', icon='MATERIAL_DATA') + + col.prop(self, 'only_active', text='Only Active Object') if self.any_ondisk_image: - self.layout.prop(self, 'ondisk_duplicate') + col.prop(self, 'ondisk_duplicate') def execute(self, context): @@ -1970,8 +2012,11 @@ def execute(self, context): if self.duplicate_material: + # Get new material name + new_mat_name = self.new_name if self.set_material_name_from_tree_name else get_unique_name(mat.name, bpy.data.materials) + # Duplicate the material - dup_mat = duplicate_mat(mat) + dup_mat = duplicate_mat(mat, new_mat_name) for obj in objs: for i, m in enumerate(obj.data.materials): if m == mat: @@ -1991,8 +2036,9 @@ def execute(self, context): for tree_name, node in tree_dict.items(): tree = bpy.data.node_groups.get(tree_name) node.node_tree = tree.copy() + if self.new_name: + node.node_tree.name = self.new_name - ypui = context.window_manager.ypui group_node = get_active_ypaint_node() tree = group_node.node_tree yp = tree.yp @@ -2058,29 +2104,6 @@ def execute(self, context): return {'FINISHED'} -def fix_missing_vcol(obj, name, src): - - ref_vcol = None - - if is_bl_newer_than(3, 2): - # Try to get reference vcol - mat = get_active_material() - objs = get_all_objects_with_same_materials(mat) - - for o in objs: - ovcols = get_vertex_colors(o) - if name in ovcols: - ref_vcol = ovcols.get(name) - break - - if ref_vcol: vcol = new_vertex_color(obj, name, ref_vcol.data_type, ref_vcol.domain) - else: vcol = new_vertex_color(obj, name) - - set_source_vcol_name(src, name) - - # Default recovered missing vcol is black - set_obj_vertex_colors(obj, vcol.name, (0.0, 0.0, 0.0, 0.0)) - def fix_missing_img(name, src, is_mask=False): img = bpy.data.images.new( name=name, width=1024, height=1024, @@ -2092,7 +2115,7 @@ def fix_missing_img(name, src, is_mask=False): src.image = img class YOptimizeNormalProcess(bpy.types.Operator): - bl_idname = "node.y_optimize_normal_process" + bl_idname = "wm.y_optimize_normal_process" bl_label = "Optimize Normal Process" bl_description = "Optimize normal process by not processing normal input when it's not connected" bl_options = {'REGISTER', 'UNDO'} @@ -2118,7 +2141,7 @@ def execute(self, context): return {'FINISHED'} class YFixMissingData(bpy.types.Operator): - bl_idname = "node.y_fix_missing_data" + bl_idname = "wm.y_fix_missing_data" bl_label = "Fix Missing Data" bl_description = "Fix missing image/vertex color data" bl_options = {'REGISTER', 'UNDO'} @@ -2194,36 +2217,7 @@ def execute(self, context): objs.append(ob) # Fix missing vcols - need_color_id_vcol = False - - for obj in objs: - if obj.type != 'MESH': continue - - for layer in yp.layers: - if layer.type == 'VCOL': - src = get_layer_source(layer) - if not get_vcol_from_source(obj, src): - fix_missing_vcol(obj, src.attribute_name, src) - - for mask in layer.masks: - if mask.type == 'VCOL': - src = get_mask_source(mask) - if not get_vcol_from_source(obj, src): - fix_missing_vcol(obj, src.attribute_name, src) - - if mask.type == 'COLOR_ID': - vcols = get_vertex_colors(obj) - if COLOR_ID_VCOL_NAME not in vcols: - need_color_id_vcol = True - - for ch in layer.channels: - if ch.override and ch.override_type == 'VCOL': - src = get_channel_source(ch, layer) - if not get_vcol_from_source(obj, src): - fix_missing_vcol(obj, src.attribute_name, src) - - # Fix missing color id missing vcol - if need_color_id_vcol: check_colorid_vcol(objs) + fix_missing_object_vcols(yp, objs) # Fix missing uvs check_uv_nodes(yp, generate_missings=True) @@ -2246,7 +2240,7 @@ def execute(self, context): return {'FINISHED'} class YRefreshTangentSignVcol(bpy.types.Operator): - bl_idname = "node.y_refresh_tangent_sign_vcol" + bl_idname = "wm.y_refresh_tangent_sign_vcol" bl_label = "Refresh Tangent Sign Vertex Colors" bl_description = "Refresh Tangent Sign Vertex Colors to make it work in Blender 2.8" bl_options = {'REGISTER', 'UNDO'} @@ -2267,7 +2261,7 @@ def execute(self, context): return {'FINISHED'} class YRemoveYPaintNode(bpy.types.Operator): - bl_idname = "node.y_remove_yp_node" + bl_idname = "wm.y_remove_yp_node" bl_label = "Remove " + get_addon_title() + " Node" bl_description = "Remove " + get_addon_title() + " node, but keep all baked channel image(s)""" bl_options = {'REGISTER', 'UNDO'} @@ -2326,7 +2320,7 @@ def execute(self, context): return {'FINISHED'} class YCleanYPCaches(bpy.types.Operator): - bl_idname = "node.y_clean_yp_caches" + bl_idname = "wm.y_clean_yp_caches" bl_label = "Clean " + get_addon_title() + " Caches" bl_description = "Clean " + get_addon_title() + " caches""" bl_options = {'REGISTER', 'UNDO'} @@ -2364,22 +2358,6 @@ def execute(self, context): return {'FINISHED'} -def get_channel_name(self): - name = self.get('name', '') # May be null - return name - -def set_channel_name(self, value): - yp = self.id_data.yp - - # Update bake target channel name - for bt in yp.bake_targets: - for letter in rgba_letters: - btc = getattr(bt, letter) - if btc.channel_name != '' and btc.channel_name == self.name: - btc.channel_name = value - - self['name'] = value - def update_channel_name(self, context): T = time.time() @@ -2390,6 +2368,16 @@ def update_channel_name(self, context): if yp.halt_reconnect or yp.halt_update: return + # Update bake target channel name + for bt in yp.bake_targets: + for letter in rgba_letters: + btc = getattr(bt, letter) + if btc.channel_name != '' and btc.channel_name == self.original_name: + btc.channel_name = self.name + + # Update channel's original name + self.original_name = self.name + input_index = self.io_index output_index = get_output_index(self) @@ -2510,8 +2498,13 @@ def set_srgb_view_transform(): scene.yp.ori_look = scene.view_settings.look scene.view_settings.look = 'None' - scene.yp.ori_use_compositing = scene.use_nodes - scene.use_nodes = False + if is_bl_newer_than(5): + if scene.compositing_node_group: + scene.yp.ori_compositing_node_name = scene.compositing_node_group.name + scene.compositing_node_group = None + else: + scene.yp.ori_use_compositing = scene.use_nodes + scene.use_nodes = False scene.yp.ori_view_transform = scene.view_settings.view_transform if is_bl_newer_than(2, 80): @@ -2558,7 +2551,12 @@ def remove_preview(mat, advanced=False): scene.view_settings.exposure = scene.yp.ori_exposure scene.view_settings.gamma = scene.yp.ori_gamma scene.view_settings.use_curve_mapping = scene.yp.ori_use_curve_mapping - scene.use_nodes = scene.yp.ori_use_compositing + if is_bl_newer_than(5): + if scene.yp.ori_compositing_node_name != '': + cng = bpy.data.node_groups.get(scene.yp.ori_compositing_node_name) + if cng: scene.compositing_node_group = cng + scene.yp.ori_compositing_node_name = '' + else: scene.use_nodes = scene.yp.ori_use_compositing #def update_merge_mask_mode(self, context): # if not self.layer_preview_mode: @@ -2731,6 +2729,7 @@ def update_active_yp_channel(self, context): obj = context.object tree = self.id_data yp = tree.yp + if len(yp.channels) == 0: return ch = yp.channels[yp.active_channel_index] if yp.preview_mode: update_preview_mode(yp, context) @@ -3089,8 +3088,8 @@ def update_channel_alpha(self, context): outputs = get_tree_outputs(group_tree) # Baked outside nodes - frame = get_node(mat.node_tree, yp.baked_outside_frame) - tex = get_node(mat.node_tree, self.baked_outside, parent=frame) + frame = get_node(mat.node_tree, yp.baked_outside_frame) if mat else None + tex = get_node(mat.node_tree, self.baked_outside, parent=frame) if mat else None # Shift fcurves if self.enable_alpha: @@ -3105,7 +3104,7 @@ def update_channel_alpha(self, context): if not self.enable_alpha: - if not any(alpha_chs): + if yp.alpha_auto_setup and not any(alpha_chs) and mat: # Set material to use opaque (only for legacy renderer) if not is_bl_newer_than(4, 2): @@ -3116,29 +3115,32 @@ def update_channel_alpha(self, context): mat.game_settings.alpha_blend = 'OPAQUE' node = get_active_ypaint_node() - inp = node.inputs[self.io_index + 1] + inp = node.inputs[self.io_index + 1] if node else None + outp = None if yp.use_baked and yp.enable_baked_outside and tex: outp = tex.outputs[1] - else: + elif node: outp = node.outputs[self.io_index + 1] # Remember the connections - if len(inp.links) > 0: + if inp and len(inp.links) > 0: self.ori_alpha_from.node = inp.links[0].from_node.name self.ori_alpha_from.socket = inp.links[0].from_socket.name - for link in outp.links: - con = self.ori_alpha_to.add() - con.node = link.to_node.name - con.socket = link.to_socket.name + + if outp: + for link in outp.links: + con = self.ori_alpha_to.add() + con.node = link.to_node.name + con.socket = link.to_socket.name # Remove connection for baked outside - if yp.use_baked and yp.enable_baked_outside and tex: + if yp.use_baked and yp.enable_baked_outside and tex and outp and mat: for l in outp.links: mat.node_tree.links.remove(link) # Try to reconnect input to output - fn = mat.node_tree.nodes.get(self.ori_alpha_from.node) + fn = mat.node_tree.nodes.get(self.ori_alpha_from.node) if mat else None if fn: fs = fn.outputs.get(self.ori_alpha_from.socket) if fs: @@ -3155,7 +3157,7 @@ def update_channel_alpha(self, context): if self.enable_alpha: - if any(alpha_chs): + if yp.alpha_auto_setup and any(alpha_chs) and mat: if is_bl_newer_than(4, 2): # Settings for eevee next @@ -3175,30 +3177,30 @@ def update_channel_alpha(self, context): # Set node default_value node = get_active_ypaint_node() - node.inputs[alpha_name].default_value = 0.0 + node.inputs[alpha_name].default_value = self.ori_alpha_value alpha_connected = False # Try to relink to original connections - #tree = context.object.active_material.node_tree - tree = mat.node_tree - try: - node_from = tree.nodes.get(self.ori_alpha_from.node) - socket_from = node_from.outputs[self.ori_alpha_from.socket] - tree.links.new(socket_from, node.inputs[alpha_name]) - except: pass - - for con in self.ori_alpha_to: - node_to = tree.nodes.get(con.node) - if not node_to: continue - socket_to = node_to.inputs.get(con.socket) - if not socket_to: continue - if len(socket_to.links) < 1: - if yp.use_baked and yp.enable_baked_outside and tex: - mat.node_tree.links.new(tex.outputs[1], socket_to) - else: - tree.links.new(node.outputs[alpha_name], socket_to) - alpha_connected = True + tree = mat.node_tree if mat else None + if tree: + try: + node_from = tree.nodes.get(self.ori_alpha_from.node) + socket_from = node_from.outputs[self.ori_alpha_from.socket] + tree.links.new(socket_from, node.inputs[alpha_name]) + except: pass + + for con in self.ori_alpha_to: + node_to = tree.nodes.get(con.node) + if not node_to: continue + socket_to = node_to.inputs.get(con.socket) + if not socket_to: continue + if len(socket_to.links) < 1: + if yp.use_baked and yp.enable_baked_outside and tex: + mat.node_tree.links.new(tex.outputs[1], socket_to) + else: + tree.links.new(node.outputs[alpha_name], socket_to) + alpha_connected = True # Try to connect alpha without prior memory if yp.alpha_auto_setup and not alpha_connected: @@ -3307,19 +3309,6 @@ def update_enable_height_tweak(self, context): reconnect_yp_nodes(self.id_data) rearrange_yp_nodes(self.id_data) -# Prevent vcol name from being null -def get_channel_vcol_name(self): - name = self.get('bake_to_vcol_name', '') # May be null - if name == '': - self['bake_to_vcol_name'] = 'Baked ' + self.name - return self['bake_to_vcol_name'] - -def set_channel_vcol_name(self, value): - if value == '': - self['bake_to_vcol_name'] = 'Baked ' + self.name - else: - self['bake_to_vcol_name'] = value - def update_use_linear_blending(self, context): Modifier.check_yp_modifier_linear_nodes(self) check_start_end_root_ch_nodes(self.id_data) @@ -3393,8 +3382,12 @@ class YPaintChannel(bpy.types.PropertyGroup): description = 'Name of the channel', default = 'Albedo', update = update_channel_name, - get = get_channel_name, - set = set_channel_name + ) + + original_name : StringProperty( + name = 'Original Channel Name', + description = 'Original channel name for updating', + default = '' ) type : EnumProperty( @@ -3477,6 +3470,13 @@ class YPaintChannel(bpy.types.PropertyGroup): update = Bake.update_enable_bake_to_vcol ) + use_baked_vcol : BoolProperty( + name = 'Use Baked Vertex Color', + description = 'Use baked vertex color', + default = False, + update = Bake.update_enable_bake_to_vcol + ) + bake_to_vcol_alpha : BoolProperty( name = 'Bake To Vertex Color Alpha', description = 'When enabled, the channel are baked only to Alpha with vertex color', @@ -3487,8 +3487,6 @@ class YPaintChannel(bpy.types.PropertyGroup): name = 'Target Vertex Color Name', description = 'Target Vertex Color Name', default = '', - get = get_channel_vcol_name, - set = set_channel_vcol_name ) # Displacement for normal channel @@ -3732,6 +3730,7 @@ class YPaintChannel(bpy.types.PropertyGroup): expand_bake_to_vcol_settings : BoolProperty(default=False) expand_input_bump_settings : BoolProperty(default=False) expand_smooth_bump_settings : BoolProperty(default=False) + expand_baked_data : BoolProperty(default=False) # Connection related ori_alpha_to : CollectionProperty(type=YNodeConnections) @@ -3741,6 +3740,10 @@ class YPaintChannel(bpy.types.PropertyGroup): ori_height_to : CollectionProperty(type=YNodeConnections) ori_max_height_to : CollectionProperty(type=YNodeConnections) + # Default value related + ori_alpha_value : FloatProperty(default=0.0) + ori_max_height_value : FloatProperty(default=0.1) + class YPaintUV(bpy.types.PropertyGroup): name : StringProperty(default='') @@ -3877,7 +3880,7 @@ class YPaint(bpy.types.PropertyGroup): items = ( ('LAYER', 'Layer', ''), ('ALPHA', 'Alpha', ''), - ('SPECIFIC_MASK', 'Active Mask / Override', ''), + ('SPECIFIC_MASK', 'Active Mask / Custom Data', ''), ), #items = layer_preview_mode_type_items, default = 'LAYER', @@ -3949,7 +3952,7 @@ class YPaint(bpy.types.PropertyGroup): update = update_enable_tangent_sign_hacks ) - # When enabled, alpha can create some node setup, disable this to avoid that + # When enabled, alpha can create some node setup and change material settings, disable this to avoid that alpha_auto_setup : BoolProperty(default=True) # HACK: Refresh tree to remove glitchy normal @@ -3989,12 +3992,6 @@ class YPaintMaterialProps(bpy.types.PropertyGroup): class YPaintTimer(bpy.types.PropertyGroup): time : StringProperty(default='') -class YPaintBrushAssetCache(bpy.types.PropertyGroup): - name : StringProperty(default='') - library_type : StringProperty(default='') - library_name : StringProperty(default='') - blend_path : StringProperty(default='') - class YPaintWMProps(bpy.types.PropertyGroup): clipboard_tree : StringProperty(default='') clipboard_layer : StringProperty(default='') @@ -4005,6 +4002,7 @@ class YPaintWMProps(bpy.types.PropertyGroup): all_icons_loaded : BoolProperty(default=False) + edit_image_editor_window_index : IntProperty(default=-1) edit_image_editor_area_index : IntProperty(default=-1) custom_srgb_name : StringProperty(default='') @@ -4014,7 +4012,16 @@ class YPaintWMProps(bpy.types.PropertyGroup): test_result_error : IntProperty(default=0) test_result_failed : IntProperty(default=0) - brush_asset_caches : CollectionProperty(type=YPaintBrushAssetCache) + default_builtin_brush : StringProperty(default='') + + correct_paint_image_name : StringProperty(default='') + + clipboard_bake_target : CollectionProperty(type=BakeTarget.YBakeTarget) + + image_editor_dict : StringProperty(default='') + image_editor_pins : StringProperty(default='') + + halt_hacks : BoolProperty(default=False) class YPaintSceneProps(bpy.types.PropertyGroup): ori_display_device : StringProperty(default='') @@ -4024,6 +4031,7 @@ class YPaintSceneProps(bpy.types.PropertyGroup): ori_look : StringProperty(default='') ori_use_curve_mapping : BoolProperty(default=False) ori_use_compositing : BoolProperty(default=False) + ori_compositing_node_name : StringProperty(default='') class YPaintObjectUVHash(bpy.types.PropertyGroup): name : StringProperty(default='') @@ -4043,6 +4051,10 @@ class YPaintObjectProps(bpy.types.PropertyGroup): mesh_hash : StringProperty(default='') uv_hashes : CollectionProperty(type=YPaintObjectUVHash) + texpaint_translation : FloatVectorProperty(size=3, default=(0.0, 0.0, 0.0)) + texpaint_rotation : FloatVectorProperty(size=3, default=(0.0, 0.0, 0.0)) + texpaint_scale : FloatVectorProperty(size=3, default=(1.0, 1.0, 1.0)) + #class YPaintMeshProps(bpy.types.PropertyGroup): # parallax_scale_min : FloatProperty(default=0.0) # parallax_scale_span : FloatProperty(default=1.0) @@ -4089,15 +4101,15 @@ def ypaint_last_object_update(scene): mat = obj.active_material ypwm = bpy.context.window_manager.ypprops + node = get_active_ypaint_node() + yp = node.node_tree.yp if node else None if ypwm.last_object != obj.name or (mat and mat.name != ypwm.last_material): ypwm.last_object = obj.name if mat: ypwm.last_material = mat.name - node = get_active_ypaint_node() # Refresh layer index to update editor image - if node: - yp = node.node_tree.yp + if yp: if yp.use_baked and len(yp.channels) > 0: update_active_yp_channel(yp, bpy.context) @@ -4105,12 +4117,23 @@ def ypaint_last_object_update(scene): try: set_active_paint_slot_entity(yp) except: print('EXCEPTIION: Cannot set image canvas!') - if obj.type == 'MESH' and ypwm.last_object == obj.name and ypwm.last_mode != obj.mode: + # HACK: Remember original image editor images before entering texture paint mode + if yp and obj.type == 'MESH' and obj.mode != 'TEXTURE_PAINT': + editor_images, editor_pins = get_editor_images_dict(return_pins=True) + ypwm.image_editor_dict = str(editor_images) + ypwm.image_editor_pins = str(editor_pins) - node = get_active_ypaint_node() - yp = node.node_tree.yp if node else None + if obj.type == 'MESH' and ypwm.last_object == obj.name and ypwm.last_mode != obj.mode: if obj.mode == 'TEXTURE_PAINT' or ypwm.last_mode == 'TEXTURE_PAINT': + + # HACK: Set original image editor images since going into texture paint mode will replace all of them + if yp and obj.mode == 'TEXTURE_PAINT' and ypwm.image_editor_dict != '': + import ast + editor_images = ast.literal_eval(ypwm.image_editor_dict) + editor_pins = ast.literal_eval(ypwm.image_editor_pins) if ypwm.image_editor_pins != '' else {} + set_editor_images(editor_images, editor_pins) + ypwm.last_mode = obj.mode if yp and len(yp.layers) > 0: image, uv_name, src_of_img, entity, mapping, vcol = get_active_image_and_stuffs(obj, yp) @@ -4127,14 +4150,22 @@ def ypaint_last_object_update(scene): obj.yp.ori_offset_v = mirror.offset_v except: print('EXCEPTIION: Cannot remember original mirror offset!') + # HACK: Just in case active image is not correct + if image: ypwm.correct_paint_image_name = image.name + + # Set image editor image + update_image_editor_image(bpy.context, image) + + # Refresh temporary UV refresh_temp_uv(obj, src_of_img) # Into edit mode if obj.mode == 'EDIT' and ypwm.last_mode != 'EDIT': ypwm.last_mode = obj.mode # Remember the space - space, area_index = get_first_unpinned_image_editor_space(bpy.context, return_index=True) - if space and area_index != -1: + space, window_index, area_index = get_first_unpinned_image_editor_space(bpy.context, return_index=True) + if space and area_index != -1 and window_index != -1: + ypwm.edit_image_editor_window_index = window_index ypwm.edit_image_editor_area_index = area_index # Trigger updating active index to update image @@ -4160,6 +4191,43 @@ def ypaint_last_object_update(scene): if ypwm.last_mode != obj.mode: ypwm.last_mode = obj.mode +@persistent +def ypaint_missmatch_paint_slot_hack(scene): + # HACK: Force material active slot to update if necessary + wmyp = bpy.context.window_manager.ypprops + if not wmyp.halt_hacks and wmyp.correct_paint_image_name != '': + + if scene.tool_settings.image_paint.mode == 'MATERIAL': + + mat = get_active_material() + + try: active_img = mat.texture_paint_images[mat.paint_active_slot] + except: active_img = None + + try: correct_img = bpy.data.images.get(wmyp.correct_paint_image_name) + except: correct_img = None + + if active_img and correct_img and active_img != correct_img: + for idx, img in enumerate(mat.texture_paint_images): + if img == None: continue + if img.name == correct_img.name: + + # HACK: Remember all original images in all image editors since setting canvas/paint slot will replace all of them + ori_editor_imgs, ori_editor_pins = get_editor_images_dict(return_pins=True) + + success = False + try: + mat.paint_active_slot = idx + success = True + except: print('EXCEPTIION: Cannot set active paint slot image!') + + # HACK: Revert back to original editor images + if success: set_editor_images(ori_editor_imgs, ori_editor_pins) + + break + + wmyp.correct_paint_image_name = '' + @persistent def ypaint_force_update_on_anim(scene): #print(scene.frame_current) @@ -4245,7 +4313,6 @@ def register(): bpy.utils.register_class(YPaint) bpy.utils.register_class(YPaintMaterialProps) bpy.utils.register_class(YPaintTimer) - bpy.utils.register_class(YPaintBrushAssetCache) bpy.utils.register_class(YPaintWMProps) bpy.utils.register_class(YPaintSceneProps) bpy.utils.register_class(YPaintObjectUVHash) @@ -4264,6 +4331,7 @@ def register(): # Handlers if is_bl_newer_than(2, 80): bpy.app.handlers.depsgraph_update_post.append(ypaint_last_object_update) + bpy.app.handlers.depsgraph_update_post.append(ypaint_missmatch_paint_slot_hack) else: bpy.app.handlers.scene_update_pre.append(ypaint_last_object_update) bpy.app.handlers.scene_update_pre.append(ypaint_hacks_and_scene_updates) @@ -4298,7 +4366,6 @@ def unregister(): bpy.utils.unregister_class(YPaint) bpy.utils.unregister_class(YPaintMaterialProps) bpy.utils.unregister_class(YPaintTimer) - bpy.utils.unregister_class(YPaintBrushAssetCache) bpy.utils.unregister_class(YPaintWMProps) bpy.utils.unregister_class(YPaintSceneProps) bpy.utils.unregister_class(YPaintObjectUVHash) @@ -4308,6 +4375,7 @@ def unregister(): # Remove handlers if is_bl_newer_than(2, 80): bpy.app.handlers.depsgraph_update_post.remove(ypaint_last_object_update) + bpy.app.handlers.depsgraph_update_post.remove(ypaint_missmatch_paint_slot_hack) else: bpy.app.handlers.scene_update_pre.remove(ypaint_hacks_and_scene_updates) bpy.app.handlers.scene_update_pre.remove(ypaint_last_object_update) diff --git a/Test.py b/Test.py index ae9b3ca7..7cfce48f 100644 --- a/Test.py +++ b/Test.py @@ -14,7 +14,7 @@ def run_tests(): class YRunAutomatedTest(bpy.types.Operator): - bl_idname = "node.y_run_automated_test" + bl_idname = "wm.y_run_automated_test" bl_label = "Run Automated Test" bl_options = {'UNDO'} diff --git a/UDIM.py b/UDIM.py index 5c370e40..bc1ccd10 100644 --- a/UDIM.py +++ b/UDIM.py @@ -36,7 +36,8 @@ def fill_tile(image, tilenum, color=None, width=0, height=0, empty_only=False): bpy.ops.image.tile_add(number=tilenum, count=1, label="", color=color, width=width, height=height, float=image.is_float, alpha=True) else: bpy.ops.image.tile_add(override, number=tilenum, count=1, label="", color=color, width=width, height=height, float=image.is_float, alpha=True) - elif not empty_only: + # HACK: UDIM image with size of 0 always need to be filled first + elif not empty_only or (tilenum == 1001 and image.size[0] == 0): image.tiles.active = tile @@ -60,25 +61,32 @@ def fill_tile(image, tilenum, color=None, width=0, height=0, empty_only=False): return True -def copy_udim_pixels(src, dest): - for tile in src.tiles: +def copy_udim_pixels(src, dest, convert_colorspace=False): + + tilenums = [tile.number for tile in src.tiles] + + for tilenum in tilenums: + tile = src.tiles.get(tilenum) + # Check if tile number exists on both images and has same sizes - dtile = dest.tiles.get(tile.number) + dtile = dest.tiles.get(tilenum) if not dtile: continue if tile.size[0] != dtile.size[0] or tile.size[1] != dtile.size[1]: continue # Swap first - if tile.number != 1001: - swap_tile(src, 1001, tile.number) - swap_tile(dest, 1001, tile.number) + if tilenum != 1001: + swap_tile(src, 1001, tilenum) + swap_tile(dest, 1001, tilenum) # Set pixels - dest.pixels = list(src.pixels) + if convert_colorspace: + copy_image_pixels_with_conversion(src, dest) + else: dest.pixels = list(src.pixels) # Swap back - if tile.number != 1001: - swap_tile(src, 1001, tile.number) - swap_tile(dest, 1001, tile.number) + if tilenum != 1001: + swap_tile(src, 1001, tilenum) + swap_tile(dest, 1001, tilenum) def get_tile_numbers(objs, uv_name): @@ -529,7 +537,7 @@ def remove_tile(image, tilenum): remove_tiles(image, [tilenum]) class YRefillUDIMTiles(bpy.types.Operator): - bl_idname = "node.y_refill_udim_tiles" + bl_idname = "wm.y_refill_udim_tiles" bl_label = "Refill UDIM Tiles" bl_description = "Refill all UDIM tiles used by all layers and masks based on their UV" bl_options = {'REGISTER', 'UNDO'} @@ -541,6 +549,9 @@ def poll(cls, context): def execute(self, context): T = time.time() + if not hasattr(context, 'layer'): + return {'CANCELLED'} + yp = context.layer.id_data.yp entities, images, segment_names, segment_name_props = get_yp_entities_images_and_segments(yp) @@ -663,6 +674,10 @@ def create_udim_atlas(tilenums, name='', width=1024, height=1024, color=(0, 0, 0 image.yua.is_udim_atlas = True image.yui.base_color = color + # Float image atlas always use premultipled alpha + if hdr: + image.alpha_mode = 'PREMUL' + # Pack image initial_pack_udim(image) @@ -1155,6 +1170,10 @@ def poll(cls, context): def execute(self, context): image = context.image + if image.yua.is_udim_atlas or image.yia.is_image_atlas: + self.report({'ERROR'}, 'Converting image atlas to/from UDIM is not supported yet!') + return {'CANCELLED'} + # Create new image new_image = bpy.data.images.new( image.name, width=image.size[0], height=image.size[1], @@ -1184,6 +1203,10 @@ def execute(self, context): initial_pack_udim(new_image, color) + # Copy colorspace and alpha mode + new_image.colorspace_settings.name = image.colorspace_settings.name + new_image.alpha_mode = image.alpha_mode + # Copy image pixels copy_image_pixels(image, new_image) diff --git a/__init__.py b/__init__.py index d7c5ca95..cb48a720 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "Ucupaint", "author": "Yusuf Umar, Agni Rakai Sahakarya, Jan Bláha, Ahmad Rifai, morirain, Patrick W. Crawford, neomonkeus, Kareem Haddad, passivestar", - "version": (2, 2, 0), + "version": (2, 3, 5), "blender": (2, 80, 0), "location": "Node Editor > Properties > Ucupaint", "warning": "", @@ -14,11 +14,13 @@ if "bpy" in locals(): import imp imp.reload(Localization) + imp.reload(BaseOperator) imp.reload(image_ops) imp.reload(common) imp.reload(bake_common) imp.reload(modifier_common) imp.reload(lib) + imp.reload(Decal) imp.reload(ui) imp.reload(subtree) imp.reload(transition_common) @@ -46,13 +48,18 @@ imp.reload(versioning) imp.reload(addon_updater_ops) imp.reload(Test) + imp.reload(downloader) + imp.reload(properties) + imp.reload(operators) + imp.reload(tex_ui) else: from . import Localization - from . import image_ops, common, bake_common, modifier_common, lib, ui, subtree, transition_common, input_outputs, node_arrangements, node_connections, preferences + from . import BaseOperator, image_ops, common, bake_common, modifier_common, lib, Decal, ui, subtree, transition_common, input_outputs, node_arrangements, node_connections, preferences from . import vector_displacement_lib, vector_displacement from . import vcol_editor, transition, BakeTarget, BakeInfo, UDIM, ImageAtlas, MaskModifier, Mask, Modifier, NormalMapModifier, Layer, ListItem, Bake, BakeToLayer, Root, versioning from . import addon_updater_ops from . import Test + from .textures_lib import downloader, properties, operators, ui as tex_ui import bpy @@ -62,6 +69,7 @@ def register(): image_ops.register() preferences.register() lib.register() + Decal.register() ui.register() vcol_editor.register() transition.register() @@ -82,6 +90,10 @@ def register(): versioning.register() addon_updater_ops.register() Test.register() + downloader.register() + properties.register() + operators.register() + tex_ui.register() print('INFO: ' + common.get_addon_title() + ' ' + common.get_current_version_str() + ' is registered!') @@ -91,6 +103,7 @@ def unregister(): image_ops.unregister() preferences.unregister() lib.unregister() + Decal.unregister() ui.unregister() vcol_editor.unregister() transition.unregister() @@ -111,6 +124,10 @@ def unregister(): versioning.unregister() addon_updater_ops.unregister() Test.unregister() + downloader.unregister() + properties.unregister() + operators.unregister() + tex_ui.unregister() print('INFO: ' + common.get_addon_title() + ' ' + common.get_current_version_str() + ' is unregistered!') diff --git a/bake_common.py b/bake_common.py index 516fdb15..2249bb75 100644 --- a/bake_common.py +++ b/bake_common.py @@ -1,12 +1,15 @@ -import bpy, time, os, numpy, tempfile +import bpy, time, os, numpy, tempfile, bmesh from bpy.props import * from .common import * from .input_outputs import * from .node_connections import * -from . import lib, Layer, ImageAtlas, UDIM +from . import lib, Layer, ImageAtlas, UDIM, image_ops, Mask BL28_HACK = True +TEMP_VCOL = '__temp__vcol__' +TEMP_EMISSION = '_TEMP_EMI_' + BAKE_PROBLEMATIC_MODIFIERS = { 'MIRROR', 'SOLIDIFY', @@ -57,6 +60,59 @@ def search_join_problematic_texcoord(tree, node): return False +def get_compositor_node_tree(scene): + if not is_bl_newer_than(5): + return scene.node_tree + + return scene.compositing_node_group + +def get_compositor_output_node(tree): + node_type = 'GROUP_OUTPUT' if is_bl_newer_than(5) else 'COMPOSITE' + for n in tree.nodes: + if n.type == node_type: + return n + + # Create new compositor output if there's none + if is_bl_newer_than(5): + n = tree.nodes.new('NodeGroupOutput') + if 'Image' not in n.inputs: + new_tree_output(tree, 'Image', 'NodeSocketColor') + else: n = tree.nodes.new('CompositorNodeComposite') + + return n + +def get_scene_bake_multires(scene): + return scene.render.bake.use_multires if is_bl_newer_than(5) else scene.render.use_bake_multires + +def get_scene_bake_clear(scene): + return scene.render.bake.use_clear if is_bl_newer_than(5) else scene.render.use_bake_clear + +def get_scene_render_bake_type(scene): + return scene.render.bake.type if is_bl_newer_than(5) else scene.render.bake_type + +def get_scene_bake_margin(scene): + return scene.render.bake.margin if is_bl_newer_than(5) else scene.render.bake_margin + +def set_scene_bake_multires(scene, value): + if not is_bl_newer_than(5): + scene.render.use_bake_multires = value + else: scene.render.bake.use_multires = value + +def set_scene_bake_clear(scene, value): + if not is_bl_newer_than(5): + scene.render.use_bake_clear = value + else: scene.render.bake.use_clear = value + +def set_scene_render_bake_type(scene, value): + if not is_bl_newer_than(5): + scene.render.bake_type = value + else: scene.render.bake.type = value + +def set_scene_bake_margin(scene, value): + if not is_bl_newer_than(5): + scene.render.bake_margin = value + else: scene.render.bake.margin = value + def is_there_any_missmatched_attribute_types(objs): # Get number of attributes founds attr_counts = {} @@ -94,8 +150,10 @@ def is_there_any_missmatched_attribute_types(objs): def is_join_objects_problematic(yp, mat=None): for layer in yp.layers: + if not layer.enable: continue for mask in layer.masks: + if not mask.enable: continue if mask.type in {'VCOL', 'HEMI', 'COLOR_ID'}: continue if mask.texcoord_type in JOIN_PROBLEMATIC_TEXCOORDS or mask.type in {'OBJECT_INDEX'}: @@ -124,16 +182,39 @@ def is_join_objects_problematic(yp, mat=None): return False -def bake_object_op(): +def get_pointiness_image_minmax_value(image): + + if is_bl_newer_than(2, 83): + pxs = numpy.empty(shape=image.size[0] * image.size[1] * 4, dtype=numpy.float32) + image.pixels.foreach_get(pxs) + + pxs.shape = (-1, image.size[0], 4) + + # Set alpha to half + pxs *= (1, 1, 1, 0.5) + + min_val = pxs.min() + max_val = pxs.max() + + return min_val, max_val + else: + # TODO: Get minimum and maximum pixel on legacy blenders + return 0.4, 0.6 + +def bake_object_op(bake_type='EMIT'): try: - bpy.ops.object.bake() + if bake_type != 'EMIT': + bpy.ops.object.bake(type=bake_type) + else: bpy.ops.object.bake() except Exception as e: scene = bpy.context.scene if scene.cycles.device == 'GPU': print('EXCEPTIION: GPU baking failed! Trying to use CPU...') scene.cycles.device = 'CPU' - bpy.ops.object.bake() + if bake_type != 'EMIT': + bpy.ops.object.bake(type=bake_type) + else: bpy.ops.object.bake() else: print('EXCEPTIION:', e) @@ -149,6 +230,7 @@ def remember_before_bake(yp=None, mat=None): book['ori_engine'] = scene.render.engine book['ori_bake_type'] = scene.cycles.bake_type book['ori_samples'] = scene.cycles.samples + book['ori_use_osl'] = scene.cycles.shading_system book['ori_threads_mode'] = scene.render.threads_mode book['ori_margin'] = scene.render.bake.margin book['ori_use_clear'] = scene.render.bake.use_clear @@ -191,10 +273,10 @@ def remember_before_bake(yp=None, mat=None): else: book['ori_material_override'] = scene.render.layers.active.material_override # Multires related - book['ori_use_bake_multires'] = scene.render.use_bake_multires - book['ori_use_bake_clear'] = scene.render.use_bake_clear - book['ori_render_bake_type'] = scene.render.bake_type - book['ori_bake_margin'] = scene.render.bake_margin + book['ori_use_bake_multires'] = get_scene_bake_multires(scene) + book['ori_use_bake_clear'] = get_scene_bake_clear(scene) + book['ori_render_bake_type'] = get_scene_render_bake_type(scene) + book['ori_bake_margin'] = get_scene_bake_margin(scene) if is_bl_newer_than(2, 81) and not is_bl_newer_than(3) and scene.cycles.device == 'GPU' and 'compute_device_type' in bpy.context.preferences.addons['cycles'].preferences: book['compute_device_type'] = bpy.context.preferences.addons['cycles'].preferences['compute_device_type'] @@ -247,22 +329,6 @@ def remember_before_bake(yp=None, mat=None): if mat: book['ori_bsdf'] = mat.yp.ori_bsdf - # Remember all objects using the same material - objs = get_all_objects_with_same_materials(obj.active_material, True) - book['ori_mat_objs'] = [o.name for o in objs] - book['ori_mat_objs_active_nodes'] = [] - - # Remember other material active nodes - for o in objs: - active_node_names = [] - for m in o.data.materials: - if m and m.use_nodes and m.node_tree.nodes.active: - active_node_names.append(m.node_tree.nodes.active.name) - continue - active_node_names.append('') - - book['ori_mat_objs_active_nodes'].append(active_node_names) - return book def get_active_render_uv_node(tree, active_render_uv_name): @@ -296,11 +362,94 @@ def add_active_render_uv_node(tree, active_render_uv_name): if n.type == 'GROUP' and n.node_tree and not n.node_tree.yp.is_ypaint_node: add_active_render_uv_node(n.node_tree, active_render_uv_name) +def prepare_other_objs_colors(yp, other_objs): + + other_mats = [] + other_sockets = [] + other_defaults = [] + other_alpha_sockets = [] + other_alpha_defaults = [] + + ori_mat_no_nodes = [] + + valid_bsdf_types = ['BSDF_PRINCIPLED', 'BSDF_DIFFUSE', 'EMISSION'] + + for o in other_objs: + # Set new material if there's no material + if len(o.data.materials) == 0: + temp_mat = get_temp_default_material() + o.data.materials.append(temp_mat) + else: + for i, m in enumerate(o.data.materials): + if m == None: + temp_mat = get_temp_default_material() + o.data.materials[i] = temp_mat + elif not m.use_nodes: + if m not in ori_mat_no_nodes: + ori_mat_no_nodes.append(m) + m.use_nodes = True + + for mat in o.data.materials: + if mat == None: continue + if mat in other_mats: continue + if not mat.use_nodes: continue + + # Get material output + output = get_material_output(mat) + if not output: continue + + socket = None + default = None + alpha_socket = None + alpha_default = 1.0 + + if mat in ori_mat_no_nodes and hasattr(mat, 'diffuse_color'): + default = mat.diffuse_color + + # Check for possible sockets available on the bsdf node + if not socket: + # Search for main bsdf + bsdf_node = get_closest_bsdf_backward(output, valid_bsdf_types) + + if bsdf_node.type == 'BSDF_PRINCIPLED': + socket = bsdf_node.inputs['Base Color'] + + elif 'Color' in bsdf_node.inputs: + socket = bsdf_node.inputs['Color'] + + if socket: + if len(socket.links) == 0: + if default == None: + default = socket.default_value + else: + socket = socket.links[0].from_socket + + # Get alpha socket + alpha_socket = bsdf_node.inputs.get('Alpha') + if alpha_socket: + + if len(alpha_socket.links) == 0: + alpha_default = alpha_socket.default_value + alpha_socket = None + else: + alpha_socket = alpha_socket.links[0].from_socket + + # Append objects and materials if socket is found + if socket or default: + other_mats.append(mat) + other_sockets.append(socket) + other_defaults.append(default) + other_alpha_sockets.append(alpha_socket) + other_alpha_defaults.append(alpha_default) + + return other_mats, other_sockets, other_defaults, other_alpha_sockets, other_alpha_defaults, ori_mat_no_nodes + def prepare_other_objs_channels(yp, other_objs): ch_other_objects = [] ch_other_mats = [] ch_other_sockets = [] ch_other_defaults = [] + ch_other_default_weights = [] ch_other_alpha_sockets = [] ch_other_alpha_defaults = [] @@ -313,6 +462,7 @@ def prepare_other_objs_channels(yp, other_objs): mats = [] sockets = [] defaults = [] + default_weights = [] alpha_sockets = [] alpha_defaults = [] @@ -339,7 +489,7 @@ def prepare_other_objs_channels(yp, other_objs): for mat in o.data.materials: if mat == None: continue - if mat in mats: continue + #if mat in mats: continue if not mat.use_nodes: continue # Get material output @@ -348,6 +498,7 @@ def prepare_other_objs_channels(yp, other_objs): socket = None default = None + default_weight = 1.0 alpha_socket = None alpha_default = 1.0 @@ -387,6 +538,22 @@ def prepare_other_objs_channels(yp, other_objs): if len(socket.links) == 0: if default == None: default = socket.default_value + + # Blender 4.0 has weight/strength value for some inputs + if is_bl_newer_than(4): + input_prefixes = ['Subsurface', 'Coat', 'Sheen', 'Emission'] + for prefix in input_prefixes: + if socket.name.startswith(prefix): + + if socket.name.startswith('Emission'): + weight_socket_name = 'Emission Strength' + else: weight_socket_name = prefix + ' Weight' + + # NOTE: Only set the default weight if there's no dedicated channel for weight in destination yp + if weight_socket_name not in yp.channels and weight_socket_name != socket.name: + weight_socket = bsdf_node.inputs.get(weight_socket_name) + if weight_socket: + default_weight = weight_socket.default_value else: socket = socket.links[0].from_socket @@ -405,6 +572,7 @@ def prepare_other_objs_channels(yp, other_objs): mats.append(mat) sockets.append(socket) defaults.append(default) + default_weights.append(default_weight) alpha_sockets.append(alpha_socket) alpha_defaults.append(alpha_default) @@ -415,10 +583,11 @@ def prepare_other_objs_channels(yp, other_objs): ch_other_mats.append(mats) ch_other_sockets.append(sockets) ch_other_defaults.append(defaults) + ch_other_default_weights.append(default_weights) ch_other_alpha_sockets.append(alpha_sockets) ch_other_alpha_defaults.append(alpha_defaults) - return ch_other_objects, ch_other_mats, ch_other_sockets, ch_other_defaults, ch_other_alpha_sockets, ch_other_alpha_defaults, ori_mat_no_nodes + return ch_other_objects, ch_other_mats, ch_other_sockets, ch_other_defaults, ch_other_default_weights, ch_other_alpha_sockets, ch_other_alpha_defaults, ori_mat_no_nodes def recover_other_objs_channels(other_objs, ori_mat_no_nodes): for o in other_objs: @@ -439,15 +608,21 @@ def prepare_bake_settings( disable_problematic_modifiers=False, hide_other_objs=True, bake_from_multires=False, tile_x=64, tile_y=64, use_selected_to_active=False, max_ray_distance=0.0, cage_extrusion=0.0, bake_target = 'IMAGE_TEXTURES', - source_objs=[], bake_device='CPU', use_denoising=False, margin_type='ADJACENT_FACES', cage_object_name='', - normal_space='TANGENT', + source_objs=[], bake_device='CPU', use_denoising=False, margin_type='ADJACENT_FACES', + use_cage=False, cage_object_name='', + normal_space='TANGENT', use_osl=False, ): scene = bpy.context.scene ypui = bpy.context.window_manager.ypui + wmyp = bpy.context.window_manager.ypprops + + # Hack function on depsgraph update can cause crash, so halt it before baking + wmyp.halt_hacks = True scene.render.engine = 'CYCLES' scene.cycles.samples = samples + scene.cycles.shading_system = use_osl scene.render.threads_mode = 'AUTO' scene.render.bake.margin = margin #scene.render.bake.use_clear = True @@ -457,7 +632,8 @@ def prepare_bake_settings( scene.render.bake.max_ray_distance = max_ray_distance scene.render.bake.cage_extrusion = cage_extrusion cage_object = bpy.data.objects.get(cage_object_name) if cage_object_name != '' else None - scene.render.bake.use_cage = True if cage_object else False + #scene.render.bake.use_cage = True if cage_object else False + scene.render.bake.use_cage = use_cage if cage_object: if is_bl_newer_than(2, 80): scene.render.bake.cage_object = cage_object else: scene.render.bake.cage_object = cage_object.name @@ -486,16 +662,16 @@ def prepare_bake_settings( else: scene.render.layers.active.material_override = None if bake_from_multires: - scene.render.use_bake_multires = True - scene.render.bake_type = bake_type - scene.render.bake_margin = margin - scene.render.use_bake_clear = False + set_scene_bake_multires(scene, True) + set_scene_render_bake_type(scene, bake_type) + set_scene_bake_margin(scene, margin) + set_scene_bake_clear(scene, False) else: - scene.render.use_bake_multires = False + set_scene_bake_multires(scene, False) scene.cycles.bake_type = bake_type # Old blender will always use CPU - if not is_bl_newer_than(2, 80): + if not is_bl_newer_than(2, 80) or use_osl: scene.cycles.device = 'CPU' else: scene.cycles.device = bake_device @@ -585,7 +761,7 @@ def prepare_bake_settings( uv_layers = get_uv_layers(obj) if len(uv_layers) == 0: scene.objects.active = obj - bpy.ops.node.y_add_simple_uvs() + bpy.ops.wm.y_add_simple_uvs() if scene.objects.active != ori_active_object: scene.objects.active = ori_active_object @@ -610,17 +786,33 @@ def prepare_bake_settings( #ypui.disable_auto_temp_uv_update = True # Set to object mode - try: bpy.ops.object.mode_set(mode = 'OBJECT') - except: pass + if bpy.context.object and bpy.context.object.mode != 'OBJECT': + try: bpy.ops.object.mode_set(mode = 'OBJECT') + except: pass # Disable parallax channel if book['parallax_ch']: book['parallax_ch'].enable_parallax = False + # Remember object materials related to baking + book['ori_mat_objs'] = [] + book['ori_mat_objs_active_nodes'] = [] + for o in objs: mat = o.active_material if not mat: continue + # Remember other material active nodes + active_node_names = [] + for m in o.data.materials: + if m and m.use_nodes and m.node_tree.nodes.active: + active_node_names.append(m.node_tree.nodes.active.name) + continue + active_node_names.append('') + + book['ori_mat_objs'].append(o.name) + book['ori_mat_objs_active_nodes'].append(active_node_names) + # Add extra uv nodes for non connected texture nodes outside yp node if uv_map != '': @@ -662,9 +854,11 @@ def recover_bake_settings(book, yp=None, recover_active_uv=False, mat=None): obj = book['obj'] uv_layers = get_uv_layers(obj) ypui = bpy.context.window_manager.ypui + wmyp = bpy.context.window_manager.ypprops scene.render.engine = book['ori_engine'] scene.cycles.samples = book['ori_samples'] + scene.cycles.shading_system = book['ori_use_osl'] scene.cycles.bake_type = book['ori_bake_type'] scene.render.threads_mode = book['ori_threads_mode'] scene.render.bake.margin = book['ori_margin'] @@ -704,10 +898,10 @@ def recover_bake_settings(book, yp=None, recover_active_uv=False, mat=None): else: scene.render.layers.active.material_override = book['ori_material_override'] # Multires related - scene.render.use_bake_multires = book['ori_use_bake_multires'] - scene.render.use_bake_clear = book['ori_use_bake_clear'] - scene.render.bake_type = book['ori_render_bake_type'] - scene.render.bake_margin = book['ori_bake_margin'] + set_scene_bake_multires(scene, book['ori_use_bake_multires']) + set_scene_bake_clear(scene, book['ori_use_bake_clear']) + set_scene_render_bake_type(scene, book['ori_render_bake_type']) + set_scene_bake_margin(scene, book['ori_bake_margin']) if 'compute_device_type' in book: bpy.context.preferences.addons['cycles'].preferences['compute_device_type'] = book['compute_device_type'] @@ -848,6 +1042,9 @@ def recover_bake_settings(book, yp=None, recover_active_uv=False, mat=None): #act_uv = m.node_tree.nodes.get(ACTIVE_UV_NODE) #if act_uv: m.node_tree.nodes.remove(act_uv) + # Bring back the hack functions + wmyp.halt_hacks = False + def prepare_composite_settings(res_x=1024, res_y=1024, use_hdr=False): book = {} @@ -855,7 +1052,7 @@ def prepare_composite_settings(res_x=1024, res_y=1024, use_hdr=False): book['ori_scene_name'] = bpy.context.scene.name # Remember active object and view layer - book['ori_viewlayer'] = bpy.context.window.view_layer.name if bpy.context.window.view_layer and is_bl_newer_than(2, 80) else '' + book['ori_viewlayer'] = bpy.context.window.view_layer.name if is_bl_newer_than(2, 80) and bpy.context.window.view_layer else '' book['ori_object'] = bpy.context.object.name if bpy.context.object else '' # Check if original viewport is using camera view @@ -864,7 +1061,9 @@ def prepare_composite_settings(res_x=1024, res_y=1024, use_hdr=False): # Create new temporary scene scene = bpy.data.scenes.new(name='TEMP_COMPOSITE_SCENE') - bpy.context.window.scene = scene + if is_bl_newer_than(2, 80): + bpy.context.window.scene = scene + else: bpy.context.screen.scene = scene # Set up render settings scene.cycles.samples = 1 @@ -875,8 +1074,13 @@ def prepare_composite_settings(res_x=1024, res_y=1024, use_hdr=False): scene.render.resolution_percentage = 100 scene.render.pixel_aspect_x = 1.0 scene.render.pixel_aspect_y = 1.0 - scene.use_nodes = True + if is_bl_newer_than(5): + comp_tree = bpy.data.node_groups.new('TEMP_COMPOSITOR_TREE__', 'CompositorNodeTree') + scene.compositing_node_group = comp_tree + else: + scene.use_nodes = True scene.view_settings.view_transform = 'Standard' if is_bl_newer_than(2, 80) else 'Default' + scene.render.dither_intensity = 0.0 # Float/HDR image related scene.render.image_settings.file_format = 'OPEN_EXR' if use_hdr else 'PNG' @@ -907,12 +1111,19 @@ def recover_composite_settings(book): remove_datablock(bpy.data.objects, cam_obj) remove_datablock(bpy.data.cameras, cam) + # Remove compositor node tree + if is_bl_newer_than(5): + comp_tree = get_compositor_node_tree(scene) + remove_datablock(bpy.data.node_groups, comp_tree) + # Remove temp scene remove_datablock(bpy.data.scenes, scene) # Go back to original scene scene = bpy.data.scenes.get(book['ori_scene_name']) - bpy.context.window.scene = scene + if is_bl_newer_than(2, 80): + bpy.context.window.scene = scene + else: bpy.context.screen.scene = scene # Recover camera view if book['ori_camera_view']: @@ -929,6 +1140,85 @@ def recover_composite_settings(book): if ori_object and bpy.context.object != ori_object: set_active_object(ori_object) +def blur_image(image, filter_type='GAUSS', size=10): + T = time.time() + print('BLUR: Doing Blur pass on', image.name + '...') + + # Preparing settings + book = prepare_composite_settings(use_hdr=image.is_float) + scene = bpy.context.scene + + # Set up compositor + tree = get_compositor_node_tree(scene) + composite = get_compositor_output_node(tree) + blur = tree.nodes.new('CompositorNodeBlur') + blur.filter_type = filter_type + if is_bl_newer_than(4, 5): + blur.inputs['Size'].default_value[0] = size + blur.inputs['Size'].default_value[1] = size + else: + blur.size_x = int(size) + blur.size_y = int(size) + image_node = tree.nodes.new('CompositorNodeImage') + image_node.image = image + + gamma = None + if image.colorspace_settings.name != get_srgb_name() and not image.is_float: + nodeid = 'ShaderNodeGamma' if is_bl_newer_than(5) else 'CompositorNodeGamma' + gamma = tree.nodes.new(nodeid) + gamma.inputs[1].default_value = 2.2 + + rgb = image_node.outputs[0] + if gamma: + tree.links.new(rgb, gamma.inputs[0]) + rgb = gamma.outputs[0] + tree.links.new(rgb, blur.inputs['Image']) + rgb = blur.outputs[0] + tree.links.new(rgb, composite.inputs[0]) + + if image.source == 'TILED': + tilenums = [tile.number for tile in image.tiles] + else: tilenums = [1001] + + # Get temporary filepath + ext = 'exr' if image.is_float else 'png' + filepath = os.path.join(tempfile.gettempdir(), 'TEST_RENDER__.' + ext) + + for tilenum in tilenums: + + # Swap tile to 1001 to access the data + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + + # Set render resolution + scene.render.resolution_x = image.size[0] + scene.render.resolution_y = image.size[1] + + # Render image! + bpy.ops.render.render() + + # Save the image + render_result = next(img for img in bpy.data.images if img.type == "RENDER_RESULT") + render_result.save_render(filepath) + temp_image = bpy.data.images.load(filepath) + + # Copy image pixels + copy_image_pixels(temp_image, image) + + # Remove temp image + remove_datablock(bpy.data.images, temp_image) + os.remove(filepath) + + # Swap back the tile + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + + # Recover settings + recover_composite_settings(book) + + print('BLUR:', image.name, 'blur pass is done in', '{:0.2f}'.format(time.time() - T), 'seconds!') + return image + def denoise_image(image): if not is_bl_newer_than(2, 81): return image @@ -940,16 +1230,19 @@ def denoise_image(image): scene = bpy.context.scene # Set up compositor - tree = scene.node_tree - composite = [n for n in tree.nodes if n.type == 'COMPOSITE'][0] + tree = get_compositor_node_tree(scene) + composite = get_compositor_output_node(tree) denoise = tree.nodes.new('CompositorNodeDenoise') - denoise.use_hdr = image.is_float + if is_bl_newer_than(5): + denoise.inputs.get('HDR').default_value = image.is_float + else: denoise.use_hdr = image.is_float image_node = tree.nodes.new('CompositorNodeImage') image_node.image = image gamma = None if image.colorspace_settings.name != get_srgb_name() and not image.is_float: - gamma = tree.nodes.new('CompositorNodeGamma') + nodeid = 'ShaderNodeGamma' if is_bl_newer_than(5) else 'CompositorNodeGamma' + gamma = tree.nodes.new(nodeid) gamma.inputs[1].default_value = 2.2 rgb = image_node.outputs[0] @@ -1003,7 +1296,123 @@ def denoise_image(image): print('DENOISE:', image.name, 'denoise pass is done in', '{:0.2f}'.format(time.time() - T), 'seconds!') return image -def blur_image(image, alpha_aware=True, factor=1.0, samples=512, bake_device='CPU'): +def dither_image(image, dither_intensity=1.0, alpha_aware=True): + if not image.is_float: + print('DITHER: Cannot dither image \''+image.name+'\' since it\'s not a float image') + return + + T = time.time() + print('DITHER: Doing dithering pass on', image.name + '...') + + # Preparing settings + book = prepare_composite_settings(use_hdr=image.is_float) + scene = bpy.context.scene + + # Set render to byte image + scene.render.image_settings.file_format = 'PNG' + scene.render.image_settings.color_mode = 'RGBA' + scene.render.image_settings.color_depth = '8' + scene.render.dither_intensity = dither_intensity + + # Set up compositor + tree = get_compositor_node_tree(scene) + composite = get_compositor_output_node(tree) + image_node = tree.nodes.new('CompositorNodeImage') + image_node.image = image + + if image.source == 'TILED': + tilenums = [tile.number for tile in image.tiles] + else: tilenums = [1001] + + prefix_filename = 'DITHER_RENDER___' + temp_images = [] + temp_filepaths = [] + + # Render dithered byte images + for i, tilenum in enumerate(tilenums): + + # Swap tile to 1001 to access the data + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + + # Get temporary filepath + filepath = os.path.join(tempfile.gettempdir(), prefix_filename+str(tilenum)+'.png') + temp_filepaths.append(filepath) + + # Set render resolution + scene.render.resolution_x = image.size[0] + scene.render.resolution_y = image.size[1] + + # Connect image's rgb + tree.links.new(image_node.outputs[0], composite.inputs[0]) + + # Disable alpha is necesarry if image has alpha + if alpha_aware: + composite.use_alpha = False + + # Render image! + bpy.ops.render.render() + + # Save the image + render_result = next(img for img in bpy.data.images if img.type == "RENDER_RESULT") + render_result.save_render(filepath) + temp_image = bpy.data.images.load(filepath) + temp_images.append(temp_image) + + if alpha_aware: + composite.use_alpha = True + + # Render alpha image! + bpy.ops.render.render() + + # Save alpha image + alpha_filepath = os.path.join(tempfile.gettempdir(), prefix_filename+str(tilenum)+'_ALPHA.png') + render_result = next(img for img in bpy.data.images if img.type == "RENDER_RESULT") + render_result.save_render(alpha_filepath) + alpha_image = bpy.data.images.load(alpha_filepath) + + copy_image_channel_pixels(alpha_image, temp_image, 3, 3) + + # Remove alpha image + remove_datablock(bpy.data.images, alpha_image) + os.remove(alpha_filepath) + + # Swap back the tile + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + + # Convert input image to byte + image = image_ops.toggle_image_bit_depth(image, no_copy=True, force_srgb=True) + + # Copy images + for i, tilenum in enumerate(tilenums): + + # Swap tile to 1001 to access the data + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + + # Get temporary image + temp_image = temp_images[i] + filepath = temp_filepaths[i] + + # Copy image pixels + copy_image_pixels(temp_image, image) + + # Remove temp image + remove_datablock(bpy.data.images, temp_image) + os.remove(filepath) + + # Swap back the tile + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + + # Recover settings + recover_composite_settings(book) + + print('DENOISE:', image.name, 'dithering pass is done in', '{:0.2f}'.format(time.time() - T), 'seconds!') + return image + +def noise_blur_image(image, alpha_aware=True, factor=1.0, samples=512, bake_device='CPU'): T = time.time() print('BLUR: Doing Blur pass on', image.name + '...') book = remember_before_bake() @@ -1265,9 +1674,7 @@ def fxaa_image(image, alpha_aware=True, bake_device='CPU', first_tile_only=False remove_datablock(bpy.data.node_groups, fxaa.node_tree, user=fxaa, user_prop='node_tree') remove_datablock(bpy.data.materials, mat) - plane = plane_obj.data - bpy.ops.object.delete() - remove_datablock(bpy.data.meshes, plane) + remove_mesh_obj(plane_obj) # Recover settings recover_bake_settings(book) @@ -1394,10 +1801,173 @@ def get_valid_filepath(img, use_hdr): return img.filepath +def is_baked_normal_without_bump_needed(root_ch): + return ( + (not is_overlay_normal_empty(root_ch) and (any_layers_using_disp(root_ch) or any_layers_using_vdisp(root_ch))) or + (root_ch.enable_subdiv_setup and (any_layers_using_disp(root_ch) or any_layers_using_vdisp(root_ch))) + ) + +def get_bake_max_height(root_ch, mat=None, node=None, tex=None, emit=None): + + T = time.time() + print('BAKE MAX HEIGHT: Doing Max Height baking on', root_ch.name + '...') + + tree = root_ch.id_data + yp = tree.yp + scene = bpy.context.scene + if not mat: mat = get_active_material() + if not node: node = get_active_ypaint_node() + + # Do setup first before baking + book = {} + ori_margin = scene.render.bake.margin + high_margin = 1000 + ori_matout_inp = None + if not tex and not emit: + obj = bpy.context.object + uv_layers = get_uv_layers(obj) + if len(uv_layers) == 0: return + uv_map = uv_layers[0].name + mat_out = get_material_output(mat) + if not mat_out: return + + book = remember_before_bake() + prepare_bake_settings(book, [obj], yp, samples=1, margin=high_margin, uv_map=uv_map, bake_device='CPU', margin_type='EXTEND') + + tex = mat.node_tree.nodes.new('ShaderNodeTexImage') + emit = mat.node_tree.nodes.new('ShaderNodeEmission') + + # Connect emit to output material + if len(mat_out.inputs[0].links) > 0: + ori_matout_inp = mat_out.inputs[0].links[0].from_socket + mat.node_tree.links.new(emit.outputs[0], mat_out.inputs[0]) + + mat.node_tree.nodes.active = tex + + else: + # Use high margin to make sure all pixels are covered + scene.render.bake.margin = high_margin + + # Check for height socket + forced_height_ios = False + if 'Height' not in node.outputs: + check_all_channel_ios(yp, reconnect=True, force_height_io=True) + forced_height_ios = True + + # Create target image + if UDIM.is_udim_supported(): + img = bpy.data.images.new( + name='____MAXHEIGHT_TEMP', width=100, height=100, + alpha=False, tiled=False, float_buffer=True + ) + else: + img = bpy.data.images.new( + name='____MAXHEIGHT_TEMP', width=100, height=100, + alpha=False, float_buffer=True + ) + + img.colorspace_settings.name = get_noncolor_name() + tex.image = img + + # Connect max height output to emit node + create_link(mat.node_tree, node.outputs[root_ch.name + io_suffix['MAX_HEIGHT']], + emit.inputs[0]) + + # Bake + print('BAKE MAX HEIGHT: Baking max height of ' + root_ch.name + ' channel...') + bake_object_op() + + # Set baked max height image + max_height_value = img.pixels[0] + #end_max_height = check_new_node(tree, root_ch, 'end_max_height', 'ShaderNodeValue', 'Max Height') + #end_max_height.outputs[0].default_value = max_height_value + + # Remove max height image + remove_datablock(bpy.data.images, img, user=tex, user_prop='image') + + if len(book) > 0: + # Reconnect original output connections + if ori_matout_inp: + mat.node_tree.links.new(ori_matout_inp, mat_out.inputs[0]) + + # Delete temporary nodes + simple_remove_node(mat.node_tree, tex) + simple_remove_node(mat.node_tree, emit) + + # Recover settings + recover_bake_settings(book, yp) + else: + # Recover margin + scene.render.bake.margin = ori_margin + + return max_height_value + +def get_bake_properties_from_self(self): + + bprops = dotdict() + + # NOTE: Getting props from keys doesn't work + #for prop in self.properties.keys(): + # try: bprops[prop] = getattr(self, prop) + # except Exception as e: print(e) + + props = [ + 'bake_device', + 'samples', + 'margin', + 'margin_type', + 'width', + 'height', + 'image_resolution', + 'use_custom_resolution', + 'name', + 'uv_map', + 'uv_map_1', + 'interpolation', + 'type', + 'use_cage', + 'cage_object_name', + 'cage_extrusion', + 'max_ray_distance', + 'normalize', + 'ao_distance', + 'bevel_samples', + 'bevel_radius', + 'multires_base', + 'target_type', + 'fxaa', + 'ssaa', + 'denoise', + 'channel_idx', + 'blend_type', + 'normal_blend_type', + 'normal_map_type', + 'hdr', + 'use_baked_disp', + 'flip_normals', + 'only_local', + 'subsurf_influence', + 'force_bake_all_polygons', + 'use_image_atlas', + 'use_udim', + 'blur', + 'blur_type', + 'blur_factor', + 'blur_size' + ] + + for prop in props: + if hasattr(self, prop): + bprops[prop] = getattr(self, prop) + elif prop == 'hdr': + bprops['hdr'] = False + + return bprops + def bake_channel( uv_map, mat, node, root_ch, width=1024, height=1024, target_layer=None, use_hdr=False, aa_level=1, force_use_udim=False, tilenums=[], interpolation='Linear', - use_float_for_displacement=False, use_float_for_normal=False + use_float_for_displacement=False, use_float_for_normal=False, bprops=None ): print('BAKE CHANNEL: Baking', root_ch.name + ' channel...') @@ -1405,6 +1975,7 @@ def bake_channel( tree = node.node_tree yp = tree.yp ypup = get_user_preferences() + scene = bpy.context.scene channel_idx = get_channel_index(root_ch) @@ -1413,19 +1984,36 @@ def bake_channel( objs = get_all_objects_with_same_materials(mat) tilenums = UDIM.get_tile_numbers(objs, uv_map) - # Check if temp bake is necessary - temp_baked = [] - if root_ch.type == 'NORMAL': + # Check if baking fake lighting is necessary + # NOTE: Only needed for Blender 2.80 or less because those are the only versions that can use non-baked fake lighting as bump + ori_bprops_name = bprops['name'] if bprops else '' + if not is_bl_newer_than(2, 81) and root_ch.type == 'NORMAL': for lay in yp.layers: - if lay.type in {'HEMI'} and not lay.use_temp_bake: - print('BAKE CHANNEL: Fake lighting layer found! Baking temporary image of ' + lay.name + ' layer...') - temp_bake(bpy.context, lay, width, height, True, 1, bpy.context.scene.render.bake.margin, uv_map) - temp_baked.append(lay) + if not lay.enable: continue + if channel_idx >= len(lay.channels): continue + ch = lay.channels[channel_idx] + if not ch.enable: continue + bake_happened = False + + if lay.type in {'HEMI'} and not lay.use_baked: + bprops['name'] = 'Baked ' + lay.name + bprops['hdr'] = is_bl_newer_than(2, 80) + bake_entity_as_image(lay, bprops, set_image_to_entity=True) + bake_happened = True + for mask in lay.masks: - if mask.type in {'HEMI'} and not mask.use_temp_bake: - print('BAKE CHANNEL: Fake lighting mask found! Baking temporary image of ' + mask.name + ' mask...') - temp_bake(bpy.context, mask, width, height, True, 1, bpy.context.scene.render.bake.margin, uv_map) - temp_baked.append(mask) + if mask.type in {'HEMI'} and not mask.use_baked: + bprops['name'] = 'Baked ' + mask.name + bprops['hdr'] = is_bl_newer_than(2, 80) + bake_entity_as_image(mask, bprops, set_image_to_entity=True) + bake_happened = True + + if bake_happened: + reconnect_layer_nodes(lay) + rearrange_layer_nodes(lay) + + # Recover bprops name + if ori_bprops_name != '': bprops['name'] = ori_bprops_name ch = None img = None @@ -1465,12 +2053,17 @@ def bake_channel( tex = mat.node_tree.nodes.new('ShaderNodeTexImage') emit = mat.node_tree.nodes.new('ShaderNodeEmission') + # Normal baking need special node setup + bsdf = None + norm = None if root_ch.type == 'NORMAL': - - norm = mat.node_tree.nodes.new('ShaderNodeGroup') - if is_bl_newer_than(2, 80) and not is_bl_newer_than(3): - norm.node_tree = get_node_tree_lib(lib.BAKE_NORMAL_ACTIVE_UV) - else: norm.node_tree = get_node_tree_lib(lib.BAKE_NORMAL_ACTIVE_UV_300) + if is_bl_newer_than(2, 80): + # Use principled bsdf for Blender 2.80+ + bsdf = mat.node_tree.nodes.new('ShaderNodeBsdfPrincipled') + else: + # Use custom normal calculation for legacy blender + norm = mat.node_tree.nodes.new('ShaderNodeGroup') + norm.node_tree = get_node_tree_lib(lib.BAKE_NORMAL_ACTIVE_UV_300) # Set tex as active node mat.node_tree.nodes.active = tex @@ -1479,6 +2072,19 @@ def bake_channel( #for l in output.inputs['Displacement'].links: # disp_from_socket = l.from_socket + # Original displacement connection + ori_disp_from_node = '' + ori_disp_from_socket = '' + + # Remove displacement link early if displacement setup is enabled and the current channel is not normal channel + height_root_ch = get_root_height_channel(yp) + if height_root_ch and root_ch != height_root_ch and height_root_ch.enable_subdiv_setup: + for link in mat_out.inputs['Displacement'].links: + ori_disp_from_node = link.from_node.name + ori_disp_from_socket = link.from_socket.name + mat.node_tree.links.remove(link) + break + # Connect emit to output material mat.node_tree.links.new(emit.outputs[0], output.inputs[0]) @@ -1589,7 +2195,8 @@ def bake_channel( # Create new udim image img = bpy.data.images.new( name=img_name, width=width, height=height, - alpha=True, tiled=True + alpha=True, tiled=True, + float_buffer = (root_ch.type == 'NORMAL' and use_float_for_normal) or use_hdr ) # Fill tiles @@ -1607,7 +2214,8 @@ def bake_channel( else: # Create new standard image img = bpy.data.images.new( - name=img_name, width=width, height=height, alpha=True + name=img_name, width=width, height=height, alpha=True, + float_buffer = (root_ch.type == 'NORMAL' and use_float_for_normal) or use_hdr ) img.generated_type = 'BLANK' @@ -1623,10 +2231,6 @@ def bake_channel( ): img.filepath = filepath - # Use hdr - if (root_ch.type == 'NORMAL' and use_float_for_normal) or use_hdr: - img.use_generated_float = True - # Set colorspace to linear if root_ch.colorspace == 'LINEAR' or root_ch.type == 'NORMAL' or (root_ch.type != 'NORMAL' and use_hdr): img.colorspace_settings.name = get_noncolor_name() @@ -1643,44 +2247,65 @@ def bake_channel( # Links to bake rgb = node.outputs[root_ch.name] - if root_ch.type == 'NORMAL': - rgb = create_link(mat.node_tree, rgb, norm.inputs[0])[0] - #elif root_ch.colorspace != 'LINEAR' and target_layer: - #elif target_layer: - #rgb = create_link(mat.node_tree, rgb, lin2srgb.inputs[0])[0] - mat.node_tree.links.new(rgb, emit.inputs[0]) + if root_ch.type == 'NORMAL': + if norm: + # Custom normal calculation setup + rgb = create_link(mat.node_tree, rgb, norm.inputs[0])[0] + mat.node_tree.links.new(rgb, emit.inputs[0]) + elif bsdf: + # Baking normal from diffuse bsdf + ori_normal_space = scene.render.bake.normal_space + scene.cycles.bake_type = 'NORMAL' + scene.render.bake.normal_space = 'TANGENT' + + # Connect bsdf node to output + mat.node_tree.links.new(rgb, bsdf.inputs['Normal']) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + # HACK: Sometimes the bsdf node need color socket to be also connected + for rch in yp.channels: + if rch.type == 'RGB': + soc = node.outputs.get(rch.name) + if soc: + mat.node_tree.links.new(soc, bsdf.inputs[0]) + break + else: + mat.node_tree.links.new(rgb, emit.inputs[0]) # Bake! print('BAKE CHANNEL: Baking main image of ' + root_ch.name + ' channel...') - bake_object_op() + bake_object_op(scene.cycles.bake_type) + + # Revert back the original bake settings + if root_ch.type == 'NORMAL' and bsdf: + scene.cycles.bake_type = 'EMIT' + scene.render.bake.normal_space = ori_normal_space + mat.node_tree.links.new(emit.outputs[0], output.inputs[0]) # Bake displacement + disp_img = None if root_ch.type == 'NORMAL': # Make sure height outputs available check_all_channel_ios(yp, reconnect=True, force_height_io=True) + # Break displacement connection if displacement setup is enabled + if root_ch.enable_subdiv_setup: + for link in mat_out.inputs['Displacement'].links: + ori_disp_from_node = link.from_node.name + ori_disp_from_socket = link.from_socket.name + mat.node_tree.links.remove(link) + break + if not target_layer: - ### Normal overlay only - if is_overlay_normal_empty(yp) and not root_ch.enable_subdiv_setup: + ### Normal without bump only + if not is_baked_normal_without_bump_needed(root_ch): # Remove baked_normal_overlay remove_node(tree, root_ch, 'baked_normal_overlay') else: - # Original displacement connection - ori_disp_from_node = '' - ori_disp_from_socket = '' - - # Remove displacement link if subdiv setup is on - if root_ch.enable_subdiv_setup: - for link in mat_out.inputs['Displacement'].links: - ori_disp_from_node = link.from_node.name - ori_disp_from_socket = link.from_socket.name - mat.node_tree.links.remove(link) - break - baked_normal_overlay = tree.nodes.get(root_ch.baked_normal_overlay) if not baked_normal_overlay: baked_normal_overlay = new_node( @@ -1726,17 +2351,27 @@ def bake_channel( create_link(tree, soc, end.inputs[root_ch.name]) #create_link(mat.node_tree, node.outputs[root_ch.name], emit.inputs[0]) + # Preparing for normal baking + if bsdf: + scene.cycles.bake_type = 'NORMAL' + scene.render.bake.normal_space = 'TANGENT' + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + # Bake - print('BAKE CHANNEL: Baking normal overlay image of ' + root_ch.name + ' channel...') - bake_object_op() + print('BAKE CHANNEL: Baking normal without bump image of ' + root_ch.name + ' channel...') + bake_object_op(scene.cycles.bake_type) - #return + # Recover normal baking related + if bsdf: + scene.cycles.bake_type = 'EMIT' + scene.render.bake.normal_space = ori_normal_space + mat.node_tree.links.new(emit.outputs[0], output.inputs[0]) # Recover connection if end_linear: create_link(tree, ori_soc, end.inputs[root_ch.name]) - # Set baked normal overlay image + # Set baked normal without bump image if baked_normal_overlay.image: temp = baked_normal_overlay.image img_users = get_all_image_users(baked_normal_overlay.image) @@ -1746,16 +2381,8 @@ def bake_channel( else: baked_normal_overlay.image = norm_img - # Recover displacement link - if ori_disp_from_node != '': - nod = mat.node_tree.nodes.get(ori_disp_from_node) - if nod: - soc = nod.outputs.get(ori_disp_from_socket) - if soc: - mat.node_tree.links.new(soc, mat_out.inputs['Displacement']) - ### Vector Displacement - if not any_layers_using_vdisp(yp): + if not any_layers_using_vdisp(root_ch): # Remove baked_vdisp remove_node(tree, root_ch, 'baked_vdisp') else: @@ -1820,105 +2447,60 @@ def bake_channel( else: baked_vdisp.image = vdisp_img - ### Max Height - - # Create target image - if UDIM.is_udim_supported(): - mh_img = bpy.data.images.new( - name='____MAXHEIGHT_TEMP', width=100, height=100, - alpha=False, tiled=False, float_buffer=True - ) + if not any_layers_using_disp(root_ch): + # Remove baked_disp + remove_node(tree, root_ch, 'baked_disp') + remove_node(tree, root_ch, 'end_max_height') else: - mh_img = bpy.data.images.new( - name='____MAXHEIGHT_TEMP', width=100, height=100, - alpha=False, float_buffer=True - ) - - mh_img.colorspace_settings.name = get_noncolor_name() - tex.image = mh_img - - # Bake setup (doing little bit doing hacky reconnection here) - start = tree.nodes.get(TREE_START) - end = tree.nodes.get(TREE_END) - ori_soc = end.inputs[root_ch.name].links[0].from_socket - max_height = start.outputs.get(root_ch.name + io_suffix['HEIGHT']) - # Get the last layer that output max height - for l in yp.layers: - if not l.enable or not l.channels[get_channel_index(root_ch)].enable: continue - lnode = tree.nodes.get(l.group_node) - outp = lnode.outputs.get(root_ch.name + io_suffix['MAX_HEIGHT']) - if outp: - max_height = outp - break - create_link(tree, max_height, end.inputs[root_ch.name]) - create_link(mat.node_tree, node.outputs[root_ch.name + io_suffix['MAX_HEIGHT']], - emit.inputs[0]) - # Use high margin to make sure all pixels are covered - ori_margin = bpy.context.scene.render.bake.margin - bpy.context.scene.render.bake.margin = 1000 + ### Max Height - # Bake - print('BAKE CHANNEL: Baking max height of ' + root_ch.name + ' channel...') - bake_object_op() + max_height_value = get_bake_max_height(root_ch, mat, node, tex, emit) + end_max_height = check_new_node(tree, root_ch, 'end_max_height', 'ShaderNodeValue', 'Max Height') + end_max_height.outputs[0].default_value = max_height_value - # Recover margin - bpy.context.scene.render.bake.margin = ori_margin + ### Displacement - # Recover connection - create_link(tree, ori_soc, end.inputs[root_ch.name]) - - # Set baked max height image - max_height_value = mh_img.pixels[0] - end_max_height = check_new_node(tree, root_ch, 'end_max_height', 'ShaderNodeValue', 'Max Height') - end_max_height.outputs[0].default_value = max_height_value - - # Remove max height image - remove_datablock(bpy.data.images, mh_img, user=tex, user_prop='image') - - ### Displacement + # Create target image + baked_disp = tree.nodes.get(root_ch.baked_disp) + if not baked_disp: + baked_disp = new_node( + tree, root_ch, 'baked_disp', 'ShaderNodeTexImage', + 'Baked ' + root_ch.name + ' Displacement' + ) + if hasattr(baked_disp, 'color_space'): + baked_disp.color_space = 'NONE' - # Create target image - baked_disp = tree.nodes.get(root_ch.baked_disp) - if not baked_disp: - baked_disp = new_node( - tree, root_ch, 'baked_disp', 'ShaderNodeTexImage', - 'Baked ' + root_ch.name + ' Displacement' - ) - if hasattr(baked_disp, 'color_space'): - baked_disp.color_space = 'NONE' - - if baked_disp.image: - disp_img_name = baked_disp.image.name - filepath = baked_disp.image.filepath - #filepath = get_valid_filepath(baked_disp.image, use_hdr) - baked_disp.image.name = '____DISP_TEMP' - else: - disp_img_name = tree.name + ' Displacement' + if baked_disp.image: + disp_img_name = baked_disp.image.name + filepath = baked_disp.image.filepath + #filepath = get_valid_filepath(baked_disp.image, use_hdr) + baked_disp.image.name = '____DISP_TEMP' + else: + disp_img_name = tree.name + ' Displacement' - # Set interpolation to cubic - baked_disp.interpolation = 'Cubic' + # Set interpolation to cubic + baked_disp.interpolation = 'Cubic' - disp_img = img.copy() - disp_img.name = disp_img_name - disp_img.use_generated_float = use_float_for_displacement - disp_img.colorspace_settings.name = get_noncolor_name() - color = (0.5, 0.5, 0.5, 1.0) + disp_img = img.copy() + disp_img.name = disp_img_name + disp_img.use_generated_float = use_float_for_displacement + disp_img.colorspace_settings.name = get_noncolor_name() + color = (0.5, 0.5, 0.5, 1.0) - if img.source == 'TILED': - UDIM.fill_tiles(disp_img, color) - UDIM.initial_pack_udim(disp_img, color) - else: - disp_img.generated_color = color - if filepath != '' and ( - (use_udim and '..' in filepath) or - (not use_udim and '..' not in filepath) - ): - disp_img.filepath = filepath + if img.source == 'TILED': + UDIM.fill_tiles(disp_img, color) + UDIM.initial_pack_udim(disp_img, color) + else: + disp_img.generated_color = color + if filepath != '' and ( + (use_udim and '..' in filepath) or + (not use_udim and '..' not in filepath) + ): + disp_img.filepath = filepath elif ch.normal_map_type == 'BUMP_MAP': disp_img = img - else: disp_img = None if disp_img: @@ -1939,20 +2521,20 @@ def bake_channel( create_link(mat.node_tree, spread_height.outputs[0], emit.inputs[0]) - #create_link(mat.node_tree, node.outputs[root_ch.name + io_suffix['HEIGHT']], srgb2lin.inputs[0]) - #create_link(mat.node_tree, srgb2lin.outputs[0], emit.inputs[0]) else: spread_height = None create_link(mat.node_tree, node.outputs[root_ch.name + io_suffix['HEIGHT']], emit.inputs[0]) tex.image = disp_img - #return - # Bake print('BAKE CHANNEL: Baking displacement image of ' + root_ch.name + ' channel...') bake_object_op() - if not target_layer: + if target_layer: + # Get max height value + max_height_value = get_bake_max_height(root_ch, mat, node, tex, emit) + if ch: set_entity_prop_value(ch, 'bump_distance', max_height_value) + else: # Set baked displacement image if baked_disp.image: @@ -2021,19 +2603,20 @@ def bake_channel( simple_remove_node(mat.node_tree, tex, remove_data = tex.image != img) simple_remove_node(mat.node_tree, emit) - #simple_remove_node(mat.node_tree, lin2srgb) - #simple_remove_node(mat.node_tree, srgb2lin) - if root_ch.type == 'NORMAL': - simple_remove_node(mat.node_tree, norm) + if bsdf: simple_remove_node(mat.node_tree, bsdf) + if norm: simple_remove_node(mat.node_tree, norm) + + # Recover displacement link + if ori_disp_from_node != '': + nod = mat.node_tree.nodes.get(ori_disp_from_node) + if nod: + soc = nod.outputs.get(ori_disp_from_socket) + if soc: + mat.node_tree.links.new(soc, mat_out.inputs['Displacement']) # Recover original bsdf mat.node_tree.links.new(ori_bsdf, output.inputs[0]) - # Recover baked temp - for ent in temp_baked: - print('BAKE CHANNEL: Removing temporary baked ' + ent.name + '...') - disable_temp_bake(ent) - # Set image to target layer if target_layer: ori_img = source.image @@ -2052,98 +2635,1868 @@ def bake_channel( return True -def temp_bake(context, entity, width, height, hdr, samples, margin, uv_map, bake_device='CPU', margin_type='ADJACENT_FACES'): +def is_object_bakeable(obj): + if obj.type != 'MESH': return False + if hasattr(obj, 'hide_viewport') and obj.hide_viewport: return False + if len(get_uv_layers(obj)) == 0: return False + if len(obj.data.polygons) == 0: return False - m1 = re.match(r'yp\.layers\[(\d+)\]$', entity.path_from_id()) - m2 = re.match(r'yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + return True - if not m1 and not m2: return +def get_bakeable_objects_and_meshes(mat, cage_object=None): + objs = [] + meshes = [] - yp = entity.id_data.yp - obj = context.object - #scene = context.scene + for ob in get_scene_objects(): + if not is_object_bakeable(ob): continue + if cage_object and cage_object == ob: continue - # Prepare bake settings - book = remember_before_bake(yp) - prepare_bake_settings(book, [obj], yp, samples, margin, uv_map, bake_device=bake_device, margin_type=margin_type) + # Do not bake objects with hide_render on + if ob.hide_render: continue + if not in_renderable_layer_collection(ob): continue + + for i, m in enumerate(ob.data.materials): + if m == mat: + ob.active_material_index = i + if ob not in objs and ob.data not in meshes: + objs.append(ob) + meshes.append(ob.data) + + return objs, meshes +def bake_to_entity(bprops, overwrite_img=None, segment=None): + + T = time.time() mat = get_active_material() - name = entity.name + ' Temp' + node = get_active_ypaint_node() + yp = node.node_tree.yp + scene = bpy.context.scene + obj = bpy.context.object + channel_idx = int(bprops.channel_idx) if 'channel_idx' in bprops and len(yp.channels) > 0 else -1 + + rdict = {} + rdict['message'] = '' + + if bprops.type == 'SELECTED_VERTICES' and obj.mode != 'EDIT': + rdict['message'] = "Should be in edit mode!" + return rdict + + if bprops.target_type == 'MASK' and len(yp.layers) == 0: + rdict['message'] = "Mask need active layer!" + return rdict + + if bprops.type in {'BEVEL_NORMAL', 'BEVEL_MASK'} and not is_bl_newer_than(2, 80): + rdict['message'] = "Blender 2.80+ is needed to use this feature!" + return rdict + + if bprops.type in {'MULTIRES_NORMAL', 'MULTIRES_DISPLACEMENT'} and not is_bl_newer_than(2, 80): + rdict['message'] = "Blender 2.80+ is needed to use this feature!" + return rdict + + if (hasattr(obj, 'hide_viewport') and obj.hide_viewport) or obj.hide_render: + rdict['message'] = "Please unhide render and viewport of active object!" + return rdict + + if bprops.type == 'FLOW' and (bprops.uv_map == '' or bprops.uv_map_1 == '' or bprops.uv_map == bprops.uv_map_1): + rdict['message'] = "UVMap and Straight UVMap cannot be the same or empty!" + return rdict + + # Get cage object + cage_object = None + if bprops.type.startswith('OTHER_OBJECT_') and bprops.use_cage and bprops.cage_object_name != '': + cage_object = bpy.data.objects.get(bprops.cage_object_name) + if cage_object: + + if any([mod for mod in cage_object.modifiers if mod.type not in {'ARMATURE'}]) or any([mod for mod in obj.modifiers if mod.type not in {'ARMATURE'}]): + rdict['message'] = "Mesh modifiers is not working with cage object for now!" + return rdict + + if len(cage_object.data.polygons) != len(obj.data.polygons): + rdict['message'] = "Invalid cage object, the cage mesh must have the same number of faces as the active object!" + return rdict + + objs = [obj] if is_object_bakeable(obj) else [] + if mat.users > 1: + objs, meshes = get_bakeable_objects_and_meshes(mat, cage_object) + + # Count multires objects + multires_count = 0 + if bprops.type.startswith('MULTIRES_'): + for ob in objs: + if get_multires_modifier(ob): + multires_count += 1 + + if not objs or (bprops.type.startswith('MULTIRES_') and multires_count == 0): + rdict['message'] = "No valid objects found to bake!" + return rdict + + do_overwrite = False + overwrite_image_name = '' + if overwrite_img: + do_overwrite = True + overwrite_image_name = overwrite_img.name + + # Get other objects for other object baking + other_objs = [] + + if bprops.type.startswith('OTHER_OBJECT_'): + + # Get other objects based on selected objects with different material + for o in bpy.context.selected_objects: + if o in objs or not o.data or not hasattr(o.data, 'materials'): continue + if mat.name not in o.data.materials: + other_objs.append(o) - # New target image - image = bpy.data.images.new( - name=name, width=width, height=height, - alpha=True, float_buffer=hdr + # Try to get other_objects from bake info + if overwrite_img: + + bi = segment.bake_info if segment else overwrite_img.y_bake_info + + scene_objs = get_scene_objects() + for oo in bi.other_objects: + if is_bl_newer_than(2, 79): + ooo = oo.object + else: ooo = scene_objs.get(oo.object_name) + + if ooo: + if is_bl_newer_than(2, 80): + # Check if object is on current view layer + layer_cols = get_object_parent_layer_collections([], bpy.context.view_layer.layer_collection, ooo) + if ooo not in other_objs and any(layer_cols): + other_objs.append(ooo) + else: + o = scene_objs.get(ooo.name) + if o and o not in other_objs: + other_objs.append(o) + + if bprops.type == 'OTHER_OBJECT_EMISSION': + other_mats, other_sockets, other_defaults, other_alpha_sockets, other_alpha_defaults, ori_mat_no_nodes = prepare_other_objs_colors(yp, other_objs) + + elif bprops.type == 'OTHER_OBJECT_CHANNELS': + ch_other_objects, ch_other_mats, ch_other_sockets, ch_other_defaults, ch_other_default_weights, ch_other_alpha_sockets, ch_other_alpha_defaults, ori_mat_no_nodes = prepare_other_objs_channels(yp, other_objs) + + if not other_objs: + if overwrite_img: + rdict['message'] = "No source objects found! They're probably deleted!" + return rdict + else: + rdict['message'] = "Source objects must be selected and it should have different material!" + return rdict + + # Get tile numbers + tilenums = [1001] + if bprops.use_udim: + tilenums = UDIM.get_tile_numbers(objs, bprops.uv_map) + + # Remember things + book = remember_before_bake(yp, mat=mat) + + # FXAA doesn't work with hdr image + # FXAA also does not works well with baked image with alpha, so other object bake will use SSAA instead + use_fxaa = not bprops.hdr and bprops.fxaa and not bprops.type.startswith('OTHER_OBJECT_') + + # For now SSAA only works with other object baking + use_ssaa = bprops.ssaa and bprops.type.startswith('OTHER_OBJECT_') + + # Denoising only available for AO bake for now + use_denoise = bprops.denoise and bprops.type in {'AO', 'BEVEL_MASK', 'BEVEL_NORMAL'} and is_bl_newer_than(2, 81) + + # SSAA will multiply size by 2 then resize it back + if use_ssaa: + width = bprops.width * 2 + height = bprops.height * 2 + else: + width = bprops.width + height = bprops.height + + # If use baked disp, need to bake normal and height map first + subdiv_setup_changes = False + height_root_ch = get_root_height_channel(yp) + if height_root_ch and bprops.use_baked_disp and not bprops.type.startswith('MULTIRES_'): + + if not height_root_ch.enable_subdiv_setup: + height_root_ch.enable_subdiv_setup = True + subdiv_setup_changes = True + + # To hold temporary objects + temp_objs = [] + + # Sometimes Cavity bake will create temporary objects + if (bprops.type == 'CAVITY' and (bprops.subsurf_influence or bprops.use_baked_disp)): + + # NOTE: Baking cavity with subdiv setup can only happen if there's only one object and no UDIM + if is_bl_newer_than(4, 2) and len(objs) == 1 and not bprops.use_udim and height_root_ch and height_root_ch.enable_subdiv_setup: + + # Check if there's VDM layer + vdm_layer = get_first_vdm_layer(yp) + vdm_uv_name = vdm_layer.uv_name if vdm_layer else bprops.uv_map + + # Get baked combined vdm image + combined_vdm_image = vector_displacement.get_combined_vdm_image(objs[0], vdm_uv_name, width=bprops.width, height=bprops.height) + + # Bake tangent and bitangent + # NOTE: Only bake the first object tangent since baking combined mesh can cause memory leak at the moment + tanimage, bitimage = vector_displacement.get_tangent_bitangent_images(objs[0], bprops.uv_map) + + # Duplicate object + objs = temp_objs = [get_merged_mesh_objects(scene, objs, True, disable_problematic_modifiers=False)] + + # Use VDM loader geometry nodes + # NOTE: Geometry nodes currently does not support UDIM, so using UDIM will cause wrong bake result + set_active_object(objs[0]) + vdm_loader = vector_displacement_lib.get_vdm_loader_geotree(bprops.uv_map, combined_vdm_image, tanimage, bitimage, 1.0) + bpy.ops.object.modifier_add(type='NODES') + geomod = objs[0].modifiers[-1] + geomod.node_group = vdm_loader + bpy.ops.object.modifier_apply(modifier=geomod.name) + + # Remove temporary datas + remove_datablock(bpy.data.node_groups, vdm_loader) + remove_datablock(bpy.data.images, combined_vdm_image) + + else: + objs = temp_objs = get_duplicated_mesh_objects(scene, objs, True) + + # Join objects then extend with other objects + elif bprops.type.startswith('OTHER_OBJECT_'): + if len(objs) > 1: + objs = [get_merged_mesh_objects(scene, objs)] + temp_objs = objs.copy() + + objs.extend(other_objs) + + # Join objects if the number of objects is higher than one + elif not bprops.type.startswith('MULTIRES_') and len(objs) > 1 and not is_join_objects_problematic(yp): + objs = temp_objs = [get_merged_mesh_objects(scene, objs, True)] + + fill_mode = 'FACE' + obj_vertex_indices = {} + if bprops.type == 'SELECTED_VERTICES': + if bpy.context.tool_settings.mesh_select_mode[0] or bpy.context.tool_settings.mesh_select_mode[1]: + fill_mode = 'VERTEX' + + if is_bl_newer_than(2, 80): + edit_objs = [o for o in objs if o.mode == 'EDIT'] + else: edit_objs = [obj] + + for ob in edit_objs: + mesh = ob.data + bm = bmesh.from_edit_mesh(mesh) + + bm.verts.ensure_lookup_table() + #bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + v_indices = [] + if fill_mode == 'FACE': + for face in bm.faces: + if face.select: + v_indices.append(face.index) + #for loop in face.loops: + # v_indices.append(loop.index) + + else: + for vert in bm.verts: + if vert.select: + v_indices.append(vert.index) + + obj_vertex_indices[ob.name] = v_indices + + bpy.ops.object.mode_set(mode = 'OBJECT') + for ob in objs: + try: + vcol = new_vertex_color(ob, TEMP_VCOL, color_fill=(0.0, 0.0, 0.0, 1.0)) + set_active_vertex_color(ob, vcol) + except: pass + bpy.ops.object.mode_set(mode = 'EDIT') + bpy.ops.mesh.y_vcol_fill(color_option ='WHITE') + bpy.ops.object.mode_set(mode = 'OBJECT') + + # Check if there's channel using alpha + alpha_outp = None + for c in yp.channels: + if c.enable_alpha: + alpha_outp = node.outputs.get(c.name + io_suffix['ALPHA']) + if alpha_outp: break + + # Prepare bake settings + if bprops.type == 'AO': + if alpha_outp: + # If there's alpha channel use standard AO bake, which has lesser quality denoising + bake_type = 'AO' + else: + # When there is no alpha channel use combined render bake, which has better denoising + bake_type = 'COMBINED' + elif bprops.type == 'MULTIRES_NORMAL': + bake_type = 'NORMALS' + elif bprops.type == 'MULTIRES_DISPLACEMENT': + bake_type = 'DISPLACEMENT' + elif bprops.type in {'OTHER_OBJECT_NORMAL', 'OBJECT_SPACE_NORMAL', 'BEVEL_NORMAL'}: + bake_type = 'NORMAL' + else: + bake_type = 'EMIT' + + # If use only local, hide other objects + hide_other_objs = bprops.type != 'AO' or bprops.only_local + + # Fit tilesize to bake resolution if samples is equal 1 + if bprops.samples <= 1: + tile_x = width + tile_y = height + else: + tile_x = 256 + tile_y = 256 + + prepare_bake_settings( + book, objs, yp, samples=bprops.samples, margin=bprops.margin, + uv_map=bprops.uv_map, bake_type=bake_type, #disable_problematic_modifiers=True, + bake_device=bprops.bake_device, hide_other_objs=hide_other_objs, + bake_from_multires=bprops.type.startswith('MULTIRES_'), tile_x = tile_x, tile_y = tile_y, + use_selected_to_active=bprops.type.startswith('OTHER_OBJECT_'), + max_ray_distance=bprops.max_ray_distance, cage_extrusion=bprops.cage_extrusion, + source_objs=other_objs, use_denoising=False, margin_type=bprops.margin_type, + use_cage = bprops.use_cage, cage_object_name = bprops.cage_object_name, + normal_space = 'TANGENT' if bprops.type != 'OBJECT_SPACE_NORMAL' else 'OBJECT' ) - image.colorspace_settings.name = get_noncolor_name() + # Set multires level + #ori_multires_levels = {} + if bprops.type.startswith('MULTIRES_'): #or bprops.type == 'AO': + for ob in objs: + mod = get_multires_modifier(ob) - if entity.type == 'HEMI': + #mod.render_levels = mod.total_levels + if mod and bprops.type.startswith('MULTIRES_'): + mod.render_levels = bprops.multires_base + mod.levels = bprops.multires_base - if m1: source = get_layer_source(entity) - else: source = get_mask_source(entity) + #ori_multires_levels[ob.name] = mod.render_levels - # Create bake nodes - source_copy = mat.node_tree.nodes.new(source.bl_idname) - source_copy.node_tree = source.node_tree + # Setup for cavity + if bprops.type == 'CAVITY': - tex = mat.node_tree.nodes.new('ShaderNodeTexImage') - emit = mat.node_tree.nodes.new('ShaderNodeEmission') - geo = mat.node_tree.nodes.new('ShaderNodeNewGeometry') - output = get_active_mat_output_node(mat.node_tree) - ori_bsdf = output.inputs[0].links[0].from_socket + tt = time.time() + print('BAKE TO LAYER: Applying subsurf/multires for Cavity bake...') - # Connect emit to output material - mat.node_tree.links.new(emit.outputs[0], output.inputs[0]) - mat.node_tree.links.new(source_copy.outputs[0], output.inputs[0]) - mat.node_tree.links.new(geo.outputs['Normal'], source_copy.inputs['Normal']) + # Set vertex color for cavity + for ob in objs: + + set_active_object(ob) + + if bprops.subsurf_influence or bprops.use_baked_disp: + need_to_be_applied_modifiers = [] + for m in ob.modifiers: + if m.type in {'SUBSURF', 'MULTIRES'} and m.levels > 0 and m.show_viewport: + + # Set multires to the highest level + if m.type == 'MULTIRES': + m.levels = m.total_levels + + need_to_be_applied_modifiers.append(m) + + # Also apply displace + if m.type == 'DISPLACE' and m.show_viewport: + need_to_be_applied_modifiers.append(m) + + # Apply shape keys and modifiers + if any(need_to_be_applied_modifiers): + if ob.data.shape_keys: + if is_bl_newer_than(3, 3): + bpy.ops.object.shape_key_remove(all=True, apply_mix=True) + else: bpy.ops.object.shape_key_remove(all=True) + + for m in need_to_be_applied_modifiers: + bpy.ops.object.modifier_apply(modifier=m.name) + + # Create new vertex color for dirt + try: + vcol = new_vertex_color(ob, TEMP_VCOL, color_fill=(1.0, 1.0, 1.0, 1.0)) + set_active_vertex_color(ob, vcol) + except: pass + + bpy.ops.paint.vertex_color_dirt(dirt_angle=math.pi / 2) + + print('BAKE TO LAYER: Applying subsurf/multires is done in', '{:0.2f}'.format(time.time() - tt), 'seconds!') + + # Setup for flow + if bprops.type == 'FLOW': + bpy.ops.object.mode_set(mode = 'OBJECT') + for ob in objs: + uv_layers = get_uv_layers(ob) + main_uv = uv_layers.get(bprops.uv_map) + straight_uv = uv_layers.get(bprops.uv_map_1) + + if main_uv and straight_uv: + flow_vcol = get_flow_vcol(ob, main_uv, straight_uv) + + # Flip normals setup + if bprops.flip_normals: + #ori_mode[obj.name] = obj.mode + if is_bl_newer_than(2, 80): + # Deselect other objects first + for o in other_objs: + o.select_set(False) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.reveal() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.flip_normals() + bpy.ops.object.mode_set(mode='OBJECT') + # Reselect other objects + for o in other_objs: + o.select_set(True) + else: + for ob in objs: + if ob in other_objs: continue + scene.objects.active = ob + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.reveal() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.flip_normals() + bpy.ops.object.mode_set(mode='OBJECT') + + # More setup + ori_mods = {} + ori_viewport_mods = {} + ori_mat_ids = {} + ori_loop_locs = {} + ori_multires_levels = {} + + # Do not disable modifiers for surface based bake types + disable_problematic_modifiers = bprops.type not in {'CAVITY', 'POINTINESS', 'BEVEL_NORMAL', 'BEVEL_MASK'} + + for ob in objs: + + # Disable few modifiers + ori_mods[ob.name] = [m.show_render for m in ob.modifiers] + ori_viewport_mods[ob.name] = [m.show_viewport for m in ob.modifiers] + if bprops.type.startswith('MULTIRES_'): + mul = get_multires_modifier(ob) + multires_index = 99 + if mul: + for i, m in enumerate(ob.modifiers): + if m == mul: multires_index = i + if i > multires_index: + m.show_render = False + m.show_viewport = False + elif disable_problematic_modifiers and ob not in other_objs: + for m in get_problematic_modifiers(ob): + m.show_render = False + + ori_mat_ids[ob.name] = [] + ori_loop_locs[ob.name] = [] + + if bprops.subsurf_influence and not bprops.use_baked_disp and not bprops.type.startswith('MULTIRES_'): + for m in ob.modifiers: + if m.type == 'MULTIRES': + ori_multires_levels[ob.name] = m.render_levels + m.render_levels = m.total_levels + break + + if len(ob.data.materials) > 1: + active_mat_id = [i for i, m in enumerate(ob.data.materials) if m == mat] + if active_mat_id: active_mat_id = active_mat_id[0] + else: continue + + uv_layers = get_uv_layers(ob) + uvl = uv_layers.get(bprops.uv_map) + + for p in ob.data.polygons: + + # Set uv location to (0,0) if not using current material + if uvl and not bprops.force_bake_all_polygons: + uv_locs = [] + for li in p.loop_indices: + uv_locs.append(uvl.data[li].uv.copy()) + if p.material_index != active_mat_id: + uvl.data[li].uv = Vector((0.0, 0.0)) + + ori_loop_locs[ob.name].append(uv_locs) + + # Need to assign all polygon to active material if there are multiple materials + ori_mat_ids[ob.name].append(p.material_index) + p.material_index = active_mat_id + + # Create bake nodes + tex = mat.node_tree.nodes.new('ShaderNodeTexImage') + bsdf = None + map_range = None + geometry = None + vector_math = None + vector_math_1 = None + if bprops.type == 'BEVEL_NORMAL': + bsdf = mat.node_tree.nodes.new('ShaderNodeBsdfDiffuse') + elif bprops.type == 'BEVEL_MASK': + geometry = mat.node_tree.nodes.new('ShaderNodeNewGeometry') + vector_math = mat.node_tree.nodes.new('ShaderNodeVectorMath') + vector_math.operation = 'CROSS_PRODUCT' + if is_bl_newer_than(2, 81): + vector_math_1 = mat.node_tree.nodes.new('ShaderNodeVectorMath') + vector_math_1.operation = 'LENGTH' + + if not bsdf: + bsdf = mat.node_tree.nodes.new('ShaderNodeEmission') + + # Get output node and remember original bsdf input + output = get_active_mat_output_node(mat.node_tree) + ori_bsdf = output.inputs[0].links[0].from_socket + + if bprops.type == 'AO': + # If there's alpha channel use standard AO bake, which has lesser quality denoising + if alpha_outp: + src = None + + if hasattr(scene.cycles, 'use_fast_gi'): + scene.cycles.use_fast_gi = True + + if scene.world: + scene.world.light_settings.distance = bprops.ao_distance + # When there is no alpha channel use combined render bake, which has better denoising + else: + src = mat.node_tree.nodes.new('ShaderNodeAmbientOcclusion') + + if 'Distance' in src.inputs: + src.inputs['Distance'].default_value = bprops.ao_distance + + # Links + if not is_bl_newer_than(2, 80): + mat.node_tree.links.new(src.outputs[0], output.inputs[0]) + else: + mat.node_tree.links.new(src.outputs['AO'], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'POINTINESS': + src = mat.node_tree.nodes.new('ShaderNodeNewGeometry') + + pointy = src.outputs['Pointiness'] + + # Map range node + if is_bl_newer_than(2, 83) and bprops.normalize: + map_range = mat.node_tree.nodes.new('ShaderNodeMapRange') + mat.node_tree.links.new(pointy, map_range.inputs[0]) + pointy = map_range.outputs[0] + + # Links + mat.node_tree.links.new(pointy, bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'CAVITY': + src = mat.node_tree.nodes.new('ShaderNodeGroup') + src.node_tree = get_node_tree_lib(lib.CAVITY) + + # Set vcol + vcol_node = src.node_tree.nodes.get('vcol') + vcol_node.attribute_name = TEMP_VCOL + + mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'DUST': + src = mat.node_tree.nodes.new('ShaderNodeGroup') + src.node_tree = get_node_tree_lib(lib.DUST) + + mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'PAINT_BASE': + src = mat.node_tree.nodes.new('ShaderNodeGroup') + src.node_tree = get_node_tree_lib(lib.PAINT_BASE) + + mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'BEVEL_NORMAL': + src = mat.node_tree.nodes.new('ShaderNodeBevel') + + src.samples = bprops.bevel_samples + src.inputs[0].default_value = bprops.bevel_radius + + mat.node_tree.links.new(src.outputs[0], bsdf.inputs['Normal']) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'BEVEL_MASK': + src = mat.node_tree.nodes.new('ShaderNodeBevel') + + src.samples = bprops.bevel_samples + src.inputs[0].default_value = bprops.bevel_radius + + mat.node_tree.links.new(geometry.outputs['Normal'], vector_math.inputs[0]) + mat.node_tree.links.new(src.outputs[0], vector_math.inputs[1]) + #mat.node_tree.links.new(src.outputs[0], bsdf.inputs['Normal']) + if is_bl_newer_than(2, 81): + mat.node_tree.links.new(vector_math.outputs[0], vector_math_1.inputs[0]) + mat.node_tree.links.new(vector_math_1.outputs[1], bsdf.inputs[0]) + else: + mat.node_tree.links.new(vector_math.outputs[1], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'SELECTED_VERTICES': + if is_bl_newer_than(2, 80): + src = mat.node_tree.nodes.new('ShaderNodeVertexColor') + src.layer_name = TEMP_VCOL + else: + src = mat.node_tree.nodes.new('ShaderNodeAttribute') + src.attribute_name = TEMP_VCOL + mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + elif bprops.type == 'FLOW': + # Set vcol + src = mat.node_tree.nodes.new('ShaderNodeAttribute') + src.attribute_name = FLOW_VCOL - # Set active texture + mat.node_tree.links.new(src.outputs[0], bsdf.inputs[0]) + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + else: + src = None + mat.node_tree.links.new(bsdf.outputs[0], output.inputs[0]) + + # Get number of target images + ch_ids = [0] + + # Other object channels related + all_other_mats = [] + ori_from_nodes = {} + ori_from_sockets = {} + + if bprops.type == 'OTHER_OBJECT_CHANNELS': + ch_ids = [i for i, coo in enumerate(ch_other_objects) if len(coo) > 0] + + # Get all other materials + for oo in other_objs: + for m in oo.data.materials: + if m == None or not m.use_nodes: continue + if m not in all_other_mats: + all_other_mats.append(m) + + # Remember original socket connected to outputs + for m in all_other_mats: + soc = None + from_node = '' + from_socket = '' + mout = get_material_output(m) + if mout: + for l in mout.inputs[0].links: + soc = l.from_socket + from_node = l.from_node.name + from_socket = l.from_socket.name + + # Create temporary emission + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if not temp_emi: + temp_emi = m.node_tree.nodes.new('ShaderNodeEmission') + temp_emi.name = TEMP_EMISSION + m.node_tree.links.new(temp_emi.outputs[0], mout.inputs[0]) + + ori_from_nodes[m.name] = from_node + ori_from_sockets[m.name] = from_socket + + elif bprops.type == 'OTHER_OBJECT_EMISSION': + # Remember original socket connected to outputs + for m in other_mats: + soc = None + from_node = '' + from_socket = '' + mout = get_material_output(m) + if mout: + for l in mout.inputs[0].links: + soc = l.from_socket + from_node = l.from_node.name + from_socket = l.from_socket.name + + # Create temporary emission + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if not temp_emi: + temp_emi = m.node_tree.nodes.new('ShaderNodeEmission') + temp_emi.name = TEMP_EMISSION + m.node_tree.links.new(temp_emi.outputs[0], mout.inputs[0]) + + ori_from_nodes[m.name] = from_node + ori_from_sockets[m.name] = from_socket + + # Newly created layer index and image + active_id = None + image = None + + for idx in ch_ids: + + # Image name and colorspace + image_name = bprops.name + colorspace = get_srgb_name() + + if bprops.type == 'OTHER_OBJECT_EMISSION': + + # Set emission connection + for i, m in enumerate(other_mats): + default = other_defaults[i] + socket = other_sockets[i] + + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if not temp_emi: continue + + if default != None: + # Set default + if type(default) == float: + temp_emi.inputs[0].default_value = (default, default, default, 1.0) + else: temp_emi.inputs[0].default_value = (default[0], default[1], default[2], 1.0) + + elif socket: + m.node_tree.links.new(socket, temp_emi.inputs[0]) + + elif bprops.type == 'OTHER_OBJECT_CHANNELS': + + root_ch = yp.channels[idx] + image_name += ' ' + yp.channels[idx].name + + # Hide irrelevant objects + for oo in other_objs: + if oo not in ch_other_objects[idx]: + oo.hide_render = True + else: oo.hide_render = False + + if root_ch.type == 'NORMAL': + bake_type = 'NORMAL' + + # Set back original socket + for m in all_other_mats: + mout = get_material_output(m) + if mout: + nod = m.node_tree.nodes.get(ori_from_nodes[m.name]) + if nod: + soc = nod.outputs.get(ori_from_sockets[m.name]) + if soc: m.node_tree.links.new(soc, mout.inputs[0]) + + else: + bake_type = 'EMIT' + + # Set emission connection + connected_mats = [] + for j, m in enumerate(ch_other_mats[idx]): + if m in connected_mats: continue + default = ch_other_defaults[idx][j] + default_weight = ch_other_default_weights[idx][j] + socket = ch_other_sockets[idx][j] + + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if not temp_emi: continue + + # Make sure temporary emission node is connected + if len(temp_emi.outputs[0].links) == 0: + mout = get_material_output(m) + m.node_tree.links.new(temp_emi.outputs[0], mout.inputs[0]) + + if default != None: + # Set default + if type(default) == float: + temp_emi.inputs[0].default_value = (default, default, default, 1.0) + else: temp_emi.inputs[0].default_value = (default[0], default[1], default[2], 1.0) + + # Break link + for l in temp_emi.inputs[0].links: + m.node_tree.links.remove(l) + elif socket: + m.node_tree.links.new(socket, temp_emi.inputs[0]) + + # Set default weight + temp_emi.inputs[1].default_value = default_weight + + connected_mats.append(m) + + colorspace = get_noncolor_name() if root_ch.colorspace == 'LINEAR' else get_srgb_name() + + elif bprops.type in {'BEVEL_NORMAL', 'MULTIRES_NORMAL', 'OTHER_OBJECT_NORMAL', 'OBJECT_SPACE_NORMAL'}: + colorspace = get_noncolor_name() + + # Using float image will always make the image linear/non-color + if bprops.hdr: + colorspace = get_noncolor_name() + + # Base color of baked image + if bprops.type == 'AO': + color = [1.0, 1.0, 1.0, 1.0] + elif bake_type in {'NORMAL', 'NORMALS'}: + color = [0.5, 0.5, 1.0, 1.0] + elif bprops.type == 'FLOW': + color = [0.5, 0.5, 0.0, 1.0] + else: + color = [0.5, 0.5, 0.5, 1.0] + + # Make image transparent if its baked from other objects + if bprops.type.startswith('OTHER_OBJECT_'): + color = [0.0, 0.0, 0.0, 0.0] + + # New target image + if bprops.use_udim: + image = bpy.data.images.new( + name=image_name, width=width, height=height, + alpha=True, float_buffer=bprops.hdr, tiled=True + ) + + # Fill tiles + for tilenum in tilenums: + UDIM.fill_tile(image, tilenum, color, width, height) + UDIM.initial_pack_udim(image, color) + + # Remember base color + image.yui.base_color = color + else: + image = bpy.data.images.new( + name=image_name, width=width, height=height, + alpha=True, float_buffer=bprops.hdr + ) + + image.generated_color = color + image.colorspace_settings.name = colorspace + + # Set image filepath if overwrite image is found + if do_overwrite: + # Get overwrite image again to avoid pointer error + overwrite_img = bpy.data.images.get(overwrite_image_name) + #if idx == 0: + if idx == min(ch_ids): + if not overwrite_img.packed_file and overwrite_img.filepath != '': + image.filepath = overwrite_img.filepath + else: + layer = yp.layers[yp.active_layer_index] + root_ch = yp.channels[idx] + ch = layer.channels[idx] + + if root_ch.type == 'NORMAL': + source = get_channel_source_1(ch, layer) + else: source = get_channel_source(ch, layer) + + if source and hasattr(source, 'image') and source.image and not source.image.packed_file and source.image.filepath != '': + image.filepath = source.image.filepath + + # Set bake image tex.image = image mat.node_tree.nodes.active = tex - # Bake - bake_object_op() + # Bake! + try: + if bprops.type.startswith('MULTIRES_'): + bpy.ops.object.bake_image() + else: + if bake_type != 'EMIT': + bpy.ops.object.bake(type=bake_type) + else: bpy.ops.object.bake() + except Exception as e: + + # Try to use CPU if GPU baking is failed + if bprops.bake_device == 'GPU': + print('EXCEPTIION: GPU baking failed! Trying to use CPU...') + bprops.bake_device = 'CPU' + scene.cycles.device = 'CPU' + + if bprops.type.startswith('MULTIRES_'): + bpy.ops.object.bake_image() + else: + if bake_type != 'EMIT': + bpy.ops.object.bake(type=bake_type) + else: bpy.ops.object.bake() + else: + print('EXCEPTIION:', e) - # Recover link - mat.node_tree.links.new(ori_bsdf, output.inputs[0]) + if use_fxaa: fxaa_image(image, False, bake_device=bprops.bake_device) - # Remove temp nodes - mat.node_tree.nodes.remove(tex) - simple_remove_node(mat.node_tree, emit) - simple_remove_node(mat.node_tree, source_copy) - simple_remove_node(mat.node_tree, geo) + if bprops.type == 'POINTINESS' and bprops.normalize and is_bl_newer_than(2, 83): + # Check for highest and lowest value of the baked image + min_val, max_val = get_pointiness_image_minmax_value(image) + + # Set map range + map_range.inputs[1].default_value = min_val + map_range.inputs[2].default_value = max_val + + # Rebake the image again + bpy.ops.object.bake(type='EMIT') + + # Bake other object alpha + if bprops.type in {'OTHER_OBJECT_NORMAL', 'OTHER_OBJECT_CHANNELS', 'OTHER_OBJECT_EMISSION'}: + + alpha_found = False + if bprops.type == 'OTHER_OBJECT_CHANNELS': + + # Set emission connection + for j, m in enumerate(ch_other_mats[idx]): + alpha_default = ch_other_alpha_defaults[idx][j] + alpha_socket = ch_other_alpha_sockets[idx][j] + + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if not temp_emi: continue + + if alpha_socket: + alpha_found = True + m.node_tree.links.new(alpha_socket, temp_emi.inputs[0]) + + else: + if alpha_default != 1.0: + alpha_found = True + + # Set alpha_default + if type(alpha_default) == float: + temp_emi.inputs[0].default_value = (alpha_default, alpha_default, alpha_default, 1.0) + else: temp_emi.inputs[0].default_value = (alpha_default[0], alpha_default[1], alpha_default[2], 1.0) + + # Break link + for l in temp_emi.inputs[0].links: + m.node_tree.links.remove(l) + + elif bprops.type == 'OTHER_OBJECT_EMISSION': + + # Set emission connection + for i, m in enumerate(other_mats): + alpha_default = other_alpha_defaults[i] + alpha_socket = other_alpha_sockets[i] + + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if not temp_emi: continue - # Set entity original type - entity.original_type = 'HEMI' + if alpha_socket: + alpha_found = True + m.node_tree.links.new(alpha_socket, temp_emi.inputs[0]) + + else: + if alpha_default != 1.0: + alpha_found = True + + # Set alpha_default + if type(alpha_default) == float: + temp_emi.inputs[0].default_value = (alpha_default, alpha_default, alpha_default, 1.0) + else: temp_emi.inputs[0].default_value = (alpha_default[0], alpha_default[1], alpha_default[2], 1.0) + + else: + alpha_found = True + + if alpha_found: + + temp_img = image.copy() + temp_img.colorspace_settings.name = get_noncolor_name() + tex.image = temp_img + + # Set temp filepath + if image.source == 'TILED': + temp_img.name = '__TEMP__' + UDIM.initial_pack_udim(temp_img) + + # Need to use clear so there's alpha on the baked image + scene.render.bake.use_clear = True + + # Bake emit can will create alpha image + bpy.ops.object.bake(type='EMIT') + + # Set tile pixels + for tilenum in tilenums: + + # Swap tile + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + UDIM.swap_tile(temp_img, 1001, tilenum) + + # Copy alpha to RGB channel, so it can be fxaa-ed + if bprops.type == 'OTHER_OBJECT_NORMAL': + copy_image_channel_pixels(temp_img, temp_img, 3, 0) + + # FXAA alpha + fxaa_image(temp_img, False, bprops.bake_device, first_tile_only=True) + + # Copy alpha to actual image + copy_image_channel_pixels(temp_img, image, 0, 3) + + # Swap tile again to recover + if tilenum != 1001: + UDIM.swap_tile(image, 1001, tilenum) + UDIM.swap_tile(temp_img, 1001, tilenum) + + # Remove temp image + remove_datablock(bpy.data.images, temp_img, user=tex, user_prop='image') + + # HACK: On Blender 4.5, tex node can be mistakenly use previous index image as current one when resize_image is called + # Set the tex node image to None before resize_image can resolve this + tex.image = None + + # Back to original size if using SSAA + if use_ssaa: + image, temp_segment = resize_image( + image, bprops.width, bprops.height, image.colorspace_settings.name, + alpha_aware=True, bake_device=bprops.bake_device + ) + + # Denoise AO image + if use_denoise: + image = denoise_image(image) + + new_segment_created = False + + if bprops.use_image_atlas: + + need_to_create_new_segment = False + if segment: + ia_image = segment.id_data + if bprops.use_udim: + need_to_create_new_segment = ia_image.is_float != bprops.hdr + if need_to_create_new_segment: + UDIM.remove_udim_atlas_segment_by_name(ia_image, segment.name, yp) + else: + need_to_create_new_segment = bprops.width != segment.width or bprops.height != segment.height or ia_image.is_float != bprops.hdr + if need_to_create_new_segment: + segment.unused = True + + if not segment or need_to_create_new_segment: + + if bprops.use_udim: + segment = UDIM.get_set_udim_atlas_segment( + tilenums, color=(0, 0, 0, 0), colorspace=get_srgb_name(), hdr=bprops.hdr, yp=yp + ) + else: + # Clearing unused image atlas segments + img_atlas = ImageAtlas.check_need_of_erasing_segments(yp, 'TRANSPARENT', bprops.width, bprops.height, bprops.hdr) + if img_atlas: ImageAtlas.clear_unused_segments(img_atlas.yia) + + segment = ImageAtlas.get_set_image_atlas_segment( + bprops.width, bprops.height, 'TRANSPARENT', bprops.hdr, yp=yp + ) + + new_segment_created = True + + ia_image = segment.id_data + + # Set baked image to segment + if bprops.use_udim: + offset = get_udim_segment_mapping_offset(segment) * 10 + copy_dict = {} + for tilenum in tilenums: + copy_dict[tilenum] = tilenum + offset + UDIM.copy_tiles(image, ia_image, copy_dict) + else: copy_image_pixels(image, ia_image, segment) + temp_img = image + image = ia_image + + # Remove original baked image + remove_datablock(bpy.data.images, temp_img) + + # Index 0 is the main image + if idx == min(ch_ids): + if do_overwrite: + + # Get overwrite image again to avoid pointer error + overwrite_img = bpy.data.images.get(overwrite_image_name) + + active_id = yp.active_layer_index + + if overwrite_img != image: + if segment and not bprops.use_image_atlas: + entities = ImageAtlas.replace_segment_with_image(yp, segment, image) + segment = None + else: entities = replace_image(overwrite_img, image, yp, bprops.uv_map) + elif segment: entities = ImageAtlas.get_entities_with_specific_segment(yp, segment) + else: entities = get_entities_with_specific_image(yp, image) + + for ent in entities: + if new_segment_created: + ent.segment_name = segment.name + ImageAtlas.set_segment_mapping(ent, segment, image) + + if ent.uv_name != bprops.uv_map: + ent.uv_name = bprops.uv_map + + if bprops.type == 'AO' and ent.type == 'AO': + set_entity_prop_value(ent, 'ao_distance', bprops.ao_distance) + elif bprops.type == 'BEVEL_MASK' and ent.type == 'EDGE_DETECT': + set_entity_prop_value(ent, 'edge_detect_radius', bprops.bevel_radius) + + if bprops.target_type == 'LAYER': + layer_ids = [i for i, l in enumerate(yp.layers) if l in entities] + if entities and yp.active_layer_index not in layer_ids: + active_id = layer_ids[0] + + # Refresh uv + refresh_temp_uv(bpy.context.object, yp.layers[active_id]) + + # Refresh Neighbor UV resolution + set_uv_neighbor_resolution(yp.layers[active_id]) + + elif bprops.target_type == 'MASK': + masks = [] + for l in yp.layers: + masks.extend([m for m in l.masks if m in entities]) + if masks: + masks[0].active_edit = True + + # Refresh uv + refresh_temp_uv(bpy.context.object, masks[0]) + + # Refresh Neighbor UV resolution + set_uv_neighbor_resolution(masks[0]) + + elif bprops.target_type == 'LAYER': + + layer_name = image.name if not bprops.use_image_atlas else bprops.name + + if bprops.use_image_atlas: + layer_name = get_unique_name(layer_name, yp.layers) + + yp.halt_update = True + layer = Layer.add_new_layer( + group_tree=node.node_tree, layer_name=layer_name, + layer_type='IMAGE', channel_idx=channel_idx, + blend_type=bprops.blend_type, normal_blend_type=bprops.normal_blend_type, + normal_map_type=bprops['normal_map_type'], texcoord_type='UV', + uv_name=bprops.uv_map, image=image, vcol=None, segment=segment, + interpolation = bprops.interpolation, + normal_space = 'OBJECT' if bprops.type == 'OBJECT_SPACE_NORMAL' else 'TANGENT' + ) + yp.halt_update = False + active_id = yp.active_layer_index + + if segment: + ImageAtlas.set_segment_mapping(layer, segment, image) + + # Refresh uv + refresh_temp_uv(bpy.context.object, layer) + + # Refresh Neighbor UV resolution + set_uv_neighbor_resolution(layer) + + + else: + active_layer = yp.layers[yp.active_layer_index] - # Set entity flag - entity.use_temp_bake = True + mask_name = image.name if not bprops.use_image_atlas else bprops.name + + if bprops.use_image_atlas: + mask_name = get_unique_name(mask_name, active_layer.masks) + + mask = Mask.add_new_mask( + active_layer, mask_name, 'IMAGE', 'UV', bprops.uv_map, + image, '', segment + ) + mask.active_edit = True + + reconnect_layer_nodes(active_layer) + rearrange_layer_nodes(active_layer) + + active_id = yp.active_layer_index + + if segment: + ImageAtlas.set_segment_mapping(mask, segment, image) + + # Refresh uv + refresh_temp_uv(bpy.context.object, mask) + + # Refresh Neighbor UV resolution + set_uv_neighbor_resolution(mask) + + # Indices > 0 are for channel override images + else: + # Set images to channel override + layer = yp.layers[yp.active_layer_index] + root_ch = yp.channels[idx] + ch = layer.channels[idx] + if not ch.enable: ch.enable = True + + # Normal channel will use second override + if root_ch.type == 'NORMAL': + if ch.normal_map_type != 'NORMAL_MAP': ch.normal_map_type = 'NORMAL_MAP' + if not ch.override_1: ch.override_1 = True + if ch.override_1_type != 'IMAGE': ch.override_1_type = 'IMAGE' + source = get_channel_source_1(ch, layer) + else: + if not ch.override: ch.override = True + if ch.override_type != 'IMAGE': ch.override_type = 'IMAGE' + source = get_channel_source(ch, layer) + + # If image already exists on source + old_image = None + if source.image and image != source.image: + old_image = source.image + source_name = old_image.name + current_name = image.name + + old_image.name = '_____temp' + image.name = source_name + old_image.name = current_name + + # Set image to source + source.image = image + source.interpolation = bprops.interpolation + + # Remove image if it's not used anymore + if old_image: safe_remove_image(old_image) + + # Set bake info to image/segment + bi = segment.bake_info if segment else image.y_bake_info + + if not bi.is_baked: bi.is_baked = True + if bi.bake_type != bprops.type: bi.bake_type = bprops.type + for attr in dir(bi): + if attr.startswith('__'): continue + if attr.startswith('bl_'): continue + if attr in {'rna_type'}: continue + try: setattr(bi, attr, bprops[attr]) + except: pass + + if other_objs: + + # Remember other objects to bake info + for o in other_objs: + if is_bl_newer_than(2, 79): + oo_recorded = any([oo for oo in bi.other_objects if oo.object == o]) + else: oo_recorded = any([oo for oo in bi.other_objects if oo.object_name == o.name]) + + if not oo_recorded: + oo = bi.other_objects.add() + if is_bl_newer_than(2, 79): + oo.object = o + oo.object_name = o.name + + # Remove unused other objects on bake info + for i, oo in reversed(list(enumerate(bi.other_objects))): + if is_bl_newer_than(2, 79): + ooo = oo.object + else: ooo = bpy.data.objects.get(oo.object_name) + + if ooo not in other_objs: + bi.other_objects.remove(i) + + if bprops.type == 'SELECTED_VERTICES': + #fill_mode = 'FACE' + #obj_vertex_indices = {} + bi.selected_face_mode = True if fill_mode == 'FACE' else False + + # Clear selected objects first + bi.selected_objects.clear() + + # Collect object to bake info + for obj_name, v_indices in obj_vertex_indices.items(): + ob = bpy.data.objects.get(obj_name) + bso = bi.selected_objects.add() + if is_bl_newer_than(2, 79): + bso.object = ob + bso.object_name = ob.name + + # Collect selected vertex data to bake info + for vi in v_indices: + bvi = bso.selected_vertex_indices.add() + bvi.index = vi + + # Recover other yps + if bprops.type in {'OTHER_OBJECT_CHANNELS', 'OTHER_OBJECT_EMISSION'}: + + mats = all_other_mats if bprops.type == 'OTHER_OBJECT_CHANNELS' else other_mats + for m in mats: + # Set back original socket + mout = get_material_output(m) + if mout: + nod = m.node_tree.nodes.get(ori_from_nodes[m.name]) + if nod: + soc = nod.outputs.get(ori_from_sockets[m.name]) + if soc: m.node_tree.links.new(soc, mout.inputs[0]) + + # Remove temp emission + temp_emi = m.node_tree.nodes.get(TEMP_EMISSION) + if temp_emi: m.node_tree.nodes.remove(temp_emi) + + # Recover other objects material settings + recover_other_objs_channels(other_objs, ori_mat_no_nodes) + + # Remove temp bake nodes + simple_remove_node(mat.node_tree, tex) + simple_remove_node(mat.node_tree, bsdf) + if src: simple_remove_node(mat.node_tree, src) + if geometry: simple_remove_node(mat.node_tree, geometry) + if map_range: simple_remove_node(mat.node_tree, map_range) + if vector_math: simple_remove_node(mat.node_tree, vector_math) + if vector_math_1: simple_remove_node(mat.node_tree, vector_math_1) + + # Recover original bsdf + mat.node_tree.links.new(ori_bsdf, output.inputs[0]) + + for ob in objs: + # Recover modifiers + for i, m in enumerate(ob.modifiers): + #print(ob.name, i) + if i >= len(ori_mods[ob.name]): break + if ori_mods[ob.name][i] != m.show_render: + m.show_render = ori_mods[ob.name][i] + if i >= len(ori_viewport_mods[ob.name]): break + if ori_viewport_mods[ob.name][i] != m.show_render: + m.show_viewport = ori_viewport_mods[ob.name][i] + + # Recover multires levels + for m in ob.modifiers: + if m.type == 'MULTIRES' and ob.name in ori_multires_levels: + m.render_levels = ori_multires_levels[ob.name] + break + + # Recover material index + if ori_mat_ids[ob.name]: + for i, p in enumerate(ob.data.polygons): + if ori_mat_ids[ob.name][i] != p.material_index: + p.material_index = ori_mat_ids[ob.name][i] + + if ori_loop_locs[ob.name]: + + # Get uv map + uv_layers = get_uv_layers(ob) + uvl = uv_layers.get(bprops.uv_map) + + # Recover uv locations + if uvl: + for i, p in enumerate(ob.data.polygons): + for j, li in enumerate(p.loop_indices): + uvl.data[li].uv = ori_loop_locs[ob.name][i][j] + + # Delete temp vcol + vcols = get_vertex_colors(ob) + if vcols: + vcol = vcols.get(TEMP_VCOL) + if vcol: vcols.remove(vcol) + + # Recover flip normals setup + if bprops.flip_normals: + #bpy.ops.object.mode_set(mode = 'EDIT') + #bpy.ops.mesh.flip_normals() + #bpy.ops.mesh.select_all(action='DESELECT') + #bpy.ops.object.mode_set(mode = ori_mode) + if is_bl_newer_than(2, 80): + # Deselect other objects first + for o in other_objs: + o.select_set(False) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.reveal() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.flip_normals() + bpy.ops.object.mode_set(mode='OBJECT') + # Reselect other objects + for o in other_objs: + o.select_set(True) + else: + for ob in objs: + if ob in other_objs: continue + scene.objects.active = ob + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.reveal() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.flip_normals() + bpy.ops.object.mode_set(mode='OBJECT') + + # Recover subdiv setup + if height_root_ch and subdiv_setup_changes: + height_root_ch.enable_subdiv_setup = not height_root_ch.enable_subdiv_setup + + # Remove flow vcols + if bprops.type == 'FLOW': + for ob in objs: + vcols = get_vertex_colors(ob) + flow_vcol = vcols.get(FLOW_VCOL) + if flow_vcol: + vcols.remove(flow_vcol) # Recover bake settings - recover_bake_settings(book, yp) + recover_bake_settings(book, yp, mat=mat) + + # Hide other objects after baking + if is_bl_newer_than(2, 79) and bprops.type.startswith('OTHER_OBJECT_') and other_objs: + for oo in other_objs: + oo.hide_viewport = True + + # Remove temporary objects + if temp_objs: + for o in temp_objs: + remove_mesh_obj(o) + + # Check linear nodes becuse sometimes bake results can be linear or srgb + check_yp_linear_nodes(yp, reconnect=True) + + # Reconnect and rearrange nodes + #reconnect_yp_layer_nodes(node.node_tree) + reconnect_yp_nodes(node.node_tree) + rearrange_yp_nodes(node.node_tree) + + # Refresh mapping and stuff + #yp.active_layer_index = yp.active_layer_index + + time_elapsed = time.time() - T + + if image: print('BAKE TO LAYER: Baking', image.name, 'is done in', '{:0.2f}'.format(time_elapsed), 'seconds!') + else: print('BAKE TO LAYER: No image created! Executed in', '{:0.2f}'.format(time_elapsed), 'seconds!') + + rdict['active_id'] = active_id + rdict['image'] = image + rdict['time_elapsed'] = time_elapsed + + return rdict + +def put_image_to_image_atlas(yp, image, tilenums=[]): + + if image.source == 'TILED': + segment = UDIM.get_set_udim_atlas_segment( + tilenums, color=(0, 0, 0, 1), colorspace=get_noncolor_name(), hdr=image.is_float, yp=yp + ) + else: + # Clearing unused image atlas segments + img_atlas = ImageAtlas.check_need_of_erasing_segments(yp, 'BLACK', image.size[0], image.size[1], image.is_float) + if img_atlas: ImageAtlas.clear_unused_segments(img_atlas.yia) + + segment = ImageAtlas.get_set_image_atlas_segment( + image.size[0], image.size[1], 'BLACK', image.is_float, yp=yp + ) + + ia_image = segment.id_data + + # Set baked image to segment + if image.source == 'TILED': + offset = get_udim_segment_mapping_offset(segment) * 10 + copy_dict = {} + for tilenum in tilenums: + copy_dict[tilenum] = tilenum + offset + UDIM.copy_tiles(image, ia_image, copy_dict) + else: copy_image_pixels(image, ia_image, segment) + + # Remove original baked image + remove_datablock(bpy.data.images, image) + + return ia_image, segment + +def bake_entity_as_image(entity, bprops, set_image_to_entity=False): + + rdict = {} + rdict['message'] = '' + + yp = entity.id_data.yp + mat = get_active_material() + obj = bpy.context.object + objs = [obj] if is_object_bakeable(obj) else [] + if mat.users > 1: + objs, _ = get_bakeable_objects_and_meshes(mat) + + if not objs: + rdict['message'] = "No valid objects found to bake!" + return rdict - # Set uv - entity.uv_name = uv_map + # Get tile numbers + tilenums = [1001] + if bprops.use_udim: + tilenums = UDIM.get_tile_numbers(objs, bprops.uv_map) - # Replace layer with temp image + m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + + ori_use_baked = False + ori_enabled_mods = [] + + #ori_enable_blur = False + modifiers_disabled = False if m1: - Layer.replace_layer_type(entity, 'IMAGE', image.name, remove_data=True) - else: Layer.replace_mask_type(entity, 'IMAGE', image.name, remove_data=True) + layer = yp.layers[int(m1.group(1))] + mask = None + source_tree = get_source_tree(layer) + elif m2: + layer = yp.layers[int(m2.group(1))] + mask = layer.masks[int(m2.group(2))] + source_tree = get_mask_tree(mask) - return image + else: + rdict['message'] = "Wrong entity!" + return rdict + + # Disable use baked first + if entity.use_baked: + ori_use_baked = True + entity.use_baked = False + + # Setting image to entity will disable modifiers + if set_image_to_entity: + for mod in entity.modifiers: + if mod.enable: + ori_enabled_mods.append(mod) + mod.enable = False + modifiers_disabled = True + #ori_enable_blur = entity.enable_blur_vector + #entity.enable_blur_vector = False + + # Get existing baked image + existing_image = None + baked_source = source_tree.nodes.get(entity.baked_source) + if baked_source: existing_image = baked_source.image + + # Remember things + book = remember_before_bake(yp) + + # FXAA doesn't work with hdr image + # FXAA also does not works well with baked image with alpha, so other object bake will use SSAA instead + use_fxaa = not bprops.hdr and bprops.fxaa + + # Remember before doing preview + ori_channel_index = yp.active_channel_index + ori_preview_mode = yp.preview_mode + ori_layer_preview_mode = yp.layer_preview_mode + ori_layer_preview_mode_type = yp.layer_preview_mode_type + + ori_layer_intensity_value = 1.0 + changed_layer_channel_index = -1 + ori_layer_channel_intensity_value = 1.0 + ori_layer_channel_blend_type = 'MIX' + ori_layer_channel_override = None + ori_layer_enable_masks = None + + # Make sure layer is enabled + ori_layer_enable = layer.enable + layer.enable = True + layer_opacity = get_entity_prop_value(layer, 'intensity_value') + if layer_opacity != 1.0: + ori_layer_intensity_value = layer_opacity + set_entity_prop_value(layer, 'intensity_value', 1.0) + + # Make sure layer is active one + ori_layer_idx = yp.active_layer_index + layer_idx = get_layer_index(layer) + if yp.active_layer_index != layer_idx: + yp.active_layer_index = layer_idx + + if mask: + # Set up active edit + ori_mask_enable = mask.enable + mask.enable = True + mask.active_edit = True + else: + # Disable masks + ori_layer_enable_masks = layer.enable_masks + if layer.enable_masks: + layer.enable_masks = False + for m in layer.masks: + if m.active_edit: m.active_edit = False + + # Preview setup + yp.layer_preview_mode_type = 'SPECIFIC_MASK' if mask else 'LAYER' + yp.layer_preview_mode = True + + # Set active channel so preview will output right value + for i, ch in enumerate(layer.channels): + if mask: + if ch.enable and mask.channels[i].enable: + yp.active_channel_index = i + break + else: + if ch.enable: + yp.active_channel_index = i + + # Make sure intensity value is 1.0 + intensity_value = get_entity_prop_value(ch, 'intensity_value') + if intensity_value != 1.0: + changed_layer_channel_index = i + ori_layer_channel_intensity_value = intensity_value + set_entity_prop_value(ch, 'intensity_value', 1.0) + + if ch.blend_type != 'MIX': + changed_layer_channel_index = i + ori_layer_channel_blend_type = ch.blend_type + ch.blend_type = 'MIX' + + if ch.override: + changed_layer_channel_index = i + ori_layer_channel_override = True + ch.override = False + + break + + # Modifier setups + ori_mods = {} + ori_viewport_mods = {} + + for obj in objs: + + # Disable few modifiers + ori_mods[obj.name] = [m.show_render for m in obj.modifiers] + ori_viewport_mods[obj.name] = [m.show_viewport for m in obj.modifiers] + + for m in get_problematic_modifiers(obj): + m.show_render = False + + prepare_bake_settings( + book, objs, yp, samples=bprops.samples, margin=bprops.margin, + uv_map=bprops.uv_map, bake_type='EMIT', bake_device=bprops.bake_device, + margin_type = bprops.margin_type + ) + + # Create bake nodes + tex = mat.node_tree.nodes.new('ShaderNodeTexImage') + + if mask: + color = (0, 0, 0, 1) + color_str = 'BLACK' + colorspace = get_noncolor_name() + else: + color = (0, 0, 0, 0) + color_str = 'TRANSPARENT' + colorspace = get_noncolor_name() if bprops.hdr else get_srgb_name() + + # Use existing image colorspace if available + if existing_image: + colorspace = existing_image.colorspace_settings.name + + # Create image + if bprops.use_udim: + image = bpy.data.images.new( + name=bprops.name, width=bprops.width, height=bprops.height, + alpha=True, float_buffer=bprops.hdr, tiled=True + ) + + # Fill tiles + for tilenum in tilenums: + UDIM.fill_tile(image, tilenum, color, bprops.width, bprops.height) + UDIM.initial_pack_udim(image, color) + + # Remember base color + image.yia.color = color_str + else: + image = bpy.data.images.new( + name=bprops.name, width=bprops.width, height=bprops.height, + alpha=True, float_buffer=bprops.hdr + ) + + image.generated_color = color + image.colorspace_settings.name = colorspace + + # Set bake image + tex.image = image + mat.node_tree.nodes.active = tex + + # Bake! + bpy.ops.object.bake() + + if bprops.blur: + samples = 4096 if is_bl_newer_than(3) else 128 + if bprops.blur_type == 'NOISE': + noise_blur_image(image, False, bake_device=bprops.bake_device, factor=bprops.blur_factor, samples=samples) + else: blur_image(image, filter_type=bprops.blur_type, size=bprops.blur_size) + if bprops.denoise: + denoise_image(image) + if use_fxaa: fxaa_image(image, False, bake_device=bprops.bake_device) + + # Remove temp bake nodes + simple_remove_node(mat.node_tree, tex, remove_data=False) + + # Recover bake settings + recover_bake_settings(book, yp) + + # Recover modifiers + for obj in objs: + # Recover modifiers + for i, m in enumerate(obj.modifiers): + #print(obj.name, i) + if i >= len(ori_mods[obj.name]): break + if ori_mods[obj.name][i] != m.show_render: + m.show_render = ori_mods[obj.name][i] + if i >= len(ori_viewport_mods[obj.name]): break + if ori_viewport_mods[obj.name][i] != m.show_render: + m.show_viewport = ori_viewport_mods[obj.name][i] + + # Recover preview + yp.active_channel_index = ori_channel_index + if yp.preview_mode != ori_preview_mode: + yp.preview_mode = ori_preview_mode + if yp.layer_preview_mode != ori_layer_preview_mode: + yp.layer_preview_mode = ori_layer_preview_mode + if yp.layer_preview_mode_type != ori_layer_preview_mode_type: + yp.layer_preview_mode_type = ori_layer_preview_mode_type + + if changed_layer_channel_index != -1: + ch = layer.channels[changed_layer_channel_index] -def disable_temp_bake(entity): - if not entity.use_temp_bake: return + if ori_layer_channel_intensity_value != 1.0: + set_entity_prop_value(ch, 'intensity_value', ori_layer_channel_intensity_value) - m1 = re.match(r'yp\.layers\[(\d+)\]$', entity.path_from_id()) - m2 = re.match(r'yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) + if ori_layer_channel_blend_type != 'MIX': + ch.blend_type = ori_layer_channel_blend_type - # Replace layer type - if m1: Layer.replace_layer_type(entity, entity.original_type, remove_data=True) - else: Layer.replace_mask_type(entity, entity.original_type, remove_data=True) + if ori_layer_channel_override != None and ch.override != ori_layer_channel_override: + ch.override = ori_layer_channel_override - # Set entity attribute - entity.use_temp_bake = False + if ori_layer_intensity_value != 1.0: + set_entity_prop_value(layer, 'intensity_value', ori_layer_intensity_value) + + if ori_layer_enable_masks != None and layer.enable_masks != ori_layer_enable_masks: + layer.enable_masks = ori_layer_enable_masks + + if ori_layer_idx != yp.active_layer_index: + yp.active_layer_index = ori_layer_idx + + if ori_layer_enable != layer.enable: + layer.enable = ori_layer_enable + + if mask and ori_mask_enable != mask.enable: + mask.enable = ori_mask_enable + + if modifiers_disabled: + for mod in ori_enabled_mods: + mod.enable = True + + #if ori_enable_blur: + # mask.enable_blur_vector = True + + if ori_use_baked: + entity.use_baked = True + + # Set up image atlas segment + segment = None + if bprops.use_image_atlas: + image, segment = put_image_to_image_atlas(yp, image, tilenums) + + if set_image_to_entity: + + layer_tree = get_tree(layer) + if mask: source_tree = get_mask_tree(mask) + else: source_tree = get_source_tree(layer) + + yp.halt_update = True + + # Set bake info to image/segment + bi = segment.bake_info if segment else image.y_bake_info + + bi.is_baked = True + bi.is_baked_entity = True + bi.baked_entity_type = entity.type + for attr in dir(bi): + if attr.startswith('__'): continue + if attr.startswith('bl_'): continue + if attr in {'rna_type'}: continue + try: setattr(bi, attr, bprops[attr]) + except: pass + + # Set bake type for some types + if entity.type == 'EDGE_DETECT': + bi.bake_type = 'BEVEL_MASK' + bi.bevel_radius = get_entity_prop_value(entity, 'edge_detect_radius') + elif entity.type == 'AO': + source = get_entity_source(entity) + bi.bake_type = 'AO' + bi.ao_distance = get_entity_prop_value(entity, 'ao_distance') + bi.only_local = source.only_local + + # Get baked source + overwrite_image = None + baked_source = source_tree.nodes.get(entity.baked_source) + if baked_source: + overwrite_image = baked_source.image + + # Remove old segment + if entity.baked_segment_name != '': + if overwrite_image.yia.is_image_atlas: + old_segment = overwrite_image.yia.segments.get(entity.baked_segment_name) + old_segment.unused = True + elif overwrite_image.yua.is_udim_atlas: + UDIM.remove_udim_atlas_segment_by_name(overwrite_image, entity.baked_segment_name, yp=yp) + + # Remove baked segment name + entity.baked_segment_name = '' + else: + baked_source = new_node(source_tree, entity, 'baked_source', 'ShaderNodeTexImage', 'Baked Mask Source') + + # Set image to baked node + if overwrite_image and not segment: + replace_image(overwrite_image, image) + else: baked_source.image = image + + height_ch = get_height_channel(layer) + if height_ch and height_ch.enable: + baked_source.interpolation = 'Cubic' + + # Set entity props + entity.baked_uv_name = bprops.uv_map + entity.use_baked = True + + yp.halt_update = False + + if segment: + # Set up baked mapping + mapping = check_new_node(layer_tree, entity, 'baked_mapping', 'ShaderNodeMapping', 'Baked Mapping') + clear_mapping(entity, use_baked=True) + ImageAtlas.set_segment_mapping(entity, segment, image, use_baked=True) + + # Set baked segment name to entity + entity.baked_segment_name = segment.name + else: + remove_node(layer_tree, entity, 'baked_mapping') + + # Refresh uv + refresh_temp_uv(bpy.context.object, entity) + + # Refresh Neighbor UV resolution + set_uv_neighbor_resolution(entity) + + # Update global uv + check_uv_nodes(yp) + + # Update layer tree inputs + check_all_layer_channel_io_and_nodes(layer) + check_start_end_root_ch_nodes(yp.id_data) + + rdict['image'] = image + rdict['segment'] = segment + + return rdict + +def rebake_baked_images(yp, specific_layers=[]): + tt = time.time() + print('INFO: Rebaking images is started...') + + entities, images, segment_names, segment_name_props = get_yp_entities_images_and_segments(yp, specific_layers=specific_layers) + + baked_counts = 0 + + for i, image in enumerate(images): + print('INFO: Rebaking image \''+image.name+'\'...') + + if image.yia.is_image_atlas: + segment = image.yia.segments.get(segment_names[i]) + elif image.yua.is_udim_atlas: + segment = image.yua.segments.get(segment_names[i]) + else: segment = None + + if ((segment and segment.bake_info.is_baked and not segment.bake_info.is_baked_channel) or + (not segment and image.y_bake_info.is_baked and not image.y_bake_info.is_baked_channel) + ): + + bi = image.y_bake_info if not segment else segment.bake_info + + # Skip outdated bake type + if bi.bake_type == 'SELECTED_VERTICES': + continue + + entity = entities[i][0] + entity_path = entity.path_from_id() + segment_name_prop = segment_name_props[i][0] + + m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity_path) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.channels\[(\d+)\]$', entity_path) + + bake_properties = dotdict() + for attr in dir(bi): + if attr.startswith('__'): continue + if attr.startswith('bl_'): continue + if attr in {'rna_type'}: continue + try: bake_properties[attr] = getattr(bi, attr) + except: pass + + bake_properties.update({ + 'type': bi.bake_type, + 'target_type': 'LAYER' if m1 or m2 else 'MASK', + 'name': image.name, + 'width': image.size[0] if not segment else segment.width, + 'height': image.size[1] if not segment else segment.height, + 'uv_map': entity.uv_name if not entity.use_baked else entity.baked_uv_name + }) + + # 'baked_segment_name' meant the entity is baked as image + if segment_name_prop == 'baked_segment_name': + bake_entity_as_image(entity, bprops=bake_properties, set_image_to_entity=True) + else: bake_to_entity(bprops=bake_properties, overwrite_img=image, segment=segment) + + baked_counts += 1 + + print('INFO: Rebaking images is done at ', '{:0.2f}'.format(time.time() - tt), 'seconds!') + + return baked_counts def get_duplicated_mesh_objects(scene, objs, hide_original=False): tt = time.time() @@ -2454,9 +4807,7 @@ def resize_image(image, width, height, colorspace='Non-Color', samples=1, margin if straight_over.node_tree.users == 1: remove_datablock(bpy.data.node_groups, straight_over.node_tree, user=straight_over, user_prop='node_tree') remove_datablock(bpy.data.materials, mat) - plane = plane_obj.data - bpy.ops.object.delete() - remove_datablock(bpy.data.meshes, plane) + remove_mesh_obj(plane_obj) # Recover settings recover_bake_settings(book) diff --git a/blender_manifest.toml b/blender_manifest.toml index a86a3f87..27398241 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "ucupaint" -version = "2.2.0" +version = "2.3.5" name = "Ucupaint" tagline = "Layer based painting for Eevee and Cycles" maintainer = "Yusuf Umar " diff --git a/common.py b/common.py index 70fc5d97..2e3aa017 100644 --- a/common.py +++ b/common.py @@ -225,6 +225,8 @@ def entity_input_items(self, context): ('GROUP', 'Group', ''), ('HEMI', 'Fake Lighting', ''), ('GABOR', 'Gabor', ''), + ('EDGE_DETECT', 'Edge Detect', ''), + ('AO', 'Ambient Occlusion', ''), ) mask_type_items = ( @@ -245,6 +247,7 @@ def entity_input_items(self, context): ('EDGE_DETECT', 'Edge Detect', ''), ('MODIFIER', 'Modifier', ''), ('GABOR', 'Gabor', ''), + ('AO', 'Ambient Occlusion', ''), ) channel_override_type_items = ( @@ -290,6 +293,8 @@ def entity_input_items(self, context): 'GROUP' : 'Group', 'HEMI' : 'Fake Lighting', 'GABOR' : 'Gabor', + 'EDGE_DETECT' : 'Edge Detect', + 'AO' : 'Ambient Occlusion', } mask_type_labels = { @@ -310,6 +315,7 @@ def entity_input_items(self, context): 'EDGE_DETECT' : 'Edge Detect', 'MODIFIER' : 'Modifier', 'GABOR' : 'Gabor', + 'AO' : 'Ambient Occlusion', } bake_type_items = ( @@ -326,7 +332,7 @@ def entity_input_items(self, context): ('MULTIRES_DISPLACEMENT', 'Multires Displacement', ''), ('OTHER_OBJECT_NORMAL', 'Other Objects Normal', 'Other object\'s normal'), - ('OTHER_OBJECT_EMISSION', 'Other Objects Emission', 'Other object\'s emission color'), + ('OTHER_OBJECT_EMISSION', 'Other Objects Color', 'Other object\'s color'), ('OTHER_OBJECT_CHANNELS', 'Other Objects Channels', 'Other object\'s Ucupaint channels'), ('SELECTED_VERTICES', 'Selected Vertices/Edges/Faces', ''), @@ -373,7 +379,7 @@ def entity_input_items(self, context): 'MULTIRES_DISPLACEMENT': 'Multires Displacement', 'OTHER_OBJECT_NORMAL': 'Other Objects Normal', - 'OTHER_OBJECT_EMISSION': 'Other Objects Emission', + 'OTHER_OBJECT_EMISSION': 'Other Objects Color', 'OTHER_OBJECT_CHANNELS': 'Other Objects Channels', 'SELECTED_VERTICES': 'Selected Vertices', @@ -397,7 +403,7 @@ def entity_input_items(self, context): 'MULTIRES_DISPLACEMENT': 'Displacement Multires', 'OTHER_OBJECT_NORMAL': 'OO Normal', - 'OTHER_OBJECT_EMISSION': 'OO Emission', + 'OTHER_OBJECT_EMISSION': 'OO Color', 'OTHER_OBJECT_CHANNELS': 'OO Channel', 'SELECTED_VERTICES': 'Selected Vertices', @@ -506,6 +512,7 @@ def entity_input_items(self, context): 'EDGE_DETECT' : 'ShaderNodeGroup', 'GABOR' : 'ShaderNodeTexGabor', 'MODIFIER' : 'ShaderNodeGroup', + 'AO' : 'ShaderNodeAmbientOcclusion', } io_suffix = { @@ -579,6 +586,14 @@ def entity_input_items(self, context): 'Erase Soft' ] +tex_default_brushes = [ + 'Airbrush', + 'Paint Hard', + 'Paint Hard Pressure', + 'Paint Soft', + 'Paint Soft Pressure', +] + rgba_letters = ['r', 'g', 'b', 'a'] nsew_letters = ['n', 's', 'e', 'w'] @@ -595,6 +610,12 @@ def entity_input_items(self, context): valid_image_extensions = [".jpg",".gif",".png",".tga", ".jpeg", ".mp4", ".webp"] +class dotdict(dict): + """dot.notation access to dictionary attributes""" + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + def version_tuple(version_string): return tuple(map(int, version_string.split('.'))) if version_string != '' else (0, 0, 0) @@ -717,6 +738,20 @@ def get_noncolor_name(): return 'Non-Color' +def get_linear_color_name(): + names = bpy.types.Image.bl_rna.properties['colorspace_settings'].fixed_type.properties['name'].enum_items.keys() + linear_name = 'Linear Rec.709' if is_bl_newer_than(4) else 'Linear' + + if linear_name not in names: + + # Try to get 'linear' in a name + for name in names: + if 'linear' in name.lower(): + linear_name = name + break + + return linear_name + def remove_datablock(blocks, block, user=None, user_prop=''): if is_bl_newer_than(2, 79): blocks.remove(block) @@ -735,6 +770,11 @@ def remove_datablock(blocks, block, user=None, user_prop=''): block.user_clear() blocks.remove(block) +def get_active_object(): + if is_bl_newer_than(2, 80): + return bpy.context.view_layer.objects.active + return bpy.context.scene.objects.active + def set_active_object(obj): if is_bl_newer_than(2, 80): try: bpy.context.view_layer.objects.active = obj @@ -842,12 +882,6 @@ def get_active_material(obj=None): return mat -def get_material_output(mat): - if mat != None and mat.node_tree: - output = [n for n in mat.node_tree.nodes if n.type == 'OUTPUT_MATERIAL' and n.is_active_output] - if output: return output[0] - return None - def get_list_of_ypaint_nodes(mat): if not mat.node_tree: return [] @@ -884,6 +918,9 @@ def is_layer_collection_hidden(obj): def get_addon_filepath(): return os.path.dirname(bpy.path.abspath(__file__)) + os.sep +def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): + return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) + def srgb_to_linear_per_element(e): if e <= 0.03928: return e / 12.92 @@ -1139,25 +1176,33 @@ def update_image_editor_image(context, image): def get_edit_image_editor_space(context): ypwm = context.window_manager.ypprops area_index = ypwm.edit_image_editor_area_index - if area_index >= 0 and area_index < len(context.screen.areas): - area = context.screen.areas[area_index] - if area.type == 'IMAGE_EDITOR': - return area.spaces[0] + window_index = ypwm.edit_image_editor_window_index + if window_index >= 0 and window_index < len(context.window_manager.windows): + window = context.window_manager.windows[window_index] + if area_index >= 0 and area_index < len(window.screen.areas): + area = window.screen.areas[area_index] + if area.type == 'IMAGE_EDITOR' and (not is_bl_newer_than(2, 80) or area.spaces[0].mode == 'UV'): + return area.spaces[0] return None -def get_first_unpinned_image_editor_space(context, return_index=False): +def get_first_unpinned_image_editor_space(context, return_index=False, uv_edit=False): space = None - index = -1 - for i, area in enumerate(context.screen.areas): - if area.type == 'IMAGE_EDITOR': - if not area.spaces[0].use_image_pin: - space = area.spaces[0] - index = i - break + area_index = -1 + window_index = -1 + for i, window in enumerate(context.window_manager.windows): + for j, area in enumerate(window.screen.areas): + if area.type == 'IMAGE_EDITOR': + if not uv_edit or not is_bl_newer_than(2, 80) or area.spaces[0].mode == 'UV': + img = area.spaces[0].image + if not area.spaces[0].use_image_pin and (not img or img.type not in {'RENDER_RESULT', 'COMPOSITING'}): + space = area.spaces[0] + window_index = i + area_index = j + break if return_index: - return space, index + return space, window_index, area_index return space @@ -1178,11 +1223,25 @@ def get_active_paint_slot_image(): return image +def safely_set_image_paint_canvas(image, scene=None): + if not scene: scene = bpy.context.scene + + # HACK: Remember all original images in all image editors since setting canvas/paint slot will replace all of them + ori_editor_imgs, ori_editor_pins = get_editor_images_dict(return_pins=True) + + try: + scene.tool_settings.image_paint.canvas = image + success = True + except Exception as e: print(e) + + # HACK: Revert back to original editor images + if success: set_editor_images(ori_editor_imgs, ori_editor_pins) + def set_image_paint_canvas(image): scene = bpy.context.scene try: scene.tool_settings.image_paint.mode = 'IMAGE' - scene.tool_settings.image_paint.canvas = image + safely_set_image_paint_canvas(image, scene) except Exception as e: print(e) # Check if name already available on the list @@ -1354,7 +1413,7 @@ def safe_remove_image(image, remove_on_disk=False, user=None, user_prop=''): # Remove image from canvas if scene.tool_settings.image_paint.canvas == image: - scene.tool_settings.image_paint.canvas = None + safely_set_image_paint_canvas(None, scene) if remove_on_disk and not image.packed_file and image.filepath != '': if image.source == 'TILED': @@ -1523,6 +1582,12 @@ def get_active_mat_output_node(tree): return None +def get_material_output(mat): + if mat != None and mat.node_tree: + output = [n for n in mat.node_tree.nodes if n.type == 'OUTPUT_MATERIAL' and n.is_active_output] + if output: return output[0] + return None + def get_all_image_users(image): users = [] @@ -1556,6 +1621,11 @@ def get_layer_ids_with_specific_image(yp, image): if source.image and source.image == image: ids.append(i) + baked_source = get_layer_source(layer, get_baked=True) + if baked_source: + if baked_source.image and baked_source.image == image and i not in ids: + ids.append(i) + return ids def get_entities_with_specific_image(yp, image): @@ -1609,6 +1679,11 @@ def get_masks_with_specific_image(layer, image): if source.image and source.image == image: masks.append(m) + baked_source = get_mask_source(m, get_baked=True) + if baked_source: + if baked_source.image and baked_source.image == image and m not in masks: + m.append(m) + return masks def get_masks_with_specific_segment(layer, segment): @@ -1643,7 +1718,7 @@ def replace_image(old_image, new_image, yp=None, uv_name=''): if old_image == new_image: return # Rename - if not new_image.yia.is_image_atlas: + if not new_image.yia.is_image_atlas and not new_image.yua.is_udim_atlas: old_name = old_image.name old_image.name = '_____temp' new_image.name = old_name @@ -1673,9 +1748,13 @@ def replace_image(old_image, new_image, yp=None, uv_name=''): for entity in entities: if entity.type == 'IMAGE': source = get_entity_source(entity) - if entity.uv_name != uv_name: + if source and source.image == new_image and entity.uv_name != uv_name: entity.uv_name = uv_name + baked_source = get_entity_source(entity, get_baked=True) + if baked_source and baked_source.image == new_image and entity.baked_uv_name != uv_name: + entity.baked_uv_name = uv_name + # Recover temp uv update #ypui.disable_auto_temp_uv_update = ori_disable_temp_uv @@ -2258,8 +2337,8 @@ def get_entity_source(entity, get_baked=False): m1 = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) - if m1: return get_layer_source(entity, get_baked) - elif m2: return get_mask_source(entity, get_baked) + if m1: return get_layer_source(entity, get_baked=get_baked) + elif m2: return get_mask_source(entity, get_baked=get_baked) return None @@ -2274,6 +2353,9 @@ def get_entity_mapping(entity, get_baked=False): return None def update_entity_uniform_scale_enabled(entity): + if not hasattr(entity, 'enable_uniform_scale'): + return + mapping = get_entity_mapping(entity) if mapping: scale_input = mapping.inputs[3] @@ -2414,6 +2496,29 @@ def change_layer_name(yp, obj, src, layer, texes): yp.halt_update = False +def copy_vertex_color_data(obj, source_name, dest_name): + if obj.type != 'MESH': return + + #ori_mode = None + if bpy.context.object and bpy.context.object.mode != 'OBJECT': + #ori_mode = obj.mode + bpy.ops.object.mode_set(mode='OBJECT') + + vcols = get_vertex_colors(obj) + source = vcols.get(source_name) + dest = vcols.get(dest_name) + + if not source or not dest: return + + num_channels = 4 if is_bl_newer_than(2, 80) else 3 + + arr = numpy.zeros(len(source.data) * num_channels) + source.data.foreach_get('color', arr) + dest.data.foreach_set('color', arr) #.T.ravel()) + + #if ori_mode: + # bpy.ops.object.mode_set(mode=ori_mode) + def set_obj_vertex_colors(obj, vcol_name, color): if obj.type != 'MESH': return @@ -3119,7 +3224,7 @@ def get_udim_segment_mapping_offset(segment): offset_y += tiles_height + 1 def is_mapping_possible(entity_type): - return entity_type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'} + return entity_type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'} def clear_mapping(entity, use_baked=False): @@ -3225,72 +3330,110 @@ def update_mapping(entity, use_baked=False): if not active_mask or (mask and active_mask == mask): yp.need_temp_uv_refresh = True -def is_active_uv_map_missmatch_entity(obj, entity): +def get_transformation(mapping, entity=None): + translation = (0.0, 0.0, 0.0) + rotation = (0.0, 0.0, 0.0) + scale = (1.0, 1.0, 1.0) + + if is_bl_newer_than(2, 81): + translation = mapping.inputs[1].default_value + rotation = mapping.inputs[2].default_value + + if entity and hasattr(entity, 'enable_uniform_scale') and entity.enable_uniform_scale: + scale_val = get_entity_prop_value(entity, 'uniform_scale_value') + scale = (scale_val, scale_val, scale_val) + else: + scale = mapping.inputs[3].default_value + + else: + translation = mapping.translation + rotation = mapping.rotation + scale = mapping.scale + + return translation, rotation, scale + +def is_active_uv_map_missmatch_active_entity(obj, layer): + + yp = layer.id_data.yp + + entity = None + + for mask in layer.masks: + if mask.active_edit: + entity = mask + entity_type = entity.type + use_baked = entity.use_baked + break + + for ch in layer.channels: + if ch.active_edit: + entity = layer + entity_type = ch.override_type + use_baked = False + break + + if ch.active_edit_1: + entity = layer + entity_type = ch.override_1_type + use_baked = False + break + + if not entity: + entity = layer + entity_type = entity.type + use_baked = entity.use_baked # Non image entity doesn't need matching UV - if not entity.use_baked and entity.type != 'IMAGE': + if not use_baked and entity_type != 'IMAGE': return False - m = re.match(r'^yp\.layers\[(\d+)\]$', entity.path_from_id()) - - #if entity.type != 'IMAGE' or entity.texcoord_type != 'UV': return False - if (m and not is_layer_using_vector(entity)) or entity.texcoord_type != 'UV': return False - mapping = get_entity_mapping(entity, get_baked=entity.use_baked) + # No need to check UV and transformation if entity is not using UV vector + if (entity == layer and not is_layer_using_vector(entity)) or entity.texcoord_type != 'UV': return False + # Get active UV uv_layers = get_uv_layers(obj) if not uv_layers: return False uv_layer = uv_layers.active - uv_name = entity.uv_name if not entity.use_baked or entity.baked_uv_name == '' else entity.baked_uv_name + # Get active entity UV name + uv_name = entity.uv_name if not use_baked or entity.baked_uv_name == '' else entity.baked_uv_name + + # Get mapping + mapping = get_entity_mapping(entity, get_baked=use_baked) - if mapping and is_transformed(mapping) and obj.mode == 'TEXTURE_PAINT': + # Check mapping transformation + if mapping and is_transformed(mapping, entity) and obj.mode == 'TEXTURE_PAINT': if uv_layer.name != TEMP_UV: return True + elif TEMP_UV: + translation, rotation, scale = get_transformation(mapping, entity) + for i in range(3): + if obj.yp.texpaint_translation[i] != translation[i]: + return True + if obj.yp.texpaint_rotation[i] != rotation[i]: + return True + if obj.yp.texpaint_scale[i] != scale[i]: + return True + # Check if current active uv matched with current entity uv elif uv_name in uv_layers and uv_name != uv_layer.name: return True return False -def is_active_uv_map_missmatch_active_entity(obj, layer): +def is_transformed(mapping, entity=None): + translation, rotation, scale = get_transformation(mapping, entity) - active_mask = None - for mask in layer.masks: - if mask.active_edit == True: - active_mask = mask + for t in translation: + if t != 0.0: return True - if active_mask: entity = active_mask - else: entity = layer + for r in rotation: + if r != 0.0: return True - return is_active_uv_map_missmatch_entity(obj, entity) + for s in scale: + if s != 1.0: return True -def is_transformed(mapping): - if is_bl_newer_than(2, 81): - if (mapping.inputs[1].default_value[0] != 0.0 or - mapping.inputs[1].default_value[1] != 0.0 or - mapping.inputs[1].default_value[2] != 0.0 or - mapping.inputs[2].default_value[0] != 0.0 or - mapping.inputs[2].default_value[1] != 0.0 or - mapping.inputs[2].default_value[2] != 0.0 or - mapping.inputs[3].default_value[0] != 1.0 or - mapping.inputs[3].default_value[1] != 1.0 or - mapping.inputs[3].default_value[2] != 1.0 - ): - return True - return False - else: - if (mapping.translation[0] != 0.0 or - mapping.translation[1] != 0.0 or - mapping.translation[2] != 0.0 or - mapping.rotation[0] != 0.0 or - mapping.rotation[1] != 0.0 or - mapping.rotation[2] != 0.0 or - mapping.scale[0] != 1.0 or - mapping.scale[1] != 1.0 or - mapping.scale[2] != 1.0 - ): - return True - return False + return False def check_uvmap_on_other_objects_with_same_mat(mat, uv_name, set_active=True): @@ -3429,18 +3572,18 @@ def refresh_temp_uv(obj, entity): try: entity_uv.active_render = True except: print('EXCEPTIION: Cannot set active uv render!') - if m3 and entity.override_type != 'IMAGE': - remove_temp_uv(obj, entity) + # Delete previous temp uv + remove_temp_uv(obj, entity) + + # No need to use temp uv if override is not using image + if m3 and ((entity.active_edit and entity.override_type != 'IMAGE') or (entity.active_edit_1 and entity.override_1_type != 'IMAGE')): return False + # No need to use temp uv if layer/mask is not using image if (m1 or m2) and (entity.type != 'IMAGE' and not entity.use_baked): - remove_temp_uv(obj, entity) return False - # Delete previous temp uv - remove_temp_uv(obj, entity) - - # Only set actual uv if not in texture paint mode + # Only set actual uv if not in texture paint or edit mode if obj.mode not in {'TEXTURE_PAINT', 'EDIT'}: return False @@ -3465,8 +3608,11 @@ def refresh_temp_uv(obj, entity): mapping = get_mask_mapping(entity, get_baked=entity.use_baked) #print('Mask!') elif m3: - source = layer_tree.nodes.get(entity.source) + if entity.active_edit_1: + source = layer_tree.nodes.get(entity.source_1) + else: source = layer_tree.nodes.get(entity.source) mapping = get_layer_mapping(layer) + entity = layer #print('Channel!') else: return False @@ -3477,7 +3623,7 @@ def refresh_temp_uv(obj, entity): if not hasattr(source, 'image'): return False img = source.image - if not img or not mapping or not is_transformed(mapping): + if not img or not mapping or not is_transformed(mapping, entity): return False set_active_object(obj) @@ -3506,9 +3652,17 @@ def refresh_temp_uv(obj, entity): rotation_y = mapping.inputs[2].default_value[1] if is_bl_newer_than(2, 81) else mapping.rotation[1] rotation_z = mapping.inputs[2].default_value[2] if is_bl_newer_than(2, 81) else mapping.rotation[2] - scale_x = mapping.inputs[3].default_value[0] if is_bl_newer_than(2, 81) else mapping.scale[0] - scale_y = mapping.inputs[3].default_value[1] if is_bl_newer_than(2, 81) else mapping.scale[1] - scale_z = mapping.inputs[3].default_value[2] if is_bl_newer_than(2, 81) else mapping.scale[2] + if hasattr(entity, 'enable_uniform_scale') and entity.enable_uniform_scale and is_bl_newer_than(2, 81): + scale_x = scale_y = scale_z = get_entity_prop_value(entity, 'uniform_scale_value') + else: + scale_x = mapping.inputs[3].default_value[0] if is_bl_newer_than(2, 81) else mapping.scale[0] + scale_y = mapping.inputs[3].default_value[1] if is_bl_newer_than(2, 81) else mapping.scale[1] + scale_z = mapping.inputs[3].default_value[2] if is_bl_newer_than(2, 81) else mapping.scale[2] + + # Remember the transformation to object props + obj.yp.texpaint_translation = (translation_x, translation_y, translation_z) + obj.yp.texpaint_rotation = (rotation_x, rotation_y, rotation_z) + obj.yp.texpaint_scale = (scale_x, scale_y, scale_z) # Create transformation matrix m1 = m2 = m3 = m4 = None @@ -4174,6 +4328,7 @@ def update_layer_bump_distance(height_ch, height_root_ch, layer, tree=None): yp = layer.id_data.yp if not tree: tree = get_tree(layer) + if not tree: return layer_node = layer.id_data.nodes.get(layer.group_node) height_proc = tree.nodes.get(height_ch.height_proc) @@ -4390,10 +4545,10 @@ def update_layer_images_interpolation(layer, interpolation='Linear', from_interp if from_interpolation == '' or source.interpolation == from_interpolation: source.interpolation = interpolation - baked_source = get_layer_source(layer, get_baked=True) - if baked_source and baked_source.image: - if from_interpolation == '' or baked_source.interpolation == from_interpolation: - baked_source.interpolation = interpolation + baked_source = get_layer_source(layer, get_baked=True) + if baked_source and baked_source.image: + if from_interpolation == '' or baked_source.interpolation == from_interpolation: + baked_source.interpolation = interpolation height_ch = get_height_channel(layer) if height_ch: @@ -4593,13 +4748,33 @@ def set_active_vertex_color_by_name(obj, vcol_name): vcol = vcols.get(vcol_name) if vcol: set_active_vertex_color(obj, vcol) -def new_vertex_color(obj, name, data_type='BYTE_COLOR', domain='CORNER'): +def new_vertex_color(obj, name, data_type='BYTE_COLOR', domain='CORNER', color_fill=()): if not obj or obj.type != 'MESH': return None + # Cannot add new vertex color in edit mode, so go to object mode + ori_edit_mode = False + if obj.mode == 'EDIT': + bpy.ops.object.mode_set(mode='OBJECT') + ori_edit_mode = True + + # Create new vertex color if not is_bl_newer_than(3, 2): - return obj.data.vertex_colors.new(name=name) + vcol = obj.data.vertex_colors.new(name=name) + else: vcol = obj.data.color_attributes.new(name, data_type, domain) + + vcol_name = vcol.name + + # Fill color + if color_fill != (): + set_obj_vertex_colors(obj, vcol.name, color_fill) + + # Back to edit mode and get the vertex color again to avoid pointer error + if ori_edit_mode: + bpy.ops.object.mode_set(mode='EDIT') + vcols = get_vertex_colors(obj) + vcol = vcols.get(vcol_name) - return obj.data.color_attributes.new(name, data_type, domain) + return vcol def get_active_render_uv(obj): uv_layers = get_uv_layers(obj) @@ -4619,20 +4794,39 @@ def get_active_render_uv(obj): return uv_name -def get_default_uv_name(obj, yp=None): - uv_layers = get_uv_layers(obj) +def get_default_uv_name(obj=None, yp=None): uv_name = '' - if obj.type == 'MESH' and len(uv_layers) > 0: - active_name = uv_layers.active.name - if active_name == TEMP_UV: - if yp and len(yp.layers) > 0: - uv_name = yp.layers[yp.active_layer_index].uv_name - else: - for uv_layer in uv_layers: - if uv_layer.name != TEMP_UV: - uv_name = uv_layer.name - else: uv_name = uv_layers.active.name + if obj and obj.type == 'MESH': + + # Get active uv name from active mesh object + uv_layers = get_uv_layers(obj) + if len(uv_layers) > 0: + active_name = uv_layers.active.name + if active_name == TEMP_UV: + if yp and len(yp.layers) > 0: + uv_name = yp.layers[yp.active_layer_index].uv_name + else: + for uv_layer in uv_layers: + if uv_layer.name != TEMP_UV: + uv_name = uv_layer.name + else: uv_name = uv_layers.active.name + + else: + # Create temporary mesh + temp_mesh = bpy.data.meshes.new('___TEMP___') + + # Create temporary uv layer + if not is_bl_newer_than(2, 80): + uv_layers = temp_mesh.uv_textures + else: uv_layers = temp_mesh.uv_layers + uv_layer = uv_layers.new() + + # Get the uv name + uv_name = uv_layer.name + + # Remove temporary mesh + remove_datablock(bpy.data.meshes, temp_mesh) return uv_name @@ -4650,6 +4844,44 @@ def get_relevant_uv(obj, yp): return uv_name +def get_editor_images_dict(return_pins=False): + editor_images = {} + editor_pins = {} + + for i, window in enumerate(bpy.context.window_manager.windows): + screen_dict = {} + screen_pin_dict = {} + for j, area in enumerate(window.screen.areas): + if area.type == 'IMAGE_EDITOR': + space = area.spaces[0] + img = space.image + if img: screen_dict[j] = img.name + else: screen_dict[j] = '' + screen_pin_dict[j] = space.use_image_pin + editor_images[i] = screen_dict + editor_pins[i] = screen_pin_dict + + if return_pins: + return editor_images, editor_pins + + return editor_images + +def set_editor_images(editor_images={}, editor_pins={}): + for i, window in enumerate(bpy.context.window_manager.windows): + if i in editor_images: + screen_dict = editor_images[i] + screen_pin_dict = editor_pins[i] if len(editor_pins) > 0 else None + for j, area in enumerate(window.screen.areas): + if area.type == 'IMAGE_EDITOR': + if j in screen_dict: + space = area.spaces[0] + img = bpy.data.images.get(screen_dict[j]) + if space.image != img: + space.image = img + + if screen_pin_dict != None and j in screen_pin_dict: + space.use_image_pin = screen_pin_dict[j] + def set_active_paint_slot_entity(yp): image = None mat = get_active_material() @@ -4657,6 +4889,7 @@ def set_active_paint_slot_entity(yp): obj = bpy.context.object scene = bpy.context.scene root_tree = yp.id_data + wmyp = bpy.context.window_manager.ypprops # Multiple materials will use single active image instead active material image # since it's the only way texture paint mode won't mess with other material image @@ -4670,28 +4903,33 @@ def set_active_paint_slot_entity(yp): if yp.use_baked and len(yp.channels) > 0: ch = yp.channels[yp.active_channel_index] - baked = root_tree.nodes.get(ch.baked) - if baked and baked.image: - if ch.type == 'NORMAL': - baked_disp = root_tree.nodes.get(ch.baked_disp) - baked_normal_overlay = root_tree.nodes.get(ch.baked_normal_overlay) - - cur_image = get_active_paint_slot_image() - - if cur_image == baked.image and baked_disp: - baked_disp.select = True - root_tree.nodes.active = baked_disp - image = baked_disp.image - elif baked_disp and cur_image == baked_disp.image and baked_normal_overlay: - baked_normal_overlay.select = True - root_tree.nodes.active = baked_normal_overlay - image = baked_normal_overlay.image - else: - baked.select = True - root_tree.nodes.active = baked - image = baked.image + if ch.type == 'NORMAL': + cur_image = get_active_paint_slot_image() + + # Cycle through all baked normal images + orders = ['baked', 'baked_normal_overlay', 'baked_disp', 'baked_vdisp'] + for i, prop in enumerate(orders): + cur_baked = root_tree.nodes.get(getattr(ch, prop)) + if cur_baked and cur_baked.image == cur_image: + next_i = i + for j in range(len(orders)): + if next_i == len(orders)-1: + next_i = 0 + else: next_i += 1 + + next_prop = orders[next_i] + next_baked = root_tree.nodes.get(getattr(ch, next_prop)) + + if next_baked: + next_baked.select = True + image = next_baked.image + root_tree.nodes.active = next_baked + break + break - else: + if not image: + baked = root_tree.nodes.get(ch.baked) + if baked and baked.image: baked.select = True root_tree.nodes.active = baked image = baked.image @@ -4779,6 +5017,10 @@ def set_active_paint_slot_entity(yp): image = source.image + + # HACK: Remember all original images in all image editors since setting canvas/paint slot will replace all of them + ori_editor_imgs, ori_editor_pins = get_editor_images_dict(return_pins=True) + if not is_multiple_mats and image and is_bl_newer_than(2, 81): scene.tool_settings.image_paint.mode = 'MATERIAL' @@ -4787,12 +5029,17 @@ def set_active_paint_slot_entity(yp): if img == None: continue if img.name == image.name: mat.paint_active_slot = idx + # HACK: Just in case paint slot does not update + wmyp.correct_paint_image_name = img.name break - + else: scene.tool_settings.image_paint.mode = 'IMAGE' scene.tool_settings.image_paint.canvas = image + # HACK: Revert back to original editor images + set_editor_images(ori_editor_imgs, ori_editor_pins) + update_image_editor_image(bpy.context, image) def get_active_image_and_stuffs(obj, yp): @@ -4876,6 +5123,12 @@ def get_active_image_and_stuffs(obj, yp): return image, uv_name, src_of_img, entity, mapping, vcol +def is_object_work_with_uv(obj): + if not is_bl_newer_than(3): + return obj.type == 'MESH' + + return obj.type in {'MESH', 'CURVE'} + def set_active_uv_layer(obj, uv_name): uv_layers = get_uv_layers(obj) @@ -4892,11 +5145,11 @@ def is_uv_input_needed(layer, uv_name): if layer.baked_source != '' and layer.use_baked and layer.baked_uv_name == uv_name: return True - if layer.texcoord_type == 'UV' and layer.uv_name == uv_name: - return True + if layer.texcoord_type == 'UV' and layer.uv_name == uv_name: + return True if layer.texcoord_type == 'UV' and layer.uv_name == uv_name: - if layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI'}: + if layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'EDGE_DETECT', 'AO'}: return True for i, ch in enumerate(layer.channels): @@ -4916,7 +5169,7 @@ def is_uv_input_needed(layer, uv_name): if not get_mask_enabled(mask): continue if mask.use_baked and mask.baked_source != '' and mask.baked_uv_name == uv_name: return True - if mask.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT'}: continue + if mask.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'AO'}: continue if (not mask.use_baked or mask.baked_source == '') and mask.texcoord_type == 'UV' and mask.uv_name == uv_name: return True @@ -4966,7 +5219,7 @@ def is_entity_need_tangent_input(entity, uv_name): return True # Fake neighbor need tangent - if height_root_ch.enable_smooth_bump and entity.type in {'VCOL', 'HEMI', 'EDGE_DETECT'} and not entity.use_baked: + if height_root_ch.enable_smooth_bump and entity.type in {'VCOL', 'HEMI', 'EDGE_DETECT', 'AO'} and not entity.use_baked: return True elif entity.uv_name == uv_name and entity.texcoord_type == 'UV': @@ -5025,6 +5278,24 @@ def is_height_process_needed(layer): return False +def is_vdisp_process_needed(layer): + yp = layer.id_data.yp + height_root_ch = get_root_height_channel(yp) + if not height_root_ch: return False + + height_ch = get_height_channel(layer) + if not height_ch or not height_ch.enable: return False + + #if yp.layer_preview_mode and height_ch.normal_map_type != 'VECTOR_DISPLACEMENT_MAP': return True + + if layer.type == 'GROUP': + if is_layer_using_vdisp_map(layer, height_root_ch): + return True + elif height_ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': # or height_ch.enable_transition_bump: + return True + + return False + def is_normal_process_needed(layer): yp = layer.id_data.yp height_root_ch = get_root_height_channel(yp) @@ -5144,7 +5415,26 @@ def is_layer_using_bump_map(layer, root_ch=None): for child in children: if is_layer_using_bump_map(child): return True - elif ch.write_height and (ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} or ch.enable_transition_bump): + elif ch.write_height and (ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} or ch.enable_transition_bump): + return True + + return False + +def is_layer_using_vdisp_map(layer, root_ch=None): + yp = layer.id_data.yp + if not root_ch: root_ch = get_root_height_channel(yp) + if not root_ch: return False + + channel_idx = get_channel_index(root_ch) + try: ch = layer.channels[channel_idx] + except: return False + if get_channel_enabled(ch, layer, root_ch): + if layer.type == 'GROUP': + children = get_list_of_direct_children(layer) + for child in children: + if is_layer_using_vdisp_map(child): + return True + elif ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': # or ch.enable_transition_bump: return True return False @@ -5225,12 +5515,17 @@ def is_any_layer_using_channel(root_ch, node=None): inp = node.inputs.get(root_ch.name + io_suffix['ALPHA']) if inp and len(inp.links): return True - inp = node.inputs.get(root_ch.name + io_suffix['HEIGHT']) - if inp and len(inp.links): - return True + if root_ch.type == 'NORMAL': + inp = node.inputs.get(root_ch.name + io_suffix['HEIGHT']) + if inp and len(inp.links): + return True + inp = node.inputs.get(root_ch.name + io_suffix['VDISP']) + if inp and len(inp.links): + return True for layer in yp.layers: - if layer.channels[ch_idx].enable: + if layer.type in {'GROUP', 'BACKGROUND'}: continue + if get_channel_enabled(layer.channels[ch_idx], layer): return True return False @@ -5336,7 +5631,7 @@ def get_all_objects_with_same_materials(mat, mesh_only=False, uv_name='', select return objs -def get_layer_images(layer, udim_only=False, ondisk_only=False, packed_only=False, udim_atlas_only=False): +def get_layer_images(layer, udim_only=False, ondisk_only=False, packed_only=False, udim_atlas_only=False, baked_only=False): layers = [layer] @@ -5382,21 +5677,13 @@ def get_layer_images(layer, udim_only=False, ondisk_only=False, packed_only=Fals if ondisk_only and (image.packed_file or image.filepath == ''): continue if packed_only and not image.packed_file and image.filepath != '': continue if udim_atlas_only and not image.yua.is_udim_atlas: continue + bi = image.y_bake_info + if baked_only and (not bi.is_baked or bi.is_baked_channel): continue if image not in filtered_images: filtered_images.append(image) return filtered_images -def any_decal_inside_layer(layer): - if layer.texcoord_type == 'Decal': - return True - - for mask in layer.masks: - if mask.texcoord_type == 'Decal': - return True - - return False - def any_dirty_images_inside_layer(layer): for image in get_layer_images(layer): if image.is_dirty: @@ -5447,7 +5734,7 @@ def get_yp_images(yp, udim_only=False, get_baked_channels=False, check_overlay_n if baked_vdisp and baked_vdisp.image and baked_vdisp.image not in images: images.append(baked_vdisp.image) - if not check_overlay_normal or not is_overlay_normal_empty(yp): + if not check_overlay_normal or not is_overlay_normal_empty(ch): baked_normal_overlay = tree.nodes.get(ch.baked_normal_overlay) if baked_normal_overlay and baked_normal_overlay.image and baked_normal_overlay.image not in images: images.append(baked_normal_overlay.image) @@ -5524,18 +5811,24 @@ def check_yp_entities_images_segments_in_lists(entity, image, segment_name, segm segment_name_props.append([segment_name_prop]) else: idx = [i for i, img in enumerate(images) if img == image][0] - entities[idx].append(entity) - segment_name_props[idx].append(segment_name_prop) + # Baked entity will be listed earlier + if segment_name_prop == 'baked_segment_name': + entities[idx].insert(0, entity) + segment_name_props[idx].insert(0, segment_name_prop) + else: + entities[idx].append(entity) + segment_name_props[idx].append(segment_name_prop) return entities, images, segment_names, segment_name_props -def get_yp_entities_images_and_segments(yp): +def get_yp_entities_images_and_segments(yp, specific_layers=[]): entities = [] images = [] segment_names = [] segment_name_props = [] for layer in yp.layers: + if specific_layers and layer not in specific_layers: continue baked_source = get_layer_source(layer, get_baked=True) if baked_source and baked_source.image: @@ -5578,13 +5871,13 @@ def check_need_prev_normal(layer): # Check if previous normal is needed need_prev_normal = False - if layer.type == 'HEMI' and layer.hemi_use_prev_normal and height_root_ch: + if layer.type in {'HEMI', 'EDGE_DETECT', 'AO'} and layer.hemi_use_prev_normal and height_root_ch: need_prev_normal = True # Also check mask if not need_prev_normal: for mask in layer.masks: - if mask.type == 'HEMI' and mask.hemi_use_prev_normal and height_root_ch: + if mask.type in {'HEMI', 'EDGE_DETECT', 'AO'} and mask.hemi_use_prev_normal and height_root_ch: need_prev_normal = True break @@ -5614,13 +5907,19 @@ def get_all_baked_channel_images(tree): return images -def is_layer_using_vector(layer): - if layer.use_baked or layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'BACKFACE'}: +def is_layer_using_vector(layer, exclude_baked=False): + yp = layer.id_data.yp + + if (not exclude_baked and layer.use_baked) or layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT', 'AO'}: return True - for ch in layer.channels: - if ch.enable and ch.override and ch.override_type not in {'VCOL', 'DEFAULT'}: - return True + for i, ch in enumerate(layer.channels): + root_ch = yp.channels[i] + if ch.enable: + if ch.override and ch.override_type not in {'VCOL', 'DEFAULT'}: + return True + if root_ch.type == 'NORMAL' and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'} and ch.override_1 and ch.override_1_type != 'DEFAULT': + return True for mask in layer.masks: if mask.enable and mask.texcoord_type == 'Layer': @@ -5629,7 +5928,7 @@ def is_layer_using_vector(layer): return False def is_mask_using_vector(mask): - if mask.use_baked or mask.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'COLOR_ID', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}: + if mask.use_baked or mask.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'COLOR_ID', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'}: return True return False @@ -5642,6 +5941,15 @@ def get_node(tree, name, parent=None): return node +def is_normal_vdisp_input_connected(root_normal_ch): + # NOTE: Assuming that the active node is using the input tree + node = get_active_ypaint_node() + if not node: return False + + io_vdisp_name = root_normal_ch.name + io_suffix['VDISP'] + vdisp_inp = node.inputs.get(io_vdisp_name) + return vdisp_inp and len(vdisp_inp.links) > 0 + def is_normal_height_input_connected(root_normal_ch): # NOTE: Assuming that the active node is using the input tree node = get_active_ypaint_node() @@ -5659,30 +5967,57 @@ def is_normal_input_connected(root_normal_ch): normal_inp = node.inputs.get(root_normal_ch.name) return normal_inp and len(normal_inp.links) > 0 -def is_overlay_normal_empty(yp): +def is_overlay_normal_empty(root_ch): + yp = root_ch.id_data.yp + channel_index = get_channel_index(root_ch) - root_ch = get_root_height_channel(yp) - if root_ch and is_normal_input_connected(root_ch): + if is_normal_input_connected(root_ch): return False for l in yp.layers: - c = get_height_channel(l) - if not c or not l.enable or not c.enable: continue + if l.type in {'GROUP', 'BACKGROUND'}: continue + if channel_index >= len(l.channels): continue + c = l.channels[channel_index] + if not get_channel_enabled(c, l): continue if c.normal_map_type == 'NORMAL_MAP' or (c.normal_map_type == 'BUMP_MAP' and not c.write_height): return False return True -def any_layers_using_vdisp(yp): +def any_layers_using_vdisp(root_ch): + yp = root_ch.id_data.yp + channel_index = get_channel_index(root_ch) + + if is_normal_vdisp_input_connected(root_ch): + return True for l in yp.layers: - c = get_height_channel(l) - if not c or not l.enable or not c.enable: continue + if l.type in {'GROUP', 'BACKGROUND'}: continue + if channel_index >= len(l.channels): continue + c = l.channels[channel_index] + if not get_channel_enabled(c, l): continue if c.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': return True return False +def any_layers_using_disp(root_ch): + yp = root_ch.id_data.yp + channel_index = get_channel_index(root_ch) + + if is_normal_height_input_connected(root_ch): + return True + + for l in yp.layers: + if l.type in {'GROUP', 'BACKGROUND'}: continue + if channel_index >= len(l.channels): continue + c = l.channels[channel_index] + if not get_channel_enabled(c, l): continue + if c.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} and c.write_height: + return True + + return False + # ShaderNodeVertexColor can't use bump map, so ShaderNodeAttribute will be used for now def get_vcol_bl_idname(): #if is_bl_newer_than(2, 81): @@ -5748,16 +6083,19 @@ def get_layer_vcol(obj, layer): src = get_layer_source(layer) return get_vcol_from_source(obj, src) -def check_colorid_vcol(objs): +def check_colorid_vcol(objs, set_as_active=False): for o in objs: vcols = get_vertex_colors(o) - if COLOR_ID_VCOL_NAME not in vcols: + vcol = vcols.get(COLOR_ID_VCOL_NAME) + if not vcol: try: - vcol = new_vertex_color(o, COLOR_ID_VCOL_NAME) - set_obj_vertex_colors(o, vcol.name, (0.0, 0.0, 0.0, 1.0)) + vcol = new_vertex_color(o, COLOR_ID_VCOL_NAME, color_fill=(0.0, 0.0, 0.0, 1.0)) #set_active_vertex_color(o, vcol) except Exception as e: print(e) + if vcol and set_as_active: + set_active_vertex_color(o, vcol) + def is_colorid_already_being_used(yp, color_id): for l in yp.layers: for m in l.masks: @@ -5779,7 +6117,7 @@ def is_colorid_vcol_still_being_used(objs): return False -def is_image_source_srgb(image, source, root_ch=None): +def is_image_source_srgb(image, source): if not is_bl_newer_than(2, 80): return source.color_space == 'COLOR' @@ -5787,98 +6125,212 @@ def is_image_source_srgb(image, source, root_ch=None): if image.source == 'TILED' and image.colorspace_settings.name == '': return True - # Float images is behaving like srgb for some reason in blender - if root_ch and root_ch.colorspace == 'SRGB' and image.is_float and image.colorspace_settings.name != get_srgb_name(): - return True + # Generated float images is behaving like srgb for some reason in blender + #if image.is_float and image.colorspace_settings.name != get_srgb_name() and image.source == 'GENERATED': + # return True return image.colorspace_settings.name == get_srgb_name() -def any_linear_images_problem(yp): - for layer in yp.layers: - if not get_layer_enabled(layer): continue - layer_tree = get_tree(layer) +def is_image_source_non_color(image, source): + if not is_bl_newer_than(2, 80): + return source.color_space == 'NONE' - for i, ch in enumerate(layer.channels): - root_ch = yp.channels[i] - if not get_channel_enabled(ch, layer, root_ch): continue + # Generated float images is behaving like srgb for some reason in blender + return image.colorspace_settings.name == get_noncolor_name() and not (image.is_float and image.source == 'GENERATED') - if not yp.use_linear_blending and ch.override and ch.override_type == 'IMAGE': - source_tree = get_channel_source_tree(ch) - linear = source_tree.nodes.get(ch.linear) - source = source_tree.nodes.get(ch.source) - if not source: continue +def get_layer_and_root_ch_from_layer_ch(ch): + yp = ch.id_data.yp + layer = None + root_ch = None + + match = re.match(r'yp\.layers\[(\d+)\]\.channels\[(\d+)\]', ch.path_from_id()) + if match: + layer = yp.layers[int(match.group(1))] + root_ch = yp.channels[int(match.group(2))] - image = source.image - if not image: continue - if ( - (is_image_source_srgb(image, source, root_ch) and not linear) or - (not is_image_source_srgb(image, source, root_ch) and linear) - ): - return True + return layer, root_ch - if ch.override_1 and ch.override_1_type == 'IMAGE': - linear_1 = layer_tree.nodes.get(ch.linear_1) - source_1 = layer_tree.nodes.get(ch.source_1) - if not source_1: continue +def get_layer_channel_gamma_value(ch, layer=None, root_ch=None): + yp = ch.id_data.yp + if not layer or not root_ch: layer, root_ch = get_layer_and_root_ch_from_layer_ch(ch) - image = source_1.image - if not image: continue - if ( - (is_image_source_srgb(image, source_1) and not linear_1) or - (not is_image_source_srgb(image, source_1) and linear_1) - ): - return True + channel_enabled = get_channel_enabled(ch, layer, root_ch) + if not channel_enabled: return 1.0 - for mask in layer.masks: - if not get_mask_enabled(mask, layer): continue - if mask.type == 'IMAGE': - source_tree = get_mask_tree(mask) - linear = source_tree.nodes.get(mask.linear) - source = source_tree.nodes.get(mask.source) - if not source: continue - image = source.image - if not image: continue - if ( - (is_image_source_srgb(image, source) and not linear) or - (not is_image_source_srgb(image, source) and linear) - ): - return True + source_tree = get_channel_source_tree(ch, layer) - if layer.type == 'IMAGE': - source_tree = get_source_tree(layer) - linear = source_tree.nodes.get(layer.linear) - source = source_tree.nodes.get(layer.source) - if not source: continue - image = source.image - if not image: continue - - if yp.use_linear_blending: - normal_ch = get_height_channel(layer) - normal_root_ch = get_root_height_channel(yp) - if normal_ch and get_channel_enabled(normal_ch, layer, normal_root_ch) and not normal_ch.override_1 and normal_ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: - ch_linear = layer_tree.nodes.get(normal_ch.linear) - #if ((is_image_source_srgb(image, source) and not ch_linear) or - # (not is_image_source_srgb(image, source) and ch_linear) - # ): - # NOTE: Float image is pretended to be sRGB even if it's using linear colorspace - if (image.is_float and ch_linear) or (not image.is_float and not ch_linear): - return True + image = None + source = None + if ch.override and ch.override_type == 'IMAGE': + source = source_tree.nodes.get(ch.source) + if source: image = source.image + elif layer.type == 'IMAGE': + source = get_layer_source(layer) + if source: image = source.image + + if yp.use_linear_blending: + + # Convert non image layer data to srgb if gamma space option is enabled + if ( + not ch.override + and ch.gamma_space + and root_ch.type != 'NORMAL' + and root_ch.colorspace == 'SRGB' + and ch.layer_input == 'RGB' + and layer.type not in {'IMAGE', 'BACKGROUND', 'GROUP'} + ): + return GAMMA + + # NOTE: Linear blending currently will only use gamma correction on normal channel + if not ch.override_1 and image and is_image_source_srgb(image, source) and root_ch.type == 'NORMAL' and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP', 'VECTOR_DISPLACEMENT_MAP'}: + return 1.0 / GAMMA + + # NOTE: These two gamma correction are unused yet for simplicity and older file compatibility + ## Convert srgb image to linear for linear channel + #if image and is_image_source_srgb(image, source) and root_ch.colorspace == 'LINEAR': + # return 1.0 / GAMMA + + ## Convert non srgb image to srgb for srgb channel + #if image and is_image_source_non_color(image, source) and root_ch.colorspace == 'SRGB': + # return GAMMA - if not image.is_float and ((is_image_source_srgb(image, source) and linear) or - (not is_image_source_srgb(image, source) and not linear) - ): - return True + else: + # Convert srgb override image to linear + if ch.override and image and is_image_source_srgb(image, source): + return 1.0 / GAMMA + + # Convert non image override data to linear + if ch.override and ch.override_type not in {'IMAGE'} and root_ch.type != 'NORMAL' and root_ch.colorspace == 'SRGB': + return 1.0 / GAMMA + + # Convert non image layer data to linear + if ( + not ch.override + and not ch.gamma_space + and root_ch.type != 'NORMAL' + and root_ch.colorspace == 'SRGB' + and ch.layer_input == 'RGB' + and layer.type not in {'IMAGE', 'BACKGROUND', 'GROUP'} + ): + return 1.0 / GAMMA + + return 1.0 + +def get_layer_channel_normal_gamma_value(ch, layer=None, root_ch=None): + yp = ch.id_data.yp + if not layer or not root_ch: layer, root_ch = get_layer_and_root_ch_from_layer_ch(ch) + + channel_enabled = get_channel_enabled(ch, layer, root_ch) + if not channel_enabled: return 1.0 + + image = None + source = None + layer_tree = get_tree(layer) + if ch.override_1 and ch.override_1_type == 'IMAGE': + source = layer_tree.nodes.get(ch.source_1) + if source: image = source.image + + # Convert srgb normal map override to linear + if ch.override_1 and image and is_image_source_srgb(image, source): + return 1.0 / GAMMA + + return 1.0 + +def get_layer_mask_gamma_value(mask, mask_tree=None): + if not mask_tree: mask_tree = get_mask_tree(mask) + + if get_mask_enabled(mask) and mask.type == 'IMAGE': - if image.is_float and ((is_image_source_srgb(image, source) and not linear) or - (not is_image_source_srgb(image, source) and linear) + source = mask_tree.nodes.get(mask.source) + image = source.image + + if not image: return 1.0 + + # Convert srgb mask image to linear + if is_image_source_srgb(image, source): + return 1.0 / GAMMA + + return 1.0 + +def get_layer_gamma_value(layer): + yp = layer.id_data.yp + + if get_layer_enabled(layer) and layer.type == 'IMAGE': + source_tree = get_source_tree(layer) + source = source_tree.nodes.get(layer.source) + image = source.image + if image: + + # Should linearize srgb image and float image + if not yp.use_linear_blending: + if image.is_float and is_bl_newer_than(2, 80): + + # Float image with srgb will use double gamma calculation + if is_image_source_srgb(image, source): + return pow(1.0 / GAMMA, 2.0) + else: return 1.0 / GAMMA + + elif is_image_source_srgb(image, source): + return 1.0 / GAMMA + + return 1.0 + +def any_linear_images_problem(yp): + for layer in yp.layers: + if not get_layer_enabled(layer): continue + layer_tree = get_tree(layer) + + for i, ch in enumerate(layer.channels): + root_ch = yp.channels[i] + #if not get_channel_enabled(ch, layer, root_ch): continue + + gamma = get_layer_channel_gamma_value(ch, layer, root_ch) + source_tree = get_channel_source_tree(ch, layer) + linear = source_tree.nodes.get(ch.linear) + + if ( + (gamma == 1.0 and linear) or + (gamma != 1.0 and (not linear or not isclose(linear.inputs[1].default_value, gamma, rel_tol=1e-5))) + ): + return True + + if root_ch.type == 'NORMAL': + gamma_1 = get_layer_channel_normal_gamma_value(ch, layer, root_ch) + linear_1 = layer_tree.nodes.get(ch.linear_1) + if ( + (gamma_1 == 1.0 and linear_1) or + (gamma_1 != 1.0 and (not linear_1 or not isclose(linear_1.inputs[1].default_value, gamma_1, rel_tol=1e-5))) ): return True - else: - if ((is_image_source_srgb(image, source) and not linear) or - (not is_image_source_srgb(image, source) and linear) - ): + for mask in layer.masks: + source_tree = get_mask_tree(mask) + gamma = get_layer_mask_gamma_value(mask, mask_tree=source_tree) + linear = source_tree.nodes.get(mask.linear) + if ( + (gamma == 1.0 and linear) or + (gamma != 1.0 and (not linear or not isclose(linear.inputs[1].default_value, gamma, rel_tol=1e-5))) + ): + return True + + # Blender 2.7x has color space option on the node + if not is_bl_newer_than(2, 80) and layer.type == 'IMAGE': + source = get_layer_source(layer) + if source: + if source.color_space == 'NONE' and yp.use_linear_blending: return True + if source.color_space == 'COLOR' and not yp.use_linear_blending: + return True + + gamma = get_layer_gamma_value(layer) + source_tree = get_source_tree(layer) + linear = source_tree.nodes.get(layer.linear) + + if ( + (gamma == 1.0 and linear) or + (gamma != 1.0 and (not linear or not isclose(linear.inputs[1].default_value, gamma, rel_tol=1e-5))) + ): + return True return False @@ -6162,9 +6614,9 @@ def get_material_drivers(mat): return drivers -def get_material_fcurves_and_drivers(yp): - fcurves = get_material_fcurves(yp) - fcurves.extend(get_material_drivers(yp)) +def get_material_fcurves_and_drivers(mat): + fcurves = get_material_fcurves(mat) + fcurves.extend(get_material_drivers(mat)) return fcurves def get_yp_fcurves(yp): @@ -6235,11 +6687,6 @@ def swap_channel_fcurves(yp, idx0, idx1): elif index == idx1: fc.data_path = fc.data_path.replace('yp.channels[' + str(idx1) + ']', 'yp.channels[' + str(idx0) + ']') - # Material fcurves - node = get_active_ypaint_node() - mat = get_active_material() - fcurves = get_material_fcurves_and_drivers(mat) - ch0 = yp.channels[idx0] ch1 = yp.channels[idx1] @@ -6255,15 +6702,29 @@ def swap_channel_fcurves(yp, idx0, idx1): if idx0 < idx1 and ch0.enable_alpha: ch0_idx += 1 - for fc in fcurves: - m = re.match(r'^nodes\["' + node.name + '"\]\.inputs\[(\d+)\]\.default_value$', fc.data_path) - if m: - index = int(m.group(1)) - if index == ch0_idx: - fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(ch1_idx) + '].default_value' + for mat in bpy.data.materials: + if not mat.node_tree: continue + + # Get yp nodes + yp_nodes = [] + for node in mat.node_tree.nodes: + if node.type == 'GROUP' and node.node_tree and node.node_tree.yp == yp: + if node not in yp_nodes: + yp_nodes.append(node) + + # Check for animation data + if len(yp_nodes) > 0: + fcurves = get_material_fcurves_and_drivers(mat) + for node in yp_nodes: + for fc in fcurves: + m = re.match(r'^nodes\["' + node.name + '"\]\.inputs\[(\d+)\]\.default_value$', fc.data_path) + if m: + index = int(m.group(1)) + if index == ch0_idx: + fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(ch1_idx) + '].default_value' - elif index == ch1_idx: - fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(ch0_idx) + '].default_value' + elif index == ch1_idx: + fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(ch0_idx) + '].default_value' def swap_layer_channel_fcurves(layer, idx0, idx1): if idx0 >= len(layer.channels) or idx1 >= len(layer.channels): return @@ -6590,30 +7051,41 @@ def shift_channel_fcurves(yp, start_index=1, direction='UP', remove_ch_mode=True if m: fc.data_path = fc.data_path.replace('.channels[' + str(i) + ']', '.channels[' + str(i+shifter) + ']') - # Material fcurves - node = get_active_ypaint_node() - mat = get_active_material() - fcurves = get_material_fcurves_and_drivers(mat) - if remove_ch_mode and start_index < len(yp.channels) and yp.channels[start_index].enable_alpha and shifter < 0: shifter -= 1 - if shifter > 0: + for mat in bpy.data.materials: + if not mat.node_tree: continue - for i, root_ch in reversed(list(enumerate(yp.channels))): - if i <= start_index: continue - io_index = root_ch.io_index - for fc in fcurves: - m = re.match(r'^nodes\["' + node.name + '"\]\.inputs\[' + str(io_index) + '\]\.default_value$', fc.data_path) - if m: fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(io_index+shifter) + '].default_value' - else: + # Get yp nodes + yp_nodes = [] + for node in mat.node_tree.nodes: + if node.type == 'GROUP' and node.node_tree and node.node_tree.yp == yp: + if node not in yp_nodes: + yp_nodes.append(node) - for i, root_ch in enumerate(yp.channels): - if i <= start_index: continue - io_index = root_ch.io_index - for fc in fcurves: - m = re.match(r'^nodes\["' + node.name + '"\]\.inputs\[' + str(io_index) + '\]\.default_value$', fc.data_path) - if m: fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(io_index+shifter) + '].default_value' + # Check for animation data + if len(yp_nodes) > 0: + fcurves = get_material_fcurves_and_drivers(mat) + + for node in yp_nodes: + + if shifter > 0: + + for i, root_ch in reversed(list(enumerate(yp.channels))): + if i <= start_index: continue + io_index = root_ch.io_index + for fc in fcurves: + m = re.match(r'^nodes\["' + node.name + '"\]\.inputs\[' + str(io_index) + '\]\.default_value$', fc.data_path) + if m: fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(io_index+shifter) + '].default_value' + else: + + for i, root_ch in enumerate(yp.channels): + if i <= start_index: continue + io_index = root_ch.io_index + for fc in fcurves: + m = re.match(r'^nodes\["' + node.name + '"\]\.inputs\[' + str(io_index) + '\]\.default_value$', fc.data_path) + if m: fc.data_path = 'nodes["' + node.name + '"].inputs[' + str(io_index+shifter) + '].default_value' def shift_mask_fcurves_up(layer, start_index=1): @@ -6766,6 +7238,20 @@ def copy_image_pixels(src, dest, segment=None, segment_src=None): dest.pixels = target_pxs +def copy_image_pixels_with_conversion(src, dest, segment=None, segment_src=None): + + copy_image_pixels(src, dest, segment, segment_src) + + # Convert image colors after copying if destination image and source image has different bit depth + if dest.is_float and not src.is_float: + # Byte to float + set_image_pixels_to_linear(dest, power=1) + multiply_image_rgb_by_alpha(dest, power=1) + else: + # Float to byte + divide_image_rgb_by_alpha(dest) + set_image_pixels_to_srgb(dest) + def set_image_pixels(image, color, segment=None): start_x = 0 @@ -6805,6 +7291,182 @@ def set_image_pixels(image, color, segment=None): image.pixels = pxs +def set_image_pixels_to_srgb(image, segment=None): + + start_x = 0 + start_y = 0 + + width = image.size[0] + height = image.size[1] + + if segment: + start_x = width * segment.tile_x + start_y = height * segment.tile_y + + width = segment.width + height = segment.height + + if is_bl_newer_than(2, 83): + pxs = numpy.empty(shape=image.size[0]*image.size[1]*4, dtype=numpy.float32) + image.pixels.foreach_get(pxs) + + # Set array to 3d + pxs.shape = (-1, image.size[0], 4) + + # Do srgb conversion + vecfunc = numpy.vectorize(linear_to_srgb_per_element) + for i in range(3): + pxs[start_y:start_y+height, start_x:start_x+width, i] = vecfunc(pxs[start_y:start_y+height, start_x:start_x+width, i]) + + image.pixels.foreach_set(pxs.ravel()) + + else: + pxs = list(image.pixels) + + for y in range(height): + source_offset_y = width * 4 * y + offset_y = image.size[0] * 4 * (y + start_y) + for x in range(width): + source_offset_x = 4 * x + offset_x = 4 * (x + start_x) + for i in range(3): + pxs[offset_y + offset_x + i] = linear_to_srgb_per_element(pxs[offset_y + offset_x + i]) + + image.pixels = pxs + +def set_image_pixels_to_linear(image, segment=None, power=1): + + start_x = 0 + start_y = 0 + + width = image.size[0] + height = image.size[1] + + if segment: + start_x = width * segment.tile_x + start_y = height * segment.tile_y + + width = segment.width + height = segment.height + + if is_bl_newer_than(2, 83): + pxs = numpy.empty(shape=image.size[0]*image.size[1]*4, dtype=numpy.float32) + image.pixels.foreach_get(pxs) + + # Set array to 3d + pxs.shape = (-1, image.size[0], 4) + + # Do linear conversion + vecfunc = numpy.vectorize(srgb_to_linear_per_element) + for p in range(power): + for i in range(3): + pxs[start_y:start_y+height, start_x:start_x+width, i] = vecfunc(pxs[start_y:start_y+height, start_x:start_x+width, i]) + + image.pixels.foreach_set(pxs.ravel()) + + else: + pxs = list(image.pixels) + + for y in range(height): + source_offset_y = width * 4 * y + offset_y = image.size[0] * 4 * (y + start_y) + for x in range(width): + source_offset_x = 4 * x + offset_x = 4 * (x + start_x) + for p in range(power): + for i in range(3): + pxs[offset_y + offset_x + i] = srgb_to_linear_per_element(pxs[offset_y + offset_x + i]) + + image.pixels = pxs + +def multiply_image_rgb_by_alpha(image, segment=None, power=1): + + start_x = 0 + start_y = 0 + + width = image.size[0] + height = image.size[1] + + if segment: + start_x = width * segment.tile_x + start_y = height * segment.tile_y + + width = segment.width + height = segment.height + + if is_bl_newer_than(2, 83): + pxs = numpy.empty(shape=image.size[0]*image.size[1]*4, dtype=numpy.float32) + image.pixels.foreach_get(pxs) + + # Set array to 3d + pxs.shape = (-1, image.size[0], 4) + + # Do linear conversion + for i in range(3): + pxs[start_y:start_y+height, start_x:start_x+width, i] *= pow(pxs[start_y:start_y+height, start_x:start_x+width, 3], power) + + image.pixels.foreach_set(pxs.ravel()) + + else: + pxs = list(image.pixels) + + for y in range(height): + source_offset_y = width * 4 * y + offset_y = image.size[0] * 4 * (y + start_y) + for x in range(width): + source_offset_x = 4 * x + offset_x = 4 * (x + start_x) + for i in range(3): + pxs[offset_y + offset_x + i] *= pow(pxs[offset_y + offset_x + 3], power) + + image.pixels = pxs + +def safe_divider(divider): + return max(divider, 0.00001) + +def divide_image_rgb_by_alpha(image, segment=None): + + start_x = 0 + start_y = 0 + + width = image.size[0] + height = image.size[1] + + if segment: + start_x = width * segment.tile_x + start_y = height * segment.tile_y + + width = segment.width + height = segment.height + + if is_bl_newer_than(2, 83): + pxs = numpy.empty(shape=image.size[0]*image.size[1]*4, dtype=numpy.float32) + image.pixels.foreach_get(pxs) + + # Set array to 3d + pxs.shape = (-1, image.size[0], 4) + + # Do linear conversion + for i in range(3): + vecfunc = numpy.vectorize(safe_divider) + pxs[start_y:start_y+height, start_x:start_x+width, i] /= vecfunc(pxs[start_y:start_y+height, start_x:start_x+width, 3]) + + image.pixels.foreach_set(pxs.ravel()) + + else: + pxs = list(image.pixels) + + for y in range(height): + source_offset_y = width * 4 * y + offset_y = image.size[0] * 4 * (y + start_y) + for x in range(width): + source_offset_x = 4 * x + offset_x = 4 * (x + start_x) + for i in range(3): + pxs[offset_y + offset_x + i] /= safe_divider(pxs[offset_y + offset_x + 3]) + + image.pixels = pxs + def is_image_filepath_unique(filepath, check_disk=True): abspath = bpy.path.abspath(filepath) for img in bpy.data.images: @@ -7074,23 +7736,100 @@ def get_uv_hash(obj, uv_name): h = hash(uv_np.tobytes()) return str(h) -def remove_decal_object(tree, entity): - if not tree: return - # NOTE: This will remove the texcoord object even if the entity is not using decal - #if entity.texcoord_type == 'Decal': - texcoord = tree.nodes.get(entity.texcoord) - if texcoord and hasattr(texcoord, 'object') and texcoord.object: - decal_obj = texcoord.object - if decal_obj.type == 'EMPTY' and decal_obj.users <= 2: - texcoord.object = None - remove_datablock(bpy.data.objects, decal_obj) - def load_image(path, directory, check_existing=True): if not is_bl_newer_than(2, 77): return bpy_extras.image_utils.load_image(path, directory) return bpy_extras.image_utils.load_image(path, directory, check_existing=check_existing) +def get_brush_image_tool(brush): + if not is_bl_newer_than(5): + return brush.image_tool + + return brush.image_brush_type + +def get_brush_sculpt_tool(brush): + if not is_bl_newer_than(5): + return brush.sculpt_tool + + return brush.sculpt_brush_type + def get_active_tool_idname(): tools = bpy.context.workspace.tools return tools.from_space_view3d_mode(bpy.context.mode).idname + +def enable_eevee_ao(): + # Enable Eevee AO to make edge detect entity works + scene = bpy.context.scene + if is_bl_newer_than(2, 80) and not is_bl_newer_than(4, 2) and not scene.eevee.use_gtao: + scene.eevee.use_gtao = True + +def is_image_available_to_open(image): + return not image.yia.is_image_atlas and not image.yua.is_udim_atlas and image.name not in {'Render Result', 'Viewer Node'} + +def fix_missing_vcol(obj, name, src=None, entity=None, entities=[]): + + ref_vcol = None + + if is_bl_newer_than(3, 2): + # Try to get reference vcol + mat = get_active_material() + objs = get_all_objects_with_same_materials(mat) + + for o in objs: + ovcols = get_vertex_colors(o) + if name in ovcols: + ref_vcol = ovcols.get(name) + break + + # Default recovered missing vcol is black + color = (0.0, 0.0, 0.0, 0.0) + + # Create missing vertex color + if ref_vcol: vcol = new_vertex_color(obj, name, ref_vcol.data_type, ref_vcol.domain, color_fill=color) + else: vcol = new_vertex_color(obj, name, color_fill=color) + + # Set attribute name back to source in case the name is different + if src: set_source_vcol_name(src, vcol.name) + + # Set the name back to entity + if entity and vcol.name not in entity.name: + entity.name = get_unique_name(vcol.name, entities) + +def fix_missing_object_vcols(yp, objs, enabled_only=False): + need_color_id_vcol = False + + for obj in objs: + if obj.type != 'MESH': continue + + for layer in yp.layers: + if enabled_only and not layer.enable: continue + + if layer.type == 'VCOL': + src = get_layer_source(layer) + if not get_vcol_from_source(obj, src): + fix_missing_vcol(obj, src.attribute_name, src, entity=layer, entities=yp.layers) + + for mask in layer.masks: + if enabled_only and not mask.enable: continue + + if mask.type == 'VCOL': + src = get_mask_source(mask) + if not get_vcol_from_source(obj, src): + fix_missing_vcol(obj, src.attribute_name, src, entity=mask, entities=layer.masks) + + if mask.type == 'COLOR_ID': + vcols = get_vertex_colors(obj) + if COLOR_ID_VCOL_NAME not in vcols: + need_color_id_vcol = True + + for ch in layer.channels: + if enabled_only and not ch.enable: continue + + if ch.override and ch.override_type == 'VCOL': + src = get_channel_source(ch, layer) + if not get_vcol_from_source(obj, src): + fix_missing_vcol(obj, src.attribute_name, src) + + # Fix missing color id missing vcol + if need_color_id_vcol: check_colorid_vcol(objs) diff --git a/godot4/blender_import.gd b/godot4/blender_import.gd new file mode 100644 index 00000000..5c87003b --- /dev/null +++ b/godot4/blender_import.gd @@ -0,0 +1,43 @@ +extends SceneTree + + +func _init(): + print("init blender_import") + + + var args = OS.get_cmdline_user_args() + + print("argss "+str(args)) + if args.size() == 0: + print("No arguments passed") + quit() + return + + for arg in args: + print(arg) + + var asset_dir = "res://" + args[1] + var name_asset = args[0] + + var new_material = ShaderMaterial.new() + var shader = load(asset_dir+"/%s.gdshader" % name_asset) + + # set shader to material + new_material.shader = shader + + var dict = {} + for i in range(1, round(args.size())/2): + var idx_key = i * 2 + var idx_val = idx_key + 1 + dict[args[idx_key]] = args[idx_val] + print(dict) + + for key in dict: + new_material.set_shader_parameter(key, load(asset_dir+"/"+dict[key])) + # new_material.set_shader_parameter("layer_0", load("./Bricks094_1K-JPG_Color.jpg")) + + ResourceSaver.save(new_material, asset_dir+"/%s.tres" % name_asset) + quit() + + + diff --git a/godot4/scripts/blender_import.gd b/godot4/scripts/blender_import.gd new file mode 100644 index 00000000..cf9031f4 --- /dev/null +++ b/godot4/scripts/blender_import.gd @@ -0,0 +1,43 @@ +extends SceneTree + + +func _init(): + print("init blender_import") + + var asset_dir = "res://assets" + + var args = OS.get_cmdline_user_args() + + print("argss "+str(args)) + if args.size() == 0: + print("No arguments passed") + quit() + return + + for arg in args: + print(arg) + + var name_asset = args[0] + + var new_material = ShaderMaterial.new() + var shader = load(asset_dir+"/%s.gdshader" % name_asset) + + # set shader to material + new_material.shader = shader + + var dict = {} + for i in range(0, round(args.size() - 1)/2): + var idx_key = 1 + i * 2 + var idx_val = idx_key + 1 + dict[args[idx_key]] = args[idx_val] + print(dict) + + for key in dict: + new_material.set_shader_parameter(key, load(asset_dir+"/"+dict[key])) + # new_material.set_shader_parameter("layer_0", load("./Bricks094_1K-JPG_Color.jpg")) + + ResourceSaver.save(new_material, asset_dir+"/%s.tres" % name_asset) + quit() + + + diff --git a/icons/dark/rgb_texture_icon.png b/icons/dark/rgb_texture_icon.png new file mode 100644 index 00000000..b4f3bffb Binary files /dev/null and b/icons/dark/rgb_texture_icon.png differ diff --git a/icons/dark/value_texture_icon.png b/icons/dark/value_texture_icon.png new file mode 100644 index 00000000..7e5d33fd Binary files /dev/null and b/icons/dark/value_texture_icon.png differ diff --git a/icons/dark/vector_texture_icon.png b/icons/dark/vector_texture_icon.png new file mode 100644 index 00000000..acde09ed Binary files /dev/null and b/icons/dark/vector_texture_icon.png differ diff --git a/icons/legacy/rgb_texture_icon.png b/icons/legacy/rgb_texture_icon.png new file mode 100644 index 00000000..2fc767b9 Binary files /dev/null and b/icons/legacy/rgb_texture_icon.png differ diff --git a/icons/legacy/value_texture_icon.png b/icons/legacy/value_texture_icon.png new file mode 100644 index 00000000..b3df853d Binary files /dev/null and b/icons/legacy/value_texture_icon.png differ diff --git a/icons/legacy/vector_texture_icon.png b/icons/legacy/vector_texture_icon.png new file mode 100644 index 00000000..3127c84b Binary files /dev/null and b/icons/legacy/vector_texture_icon.png differ diff --git a/icons/light/rgb_texture_icon.png b/icons/light/rgb_texture_icon.png new file mode 100644 index 00000000..0bc41a6f Binary files /dev/null and b/icons/light/rgb_texture_icon.png differ diff --git a/icons/light/value_texture_icon.png b/icons/light/value_texture_icon.png new file mode 100644 index 00000000..e01abcb0 Binary files /dev/null and b/icons/light/value_texture_icon.png differ diff --git a/icons/light/vector_texture_icon.png b/icons/light/vector_texture_icon.png new file mode 100644 index 00000000..9850a28c Binary files /dev/null and b/icons/light/vector_texture_icon.png differ diff --git a/image_ops.py b/image_ops.py index a577fede..37a38ad3 100644 --- a/image_ops.py +++ b/image_ops.py @@ -1,13 +1,32 @@ -import bpy, os +import bpy, os, subprocess, sys import tempfile from bpy.props import * from bpy_extras.io_utils import ExportHelper from .common import * import time -from . import UDIM +from . import UDIM, subtree, BaseOperator + +def preserve_float_color_hack_before_saving(image): + if not image.is_float or not is_bl_newer_than(2, 80): return + + # HACK: Need more calculation for image saved using straight alpha + if image.alpha_mode == 'STRAIGHT': + if image.colorspace_settings.name == get_srgb_name(): + multiply_image_rgb_by_alpha(image) + elif image.colorspace_settings.name == get_linear_color_name(): + divide_image_rgb_by_alpha(image) + + # TODO: Saved SRGB Straight still has black glitch around alpha transition + # and saved SRGB Premultiplied still looks horrible def save_float_image(image): + + # NOTE: This hack function is probably not a good idea since it uses a lot of assumption + #preserve_float_color_hack_before_saving(image) + + # Remembers original_path = image.filepath + ori_colorspace = image.colorspace_settings.name # Create temporary scene tmpscene = bpy.data.scenes.new('Temp Scene') @@ -28,7 +47,10 @@ def save_float_image(image): elif settings.file_format in {'PNG', 'TIFF'}: settings.color_depth = '16' - #ori_colorspace = image.colorspace_settings.name + # Need to pack first to save the image + if is_bl_newer_than(2, 81) and image.is_dirty: + pack_image(image) + full_path = bpy.path.abspath(image.filepath) image.save_render(full_path, scene=tmpscene) # HACK: If image still dirty after saving, save using standard save method @@ -38,7 +60,18 @@ def save_float_image(image): # Delete temporary scene remove_datablock(bpy.data.scenes, tmpscene) -def pack_float_image(image): + # Set back colorspace + if image.colorspace_settings.name != ori_colorspace: + image.colorspace_settings.name = ori_colorspace + + # Remove packed flag + if is_bl_newer_than(2, 81) and image.packed_file: + image.unpack(method='REMOVE') + + # Reload image + image.reload() + +def pack_float_image_27x(image): original_path = image.filepath # Create temporary scene @@ -94,6 +127,39 @@ def pack_float_image(image): image.filepath = original_path os.remove(temp_filepath) +def preserve_float_color_hack_before_packing(image): + if not image.is_float or not is_bl_newer_than(2, 80): return + + # HACK: Divide by alpha if using straight alpha + if image.alpha_mode == 'STRAIGHT': + divide_image_rgb_by_alpha(image) + + # Check if image is using srgb colorspace + if image.colorspace_settings.name == get_srgb_name(): + + # HACK: Multiply by alpha if using premultiplied alpha + if image.alpha_mode == 'PREMUL': + multiply_image_rgb_by_alpha(image) + + # HACK: If float image use srgb colorspace, it need to be converted to srgb first before packing + set_image_pixels_to_srgb(image) + +def pack_image(image, reload_float=False): + + # NOTE: This hack function is probably not a good idea since it uses a lot of assumption + #preserve_float_color_hack_before_packing(image) + + if is_bl_newer_than(2, 80): + image.pack() + else: + if image.is_float: + pack_float_image_27x(image) + else: image.pack(as_png=True) + + # HACK: Some operation need Float image to be reloaded to be showed correctly + if image.is_float and reload_float: + image.reload() + def clean_object_references(image): removed_references = [] if image.yia.is_image_atlas: @@ -182,7 +248,7 @@ def create_temp_scene(): def save_pack_all(yp): - images = get_yp_images(yp, get_baked_channels=True, check_overlay_normal=True) + images = get_yp_images(yp, get_baked_channels=True) #, check_overlay_normal=True) packed_float_images = [] # Temporary scene for some saving hack @@ -224,7 +290,7 @@ def save_pack_all(yp): temp_saved = True - image.pack() + pack_image(image, reload_float=True) if temp_saved: # Remove file if they are using temporary directory @@ -232,11 +298,9 @@ def save_pack_all(yp): UDIM.remove_udim_files_from_disk(image, temp_udim_dir, True) else: + pack_image(image, reload_float=True) if image.is_float: - pack_float_image(image) packed_float_images.append(image) - else: - image.pack(as_png=True) print('INFO:', image.name, 'image is packed in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') else: @@ -312,9 +376,46 @@ def save_pack_all(yp): for image in images: clean_object_references(image) +class YCopyImagePathToClipboard(bpy.types.Operator): + bl_idname = "wm.copy_image_path_to_clipboard" + bl_label = "Copy Image Path To Clipboard" + bl_description = get_addon_title() + " Copy the image file path to the system clipboard" + + clipboard_text : bpy.props.StringProperty() + + def execute(self, context): + context.window_manager.clipboard = self.clipboard_text + self.report({'INFO'}, "Copied: " + self.clipboard_text) + return {'FINISHED'} + +class YOpenContainingImageFolder(bpy.types.Operator): + bl_idname = "wm.open_containing_image_folder" + bl_label = "Open Containing Image Folder" + bl_description = get_addon_title() + " Open the folder containing the image file and highlight it" + + file_path : bpy.props.StringProperty() + + def execute(self, context): + filepath = bpy.path.abspath(self.file_path) + if not os.path.exists(filepath): + self.report({'ERROR'}, "File does not exist") + return {'CANCELLED'} + try: + # Add more branches below for different operating systems + if sys.platform == 'win32': # Windows + subprocess.call(["explorer", "/select,", filepath]) + elif sys.platform == 'darwin': # Mac + subprocess.call(["open", "-R", filepath]) + elif sys.platform == 'linux': # Linux + subprocess.check_call(['dbus-send', '--session', '--print-reply', '--dest=org.freedesktop.FileManager1', '--type=method_call', '/org/freedesktop/FileManager1', 'org.freedesktop.FileManager1.ShowItems', 'array:string:file://'+os.path.normpath(filepath), 'string:""']) + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'CANCELLED'} + return {'FINISHED'} + class YInvertImage(bpy.types.Operator): """Invert Image""" - bl_idname = "node.y_invert_image" + bl_idname = "wm.y_invert_image" bl_label = "Invert Image" bl_options = {'REGISTER', 'UNDO'} @@ -351,7 +452,7 @@ def execute(self, context): class YRefreshImage(bpy.types.Operator): """Reload Image""" - bl_idname = "node.y_reload_image" + bl_idname = "wm.y_reload_image" bl_label = "Reload Image" bl_options = {'REGISTER', 'UNDO'} @@ -372,7 +473,7 @@ def execute(self, context): class YPackImage(bpy.types.Operator): """Pack Image""" - bl_idname = "node.y_pack_image" + bl_idname = "wm.y_pack_image" bl_label = "Pack Image" bl_options = {'REGISTER', 'UNDO'} @@ -384,14 +485,7 @@ def execute(self, context): T = time.time() - # Save file to temporary place first if image is float - if is_bl_newer_than(2, 80): - context.image.pack() - else: - if context.image.is_float: - pack_float_image(context.image) - else: context.image.pack(as_png=True) - + pack_image(context.image) context.image.filepath = '' node = get_active_ypaint_node() @@ -404,35 +498,18 @@ def execute(self, context): baked_disp = tree.nodes.get(ch.baked_disp) if baked_disp and baked_disp.image and not baked_disp.image.packed_file: - if is_bl_newer_than(2, 80): - baked_disp.image.pack() - else: - if baked_disp.image.is_float: - pack_float_image(baked_disp.image) - else: baked_disp.image.pack(as_png=True) - + pack_image(baked_disp.image) baked_disp.image.filepath = '' baked_vdisp = tree.nodes.get(ch.baked_vdisp) if baked_vdisp and baked_vdisp.image and not baked_vdisp.image.packed_file: - if is_bl_newer_than(2, 80): - baked_vdisp.image.pack() - else: - if baked_vdisp.image.is_float: - pack_float_image(baked_vdisp.image) - else: baked_vdisp.image.pack(as_png=True) - + pack_image(baked_vdisp.image) baked_vdisp.image.filepath = '' - if not is_overlay_normal_empty(yp): + if not is_overlay_normal_empty(ch): baked_normal_overlay = tree.nodes.get(ch.baked_normal_overlay) if baked_normal_overlay and baked_normal_overlay.image and not baked_normal_overlay.image.packed_file: - if is_bl_newer_than(2, 80): - baked_normal_overlay.image.pack() - else: - if baked_normal_overlay.image.is_float: - pack_float_image(baked_normal_overlay.image) - else: baked_normal_overlay.image.pack(as_png=True) + pack_image(baked_normal_overlay.image) baked_normal_overlay.image.filepath = '' @@ -442,7 +519,7 @@ def execute(self, context): class YSaveImage(bpy.types.Operator): """Save Image""" - bl_idname = "node.y_save_image" + bl_idname = "wm.y_save_image" bl_label = "Save Image" bl_options = {'REGISTER', 'UNDO'} @@ -594,7 +671,7 @@ def remove_unpacked_image_path(image, filepath, default_dir, default_dir_found, class YSaveAllBakedImages(bpy.types.Operator): """Save All Baked Images to directory""" - bl_idname = "node.y_save_all_baked_images" + bl_idname = "wm.y_save_all_baked_images" bl_label = "Save All Baked Images" bl_options = {'REGISTER', 'UNDO'} @@ -627,6 +704,12 @@ class YSaveAllBakedImages(bpy.types.Operator): description = 'Create a new image file without modifying the current image in Blender', default = False ) + + force_exr_vdisp : BoolProperty( + name = 'Use EXR for Baked VDM', + description = 'Always use EXR file format for baked vector displacement image', + default = True + ) def invoke(self, context, event): # Open browser, take reference to 'self' read the path to selected @@ -645,6 +728,16 @@ def draw(self, context): self.layout.prop(self, 'copy') + node = get_active_ypaint_node() + if node and self.file_format != 'OPEN_EXR': + tree= node.node_tree + yp = tree.yp + height_root_ch = get_root_height_channel(yp) + if height_root_ch: + baked_vdisp = tree.nodes.get(height_root_ch.baked_vdisp) + if baked_vdisp and baked_vdisp.image: + self.layout.prop(self, 'force_exr_vdisp') + def execute(self, context): node = get_active_ypaint_node() @@ -663,6 +756,7 @@ def execute(self, context): height_root_ch = get_root_height_channel(yp) # Baked images + baked_vdisp_image = None for ch in yp.channels: if ch.no_layer_using: continue @@ -679,8 +773,9 @@ def execute(self, context): baked_vdisp = tree.nodes.get(ch.baked_vdisp) if baked_vdisp and baked_vdisp.image: images.append(baked_vdisp.image) + baked_vdisp_image = baked_vdisp.image - if not is_overlay_normal_empty(yp): + if not is_overlay_normal_empty(ch): baked_normal_overlay = tree.nodes.get(ch.baked_normal_overlay) if baked_normal_overlay and baked_normal_overlay.image: images.append(baked_normal_overlay.image) @@ -708,7 +803,10 @@ def execute(self, context): for image in images: - settings.file_format = self.file_format + if image == baked_vdisp_image and self.force_exr_vdisp: + settings.file_format = 'OPEN_EXR' + else: settings.file_format = self.file_format + settings.color_depth = '8' if settings.file_format != 'OPEN_EXR' else '16' if image.is_float: settings.color_depth = '16' if settings.file_format != 'OPEN_EXR' else '32' @@ -737,12 +835,7 @@ def execute(self, context): # Need to pack first to save the image if image.is_dirty: - if is_bl_newer_than(2, 80): - image.pack() - else: - if image.is_float: - pack_float_image(image) - else: image.pack(as_png=True) + pack_image(image) # Some images need to set to srgb when saving ori_colorspace = image.colorspace_settings.name @@ -815,9 +908,9 @@ def get_file_format_items(): return items -class YSaveAsImage(bpy.types.Operator, ExportHelper): +class YSaveAsImage(bpy.types.Operator, ExportHelper, BaseOperator.FileSelectOptions): """Save As Image""" - bl_idname = "node.y_save_as_image" + bl_idname = "wm.y_save_as_image" bl_label = "Save As Image" bl_options = {'REGISTER', 'UNDO'} @@ -828,20 +921,6 @@ class YSaveAsImage(bpy.types.Operator, ExportHelper): update = update_save_as_file_format ) - # File browser filter - filter_folder : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - filter_image : BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) - display_type : EnumProperty( - items = ( - ('FILE_DEFAULTDISPLAY', 'Default', ''), - ('FILE_SHORTDISLPAY', 'Short List', ''), - ('FILE_LONGDISPLAY', 'Long List', ''), - ('FILE_IMGDISPLAY', 'Thumbnails', '') - ), - default = 'FILE_IMGDISPLAY', - options = {'HIDDEN', 'SKIP_SAVE'} - ) - copy : BoolProperty( name = 'Copy', description = 'Create a new image file without modifying the current image in Blender', @@ -1086,14 +1165,23 @@ def execute(self, context): if self.copy: image = self.image = duplicate_image(image, ondisk_duplicate=False) + # Remembers + ori_colorspace = image.colorspace_settings.name + ori_alpha_mode = image.alpha_mode + + if image.is_float: + # HACK: Set image color to linear first to save linear float image + if image.colorspace_settings.name == get_linear_color_name(): + set_image_pixels_to_linear(image) + + # HACK: Need to do image operation before saving srgb float image + if image.colorspace_settings.name == get_srgb_name(): + if image.alpha_mode == 'STRAIGHT': + multiply_image_rgb_by_alpha(image, power=2) + # Need to pack first to save the image if image.is_dirty: - if is_bl_newer_than(2, 80): - image.pack() - else: - if image.is_float: - pack_float_image(image) - else: image.pack(as_png=True) + pack_image(image) # Unpack image if image is packed (Only necessary for Blender 2.80 and lower) # Packing and unpacking sometimes does not work if the blend file is not saved yet @@ -1102,11 +1190,19 @@ def execute(self, context): unpacked_to_disk = True self.unpack_image(context) - # Some image need to set to srgb when saving - ori_colorspace = image.colorspace_settings.name - if not image.is_float and not image.is_dirty: - image.colorspace_settings.name = get_srgb_name() + if not image.is_dirty: + if not image.is_float: + # HACK: Non float image need to set to srgb when saving + image.colorspace_settings.name = get_srgb_name() + + else: + # HACK: Need to change flip alpha mode before saving float image + if image.alpha_mode == 'PREMUL': + image.alpha_mode = 'STRAIGHT' + elif image.alpha_mode == 'STRAIGHT': + image.alpha_mode = 'PREMUL' + # Save image if image.source == 'TILED': override = bpy.context.copy() @@ -1167,6 +1263,10 @@ def execute(self, context): if image.colorspace_settings.name != ori_colorspace: image.colorspace_settings.name = ori_colorspace + # Set back alpha mode + if image.alpha_mode != ori_alpha_mode: + image.alpha_mode = ori_alpha_mode + # Delete copied image if self.copy: remove_datablock(bpy.data.images, image) @@ -1175,7 +1275,7 @@ def execute(self, context): class YSavePackAll(bpy.types.Operator): """Save and Pack All Image Layers""" - bl_idname = "node.y_save_pack_all" + bl_idname = "wm.y_save_pack_all" bl_label = "Save and Pack All Image Layers" bl_options = {'REGISTER', 'UNDO'} @@ -1192,6 +1292,70 @@ def execute(self, context): ypui.refresh_image_hack = False return {'FINISHED'} +def toggle_image_bit_depth(image, no_copy=False, force_srgb=False, convert_colorspace=False): + + if image.yua.is_udim_atlas or image.yia.is_image_atlas: return + + # Create new image based on original image but with different bit depth + if image.source == 'TILED': + + # Make sure image has filepath + if image.filepath == '': UDIM.initial_pack_udim(image) + + tilenums = [tile.number for tile in image.tiles] + new_image = bpy.data.images.new( + image.name, width=image.size[0], height=image.size[1], + alpha=True, float_buffer=not image.is_float, tiled=True + ) + + # Fill tiles + color = (0, 0, 0, 0) + for tilenum in tilenums: + ori_width = image.tiles.get(tilenum).size[0] + ori_height = image.tiles.get(tilenum).size[1] + UDIM.fill_tile(new_image, tilenum, color, ori_width, ori_height) + UDIM.initial_pack_udim(new_image, color) + + else: + new_image = bpy.data.images.new( + image.name, width=image.size[0], height=image.size[1], + alpha=True, float_buffer=not image.is_float + ) + + if image.filepath != '': + new_image.filepath = image.filepath + + if force_srgb: + new_image.colorspace_settings.name = get_srgb_name() + elif convert_colorspace: + if new_image.is_float: + # Float image will use linear color and premultiplied alpha + new_image.colorspace_settings.name = get_linear_color_name() + new_image.alpha_mode = 'PREMUL' + else: + # Byte image will use srgb color and straight alpha + new_image.colorspace_settings.name = get_srgb_name() + new_image.alpha_mode = 'STRAIGHT' + else: new_image.colorspace_settings.name = image.colorspace_settings.name + + # Copy image pixels + if no_copy == False: + if image.source == 'TILED': + UDIM.copy_udim_pixels(image, new_image, convert_colorspace=convert_colorspace) + else: + if convert_colorspace: + copy_image_pixels_with_conversion(image, new_image) + else: copy_image_pixels(image, new_image) + + # Pack image + if image.source != 'TILED' and image.packed_file: + pack_image(new_image, reload_float=True) + + # Replace image + replace_image(image, new_image) + + return new_image + class YConvertImageBitDepth(bpy.types.Operator): """Convert Image Bit Depth""" bl_idname = "image.y_convert_image_bit_depth" @@ -1206,70 +1370,41 @@ def execute(self, context): node = get_active_ypaint_node() yp = node.node_tree.yp + # Do not convert colorspace if entity is a mask + convert_colorspace = False + m1 = re.match(r'^yp\.layers\[(\d+)\]$', context.entity.path_from_id()) + m2 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', context.entity.path_from_id()) + m3 = re.match(r'^yp\.layers\[(\d+)\]\.channels\[(\d+)\]$', context.entity.path_from_id()) + if m1: + layer = yp.layers[int(m1.group(1))] + convert_colorspace = True + elif m2: + layer = yp.layers[int(m2.group(1))] + elif m3: + layer = yp.layers[int(m3.group(1))] + else: + self.report({'ERROR'}, "Wrong context!") + return {'CANCELLED'} + image = context.image if image.yua.is_udim_atlas or image.yia.is_image_atlas: self.report({'ERROR'}, 'Cannot convert image atlas segment to different bit depth!') return {'CANCELLED'} - # Create new image based on original image but with different bit depth - if image.source == 'TILED': - - # Make sure image has filepath - if image.filepath == '': UDIM.initial_pack_udim(image) - - tilenums = [tile.number for tile in image.tiles] - new_image = bpy.data.images.new( - image.name, width=image.size[0], height=image.size[1], - alpha=True, float_buffer=not image.is_float, tiled=True - ) - - # Fill tiles - color = (0, 0, 0, 0) - for tilenum in tilenums: - ori_width = image.tiles.get(tilenum).size[0] - ori_height = image.tiles.get(tilenum).size[1] - UDIM.fill_tile(new_image, tilenum, color, ori_width, ori_height) - UDIM.initial_pack_udim(new_image, color) - - else: - new_image = bpy.data.images.new( - image.name, width=image.size[0], height=image.size[1], - alpha=True, float_buffer=not image.is_float - ) - - if image.filepath != '': - new_image.filepath = image.filepath - - new_image.colorspace_settings.name = image.colorspace_settings.name - - # Copy image pixels - if image.source == 'TILED': - UDIM.copy_udim_pixels(image, new_image) - else: copy_image_pixels(image, new_image) - - # Pack image - if image.packed_file and image.source != 'TILED': - if is_bl_newer_than(2, 80): - new_image.pack() - else: - if new_image.is_float: - pack_float_image(new_image) - else: new_image.pack(as_png=True) - - # HACK: Float image need to be reloaded after packing to be showed correctly - if new_image.is_float: - new_image.reload() - - # Replace image - replace_image(image, new_image) + toggle_image_bit_depth(image, convert_colorspace=convert_colorspace) # Update image editor by setting active layer index yp.active_layer_index = yp.active_layer_index + # Refresh linear nodes + subtree.check_yp_linear_nodes(yp, specific_layer=layer, reconnect=True) + return {'FINISHED'} def register(): + bpy.utils.register_class(YCopyImagePathToClipboard) + bpy.utils.register_class(YOpenContainingImageFolder) bpy.utils.register_class(YInvertImage) bpy.utils.register_class(YRefreshImage) bpy.utils.register_class(YPackImage) @@ -1280,6 +1415,8 @@ def register(): bpy.utils.register_class(YConvertImageBitDepth) def unregister(): + bpy.utils.unregister_class(YCopyImagePathToClipboard) + bpy.utils.unregister_class(YOpenContainingImageFolder) bpy.utils.unregister_class(YInvertImage) bpy.utils.unregister_class(YRefreshImage) bpy.utils.unregister_class(YPackImage) diff --git a/input_outputs.py b/input_outputs.py index 11b80b86..7a62ddc8 100644 --- a/input_outputs.py +++ b/input_outputs.py @@ -1,5 +1,5 @@ import bpy, re -from . import lib +from . import lib, Decal from .common import * from .transition_common import * from .subtree import * @@ -260,7 +260,7 @@ def check_start_end_root_ch_nodes(group_tree, specific_channel=None): remove_node(group_tree, channel, 'end_max_height_tweak') # Engine filter is needed if subdiv is on and channel is baked - if yp.use_baked and channel.enable_subdiv_setup and any_layers_using_displacement(channel): + if yp.use_baked and channel.enable_subdiv_setup and (any_layers_using_disp(channel) or any_layers_using_vdisp(channel)): lib_name = lib.ENGINE_FILTER if is_bl_newer_than(2, 80) else lib.ENGINE_FILTER_LEGACY end_normal_engine_filter = replace_new_node( @@ -328,6 +328,7 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= #print("Checking YP IO. Specific Layer: " + str(specific_layer)) + group_node = get_active_ypaint_node() group_tree = yp.id_data input_index = 0 @@ -394,8 +395,6 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= # Displacement IO if ch.type == 'NORMAL' and (ch.enable_subdiv_setup or force_height_io): - group_node = get_active_ypaint_node() - name = ch.name + io_suffix['HEIGHT'] height_default_value = 0.0 @@ -403,7 +402,7 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= group_tree, name, 'NodeSocketFloatFactor', valid_inputs, input_index, min_value=0.0, max_value=1.0, default_value=height_default_value, hide_value=True ) - if group_node.node_tree == group_tree: + if group_node and group_node.node_tree == group_tree: group_node.inputs[name].default_value = height_default_value input_index += 1 @@ -412,11 +411,10 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= name = ch.name + io_suffix['MAX_HEIGHT'] - max_height_default_value = 0.1 - create_input(group_tree, name, 'NodeSocketFloat', valid_inputs, input_index, default_value=max_height_default_value) - # Set node default value - if group_node.node_tree == group_tree: - group_node.inputs[name].default_value = max_height_default_value + if create_input(group_tree, name, 'NodeSocketFloat', valid_inputs, input_index, default_value=0.1): + # Set node default value + if group_node and group_node.node_tree == group_tree: + group_node.inputs[name].default_value = ch.ori_max_height_value input_index += 1 create_output(group_tree, name, 'NodeSocketFloat', valid_outputs, output_index) @@ -445,6 +443,19 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= # Check for invalid io for inp in get_tree_inputs(group_tree): if inp not in valid_inputs: + + # Remember default values + for ch in yp.channels: + + if group_node and inp.name == ch.name + io_suffix['ALPHA']: + node_inp = group_node.inputs.get(inp.name) + ch.ori_alpha_value = node_inp.default_value + + if ch.type == 'NORMAL': + if group_node and inp.name == ch.name + io_suffix['MAX_HEIGHT']: + node_inp = group_node.inputs.get(inp.name) + ch.ori_max_height_value = node_inp.default_value + remove_tree_input(group_tree, inp) for outp in get_tree_outputs(group_tree): @@ -460,7 +471,7 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= specific_ch = None if yp.layer_preview_mode and yp.active_channel_index < len(layer.channels): specific_ch = layer.channels[yp.active_channel_index] - check_all_layer_channel_io_and_nodes(layer, specific_ch=specific_ch, do_recursive=False, remove_props=False, hard_reset=hard_reset) + check_all_layer_channel_io_and_nodes(layer, specific_ch=specific_ch, do_recursive=False, remove_props=remove_props, hard_reset=hard_reset) if reconnect: # Rearrange layers @@ -473,183 +484,6 @@ def check_all_channel_ios(yp, reconnect=True, specific_layer=None, remove_props= reconnect_yp_nodes(group_tree) rearrange_yp_nodes(group_tree) -def create_decal_empty(): - obj = bpy.context.object - scene = bpy.context.scene - empty_name = get_unique_name('Decal', bpy.data.objects) - empty = bpy.data.objects.new(empty_name, None) - if is_bl_newer_than(2, 80): - empty.empty_display_type = 'SINGLE_ARROW' - else: empty.empty_draw_type = 'SINGLE_ARROW' - custom_collection = obj.users_collection[0] if is_bl_newer_than(2, 80) and len(obj.users_collection) > 0 else None - link_object(scene, empty, custom_collection) - if is_bl_newer_than(2, 80): - empty.location = scene.cursor.location.copy() - empty.rotation_euler = scene.cursor.rotation_euler.copy() - else: - empty.location = scene.cursor_location.copy() - - # Parent empty to active object - empty.parent = obj - empty.matrix_parent_inverse = obj.matrix_world.inverted() - - return empty - -def check_mask_texcoord_nodes(layer, mask, tree=None): - yp = layer.id_data.yp - if not tree: tree = get_tree(layer) - - height_root_ch = get_root_height_channel(yp) - height_ch = get_height_channel(layer) - height_ch_enabled = get_channel_enabled(height_ch) if height_ch else False - - # Create texcoord node if decal is used - texcoord = tree.nodes.get(mask.texcoord) - if get_mask_enabled(mask) and mask.texcoord_type == 'Decal' and is_mapping_possible(mask.type): - - # Set image extension type to clip - image = None - source = get_mask_source(mask) - if mask.type == 'IMAGE' and source: - image = source.image - - # Create new empty object if there's no texcoord yet - if not texcoord: - empty = create_decal_empty() - texcoord = new_node(tree, mask, 'texcoord', 'ShaderNodeTexCoord', 'TexCoord') - texcoord.object = empty - - decal_process = tree.nodes.get(mask.decal_process) - if not decal_process: - decal_process = new_node(tree, mask, 'decal_process', 'ShaderNodeGroup', 'Decal Process') - decal_process.node_tree = get_node_tree_lib(lib.DECAL_PROCESS) - - # Set image extension only after decal process node is initialized - if image and source: - mask.original_image_extension = source.extension - source.extension = 'CLIP' - - # Set decal aspect ratio - if image: - if image.size[0] > image.size[1]: - decal_process.inputs['Scale'].default_value = (image.size[1] / image.size[0], 1.0, 1.0) - else: decal_process.inputs['Scale'].default_value = (1.0, image.size[0] / image.size[1], 1.0) - - decal_alpha = check_new_node(tree, mask, 'decal_alpha', 'ShaderNodeMath', 'Decal Alpha') - if decal_alpha.operation != 'MULTIPLY': - decal_alpha.operation = 'MULTIPLY' - - if height_ch and height_ch_enabled and height_root_ch.enable_smooth_bump: - for letter in nsew_letters: - decal_alpha = check_new_node(tree, mask, 'decal_alpha_' + letter, 'ShaderNodeMath', 'Decal Alpha ' + letter.upper()) - if decal_alpha.operation != 'MULTIPLY': - decal_alpha.operation = 'MULTIPLY' - else: - for letter in nsew_letters: - remove_node(tree, mask, 'decal_alpha_' + letter) - - else: - if not texcoord or not hasattr(texcoord, 'object') or not texcoord.object: - remove_node(tree, mask, 'texcoord') - remove_node(tree, mask, 'decal_process') - remove_node(tree, mask, 'decal_alpha') - - if height_ch: - for letter in nsew_letters: - remove_node(tree, mask, 'decal_alpha_' + letter) - - # Recover image extension type - if mask.type == 'IMAGE' and mask.original_texcoord == 'Decal' and mask.original_image_extension != '': - source = get_mask_source(mask) - if source: - source.extension = mask.original_image_extension - mask.original_image_extension = '' - - # Save original texcoord type - if mask.original_texcoord != mask.texcoord_type: - mask.original_texcoord = mask.texcoord_type - -def check_layer_texcoord_nodes(layer, tree=None): - yp = layer.id_data.yp - if not tree: tree = get_tree(layer) - - # Create texcoord node if decal is used - texcoord = tree.nodes.get(layer.texcoord) - if get_layer_enabled(layer) and layer.texcoord_type == 'Decal' and is_mapping_possible(layer.type): - - # Set image extension type to clip - image = None - source = get_layer_source(layer) - if layer.type == 'IMAGE' and source: - image = source.image - - # Create new empty object if there's no texcoord yet - if not texcoord: - empty = create_decal_empty() - texcoord = new_node(tree, layer, 'texcoord', 'ShaderNodeTexCoord', 'TexCoord') - texcoord.object = empty - - decal_process = tree.nodes.get(layer.decal_process) - if not decal_process: - decal_process = new_node(tree, layer, 'decal_process', 'ShaderNodeGroup', 'Decal Process') - decal_process.node_tree = get_node_tree_lib(lib.DECAL_PROCESS) - - # Set image extension only after decal process node is initialized - if image and source: - layer.original_image_extension = source.extension - source.extension = 'CLIP' - - # Set decal aspect ratio - if image: - if image.size[0] > image.size[1]: - decal_process.inputs['Scale'].default_value = (image.size[1] / image.size[0], 1.0, 1.0) - else: decal_process.inputs['Scale'].default_value = (1.0, image.size[0] / image.size[1], 1.0) - - # Create decal alpha nodes - for i, ch in enumerate(layer.channels): - root_ch = yp.channels[i] - ch_enabled = get_channel_enabled(ch) - if ch_enabled: - decal_alpha = check_new_node(tree, ch, 'decal_alpha', 'ShaderNodeMath', 'Decal Alpha') - if decal_alpha.operation != 'MULTIPLY': - decal_alpha.operation = 'MULTIPLY' - else: - remove_node(tree, ch, 'decal_alpha') - - if root_ch.type == 'NORMAL': - if ch_enabled and root_ch.enable_smooth_bump: - for letter in nsew_letters: - decal_alpha = check_new_node(tree, ch, 'decal_alpha_' + letter, 'ShaderNodeMath', 'Decal Alpha ' + letter.upper()) - if decal_alpha.operation != 'MULTIPLY': - decal_alpha.operation = 'MULTIPLY' - else: - for letter in nsew_letters: - remove_node(tree, ch, 'decal_alpha_' + letter) - - else: - if not texcoord or not hasattr(texcoord, 'object') or not texcoord.object: - remove_node(tree, layer, 'texcoord') - remove_node(tree, layer, 'decal_process') - - for i, ch in enumerate(layer.channels): - root_ch = yp.channels[i] - remove_node(tree, ch, 'decal_alpha') - - if root_ch.type == 'NORMAL': - for letter in nsew_letters: - remove_node(tree, ch, 'decal_alpha_' + letter) - - # Recover image extension type - if layer.type == 'IMAGE' and layer.original_texcoord == 'Decal' and layer.original_image_extension != '': - source = get_layer_source(layer) - if source: - source.extension = layer.original_image_extension - layer.original_image_extension = '' - - # Save original texcoord type - if layer.original_texcoord != layer.texcoord_type: - layer.original_texcoord = layer.texcoord_type - def check_all_layer_channel_io_and_nodes(layer, tree=None, specific_ch=None, do_recursive=True, remove_props=False, hard_reset=False): #, check_uvs=False): #, has_parent=False): #print("Checking layer IO. Layer: " + layer.name + ' Specific Channel: ' + str(specific_ch)) @@ -662,10 +496,10 @@ def check_all_layer_channel_io_and_nodes(layer, tree=None, specific_ch=None, do_ # check_uv_nodes(yp) # Check layer tree io - check_layer_tree_ios(layer, tree, remove_props, hard_reset=hard_reset) + check_layer_tree_ios(layer, tree, remove_props=remove_props, hard_reset=hard_reset) # Check texcoord nodes - check_layer_texcoord_nodes(layer, tree) + Decal.check_entity_decal_nodes(layer, tree) # Create mapping if necessary if is_layer_using_vector(layer): @@ -712,7 +546,7 @@ def check_all_layer_channel_io_and_nodes(layer, tree=None, specific_ch=None, do_ # Mask nodes for mask in layer.masks: - check_mask_texcoord_nodes(layer, mask, tree) + Decal.check_entity_decal_nodes(mask, tree) #check_mask_image_linear_node(mask) # Linear nodes @@ -876,10 +710,20 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) dirty = create_prop_input(layer, 'decal_distance_value', valid_inputs, input_index, dirty) input_index += 1 - if is_bl_newer_than(2, 81) and layer.enable_uniform_scale and is_layer_using_vector(layer): + if is_bl_newer_than(2, 81) and layer.enable_uniform_scale and is_layer_using_vector(layer) and layer.segment_name == '': dirty = create_prop_input(layer, 'uniform_scale_value', valid_inputs, input_index, dirty) input_index += 1 + + # Edge Detect + if layer.type == 'EDGE_DETECT': + dirty = create_prop_input(layer, 'edge_detect_radius', valid_inputs, input_index, dirty) + input_index += 1 + # AO + elif layer.type == 'AO': + dirty = create_prop_input(layer, 'ao_distance', valid_inputs, input_index, dirty) + input_index += 1 + # Channel prop inputs for i, ch in enumerate(layer.channels): if not get_channel_enabled(ch): continue @@ -909,30 +753,32 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) if root_ch.type == 'NORMAL': - # Height/bump distance input - if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: - dirty = create_prop_input(ch, 'bump_distance', valid_inputs, input_index, dirty) - input_index += 1 + if layer.type != 'GROUP': - # Height/bump midlevel input - if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: - dirty = create_prop_input(ch, 'bump_midlevel', valid_inputs, input_index, dirty) - input_index += 1 - - # Normal map strength input - if ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: - dirty = create_prop_input(ch, 'normal_strength', valid_inputs, input_index, dirty) - input_index += 1 - elif ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': - dirty = create_prop_input(ch, 'vdisp_strength', valid_inputs, input_index, dirty) - input_index += 1 + # Height/bump distance input + if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: + dirty = create_prop_input(ch, 'bump_distance', valid_inputs, input_index, dirty) + input_index += 1 - # Smooth bump multiplier input: - if root_ch.enable_smooth_bump: + # Height/bump midlevel input if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: - dirty = create_prop_input(ch, 'bump_smooth_multiplier', valid_inputs, input_index, dirty) + dirty = create_prop_input(ch, 'bump_midlevel', valid_inputs, input_index, dirty) + input_index += 1 + + # Normal map strength input + if ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: + dirty = create_prop_input(ch, 'normal_strength', valid_inputs, input_index, dirty) + input_index += 1 + elif ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': + dirty = create_prop_input(ch, 'vdisp_strength', valid_inputs, input_index, dirty) input_index += 1 + # Smooth bump multiplier input: + if root_ch.enable_smooth_bump: + if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: + dirty = create_prop_input(ch, 'bump_smooth_multiplier', valid_inputs, input_index, dirty) + input_index += 1 + # Normal height/bump distance input #if ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: # dirty = create_prop_input( ch, 'normal_bump_distance', valid_inputs, input_index, dirty) @@ -996,7 +842,7 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) dirty = create_prop_input(mask, 'intensity_value', valid_inputs, input_index, dirty) input_index += 1 - if is_bl_newer_than(2, 81) and mask.enable_uniform_scale and is_mask_using_vector(mask): + if is_bl_newer_than(2, 81) and mask.enable_uniform_scale and is_mask_using_vector(mask) and mask.segment_name == '': dirty = create_prop_input(mask, 'uniform_scale_value', valid_inputs, input_index, dirty) input_index += 1 @@ -1020,6 +866,11 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) dirty = create_prop_input(mask, 'edge_detect_radius', valid_inputs, input_index, dirty) input_index += 1 + # AO + elif mask.type == 'AO': + dirty = create_prop_input(mask, 'ao_distance', valid_inputs, input_index, dirty) + input_index += 1 + # Tree input and outputs for i, ch in enumerate(layer.channels): root_ch = yp.channels[i] @@ -1126,6 +977,18 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) dirty = create_output(tree, name, 'NodeSocketVector', valid_outputs, output_index, dirty) output_index += 1 + if has_parent: + name = root_ch.name + io_suffix['VDISP'] + io_suffix['ALPHA'] + + if channel_enabled: # or force_normal_input: + + dirty = create_input(tree, name, 'NodeSocketFloatFactor', valid_inputs, input_index, dirty) + input_index += 1 + + if channel_enabled: + dirty = create_output(tree, name, 'NodeSocketFloat', valid_outputs, output_index, dirty) + output_index += 1 + # Tree background inputs if layer.type in {'BACKGROUND', 'GROUP'}: @@ -1158,36 +1021,47 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) input_index += 1 # Displacement Input - if root_ch.type == 'NORMAL' and layer.type == 'GROUP' and is_height_process_needed(layer): + if root_ch.type == 'NORMAL' and layer.type == 'GROUP': - #if not root_ch.enable_smooth_bump: + if is_height_process_needed(layer): - name = root_ch.name + io_suffix['HEIGHT'] + io_suffix['GROUP'] - dirty = create_input(tree, name, 'NodeSocketFloat', - valid_inputs, input_index, dirty) - input_index += 1 + name = root_ch.name + io_suffix['HEIGHT'] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketFloat', + valid_inputs, input_index, dirty) + input_index += 1 - if root_ch.enable_smooth_bump: + if root_ch.enable_smooth_bump: - for letter in nsew_letters: - name = root_ch.name + io_suffix['HEIGHT_' + letter.upper()] + io_suffix['GROUP'] - dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) - input_index += 1 + for letter in nsew_letters: + name = root_ch.name + io_suffix['HEIGHT_' + letter.upper()] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) + input_index += 1 - name = root_ch.name + io_suffix['HEIGHT'] + io_suffix['ALPHA'] + io_suffix['GROUP'] - dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) - input_index += 1 + name = root_ch.name + io_suffix['HEIGHT'] + io_suffix['ALPHA'] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) + input_index += 1 - if root_ch.enable_smooth_bump: + if root_ch.enable_smooth_bump: - for letter in nsew_letters: - name = root_ch.name + io_suffix['HEIGHT_' + letter.upper()] + io_suffix['ALPHA'] + io_suffix['GROUP'] - dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) - input_index += 1 + for letter in nsew_letters: + name = root_ch.name + io_suffix['HEIGHT_' + letter.upper()] + io_suffix['ALPHA'] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) + input_index += 1 - name = root_ch.name + io_suffix['MAX_HEIGHT'] + io_suffix['GROUP'] - dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) - input_index += 1 + name = root_ch.name + io_suffix['MAX_HEIGHT'] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) + input_index += 1 + + if is_vdisp_process_needed(layer): + + name = root_ch.name + io_suffix['VDISP'] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketVector', + valid_inputs, input_index, dirty) + input_index += 1 + + name = root_ch.name + io_suffix['VDISP'] + io_suffix['ALPHA'] + io_suffix['GROUP'] + dirty = create_input(tree, name, 'NodeSocketFloat', valid_inputs, input_index, dirty) + input_index += 1 # Create UV inputs for uv in yp.uvs: @@ -1210,7 +1084,7 @@ def check_layer_tree_ios(layer, tree=None, remove_props=False, hard_reset=False) texcoords = [] # Check layer texcoords - if layer_enabled and layer.texcoord_type not in {'UV', 'Decal'} and layer.type not in {'VCOL', 'COLOR', 'HEMI', 'GROUP', 'BACKGROUND'}: + if layer_enabled and layer.texcoord_type not in {'UV', 'Decal'} and is_layer_using_vector(layer): texcoords.append(layer.texcoord_type) for mask in layer.masks: diff --git a/lib.blend b/lib.blend index ecb619e7..5a4d2e2d 100644 Binary files a/lib.blend and b/lib.blend differ diff --git a/lib.py b/lib.py index f65a0ab0..d91c8b78 100644 --- a/lib.py +++ b/lib.py @@ -163,6 +163,7 @@ TB_DELTA_CALC = '~yPL Transition Bump Delta Calculation' CH_MAX_HEIGHT_CALC = '~yPL Layer Channel Max Height' +CH_MAX_HEIGHT_ADD_CALC = '~yPL Layer Channel Max Height Add' CH_MAX_HEIGHT_TB_CALC = '~yPL Layer Channel Max Height with Transition Bump' CH_MAX_HEIGHT_TB_ADD_CALC = '~yPL Layer Channel Max Height with Transition Bump Add' CH_MAX_HEIGHT_TBC_CALC = '~yPL Layer Channel Max Height with Transition Bump Crease' diff --git a/lib_281.blend b/lib_281.blend index 91b27b21..d1ed20f3 100644 Binary files a/lib_281.blend and b/lib_281.blend differ diff --git a/modifier_common.py b/modifier_common.py index 92b11715..a3674bb6 100644 --- a/modifier_common.py +++ b/modifier_common.py @@ -360,42 +360,51 @@ def check_modifier_nodes(m, tree, ref_tree=None): copy_node_props(color_ramp_ref, color_ramp) ref_tree.nodes.remove(color_ramp_ref) + # Remove deprecated nodes + remove_node(ref_tree, m, 'color_ramp_mix_rgb') # Deprecated + remove_node(ref_tree, m, 'color_ramp_mix_alpha') # Deprecated + remove_node(tree, m, 'color_ramp_linear_start') remove_node(tree, m, 'color_ramp_linear') remove_node(tree, m, 'color_ramp_alpha_multiply') - remove_node(tree, m, 'color_ramp_mix_rgb') - remove_node(tree, m, 'color_ramp_mix_alpha') + + # Remove deprecated nodes + remove_node(tree, m, 'color_ramp_mix_rgb') # Deprecated + remove_node(tree, m, 'color_ramp_mix_alpha') # Deprecated else: + + color_ramp_alpha_multiply = None + if ref_tree: color_ramp_alpha_multiply_ref = ref_tree.nodes.get(m.color_ramp_alpha_multiply) color_ramp_linear_start_ref = ref_tree.nodes.get(m.color_ramp_linear_start) color_ramp_ref = ref_tree.nodes.get(m.color_ramp) color_ramp_linear_ref = ref_tree.nodes.get(m.color_ramp_linear) - color_ramp_mix_alpha_ref = ref_tree.nodes.get(m.color_ramp_mix_alpha) - color_ramp_mix_rgb_ref = ref_tree.nodes.get(m.color_ramp_mix_rgb) # Create new nodes if reference is used - color_ramp_alpha_multiply = new_mix_node(tree, m, 'color_ramp_alpha_multiply', 'ColorRamp Alpha Multiply') + if m.affect_alpha and m.affect_color: + color_ramp_alpha_multiply = new_mix_node(tree, m, 'color_ramp_alpha_multiply', 'ColorRamp Alpha Multiply') + color_ramp_linear_start = new_node(tree, m, 'color_ramp_linear_start', 'ShaderNodeGamma', 'ColorRamp Linear Start') color_ramp = new_node(tree, m, 'color_ramp', 'ShaderNodeValToRGB', 'ColorRamp') color_ramp_linear = new_node(tree, m, 'color_ramp_linear', 'ShaderNodeGamma', 'ColorRamp Linear') - color_ramp_mix_alpha = new_mix_node(tree, m, 'color_ramp_mix_alpha', 'ColorRamp Mix Alpha') - color_ramp_mix_rgb = new_mix_node(tree, m, 'color_ramp_mix_rgb', 'ColorRamp Mix RGB') dirty = True ramp_dirty = False else: - color_ramp_alpha_multiply, dirty = check_new_mix_node(tree, m, 'color_ramp_alpha_multiply', 'ColorRamp Alpha Multiply', True) + dirty = False + if m.affect_alpha and m.affect_color: + color_ramp_alpha_multiply, dirty = check_new_mix_node(tree, m, 'color_ramp_alpha_multiply', 'ColorRamp Alpha Multiply', True) + color_ramp_linear_start = check_new_node(tree, m, 'color_ramp_linear_start', 'ShaderNodeGamma', 'ColorRamp Linear Start') color_ramp, ramp_dirty = check_new_node(tree, m, 'color_ramp', 'ShaderNodeValToRGB', 'ColorRamp', True) color_ramp_linear = check_new_node(tree, m, 'color_ramp_linear', 'ShaderNodeGamma', 'ColorRamp Linear') - color_ramp_mix_alpha = check_new_mix_node(tree, m, 'color_ramp_mix_alpha', 'ColorRamp Mix Alpha') - color_ramp_mix_rgb = check_new_mix_node(tree, m, 'color_ramp_mix_rgb', 'ColorRamp Mix RGB') if ref_tree: if color_ramp_alpha_multiply_ref: - copy_node_props(color_ramp_alpha_multiply_ref, color_ramp_alpha_multiply) + if color_ramp_alpha_multiply: + copy_node_props(color_ramp_alpha_multiply_ref, color_ramp_alpha_multiply) ref_tree.nodes.remove(color_ramp_alpha_multiply_ref) if color_ramp_linear_start_ref: @@ -410,20 +419,14 @@ def check_modifier_nodes(m, tree, ref_tree=None): copy_node_props(color_ramp_linear_ref, color_ramp_linear) ref_tree.nodes.remove(color_ramp_linear_ref) - if color_ramp_mix_alpha_ref: - copy_node_props(color_ramp_mix_alpha_ref, color_ramp_mix_alpha) - ref_tree.nodes.remove(color_ramp_mix_alpha_ref) - - if color_ramp_mix_rgb_ref: - copy_node_props(color_ramp_mix_rgb_ref, color_ramp_mix_rgb) - ref_tree.nodes.remove(color_ramp_mix_rgb_ref) - if dirty: - color_ramp_alpha_multiply.inputs[0].default_value = 1.0 - color_ramp_alpha_multiply.blend_type = 'MULTIPLY' - color_ramp_mix_alpha.inputs[0].default_value = 1.0 - color_ramp_mix_rgb.inputs[0].default_value = 1.0 + if color_ramp_alpha_multiply: + color_ramp_alpha_multiply.inputs[0].default_value = 1.0 + color_ramp_alpha_multiply.blend_type = 'MULTIPLY' + + if not m.affect_alpha or not m.affect_color: + remove_node(tree, m, 'color_ramp_alpha_multiply') if non_color or yp.use_linear_blending: remove_node(tree, m, 'color_ramp_linear_start') @@ -598,8 +601,8 @@ def delete_modifier_nodes(tree, mod): remove_node(tree, mod, 'color_ramp') remove_node(tree, mod, 'color_ramp_linear') remove_node(tree, mod, 'color_ramp_alpha_multiply') - remove_node(tree, mod, 'color_ramp_mix_rgb') - remove_node(tree, mod, 'color_ramp_mix_alpha') + remove_node(tree, mod, 'color_ramp_mix_rgb') # Deprecated + remove_node(tree, mod, 'color_ramp_mix_alpha') # Deprecated elif mod.type == 'RGB_CURVE': remove_node(tree, mod, 'rgb_curve') diff --git a/node_arrangements.py b/node_arrangements.py index f6ea16d5..9ec870d0 100644 --- a/node_arrangements.py +++ b/node_arrangements.py @@ -37,8 +37,14 @@ def get_mod_y_offsets(mod, is_value=False): def check_set_node_loc(tree, node_name, loc, hide=False, parent_unset=False): node = tree.nodes.get(node_name) if node: - if node.location != loc: + # Blender 4.4+ has new parent and node calculation + if is_bl_newer_than(4, 4) and node.parent != None: + if node.location != loc - node.parent.location: + node.location = loc - node.parent.location + + elif node.location != loc: node.location = loc + if node.hide != hide: node.hide = hide @@ -204,6 +210,7 @@ def rearrange_layer_frame_nodes(layer, tree=None): check_set_node_parent(tree, ch.layer_intensity, frame) check_set_node_parent(tree, ch.intensity, frame) check_set_node_parent(tree, ch.extra_alpha, frame) + check_set_node_parent(tree, ch.vdisp_blend, frame) check_set_node_parent(tree, ch.blend, frame) if root_ch.type == 'NORMAL': @@ -230,6 +237,8 @@ def rearrange_layer_frame_nodes(layer, tree=None): check_set_node_parent(tree, ch.normal_proc, frame) check_set_node_parent(tree, ch.normal_flip, frame) + check_set_node_parent(tree, ch.vdisp_intensity, frame) + check_set_node_parent(tree, ch.vdisp_flip_yz, frame) check_set_node_parent(tree, ch.vdisp_proc, frame) #check_set_node_parent(tree, ch.blend_height, frame) @@ -279,10 +288,6 @@ def rearrange_layer_frame_nodes(layer, tree=None): check_set_node_parent(tree, c.mix_remains, frame) check_set_node_parent(tree, c.mix_limit, frame) check_set_node_parent(tree, c.mix_limit_normal, frame) - #check_set_node_parent(tree, c.mix_n, frame) - #check_set_node_parent(tree, c.mix_s, frame) - #check_set_node_parent(tree, c.mix_e, frame) - #check_set_node_parent(tree, c.mix_w, frame) clean_unused_frames(tree) @@ -466,6 +471,7 @@ def rearrange_source_tree_nodes(layer): #loc.y += 390 loc.y = 0 + bookmark_x = loc.x #if check_set_node_loc(source_tree, layer.mapping, loc): # loc.x += 380 @@ -473,6 +479,13 @@ def rearrange_source_tree_nodes(layer): if check_set_node_loc(source_tree, layer.source, loc): loc.x += 280 + if layer.baked_source != '': + loc.x = bookmark_x + loy.y -= 320 + check_set_node_loc(source_tree, layer.baked_source, loc) + loc.x += 280 + loc.y = 0 + if check_set_node_loc(source_tree, layer.divider_alpha, loc): loc.x += 200 @@ -698,6 +711,10 @@ def rearrange_layer_nodes(layer, tree=None): loc.y -= 170 cache_found = True + if check_set_node_loc(tree, layer.cache_gabor, loc, hide=False): + loc.y -= 170 + cache_found = True + if check_set_node_loc(tree, layer.cache_wave, loc, hide=False): loc.y -= 260 cache_found = True @@ -749,6 +766,10 @@ def rearrange_layer_nodes(layer, tree=None): loc.y -= 170 cache_found = True + if check_set_node_loc(tree, ch.cache_gabor, loc, hide=False): + loc.y -= 170 + cache_found = True + if check_set_node_loc(tree, ch.cache_wave, loc, hide=False): loc.y -= 260 cache_found = True @@ -804,6 +825,10 @@ def rearrange_layer_nodes(layer, tree=None): loc.y -= 170 cache_found = True + if check_set_node_loc(tree, mask.cache_gabor, loc, hide=False, parent_unset=True): + loc.y -= 170 + cache_found = True + if check_set_node_loc(tree, mask.cache_wave, loc, hide=False, parent_unset=True): loc.y -= 260 cache_found = True @@ -857,6 +882,9 @@ def rearrange_layer_nodes(layer, tree=None): else: loc.y -= 320 + if check_set_node_loc(tree, layer.baked_source, loc, hide=False): + loc.y -= 320 + if check_set_node_loc(tree, layer.source_n, loc, hide=True): loc.y -= 40 @@ -1249,38 +1277,31 @@ def rearrange_layer_nodes(layer, tree=None): loc.x = bookmark_x bookmark_y = loc.y - #mix_n = tree.nodes.get(c.mix_n) mix_pure = tree.nodes.get(c.mix_pure) mix_remains = tree.nodes.get(c.mix_remains) mix_normal = tree.nodes.get(c.mix_normal) - #if not mix_n and not mix_pure and not mix_remains and not mix_normal: - - if check_set_node_loc(tree, c.mix, loc, True): - loc.y -= 40 - - if check_set_node_loc(tree, c.mix_pure, loc, True): - loc.y -= 40 + mix_vdisp = tree.nodes.get(c.mix_vdisp) + mix_limit_normal = tree.nodes.get(c.mix_limit_normal) - if check_set_node_loc(tree, c.mix_remains, loc, True): - loc.y -= 40 + if mix_pure or mix_remains or mix_normal or mix_limit_normal or mix_vdisp: - if check_set_node_loc(tree, c.mix_normal, loc, True): - loc.y -= 40 + if check_set_node_loc(tree, c.mix, loc, True): + loc.y -= 40 - if check_set_node_loc(tree, c.mix_limit_normal, loc, True): - loc.y -= 40 + if check_set_node_loc(tree, c.mix_pure, loc, True): + loc.y -= 40 - #if check_set_node_loc(tree, c.mix_n, loc, True): - # loc.y -= 40 + if check_set_node_loc(tree, c.mix_remains, loc, True): + loc.y -= 40 - #if check_set_node_loc(tree, c.mix_s, loc, True): - # loc.y -= 40 + if check_set_node_loc(tree, c.mix_normal, loc, True): + loc.y -= 40 - #if check_set_node_loc(tree, c.mix_e, loc, True): - # loc.y -= 40 + if check_set_node_loc(tree, c.mix_vdisp, loc, True): + loc.y -= 40 - #if check_set_node_loc(tree, c.mix_w, loc, True): - # loc.y -= 40 + if check_set_node_loc(tree, c.mix_limit_normal, loc, True): + loc.y -= 40 if check_set_node_loc(tree, c.mix, loc): if root_ch.type == 'NORMAL' and root_ch.enable_smooth_bump: @@ -1300,7 +1321,7 @@ def rearrange_layer_nodes(layer, tree=None): loc.y = bookmark_y # Transition effects - if i == chain-1: + if i == chain-1 and bump_ch: ch = layer.channels[j] @@ -1528,6 +1549,9 @@ def rearrange_layer_nodes(layer, tree=None): if check_set_node_loc(tree, ch.normal_flip, loc): loc.x += 200 + if check_set_node_loc(tree, ch.vdisp_intensity, loc): + loc.x += 200 + if check_set_node_loc(tree, ch.vdisp_flip_yz, loc): loc.x += 200 @@ -1558,6 +1582,9 @@ def rearrange_layer_nodes(layer, tree=None): if check_set_node_loc(tree, ch.extra_alpha, loc): loc.x += 200 + if check_set_node_loc(tree, ch.vdisp_blend, loc): + loc.x += 200 + if check_set_node_loc(tree, ch.blend, loc): loc.x += 250 diff --git a/node_connections.py b/node_connections.py index 95152579..bd6decde 100644 --- a/node_connections.py +++ b/node_connections.py @@ -61,107 +61,89 @@ def reconnect_modifier_nodes(tree, mod, start_rgb, start_alpha): if mod.type == 'INVERT': invert = tree.nodes.get(mod.invert) - create_link(tree, start_rgb, invert.inputs[0]) - create_link(tree, start_alpha, invert.inputs[1]) - - rgb = invert.outputs[0] - alpha = invert.outputs[1] + if invert: + rgb = create_link(tree, rgb, invert.inputs[0])[0] + alpha = create_link(tree, alpha, invert.inputs[1])[1] elif mod.type == 'RGB_TO_INTENSITY': rgb2i = tree.nodes.get(mod.rgb2i) - create_link(tree, start_rgb, rgb2i.inputs[0]) - create_link(tree, start_alpha, rgb2i.inputs[1]) - - rgb = rgb2i.outputs[0] - alpha = rgb2i.outputs[1] + if rgb2i: + rgb = create_link(tree, rgb, rgb2i.inputs[0])[0] + alpha = create_link(tree, alpha, rgb2i.inputs[1])[1] elif mod.type == 'INTENSITY_TO_RGB': i2rgb = tree.nodes.get(mod.i2rgb) - create_link(tree, start_rgb, i2rgb.inputs[0]) - create_link(tree, start_alpha, i2rgb.inputs[1]) - - rgb = i2rgb.outputs[0] - alpha = i2rgb.outputs[1] + if i2rgb: + rgb = create_link(tree, rgb, i2rgb.inputs[0])[0] + alpha = create_link(tree, alpha, i2rgb.inputs[1])[1] elif mod.type == 'OVERRIDE_COLOR': oc = tree.nodes.get(mod.oc) - create_link(tree, start_rgb, oc.inputs[0]) - create_link(tree, start_alpha, oc.inputs[1]) - - rgb = oc.outputs[0] - alpha = oc.outputs[1] + if oc: + rgb = create_link(tree, rgb, oc.inputs[0])[0] + alpha = create_link(tree, alpha, oc.inputs[1])[1] elif mod.type == 'COLOR_RAMP': - color_ramp_alpha_multiply = tree.nodes.get(mod.color_ramp_alpha_multiply) - color_ramp_linear_start = tree.nodes.get(mod.color_ramp_linear_start) color_ramp = tree.nodes.get(mod.color_ramp) - color_ramp_linear = tree.nodes.get(mod.color_ramp_linear) - color_ramp_mix_alpha = tree.nodes.get(mod.color_ramp_mix_alpha) - color_ramp_mix_rgb = tree.nodes.get(mod.color_ramp_mix_rgb) - - am_mixcol0, am_mixcol1, am_mixout = get_mix_color_indices(color_ramp_alpha_multiply) - ma_mixcol0, ma_mixcol1, ma_mixout = get_mix_color_indices(color_ramp_mix_alpha) - mr_mixcol0, mr_mixcol1, mr_mixout = get_mix_color_indices(color_ramp_mix_rgb) - - create_link(tree, start_rgb, color_ramp_alpha_multiply.inputs[am_mixcol0]) - create_link(tree, start_alpha, color_ramp_alpha_multiply.inputs[am_mixcol1]) - if color_ramp_linear_start: - create_link(tree, color_ramp_alpha_multiply.outputs[am_mixout], color_ramp_linear_start.inputs[0]) - create_link(tree, color_ramp_linear_start.outputs[0], color_ramp.inputs[0]) - else: - create_link(tree, color_ramp_alpha_multiply.outputs[am_mixout], color_ramp.inputs[0]) - create_link(tree, start_rgb, color_ramp_mix_rgb.inputs[mr_mixcol0]) - if color_ramp_linear_start: - create_link(tree, color_ramp.outputs[0], color_ramp_linear.inputs[0]) - create_link(tree, color_ramp_linear.outputs[0], color_ramp_mix_rgb.inputs[mr_mixcol1]) - else: - create_link(tree, color_ramp.outputs[0], color_ramp_mix_rgb.inputs[mr_mixcol1]) + if color_ramp and (mod.affect_alpha or mod.affect_color): - create_link(tree, start_alpha, color_ramp_mix_alpha.inputs[ma_mixcol0]) - create_link(tree, color_ramp.outputs[1], color_ramp_mix_alpha.inputs[ma_mixcol1]) + color_ramp_alpha_multiply = tree.nodes.get(mod.color_ramp_alpha_multiply) + if color_ramp_alpha_multiply: + am_mixcol0, am_mixcol1, am_mixout = get_mix_color_indices(color_ramp_alpha_multiply) + rgb = create_link(tree, rgb, color_ramp_alpha_multiply.inputs[am_mixcol0])[am_mixout] + create_link(tree, alpha, color_ramp_alpha_multiply.inputs[am_mixcol1]) - rgb = color_ramp_mix_rgb.outputs[mr_mixout] - alpha = color_ramp_mix_alpha.outputs[ma_mixout] + if mod.affect_alpha and not mod.affect_color: + alpha = create_link(tree, alpha, color_ramp.inputs[0])[0] + else: + color_ramp_linear_start = tree.nodes.get(mod.color_ramp_linear_start) + if color_ramp_linear_start: + rgb = create_link(tree, rgb, color_ramp_linear_start.inputs[0])[0] + + rgb = create_link(tree, rgb, color_ramp.inputs[0])[0] + + if mod.affect_alpha and mod.affect_color: + alpha = color_ramp.outputs[1] + + color_ramp_linear = tree.nodes.get(mod.color_ramp_linear) + if color_ramp_linear: + rgb = create_link(tree, rgb, color_ramp_linear.inputs[0])[0] elif mod.type == 'RGB_CURVE': rgb_curve = tree.nodes.get(mod.rgb_curve) - create_link(tree, start_rgb, rgb_curve.inputs[1]) - rgb = rgb_curve.outputs[0] + if rgb_curve: + rgb = create_link(tree, rgb, rgb_curve.inputs[1])[0] elif mod.type == 'HUE_SATURATION': huesat = tree.nodes.get(mod.huesat) - create_link(tree, start_rgb, huesat.inputs[4]) - rgb = huesat.outputs[0] + if huesat: + rgb = create_link(tree, rgb, huesat.inputs[4])[0] elif mod.type == 'BRIGHT_CONTRAST': brightcon = tree.nodes.get(mod.brightcon) - create_link(tree, start_rgb, brightcon.inputs[0]) - rgb = brightcon.outputs[0] + if brightcon: + rgb = create_link(tree, rgb, brightcon.inputs[0])[0] elif mod.type == 'MULTIPLIER': multiplier = tree.nodes.get(mod.multiplier) - create_link(tree, start_rgb, multiplier.inputs[0]) - create_link(tree, start_alpha, multiplier.inputs[1]) - - rgb = multiplier.outputs[0] - alpha = multiplier.outputs[1] + if multiplier: + rgb = create_link(tree, rgb, multiplier.inputs[0])[0] + alpha = create_link(tree, alpha, multiplier.inputs[1])[1] elif mod.type == 'MATH': - math = tree.nodes.get(mod.math) - create_link(tree, start_rgb, math.inputs[0]) - create_link(tree, start_alpha, math.inputs[1]) - - rgb = math.outputs[0] - alpha = math.outputs[1] + mmath = tree.nodes.get(mod.math) + if mmath: + rgb = create_link(tree, rgb, mmath.inputs[0])[0] + alpha = create_link(tree, alpha, mmath.inputs[1])[1] return rgb, alpha @@ -247,6 +229,10 @@ def remove_all_prev_inputs(tree, layer, node): #, height_only=False): if io_name in node.inputs: break_input_link(tree, node.inputs[io_name]) + io_name = root_ch.name + io_suffix['VDISP'] + io_suffix['ALPHA'] + if io_name in node.inputs: + break_input_link(tree, node.inputs[io_name]) + #if height_only: continue io_name = root_ch.name @@ -963,10 +949,6 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): #print('Reconnect tree ' + tree.name) - start = nodes.get(TREE_START) - end = nodes.get(TREE_END) - - texcoord = nodes.get(TEXCOORD) parallax = tree.nodes.get(PARALLAX) # Parallax @@ -1053,7 +1035,6 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): for tc in texcoord_lists: parallax_prep = tree.nodes.get(tc + PARALLAX_PREP_SUFFIX) if parallax_prep: - #create_link(tree, texcoord.outputs[tc], parallax_prep.inputs[0]) create_link(tree, get_essential_node(tree, TEXCOORD)[tc], parallax_prep.inputs[0]) if tangent and bitangent: create_link(tree, tangent, parallax_prep.inputs['Tangent']) @@ -1065,11 +1046,11 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): if parallax_ch: if parallax: - height = start.outputs.get(parallax_ch.name + io_suffix['HEIGHT']) + height = get_essential_node(tree, TREE_START).get(parallax_ch.name + io_suffix['HEIGHT']) if height: create_link(tree, height, parallax.inputs['base']) if baked_parallax: - height = start.outputs.get(parallax_ch.name + io_suffix['HEIGHT']) + height = get_essential_node(tree, TREE_START).get(parallax_ch.name + io_suffix['HEIGHT']) if height: create_link(tree, height, baked_parallax.inputs['base']) #print() @@ -1105,10 +1086,10 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): io_max_height_name = ch.name + io_suffix['MAX_HEIGHT'] io_vdisp_name = ch.name + io_suffix['VDISP'] - rgb = start.outputs[io_name] + rgb = get_essential_node(tree, TREE_START)[io_name] #if ch.enable_alpha and ch.type == 'RGB': if ch.enable_alpha: - alpha = start.outputs[io_alpha_name] + alpha = get_essential_node(tree, TREE_START)[io_alpha_name] else: alpha = get_essential_node(tree, ONE_VALUE)[0] height = None @@ -1128,14 +1109,14 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): vdisp = None if ch.type == 'NORMAL': - height_input = start.outputs.get(io_height_name) + height_input = get_essential_node(tree, TREE_START).get(io_height_name) height = height_input if height_input else get_essential_node(tree, ZERO_VALUE)[0] if is_normal_height_input_connected(ch): - max_height = start.outputs[io_max_height_name] + max_height = get_essential_node(tree, TREE_START)[io_max_height_name] else: max_height = get_essential_node(tree, ZERO_VALUE)[0] - vdisp_input = start.outputs.get(io_vdisp_name) + vdisp_input = get_essential_node(tree, TREE_START).get(io_vdisp_name) vdisp = vdisp_input if vdisp_input else get_essential_node(tree, ZERO_VALUE)[0] if ch.enable_smooth_bump: @@ -1190,8 +1171,8 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): if ch == yp.channels[yp.active_channel_index] and layer == yp.layers[yp.active_layer_index]: - col_preview = end.inputs.get(LAYER_VIEWER) - alpha_preview = end.inputs.get(LAYER_ALPHA_VIEWER) + col_preview = get_essential_node(tree, TREE_END).get(LAYER_VIEWER) + alpha_preview = get_essential_node(tree, TREE_END).get(LAYER_ALPHA_VIEWER) if col_preview: #create_link(tree, rgb, col_preview) if not layer.enable: @@ -1283,7 +1264,6 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): if parallax_ch and parallax: create_link(tree, parallax.outputs[TEXCOORD_IO_PREFIX + tc], inp) else: - #create_link(tree, texcoord.outputs[tc], inp) create_link(tree, get_essential_node(tree, TEXCOORD)[tc], inp) # Background layer @@ -1437,7 +1417,7 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): mixcol0, mixcol1, mixout = get_mix_color_indices(clamp) rgb = create_link(tree, rgb, clamp.inputs[mixcol0])[mixout] - if yp.use_baked and not ch.no_layer_using and not ch.disable_global_baked and not ch.enable_bake_to_vcol: # and baked_uv: + if yp.use_baked and not ch.no_layer_using and not ch.disable_global_baked and not ch.use_baked_vcol: # and baked_uv: baked = nodes.get(ch.baked) if baked: rgb = baked.outputs[0] @@ -1503,7 +1483,7 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): #create_link(tree, geometry.outputs['Backfacing'], end_backface.inputs[1]) create_link(tree, get_essential_node(tree, GEOMETRY)['Backfacing'], end_backface.inputs[1]) - if yp.use_baked and ch.enable_bake_to_vcol and not ch.disable_global_baked: + if yp.use_baked and ch.use_baked_vcol and not ch.disable_global_baked: baked_vcol = nodes.get(ch.baked_vcol) if baked_vcol: if ch.bake_to_vcol_alpha: @@ -1516,19 +1496,19 @@ def reconnect_yp_nodes(tree, merged_layer_ids = []): #print(rgb) # Blender 2.79 cycles does not need bump normal if not is_bl_newer_than(2, 80) and normal_no_bump and ch.type == 'NORMAL' and ch.enable_subdiv_setup: - create_link(tree, normal_no_bump, end.inputs[io_name]) - else: create_link(tree, rgb, end.inputs[io_name]) + create_link(tree, normal_no_bump, get_essential_node(tree, TREE_END)[io_name]) + else: create_link(tree, rgb, get_essential_node(tree, TREE_END)[io_name]) #if ch.type == 'RGB' and ch.enable_alpha: if ch.enable_alpha: - create_link(tree, alpha, end.inputs[io_alpha_name]) - if ch.type == 'NORMAL' and not ch.enable_bake_to_vcol: - if height and io_height_name in end.inputs: create_link(tree, height, end.inputs[io_height_name]) - if max_height and io_max_height_name in end.inputs: create_link(tree, max_height, end.inputs[io_max_height_name]) - if io_vdisp_name in end.inputs: + create_link(tree, alpha, get_essential_node(tree, TREE_END)[io_alpha_name]) + if ch.type == 'NORMAL' and not ch.use_baked_vcol: + if height and io_height_name in get_essential_node(tree, TREE_END): create_link(tree, height, get_essential_node(tree, TREE_END)[io_height_name]) + if max_height and io_max_height_name in get_essential_node(tree, TREE_END): create_link(tree, max_height, get_essential_node(tree, TREE_END)[io_max_height_name]) + if io_vdisp_name in get_essential_node(tree, TREE_END): if yp.sculpt_mode: - create_link(tree, get_essential_node(tree, ZERO_VALUE)[0], end.inputs[io_vdisp_name]) - elif vdisp: create_link(tree, vdisp, end.inputs[io_vdisp_name]) + create_link(tree, get_essential_node(tree, ZERO_VALUE)[0], get_essential_node(tree, TREE_END)[io_vdisp_name]) + elif vdisp: create_link(tree, vdisp, get_essential_node(tree, TREE_END)[io_vdisp_name]) # Bake target image nodes for bt in yp.bake_targets: @@ -1670,7 +1650,7 @@ def reconnect_source_internal_nodes(layer): if flip_y: rgb = create_link(tree, rgb, flip_y.inputs[0])[0] - if layer.type not in {'IMAGE', 'VCOL', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE'}: + if layer.type not in {'IMAGE', 'VCOL', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE', 'EDGE_DETECT', 'AO'}: rgb_1 = source.outputs[1] alpha = get_essential_node(tree, ONE_VALUE)[0] alpha_1 = get_essential_node(tree, ONE_VALUE)[0] @@ -1687,7 +1667,7 @@ def reconnect_source_internal_nodes(layer): create_link(tree, rgb_1, end.inputs[2]) create_link(tree, alpha_1, end.inputs[3]) - if layer.type in {'IMAGE', 'VCOL', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE'}: + if layer.type in {'IMAGE', 'VCOL', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE', 'EDGE_DETECT', 'AO'}: rgb, alpha = reconnect_all_modifier_nodes(tree, layer, rgb, alpha) @@ -1712,7 +1692,7 @@ def reconnect_mask_internal_nodes(mask, mask_source_index=0): if mask.type == 'MODIFIER' and mask.modifier_type in {'INVERT', 'CURVE'}: create_link(tree, start.outputs[0], source.inputs[1]) - elif mask.use_baked or mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT'}: + elif mask.use_baked or mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT', 'AO'}: create_link(tree, start.outputs[0], source.inputs[0]) val = source.outputs[mask_source_index] @@ -1748,6 +1728,10 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): reconnect_source_internal_nodes(layer) else: source = nodes.get(layer.source) + baked_source = None + if layer.use_baked: + baked_source = nodes.get(layer.baked_source) + # Direction sources source_n = nodes.get(layer.source_n) source_s = nodes.get(layer.source_s) @@ -1810,7 +1794,20 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): if tangent and 'Tangent' in bump_process.inputs: create_link(tree, tangent, bump_process.inputs['Tangent']) if bitangent and 'Bitangent' in bump_process.inputs: create_link(tree, bitangent, bump_process.inputs['Bitangent']) - if layer.type == 'HEMI': + # Edge Detect related + if layer.type == 'EDGE_DETECT': + edge_detect_radius_val = get_essential_node(tree, TREE_START).get(get_entity_input_name(layer, 'edge_detect_radius')) + if edge_detect_radius_val and 'Radius' in source.inputs: + create_link(tree, edge_detect_radius_val, source.inputs['Radius']) + + # AO Related + elif layer.type == 'AO': + ao_distance_val = get_essential_node(tree, TREE_START).get(get_entity_input_name(layer, 'ao_distance')) + if ao_distance_val and 'Distance' in source.inputs: + create_link(tree, ao_distance_val, source.inputs['Distance']) + + # Use previous normal + if layer.type in {'HEMI', 'EDGE_DETECT', 'AO'}: if layer.hemi_use_prev_normal and bump_process: create_link(tree, bump_process.outputs['Normal'], source.inputs['Normal']) elif 'Normal' in source.inputs: create_link(tree, get_essential_node(tree, GEOMETRY)['Normal'], source.inputs['Normal']) @@ -1818,9 +1815,15 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): # Find override channels #using_vector = is_channel_override_using_vector(layer) + baked_vector = None + if layer.use_baked: + baked_vector = get_essential_node(tree, TREE_START).get(layer.baked_uv_name + io_suffix['UV']) + + if baked_vector and baked_source: + create_link(tree, baked_vector, baked_source.inputs[0]) + # Texcoord vector = None - #if layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX'} or using_vector: if is_layer_using_vector(layer): if layer.texcoord_type == 'UV': vector = get_essential_node(tree, TREE_START).get(layer.uv_name + io_suffix['UV']) @@ -1851,47 +1854,54 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): else: break_link(tree, uniform_scale_value, mapping.inputs[3]) - if vector and layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX'}: - create_link(tree, vector, source.inputs[0]) + if vector: + if 'Vector' in source.inputs: + create_link(tree, vector, source.inputs['Vector']) + + if layer.type not in {'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT', 'AO'}: - if uv_neighbor: - create_link(tree, vector, uv_neighbor.inputs[0]) + if uv_neighbor: + create_link(tree, vector, uv_neighbor.inputs[0]) - if tangent and 'Tangent' in uv_neighbor.inputs: - create_link(tree, tangent, uv_neighbor.inputs['Tangent']) - create_link(tree, bitangent, uv_neighbor.inputs['Bitangent']) + if tangent and 'Tangent' in uv_neighbor.inputs: + create_link(tree, tangent, uv_neighbor.inputs['Tangent']) + create_link(tree, bitangent, uv_neighbor.inputs['Bitangent']) - if layer_tangent: - if 'Entity Tangent' in uv_neighbor.inputs: - create_link(tree, layer_tangent, uv_neighbor.inputs['Entity Tangent']) - create_link(tree, layer_bitangent, uv_neighbor.inputs['Entity Bitangent']) + if layer_tangent: + if 'Entity Tangent' in uv_neighbor.inputs: + create_link(tree, layer_tangent, uv_neighbor.inputs['Entity Tangent']) + create_link(tree, layer_bitangent, uv_neighbor.inputs['Entity Bitangent']) - if 'Mask Tangent' in uv_neighbor.inputs: - create_link(tree, layer_tangent, uv_neighbor.inputs['Mask Tangent']) - create_link(tree, layer_bitangent, uv_neighbor.inputs['Mask Bitangent']) + if 'Mask Tangent' in uv_neighbor.inputs: + create_link(tree, layer_tangent, uv_neighbor.inputs['Mask Tangent']) + create_link(tree, layer_bitangent, uv_neighbor.inputs['Mask Bitangent']) - #if 'Tangent' in uv_neighbor.inputs: - # create_link(tree, tangent, uv_neighbor.inputs['Tangent']) - #if 'Bitangent' in uv_neighbor.inputs: - # create_link(tree, bitangent, uv_neighbor.inputs['Bitangent']) + #if 'Tangent' in uv_neighbor.inputs: + # create_link(tree, tangent, uv_neighbor.inputs['Tangent']) + #if 'Bitangent' in uv_neighbor.inputs: + # create_link(tree, bitangent, uv_neighbor.inputs['Bitangent']) - if source_n: create_link(tree, uv_neighbor.outputs['n'], source_n.inputs[0]) - if source_s: create_link(tree, uv_neighbor.outputs['s'], source_s.inputs[0]) - if source_e: create_link(tree, uv_neighbor.outputs['e'], source_e.inputs[0]) - if source_w: create_link(tree, uv_neighbor.outputs['w'], source_w.inputs[0]) + if source_n: create_link(tree, uv_neighbor.outputs['n'], source_n.inputs[0]) + if source_s: create_link(tree, uv_neighbor.outputs['s'], source_s.inputs[0]) + if source_e: create_link(tree, uv_neighbor.outputs['e'], source_e.inputs[0]) + if source_w: create_link(tree, uv_neighbor.outputs['w'], source_w.inputs[0]) # RGB - if is_bl_newer_than(2, 81) and layer.type == 'VORONOI' and layer.voronoi_feature == 'N_SPHERE_RADIUS' and 'Radius' in source.outputs: + if baked_source: + start_rgb = baked_source.outputs[0] + elif is_bl_newer_than(2, 81) and layer.type == 'VORONOI' and layer.voronoi_feature == 'N_SPHERE_RADIUS' and 'Radius' in source.outputs: start_rgb = source.outputs['Radius'] else: start_rgb = source.outputs[0] start_rgb_1 = None - if layer.type not in {'COLOR', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE'} and len(source.outputs) > 1: + if layer.type not in {'COLOR', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE', 'EDGE_DETECT', 'AO'} and len(source.outputs) > 1: start_rgb_1 = source.outputs[1] # Alpha - if layer.type == 'IMAGE' or source_group: + if baked_source: + start_alpha = baked_source.outputs[1] + elif layer.type == 'IMAGE' or source_group: start_alpha = source.outputs[1] elif layer.type == 'VCOL' and 'Alpha' in source.outputs: start_alpha = source.outputs['Alpha'] @@ -1909,7 +1919,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): if linear: start_rgb = create_link(tree, start_rgb, linear.inputs[0])[0] if flip_y: start_rgb = create_link(tree, start_rgb, flip_y.inputs[0])[0] - if source_group and layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE'}: + if source_group and layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE', 'EDGE_DETECT', 'AO'}: start_rgb_1 = source_group.outputs[2] start_alpha_1 = source_group.outputs[3] @@ -1927,15 +1937,15 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): tree, layer, start_rgb, start_alpha, mod_group ) - if layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE'}: + if layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE', 'EDGE_DETECT', 'AO'}: mod_group_1 = nodes.get(layer.mod_group_1) start_rgb_1, start_alpha_1 = reconnect_all_modifier_nodes( tree, layer, source.outputs[1], get_essential_node(tree, ONE_VALUE)[0], mod_group_1 ) # UV neighbor vertex color - if layer.type in {'VCOL', 'GROUP', 'HEMI', 'OBJECT_INDEX'} and uv_neighbor: - if layer.type in {'VCOL', 'HEMI', 'OBJECT_INDEX'}: + if layer.type in {'VCOL', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT', 'AO'} and uv_neighbor: + if layer.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT', 'AO'}: create_link(tree, start_rgb, uv_neighbor.inputs[0]) if tangent and bitangent: @@ -1998,7 +2008,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): for i, mask in enumerate(layer.masks): # Get source output index mask_source_index = 0 - if not mask.use_baked and mask.type not in {'COLOR_ID', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT'}: + if not mask.use_baked and mask.type not in {'COLOR_ID', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT', 'AO'}: # Noise and voronoi output has flipped order since Blender 2.81 if is_bl_newer_than(2, 81) and mask.type == 'VORONOI' and mask.voronoi_feature == 'DISTANCE_TO_EDGE': mask_source_index = 'Distance' @@ -2073,8 +2083,14 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): if edge_detect_radius_val and 'Radius' in mask_source.inputs: create_link(tree, edge_detect_radius_val, mask_source.inputs['Radius']) + # AO related + elif mask.type == 'AO': + ao_distance_val = get_essential_node(tree, TREE_START).get(get_entity_input_name(mask, 'ao_distance')) + if ao_distance_val and 'Distance' in mask_source.inputs: + create_link(tree, ao_distance_val, mask_source.inputs['Distance']) + # Hemi related - if mask.type == 'HEMI' and not mask.use_baked: #and 'Normal' in mask_source.inputs: + if mask.type in {'HEMI', 'EDGE_DETECT', 'AO'} and not mask.use_baked: #and 'Normal' in mask_source.inputs: if mask.hemi_use_prev_normal and bump_process: create_link(tree, bump_process.outputs['Normal'], mask_source.inputs['Normal']) elif 'Normal' in mask_source.inputs: create_link(tree, get_essential_node(tree, GEOMETRY)['Normal'], mask_source.inputs['Normal']) @@ -2088,7 +2104,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): # Mask start mask_vector = None mask_uv_name = mask.uv_name if not mask.use_baked or mask.baked_uv_name == '' else mask.baked_uv_name - if mask.use_baked or mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT'}: + if mask.use_baked or mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'AO'}: if mask.use_baked or mask.texcoord_type == 'UV': mask_vector = get_essential_node(tree, TREE_START).get(mask_uv_name + io_suffix['UV']) elif mask.texcoord_type == 'Decal': @@ -2136,7 +2152,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): mask_uv_neighbor = nodes.get(mask.uv_neighbor) if mask.texcoord_type != 'Layer' else uv_neighbor if mask_uv_neighbor: - if not mask.use_baked and mask.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT'}: + if not mask.use_baked and mask.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'AO'}: create_link(tree, mask_val, mask_uv_neighbor.inputs[0]) else: if mask_vector and mask.texcoord_type != 'Layer': @@ -2218,11 +2234,13 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): mix_pure = nodes.get(c.mix_pure) mix_remains = nodes.get(c.mix_remains) mix_normal = nodes.get(c.mix_normal) + mix_vdisp = nodes.get(c.mix_vdisp) mmixcol0, mmixcol1, mmixout = get_mix_color_indices(mask_mix) mp_mixcol0, mp_mixcol1, mp_mixout = get_mix_color_indices(mix_pure) mr_mixcol0, mr_mixcol1, mr_mixout = get_mix_color_indices(mix_remains) mn_mixcol0, mn_mixcol1, mn_mixout = get_mix_color_indices(mix_normal) + mv_mixcol0, mv_mixcol1, mv_mixout = get_mix_color_indices(mix_vdisp) if mix_pure: create_link(tree, mask_val, mix_pure.inputs[mp_mixcol1]) @@ -2236,13 +2254,17 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): create_link(tree, mask_val, mix_normal.inputs[mn_mixcol1]) if mask_intensity: create_link(tree, mask_intensity, mix_normal.inputs[0]) + if mix_vdisp: + create_link(tree, mask_val, mix_vdisp.inputs[mv_mixcol1]) + if mask_intensity: create_link(tree, mask_intensity, mix_vdisp.inputs[0]) + if mask_mix: if mask_intensity: create_link(tree, mask_intensity, mask_mix.inputs[0]) create_link(tree, mask_val, mask_mix.inputs[mmixcol1]) if root_ch.type == 'NORMAL' and root_ch.enable_smooth_bump: - if not mask.use_baked and mask.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT'}: + if not mask.use_baked and mask.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'AO'}: if mask_uv_neighbor: if 'Color2 n' in mask_mix.inputs: create_link(tree, mask_uv_neighbor.outputs['n'], mask_mix.inputs['Color2 n']) @@ -2315,6 +2337,11 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): prev_vdisp = None next_vdisp = None + prev_vdisp_alpha = None + next_vdisp_alpha = None + + group_vdisp = None + vdisp_alpha = None height_alpha = None normal_alpha = None @@ -2347,6 +2374,10 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): group_channel_alpha = source.outputs.get(root_ch.name + io_suffix['ALPHA'] + io_suffix['GROUP']) if group_channel_alpha: alpha = group_channel_alpha + # Vector displacement from group + group_vdisp = source.outputs.get(root_ch.name + io_suffix['VDISP'] + io_suffix['GROUP']) + vdisp_alpha = source.outputs.get(root_ch.name + io_suffix['VDISP'] + io_suffix['ALPHA'] + io_suffix['GROUP']) + group_alpha = alpha elif layer.type == 'BACKGROUND': @@ -2359,7 +2390,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): # Get source output index source_index = 0 - if layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE'}: + if not layer.use_baked and layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'HEMI', 'OBJECT_INDEX', 'MUSGRAVE', 'EDGE_DETECT', 'AO'}: # Noise and voronoi output has flipped order since Blender 2.81 if is_bl_newer_than(2, 81) and (layer.type == 'NOISE' or (layer.type == 'VORONOI' and layer.voronoi_feature not in {'DISTANCE_TO_EDGE', 'N_SPHERE_RADIUS'})): if ch.layer_input == 'RGB': @@ -2582,6 +2613,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): normal_proc = nodes.get(ch.normal_proc) normal_map_proc = nodes.get(ch.normal_map_proc) vdisp_proc = nodes.get(ch.vdisp_proc) + vdisp_blend = None if root_ch.type == 'NORMAL': @@ -2599,13 +2631,15 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): if normal_proc and 'Intensity' in normal_proc.inputs: create_link(tree, ch_intensity, normal_proc.inputs['Intensity']) - # Set normal strength - if normal_proc and ch_normal_strength and 'Strength' in normal_proc.inputs: - create_link(tree, ch_normal_strength, normal_proc.inputs['Strength']) + if normal_proc: + # Set normal strength + if ch_normal_strength and 'Strength' in normal_proc.inputs: + create_link(tree, ch_normal_strength, normal_proc.inputs['Strength']) - if normal_map_proc: + if ch_normal_strength and normal_map_proc: create_link(tree, ch_normal_strength, normal_map_proc.inputs['Strength']) + vdisp_blend = nodes.get(ch.vdisp_blend) height_blend = nodes.get(ch.height_blend) hbcol0, hbcol1, hbout = get_mix_color_indices(height_blend) @@ -2645,6 +2679,9 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): prev_vdisp = get_essential_node(tree, TREE_START).get(root_ch.name + io_suffix['VDISP']) next_vdisp = get_essential_node(tree, TREE_END).get(root_ch.name + io_suffix['VDISP']) + prev_vdisp_alpha = get_essential_node(tree, TREE_START).get(root_ch.name + io_suffix['VDISP'] + io_suffix['ALPHA']) + next_vdisp_alpha = get_essential_node(tree, TREE_END).get(root_ch.name + io_suffix['VDISP'] + io_suffix['ALPHA']) + # Get neighbor rgb alpha_n = alpha_after_mod alpha_s = alpha_after_mod @@ -2674,7 +2711,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): alpha_e = source_e.outputs[source_index+1] alpha_w = source_w.outputs[source_index+1] - elif layer.type in {'VCOL', 'HEMI', 'OBJECT_INDEX'} and uv_neighbor: + elif layer.type in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'EDGE_DETECT', 'AO'} and uv_neighbor: rgb_n = uv_neighbor.outputs['n'] rgb_s = uv_neighbor.outputs['s'] rgb_e = uv_neighbor.outputs['e'] @@ -2849,12 +2886,14 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): mix_pure = nodes.get(c.mix_pure) mix_remains = nodes.get(c.mix_remains) mix_normal = nodes.get(c.mix_normal) + mix_vdisp = nodes.get(c.mix_vdisp) mix_limit_normal = nodes.get(c.mix_limit_normal) mmixcol0, mmixcol1, mmixout = get_mix_color_indices(mask_mix) mp_mixcol0, mp_mixcol1, mp_mixout = get_mix_color_indices(mix_pure) mr_mixcol0, mr_mixcol1, mr_mixout = get_mix_color_indices(mix_remains) mn_mixcol0, mn_mixcol1, mn_mixout = get_mix_color_indices(mix_normal) + mv_mixcol0, mv_mixcol1, mv_mixout = get_mix_color_indices(mix_vdisp) if mask.type == 'MODIFIER' and root_ch.enable_smooth_bump: mask_source_n = nodes.get(mask.source_n) @@ -2884,6 +2923,9 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): normal_alpha = create_link(tree, normal_alpha, mix_limit_normal.inputs[0])[0] create_link(tree, group_alpha, mix_limit_normal.inputs[1]) + if vdisp_alpha and mix_vdisp: + vdisp_alpha = create_link(tree, vdisp_alpha, mix_vdisp.inputs[mv_mixcol0])[mv_mixout] + if root_ch.enable_smooth_bump and mask_mix: if j == chain and trans_bump_ch == ch and trans_bump_crease: alpha_n = create_link(tree, get_essential_node(tree, ONE_VALUE)[0], mask_mix.inputs['Color1 n'])['Color n'] @@ -2997,7 +3039,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): if normal_proc: create_link(tree, normal, normal_proc.inputs['Normal']) height_group = source.outputs.get(root_ch.name + io_suffix['HEIGHT'] + io_suffix['GROUP']) - if height_proc and height_group: create_link(tree, height_group, height_proc.inputs['Height']) + if height_proc and height_group and 'Height' in height_proc.inputs: create_link(tree, height_group, height_proc.inputs['Height']) if height_proc and root_ch.enable_smooth_bump: if rgb_n and 'Height n' in height_proc.inputs: create_link(tree, rgb_n, height_proc.inputs['Height n']) @@ -3007,9 +3049,10 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): elif normal_map_proc and normal_proc and 'Normal' in normal_proc.inputs: create_link(tree, normal_map_proc.outputs[0], normal_proc.inputs['Normal']) else: - prev_normal = get_essential_node(tree, TREE_START).get(root_ch.name) - if prev_normal and normal_proc and 'Normal' in normal_proc.inputs: - create_link(tree, prev_normal, normal_proc.inputs['Normal']) + # NOTE: Using previous normal doesn't really has actual use cases, so use geometry normal instead + #prev_normal = get_essential_node(tree, TREE_START).get(root_ch.name) + if normal_proc and 'Normal' in normal_proc.inputs: + create_link(tree, get_essential_node(tree, GEOMETRY).get('Normal'), normal_proc.inputs['Normal']) height_alpha = alpha #alpha_ns = None @@ -3152,7 +3195,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): create_link(tree, end_chain, height_proc.inputs['Alpha']) else: create_link(tree, alpha_before_intensity, height_proc.inputs['Alpha']) - if ch.normal_map_type == 'NORMAL_MAP': + if ch.normal_map_type == 'NORMAL_MAP' and 'Transition' in height_proc.inputs: if not write_height and not root_ch.enable_smooth_bump: create_link(tree, end_chain, height_proc.inputs['Transition']) else: create_link(tree, alpha_before_intensity, height_proc.inputs['Transition']) @@ -3329,7 +3372,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): rgb = normal_map_proc.outputs[0] # Bump turned normal map when 'Write Height' is disabled - if normal_proc and ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} and not ch.write_height: + if normal_proc and (ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} or layer.type == 'GROUP') and not ch.write_height: rgb = normal_proc.outputs[0] # Default normal @@ -3345,7 +3388,7 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): ch_vdisp_strength = get_essential_node(tree, TREE_START).get(get_entity_input_name(ch, 'vdisp_strength')) rgb = create_link(tree, rgb, vdisp_proc.inputs[inp0])[outp0] - create_link(tree, ch_vdisp_strength, vdisp_proc.inputs[inp1]) + if ch_vdisp_strength: create_link(tree, ch_vdisp_strength, vdisp_proc.inputs[inp1]) if not root_ch.enable_smooth_bump and not write_height: normal_flip = nodes.get(ch.normal_flip) @@ -3626,25 +3669,59 @@ def reconnect_layer_nodes(layer, ch_idx=-1, merge_mask=False): create_link(tree, normal_alpha, blend.inputs[0]) else: create_link(tree, alpha, blend.inputs[0]) - if root_ch.type == 'NORMAL' and ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': - if prev_vdisp: create_link(tree, prev_vdisp, blend.inputs[bcol0]) - else: - if prev_rgb: create_link(tree, prev_rgb, blend.inputs[bcol0]) + if prev_rgb: create_link(tree, prev_rgb, blend.inputs[bcol0]) # Armory can't recognize mute node, so reconnect input to output directly #if layer.enable and ch.enable: # create_link(tree, blend.outputs[0], next_rgb) #else: create_link(tree, prev_rgb, next_rgb) - if root_ch.type == 'NORMAL' and ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': - if next_vdisp: create_link(tree, blend.outputs[bout], next_vdisp) - if prev_rgb and next_rgb: create_link(tree, prev_rgb, next_rgb) - else: - if next_rgb: create_link(tree, blend.outputs[bout], next_rgb) + if next_rgb: create_link(tree, blend.outputs[bout], next_rgb) elif prev_rgb and next_rgb: create_link(tree, prev_rgb, next_rgb) - if root_ch.type == 'NORMAL' and ch.normal_map_type != 'VECTOR_DISPLACEMENT_MAP' and prev_vdisp and next_vdisp: + if prev_vdisp_alpha and next_vdisp_alpha: + create_link(tree, prev_vdisp_alpha, next_vdisp_alpha) + + if vdisp_blend: + bcol0, bcol1, bout = get_mix_color_indices(vdisp_blend) + + if prev_vdisp: create_link(tree, prev_vdisp, vdisp_blend.inputs[bcol0]) + + vdisp = rgb + if vdisp_alpha == None: + vdisp_alpha = alpha + + if layer.type == 'GROUP': + if group_vdisp: vdisp = group_vdisp + + vdisp_intensity = tree.nodes.get(ch.vdisp_intensity) + + # Dedicated vector displacement channel intensity connection only works with group layer for now + if vdisp_intensity and ch_intensity: + vdisp_alpha = create_link(tree, vdisp_alpha, vdisp_intensity.inputs[0])[0] + create_link(tree, ch_intensity, vdisp_intensity.inputs[1]) + + if vdisp: + create_link(tree, vdisp, vdisp_blend.inputs[bcol1]) + + if next_vdisp: create_link(tree, vdisp_blend.outputs[bout], next_vdisp) + + if vdisp_alpha: + if 'Alpha2' in vdisp_blend.inputs: + create_link(tree, vdisp_alpha, vdisp_blend.inputs['Alpha2']) + else: create_link(tree, vdisp_alpha, vdisp_blend.inputs[0]) + + if prev_vdisp_alpha and 'Alpha1' in vdisp_blend.inputs: + create_link(tree, prev_vdisp_alpha, vdisp_blend.inputs['Alpha1']) + + if next_vdisp_alpha: + if 'Value' in vdisp_blend.outputs: + create_link(tree, vdisp_blend.outputs['Value'], next_vdisp_alpha) + elif 'Alpha' in vdisp_blend.outputs: + create_link(tree, vdisp_blend.outputs['Alpha'], next_vdisp_alpha) + + elif prev_vdisp and next_vdisp: create_link(tree, prev_vdisp, next_vdisp) if next_alpha: diff --git a/preferences.py b/preferences.py index 353eaed8..e85cc72b 100644 --- a/preferences.py +++ b/preferences.py @@ -157,6 +157,12 @@ class YPaintPreferences(AddonPreferences): default=1, min=0, max=59 ) + library_location : StringProperty( + name = 'Texture library path', + description = 'Location of texture library', + subtype='DIR_PATH', + ) + default_image_resolution : EnumProperty( name = 'Default Image Size', items = ( @@ -211,6 +217,29 @@ def draw(self, context): check_col = sub_row.column(align=True) check_col.prop(self, "updater_interval_minutes") check_col = sub_row.column(align=True) + + addon_updater_ops.update_settings_ui(self, context) + self.layout.prop(self, 'library_location') + if not self.library_location.strip(): + self.layout.label(text = "Please select the folder of textures library", icon = 'INFO') +@persistent +def setup_library(scene): + bpy.app.handlers.load_post.remove(setup_library) + ypup:YPaintPreferences = get_user_preferences() + + if not ypup.library_location: + libraries = bpy.context.preferences.filepaths.asset_libraries + if len(libraries): + for lib in libraries: + if os.path.exists(lib.path): + ypup.library_location = lib.path + break + if not ypup.library_location: + home = os.path.expanduser("~") + new_path = os.path.join(home, "ucupaint-library") + os.sep + if not os.path.exists(new_path): + os.mkdir(new_path) + ypup.library_location = new_path @persistent def auto_save_images(scene): @@ -253,8 +282,8 @@ def refresh_float_image_hack(scene): def register(): bpy.utils.register_class(YPaintPreferences) - bpy.app.handlers.save_pre.append(auto_save_images) + bpy.app.handlers.load_post.append(setup_library) bpy.app.handlers.save_post.append(refresh_float_image_hack) def unregister(): diff --git a/subtree.py b/subtree.py index 607c40ab..260753ba 100644 --- a/subtree.py +++ b/subtree.py @@ -150,11 +150,11 @@ def enable_layer_source_tree(layer, rearrange=False): # Check if source tree is already available if layer.type in {'BACKGROUND', 'COLOR'}: return - if layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE'} and layer.source_group != '': return + if layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT'} and layer.source_group != '': return layer_tree = get_tree(layer) - if layer.type not in {'VCOL', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'BACKFACE'}: + if layer.type not in {'VCOL', 'GROUP', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT'}: # Get current source for reference source_ref = layer_tree.nodes.get(layer.source) linear_ref = layer_tree.nodes.get(layer.linear) @@ -211,7 +211,7 @@ def enable_layer_source_tree(layer, rearrange=False): move_mod_group(layer, layer_tree, source_tree) # Create uv neighbor - if layer.type in {'VCOL', 'HEMI'}: + if layer.type in {'VCOL', 'HEMI', 'EDGE_DETECT'}: uv_neighbor = replace_new_node( layer_tree, layer, 'uv_neighbor', 'ShaderNodeGroup', 'Neighbor UV', lib.NEIGHBOR_FAKE, hard_replace=True @@ -249,6 +249,7 @@ def disable_channel_source_tree(layer, root_ch, ch, rearrange=True, force=False) return layer_tree = get_tree(layer) + if not layer_tree: return #if ch.override_type not in {'DEFAULT'}: source_group = layer_tree.nodes.get(ch.source_group) @@ -293,12 +294,12 @@ def disable_layer_source_tree(layer, rearrange=True, force=False): if root_ch.type == 'NORMAL' and root_ch.enable_smooth_bump and get_channel_enabled(layer.channels[i], layer, root_ch) and is_height_process_needed(layer): smooth_bump_ch = root_ch - if (layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE'} and layer.source_group == '') or smooth_bump_ch: + if (layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT'} and layer.source_group == '') or smooth_bump_ch: return layer_tree = get_tree(layer) - if force or layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE'}: + if force or layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'BACKFACE', 'EDGE_DETECT'}: source_group = layer_tree.nodes.get(layer.source_group) if source_group: source_ref = source_group.node_tree.nodes.get(layer.source) @@ -580,6 +581,7 @@ def check_mask_mix_nodes(layer, tree=None, specific_mask=None, specific_ch=None) yp = layer.id_data.yp if not tree: tree = get_tree(layer) + if not tree: return False need_reconnect = False @@ -610,6 +612,7 @@ def check_mask_mix_nodes(layer, tree=None, specific_mask=None, specific_ch=None) if root_ch.type == 'NORMAL': if remove_node(tree, c, 'mix_pure'): need_reconnect = True if remove_node(tree, c, 'mix_normal'): need_reconnect = True + if remove_node(tree, c, 'mix_vdisp'): need_reconnect = True continue if (root_ch.type == 'NORMAL' and root_ch.enable_smooth_bump and height_process_needed and @@ -686,6 +689,20 @@ def check_mask_mix_nodes(layer, tree=None, specific_mask=None, specific_ch=None) else: if remove_node(tree, c, 'mix_normal'): need_reconnect = True + if layer.type == 'GROUP' and is_layer_using_vdisp_map(layer): + mix_vdisp = tree.nodes.get(c.mix_vdisp) + if not mix_vdisp: + need_reconnect = True + mix_vdisp = new_mix_node(tree, c, 'mix_vdisp', 'Mask VDisp') + mix_vdisp.inputs[0].default_value = mask.intensity_value + if mix_vdisp.blend_type != mask.blend_type: + mix_vdisp.blend_type = mask.blend_type + # Use clamp to keep value between 0.0 to 1.0 + if mask.blend_type not in {'MIX', 'MULTIPLY'}: + set_mix_clamp(mix_vdisp, True) + else: + if remove_node(tree, c, 'mix_vdisp'): need_reconnect = True + else: if (trans_bump and i >= chain and ( (trans_bump_flip and ch.enable_transition_ramp) or @@ -1659,6 +1676,16 @@ def check_uv_nodes(yp, generate_missings=False): if uv.name not in uv_names: uv_names.append(uv.name) + if layer.use_baked and layer.baked_uv_name != '': + uv = yp.uvs.get(layer.baked_uv_name) + if not uv: + dirty = True + uv = yp.uvs.add() + uv.name = layer.baked_uv_name + + if uv.name not in uv_names: + uv_names.append(uv.name) + for mask in layer.masks: if mask.texcoord_type == 'UV' and mask.uv_name != '': uv = yp.uvs.get(mask.uv_name) @@ -1670,6 +1697,16 @@ def check_uv_nodes(yp, generate_missings=False): if uv.name not in uv_names: uv_names.append(uv.name) + if mask.use_baked and mask.baked_uv_name != '': + uv = yp.uvs.get(mask.baked_uv_name) + if not uv: + dirty = True + uv = yp.uvs.add() + uv.name = mask.baked_uv_name + + if uv.name not in uv_names: + uv_names.append(uv.name) + # Get unused uv objects unused_uvs = [] unused_ids = [] @@ -1865,7 +1902,9 @@ def check_channel_normal_map_nodes(tree, layer, root_ch, ch, need_reconnect=Fals lib_name = lib.CH_MAX_HEIGHT_TB_ADD_CALC else: lib_name = lib.CH_MAX_HEIGHT_TB_CALC else: - lib_name = lib.CH_MAX_HEIGHT_CALC + if ch.normal_blend_type == 'OVERLAY': + lib_name = lib.CH_MAX_HEIGHT_ADD_CALC + else: lib_name = lib.CH_MAX_HEIGHT_CALC if ch.write_height: max_height_calc, need_reconnect = replace_new_node( @@ -1879,7 +1918,7 @@ def check_channel_normal_map_nodes(tree, layer, root_ch, ch, need_reconnect=Fals if remove_node(tree, ch, 'max_height_calc'): need_reconnect = True # Height Process - if ch.normal_map_type == 'NORMAL_MAP': + if layer.type != 'GROUP' and ch.normal_map_type == 'NORMAL_MAP': if root_ch.enable_smooth_bump: if ch.enable_transition_bump: if ch.transition_bump_crease and not ch.transition_bump_flip: @@ -2065,7 +2104,7 @@ def check_channel_normal_map_nodes(tree, layer, root_ch, ch, need_reconnect=Fals lib_name = lib.NORMAL_MAP # Normal map - if ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: + if layer.type != 'GROUP' and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: normal_map_proc, need_reconnect = check_new_node(tree, ch, 'normal_map_proc', 'ShaderNodeNormalMap', 'Normal Map Process', True) normal_map_proc.uv_map = layer.uv_name normal_map_proc.space = ch.normal_space @@ -2111,24 +2150,56 @@ def check_channel_normal_map_nodes(tree, layer, root_ch, ch, need_reconnect=Fals if remove_node(tree, ch, 'normal_proc'): need_reconnect = True if remove_node(tree, ch, 'normal_flip'): need_reconnect = True - if channel_enabled and ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': + if channel_enabled and is_vdisp_process_needed(layer): - if ch.vdisp_enable_flip_yz: - vdisp_flip_yz, dirty = check_new_node(tree, ch, 'vdisp_flip_yz', 'ShaderNodeGroup', 'Flip Y/Z', True) - vdisp_flip_yz.node_tree = lib.get_node_tree_lib(lib.FLIP_YZ) + # Dedicated vdisp intensity currently is needed for group + if layer.type == 'GROUP': + vdisp_intensity, dirty = check_new_node(tree, ch, 'vdisp_intensity', 'ShaderNodeMath', 'VDisp Opacity', True) + vdisp_intensity.operation = 'MULTIPLY' if dirty: need_reconnect = True - else: + + if remove_node(tree, ch, 'vdisp_proc'): need_reconnect = True if remove_node(tree, ch, 'vdisp_flip_yz'): need_reconnect = True - vdisp_proc, need_reconnect = replace_new_mix_node( - tree, ch, 'vdisp_proc', 'Vector Displacement Process', - return_status=True, hard_replace=True, dirty=need_reconnect - ) - vdisp_proc.blend_type = 'MULTIPLY' - vdisp_proc.inputs[0].default_value = 1.0 + else: + if ch.vdisp_enable_flip_yz: + vdisp_flip_yz, dirty = check_new_node(tree, ch, 'vdisp_flip_yz', 'ShaderNodeGroup', 'Flip Y/Z', True) + vdisp_flip_yz.node_tree = lib.get_node_tree_lib(lib.FLIP_YZ) + if dirty: need_reconnect = True + else: + if remove_node(tree, ch, 'vdisp_flip_yz'): need_reconnect = True + + vdisp_proc, need_reconnect = replace_new_mix_node( + tree, ch, 'vdisp_proc', 'Vector Displacement Process', + return_status=True, hard_replace=True, dirty=need_reconnect + ) + vdisp_proc.blend_type = 'MULTIPLY' + vdisp_proc.inputs[0].default_value = 1.0 + + if remove_node(tree, ch, 'vdisp_intensity'): need_reconnect = True + + if layer.parent_idx != -1 and ch.normal_blend_type == 'MIX': + vdisp_blend, need_reconnect = replace_new_node( + tree, ch, 'vdisp_blend', 'ShaderNodeGroup', 'VDisp Blend', lib.STRAIGHT_OVER_HEIGHT_MIX, + return_status=True, hard_replace=True, dirty=need_reconnect + ) + elif layer.parent_idx != -1 and ch.normal_blend_type == 'OVERLAY': + vdisp_blend, need_reconnect = replace_new_node( + tree, ch, 'vdisp_blend', 'ShaderNodeGroup', 'VDisp Blend', lib.STRAIGHT_OVER_HEIGHT_ADD, + return_status=True, hard_replace=True, dirty=need_reconnect + ) + else: + vdisp_blend, need_reconnect = replace_new_mix_node( + tree, ch, 'vdisp_blend', 'VDisp Blend', + return_status=True, hard_replace=True, dirty=need_reconnect + ) + vdisp_blend.blend_type = 'ADD' if ch.normal_blend_type == 'OVERLAY' else 'MIX' + else: if remove_node(tree, ch, 'vdisp_proc'): need_reconnect = True if remove_node(tree, ch, 'vdisp_flip_yz'): need_reconnect = True + if remove_node(tree, ch, 'vdisp_blend'): need_reconnect = True + if remove_node(tree, ch, 'vdisp_intensity'): need_reconnect = True return need_reconnect @@ -2407,13 +2478,13 @@ def check_blend_type_nodes(root_ch, layer, ch): return_status=True, hard_replace=True, dirty=need_reconnect ) - elif channel_enabled and ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': + #elif channel_enabled and ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP': - blend, need_reconnect = replace_new_mix_node( - tree, ch, 'blend', 'Blend', - return_status=True, hard_replace=True, dirty=need_reconnect - ) - blend.blend_type = 'ADD' if ch.normal_blend_type == 'OVERLAY' else 'MIX' + # blend, need_reconnect = replace_new_mix_node( + # tree, ch, 'blend', 'Blend', + # return_status=True, hard_replace=True, dirty=need_reconnect + # ) + # blend.blend_type = 'ADD' if ch.normal_blend_type == 'OVERLAY' else 'MIX' else: if remove_node(tree, ch, 'blend'): need_reconnect = True @@ -2424,7 +2495,7 @@ def check_blend_type_nodes(root_ch, layer, ch): # Intensity nodes intensity = tree.nodes.get(ch.intensity) if not intensity: - intensity = new_node(tree, ch, 'intensity', 'ShaderNodeMath', 'Channel Intensity') + intensity = new_node(tree, ch, 'intensity', 'ShaderNodeMath', 'Channel Opacity') intensity.operation = 'MULTIPLY' # Channel intensity @@ -2474,167 +2545,88 @@ def check_extra_alpha(layer, need_reconnect=False): def check_layer_channel_linear_node(ch, layer=None, root_ch=None, reconnect=False): yp = ch.id_data.yp - - if not layer or not root_ch: - match = re.match(r'yp\.layers\[(\d+)\]\.channels\[(\d+)\]', ch.path_from_id()) - layer = yp.layers[int(match.group(1))] - root_ch = yp.channels[int(match.group(2))] + if not layer or not root_ch: layer, root_ch = get_layer_and_root_ch_from_layer_ch(ch) source_tree = get_channel_source_tree(ch, layer) - image = None - source = None - if ch.override and ch.override_type == 'IMAGE': - source = source_tree.nodes.get(ch.source) - if source: image = source.image - elif layer.type == 'IMAGE': - source = get_layer_source(layer) - if source: image = source.image - - channel_enabled = get_channel_enabled(ch, layer, root_ch) + gamma = get_layer_channel_gamma_value(ch, layer, root_ch) - if channel_enabled and (( - not yp.use_linear_blending - and ch.override - and ( - (image and is_image_source_srgb(image, source, root_ch)) or - ( - ch.override_type not in {'IMAGE'} - and root_ch.type != 'NORMAL' - and root_ch.colorspace == 'SRGB' - )) - ) or ( - not yp.use_linear_blending - and not ch.override - and root_ch.type != 'NORMAL' - and root_ch.colorspace == 'SRGB' - and ( - (not ch.gamma_space and ch.layer_input == 'RGB' and layer.type not in {'IMAGE', 'BACKGROUND', 'GROUP'}) - or (layer.type == 'IMAGE' and image.is_float and image.colorspace_settings.name != get_srgb_name()) # Float images need to converted to linear for some reason in Blender - ) - ) or ( - yp.use_linear_blending - and not ch.override_1 - and root_ch.type == 'NORMAL' - and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'} - and source and source.image and not source.image.is_float #and is_image_source_srgb(image, source) # NOTE: No need for channel linear if the image is float - )): + if gamma != 1.0: + # Create linear node if root_ch.type == 'VALUE': linear = replace_new_node(source_tree, ch, 'linear', 'ShaderNodeMath', 'Linear') linear.operation = 'POWER' else: linear = replace_new_node(source_tree, ch, 'linear', 'ShaderNodeGamma', 'Linear') - - linear.inputs[1].default_value = 1.0 / GAMMA - - elif channel_enabled and ( - yp.use_linear_blending - and root_ch.type != 'NORMAL' - and root_ch.colorspace == 'SRGB' - and ( - (ch.gamma_space and ch.layer_input == 'RGB' and layer.type not in {'IMAGE', 'BACKGROUND', 'GROUP'}) - #or (layer.type == 'IMAGE' and image.is_float and image.colorspace_settings.name == get_srgb_name()) - ) - ): - if root_ch.type == 'VALUE': - linear = replace_new_node(source_tree, ch, 'linear', 'ShaderNodeMath', 'Linear') - linear.operation = 'POWER' - else: linear = replace_new_node(source_tree, ch, 'linear', 'ShaderNodeGamma', 'Linear') - - linear.inputs[1].default_value = GAMMA - + linear.inputs[1].default_value = gamma else: + # Delete linear node remove_node(source_tree, ch, 'linear') - image_1 = None - layer_tree = get_tree(layer) - if ch.override_1 and ch.override_1_type == 'IMAGE': - source_1 = layer_tree.nodes.get(ch.source_1) - if source_1: image_1 = source_1.image - if channel_enabled and ch.override_1 and image_1 and is_image_source_srgb(image_1, source_1): - linear_1 = replace_new_node(layer_tree, ch, 'linear_1', 'ShaderNodeGamma', 'Linear 1') - linear_1.inputs[1].default_value = 1.0 / GAMMA - else: - remove_node(layer_tree, ch, 'linear_1') + if root_ch.type == 'NORMAL': + gamma_1 = get_layer_channel_normal_gamma_value(ch, layer, root_ch) + if gamma_1 != 1.0: + # Create linear node + layer_tree = get_tree(layer) + linear_1 = replace_new_node(layer_tree, ch, 'linear_1', 'ShaderNodeGamma', 'Linear 1') + linear_1.inputs[1].default_value = gamma_1 + else: + # Delete linear node + remove_node(source_tree, ch, 'linear_1') if reconnect: reconnect_layer_nodes(layer) rearrange_layer_nodes(layer) - return image - def check_layer_image_linear_node(layer, source_tree=None): yp = layer.id_data.yp if not source_tree: source_tree = get_source_tree(layer) - if get_layer_enabled(layer) and layer.type == 'IMAGE': - - source = source_tree.nodes.get(layer.source) - image = source.image - if not image: return - - # Create linear if image type is srgb or float image - if is_image_source_srgb(image, source) and (not yp.use_linear_blending or (yp.use_linear_blending and image.is_float)): - linear = source_tree.nodes.get(layer.linear) - if not linear: - linear = new_node(source_tree, layer, 'linear', 'ShaderNodeGamma', 'Linear') - linear.inputs[1].default_value = 1.0 / GAMMA - - return - - elif yp.use_linear_blending and not image.is_float and not is_image_source_srgb(image, source): - linear = source_tree.nodes.get(layer.linear) - if not linear: - linear = new_node(source_tree, layer, 'linear', 'ShaderNodeGamma', 'Linear') - linear.inputs[1].default_value = GAMMA + # Blender 2.7x has color space option on the node + if not is_bl_newer_than(2, 80) and layer.type == 'IMAGE': + source = get_layer_source(layer) + if yp.use_linear_blending: + if source.color_space != 'COLOR': source.color_space = 'COLOR' + else: + if source.color_space != 'NONE': source.color_space = 'NONE' - return + gamma = get_layer_gamma_value(layer) - # Delete linear - remove_node(source_tree, layer, 'linear') + if gamma != 1.0: + # Create linear node + linear = check_new_node(source_tree, layer, 'linear', 'ShaderNodeGamma', 'Linear') + linear.inputs[1].default_value = gamma + else: + # Delete linear node + remove_node(source_tree, layer, 'linear') def check_mask_image_linear_node(mask, mask_tree=None): if not mask_tree: mask_tree = get_mask_tree(mask) - if get_mask_enabled(mask) and mask.type == 'IMAGE': - - source = mask_tree.nodes.get(mask.source) - image = source.image - - if not image: return + gamma = get_layer_mask_gamma_value(mask, mask_tree) - # Create linear if image type is srgb - if is_image_source_srgb(image, source): - linear = mask_tree.nodes.get(mask.linear) - if not linear: - linear = new_node(mask_tree, mask, 'linear', 'ShaderNodeGamma', 'Linear') - linear.inputs[1].default_value = 1.0 / GAMMA - - return - - # Delete linear - remove_node(mask_tree, mask, 'linear') + if gamma != 1.0: + # Create linear node + linear = check_new_node(mask_tree, mask, 'linear', 'ShaderNodeGamma', 'Linear') + linear.inputs[1].default_value = gamma + else: + # Delete linear node + remove_node(mask_tree, mask, 'linear') def check_yp_linear_nodes(yp, specific_layer=None, reconnect=True): for layer in yp.layers: if specific_layer and layer != specific_layer: continue - image_found = False if layer.type == 'IMAGE': check_layer_image_linear_node(layer) - image_found = True for ch in layer.channels: - #if ch.override_type == 'IMAGE' or ch.override_1_type == 'IMAGE': - if check_layer_channel_linear_node(ch): - image_found = True + check_layer_channel_linear_node(ch) for mask in layer.masks: if mask.type == 'IMAGE': check_mask_image_linear_node(mask) - image_found = True - #if image_found and reconnect: if reconnect: reconnect_layer_nodes(layer) rearrange_layer_nodes(layer) diff --git a/tests/test_quicksetup.py b/tests/test_quicksetup.py index 7a765fa4..d33a6c0b 100644 --- a/tests/test_quicksetup.py +++ b/tests/test_quicksetup.py @@ -22,14 +22,14 @@ def get_texdir(): class TestPrincipled(unittest.TestCase): def test_00_principled(self): # Quick Setup - bpy.ops.node.y_quick_ypaint_node_setup(type='BSDF_PRINCIPLED') + bpy.ops.wm.y_quick_ypaint_node_setup(type='BSDF_PRINCIPLED') # Get yp yp = get_yp() self.assertTrue(yp != None) def test_99_remove_yp_node(self): - bpy.ops.node.y_remove_yp_node() + bpy.ops.wm.y_remove_yp_node() # Get yp yp = get_yp() @@ -38,7 +38,7 @@ def test_99_remove_yp_node(self): class TestNewLayer(TestPrincipled): def test_01_new_layer(self): # Add new image layer - bpy.ops.node.y_new_layer(name='Image', uv_map='UVMap') + bpy.ops.wm.y_new_layer(name='Image', uv_map='UVMap') yp = get_yp() self.assertTrue(len(yp.layers) == 1) @@ -46,7 +46,7 @@ def test_01_new_layer(self): class TestNewLayerWithMask(TestPrincipled): def test_01_new_layer_with_mask(self): # Add new solid color layer with image mask - bpy.ops.node.y_new_layer(name='Solid Color', type='COLOR', uv_map='UVMap', add_mask=True, mask_uv_name='UVMap') + bpy.ops.wm.y_new_layer(name='Solid Color', type='COLOR', uv_map='UVMap', add_mask=True, mask_uv_name='UVMap') yp = get_yp() self.assertTrue(len(yp.layers) == 1) @@ -56,7 +56,7 @@ def test_01_open_image_to_layer(self): texdir = get_texdir() # Open image as layer - bpy.ops.node.y_open_image_to_layer( + bpy.ops.wm.y_open_image_to_layer( files = [{"name":"blender_color.png"}], directory = texdir, uv_map="UVMap" @@ -70,7 +70,7 @@ def test_01_open_images_to_single_layer_with_mask(self): texdir = get_texdir() # Open images to single layer with white mask - bpy.ops.node.y_open_images_to_single_layer( + bpy.ops.wm.y_open_images_to_single_layer( files = [ {"name":"blender_color.png"}, {"name":"blender_metallic.png"}, @@ -91,7 +91,7 @@ class TestNewMask(TestOpenImageAsLayer): def test_02_new_mask(self): # New layer mask - bpy.ops.node.y_new_layer_mask(name='Image Mask', uv_name='UVMap', color_option='WHITE') + bpy.ops.wm.y_new_layer_mask(name='Image Mask', uv_name='UVMap', color_option='WHITE') layer = get_active_layer() self.assertTrue(len(layer.masks) == 1) @@ -121,7 +121,7 @@ class TestDuplicateLayer(TestNewLayer): def test_02_duplicate_layer(self): # Duplicate Layer - bpy.ops.node.y_duplicate_layer() + bpy.ops.wm.y_duplicate_layer() yp = get_yp() self.assertTrue(len(yp.layers) == 2) diff --git a/textures_lib/blender_assets.cats.txt b/textures_lib/blender_assets.cats.txt new file mode 100644 index 00000000..92519530 --- /dev/null +++ b/textures_lib/blender_assets.cats.txt @@ -0,0 +1,9 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Empty lines and lines starting with `#` will be ignored. +# The first non-ignored line should be the version indicator. +# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name" + +VERSION 1 + +89d938da-d2cf-420d-b837-c396e2f5f825:Materials:Materials diff --git a/textures_lib/create_blend.py b/textures_lib/create_blend.py new file mode 100644 index 00000000..4279115c --- /dev/null +++ b/textures_lib/create_blend.py @@ -0,0 +1,180 @@ +import sys, bpy, os +from mathutils import * + + +argv = sys.argv +argv = argv[argv.index("--") + 1 :] # get all args after "--" + +arg_dict = {} + +for i, content in enumerate(argv): + print(i, " >> ",argv[i]) + + +# parse arguments +for i in range(0, len(argv), 2): + key = argv[i] + key = key.replace("--", "") + + arg_dict[key] = argv[i + 1] + +print(arg_dict) + + +dir_target = arg_dict["target"] #os.path.join(arg_dict["target"], arg_dict["id"]) +# dir_source = arg_dict["source"] +tags = arg_dict["tags"].split(";") + +# check dir exist or make dir +if not os.path.exists(dir_target): + os.makedirs(dir_target) + +cat_id = arg_dict["category_id"] + +# create blend file +bpy.ops.wm.read_factory_settings(use_empty=True) + + +# save to blend file +target_path = os.path.join(dir_target, arg_dict["id"]+".blend") +print("target path: ", target_path) +bpy.ops.wm.save_as_mainfile(filepath=target_path) + + +# Load image textures and create materials +image_paths = [] +for f in os.listdir(dir_target): + if f.endswith(".blend"): + continue + image_paths.append(f) +print("image paths: ", image_paths) + +# materials = [] + +# print current directory +print("current directory: ", os.getcwd()) +# change active directory +os.chdir(dir_target) +print("current directory next: ", os.getcwd()) + +new_material = bpy.data.materials.new(name=arg_dict["id"]+"_"+arg_dict["attribute"]) +# Create material +new_material.use_nodes = True +bsdf = new_material.node_tree.nodes["Principled BSDF"] + +output = [n for n in new_material.node_tree.nodes if n.type == 'OUTPUT_MATERIAL' and n.is_active_output] +if output: output = output[0] + +normal_dx = None +normal_gl = None + +base_pos = Vector((0, 0)) + +base_pos.x = bsdf.location.x - 700 +base_pos.y = bsdf.location.y + +base_pos_extra = Vector((0, 0)) +base_pos_extra.x = bsdf.location.x - 300 +base_pos_extra.y = bsdf.location.y - 300 + +id = arg_dict["id"].lower() + +for image_path in image_paths: + + print("load image: ", image_path) + # Load image + image = bpy.data.images.load("//"+image_path) + + # Create texture + base_name = os.path.basename(image_path) + + texture = bpy.data.textures.new(name=base_name, type='IMAGE') + texture.image = image + + check_name = base_name.lower() + # remove id from name + if id in check_name: + check_name = check_name.replace(id, "") + + update_loc = True + tex_image = None + if "color" in check_name: + tex_image = new_material.node_tree.nodes.new('ShaderNodeTexImage') + tex_image.image = image + new_material.node_tree.links.new(bsdf.inputs['Base Color'], tex_image.outputs['Color']) + elif "normal" in check_name: + if "dx" in check_name: + normal_dx = image + if "gl" in check_name: + normal_gl = image + update_loc = False + elif "metal" in check_name: + tex_image = new_material.node_tree.nodes.new('ShaderNodeTexImage') + tex_image.image = image + new_material.node_tree.links.new(bsdf.inputs['Metallic'], tex_image.outputs['Color']) + elif "rough" in check_name: + tex_image = new_material.node_tree.nodes.new('ShaderNodeTexImage') + tex_image.image = image + new_material.node_tree.links.new(bsdf.inputs['Roughness'], tex_image.outputs['Color']) + elif "disp" in check_name: + tex_image = new_material.node_tree.nodes.new('ShaderNodeTexImage') + tex_image.image = image + + displacement_node = new_material.node_tree.nodes.new('ShaderNodeDisplacement') + new_material.node_tree.links.new(displacement_node.inputs['Height'], tex_image.outputs['Color']) + new_material.node_tree.links.new(output.inputs['Displacement'], displacement_node.outputs['Displacement']) + + displacement_node.location = base_pos_extra + base_pos_extra.y -= 300 + else: + update_loc = False + + # Set location + if tex_image != None and update_loc: + tex_image.location = base_pos + base_pos.y -= 300 + +if normal_gl != None or normal_dx != None: + tex_image = new_material.node_tree.nodes.new('ShaderNodeTexImage') + normal_node = new_material.node_tree.nodes.new('ShaderNodeNormalMap') + + if normal_gl != None: + tex_image.image = normal_gl + new_material.node_tree.links.new(normal_node.inputs['Color'], tex_image.outputs['Color']) + elif normal_dx != None: + tex_image.image = normal_dx + + new_material.node_tree.links.new(normal_node.inputs['Color'], tex_image.outputs['Color']) + new_material.node_tree.links.new(bsdf.inputs['Normal'], normal_node.outputs['Normal']) + + normal_node.location = base_pos_extra + tex_image.location = base_pos + +new_material.asset_mark() +if cat_id: + new_material.asset_data.catalog_id = cat_id + +if tags: + for t in tags: + new_material.asset_data.tags.new(t, skip_if_exists=True) +new_material.asset_data.description = "Material by ambientcg.com" + +override = bpy.context.copy() +override["id"] = new_material + +thumbnail_file = os.path.join(dir_target, arg_dict["id"]+".png") +print("thumbfile: ", thumbnail_file) + +with bpy.context.temp_override(**override): + bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbnail_file) + +bpy.ops.mesh.primitive_plane_add(size=2) +obj = bpy.context.active_object +obj.name = arg_dict["id"] +if obj.data.materials: + obj.data.materials[0] = new_material +else: + obj.data.materials.append(new_material) + +bpy.context.preferences.filepaths.save_version = 0 # Avoid .blend1 +bpy.ops.wm.save_mainfile(relative_remap=True) diff --git a/textures_lib/data.py b/textures_lib/data.py new file mode 100644 index 00000000..ef7da1a1 --- /dev/null +++ b/textures_lib/data.py @@ -0,0 +1,116 @@ +import json +from typing import Any + +class SourceType: + SOURCE_AMBIENTCG = "ambientcg" + SOURCE_POLYHAVEN = "polyhaven" + +class AssetItem: + def __init__(self): + self.id = "" + self.name = "" + self.thumbnail = "" + self.tags:list[str] = [] + self.source_type = "" + + self.attributes:dict[str, AssetAttribute] = {} + + def add_attribute(self, attribute:str, link:str, file_name:str, size:int) -> 'AssetAttribute': + new_attribute = AssetAttribute() + new_attribute.attribute = attribute + new_attribute.asset = AssetDownload(link, file_name, size) + + self.attributes[attribute] = new_attribute + return new_attribute + + def to_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "name": self.name, + "thumbnail": self.thumbnail, + "source_type": self.source_type, + "tags": self.tags, + "attributes": {k: v.to_dict() for k, v in self.attributes.items()} + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> 'AssetItem': + instance = cls() + instance.id = data.get("id", "") + instance.name = data.get("name", "") + instance.thumbnail = data.get("thumbnail", "") + instance.source_type = data.get("source_type", "") + instance.tags = data.get("tags", []) + instance.attributes = {k: AssetAttribute.from_dict(v) for k, v in data.get("attributes", {}).items()} + + return instance + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> 'AssetItem': + data = json.loads(json_str) + return cls.from_dict(data) + +class AssetAttribute: + def __init__(self): + self.attribute = "" + self.asset:AssetDownload = None + self.textures:list[AssetDownload] = [] # texture files for + + def add_texture(self, texture:str, file_name:str, size:int) -> None: + self.textures.append(AssetDownload(texture, file_name, size)) + + def to_dict(self) -> dict[str, Any]: + return { + # "attribute": self.attribute, + # "asset": self.asset.to_dict() if self.asset else None, + # "textures": [t.to_dict() for t in self.textures] + "attribute": self.attribute, + "asset": self.asset.to_dict(), + "textures": [t.to_dict() for t in self.textures] + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> 'AssetAttribute': + instance = cls() + instance.attribute = data.get("attribute", "") + instance.asset = AssetDownload.from_dict(data.get("asset", {})) + instance.textures = [AssetDownload.from_dict(t) for t in data.get("textures", [])] + return instance + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> 'AssetAttribute': + data = json.loads(json_str) + return cls.from_dict(data) + +class AssetDownload: + def __init__(self, link:str, file_name:str, size:int): + self.size = size + self.link = link + self.file_name = file_name + + def to_dict(self) -> dict[str, Any]: + return { + "size": self.size, + "link": self.link, + "file_name": self.file_name + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> 'AssetDownload': + instance = cls(data.get("link", ""), data.get("file_name", ""), data.get("size", 0)) + return instance + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> 'AssetDownload': + data = json.loads(json_str) + return cls.from_dict(data) + \ No newline at end of file diff --git a/textures_lib/downloader.py b/textures_lib/downloader.py new file mode 100644 index 00000000..3896ecc3 --- /dev/null +++ b/textures_lib/downloader.py @@ -0,0 +1,536 @@ +import bpy, threading, os, json, zipfile, requests + +from ..preferences import * + +THREAD_SEARCHING = "thread_searching" +threads:dict[str, threading.Thread] = {} # progress:int, + +def get_thread_id(asset_id:str, asset_attribute:str, texture_index:int = -1): + if texture_index >= 0: + return asset_id+"_"+asset_attribute+"_"+str(texture_index) + else: + return asset_id+"_"+asset_attribute + +def get_thread(id:str) -> threading.Thread: + if id in threads: + return threads[id] + return None + +def get_searching_thread() -> threading.Thread: + if THREAD_SEARCHING in threads: + return threads[THREAD_SEARCHING] + return None + +def set_searching_thread(thread:threading.Thread): + threads[THREAD_SEARCHING] = thread + +def download_stream(links:list[str], file_names:list[str], thread_id:str, + timeout:int = 10,skipExisting:bool = False): + thread = get_thread(thread_id) + file_num = len(file_names) + # percent_per_file = 100/file_num + + prog_total = 0 + for idx, file_name in enumerate(file_names): + link = links[idx] + prog = 0 + with open(file_name, "wb") as f: + try: + response = requests.get(link, stream=True, timeout = timeout) + total_length = response.headers.get('content-length') + print("total size = "+total_length) + if not total_length: + print('Error #1 while downloading', link, ':', "Empty Response.") + return + + dl = 0 + total_length = int(total_length) + # TODO a way for calculating the chunk size + for data in response.iter_content(chunk_size = 4096): + if thread is not None and thread.cancel: + response.close() + break + + dl += len(data) + f.write(data) + + prog = int((100 * dl) / (total_length * file_num)) + # dl_threads[0].progress = prog + thread.progress = prog_total + prog + # print("proggg "+str(prog)+"% "+str(dl)+"/"+str(total_length)) + # dl_threads.pop(0) + except Exception as e: + print('Error #2 while downloading', link, ':', e) + prog_total += prog + +def monitor_downloads(): + + searching = False + if THREAD_SEARCHING in threads: + thread_search = threads[THREAD_SEARCHING] + searching = True + + if not hasattr(bpy, 'context'): + print("no context") + return 2 + + wm = bpy.context.window_manager + + from .properties import TexLibProps + + txlb:TexLibProps = wm.ytexlib + downloads = txlb.downloads + + if len(downloads) == 0 and not searching: + # print("KOSONG") + return 2 + + if searching: + prog_search = thread_search.progress + txlb.searching_download.progress = prog_search + if thread_search.progress >= 100: + txlb.searching_download.alive = False + + if not thread_search.is_alive(): + del threads[THREAD_SEARCHING] + + # print("downloadku", len(downloads)) + # for index, dwn in enumerate(downloads): + # print("cek aja",dwn.asset_id," | ",dwn.asset_attribute) + + if len(downloads): + to_remove = [] + dwn:DownloadQueue + for index, dwn in enumerate(downloads): + # if dwn.source_type == SourceType.SOURCE_POLYHAVEN_TEXTURE: + # txt_idx = dwn.texture_index + # thread_id = get_thread_id(dwn.asset_id, dwn.asset_attribute, txt_idx) + # else: + thread_id = get_thread_id(dwn.asset_id, dwn.asset_attribute) + thread = get_thread(thread_id) + if thread == None: + print("thread id", thread_id, ">",dwn.asset_id,">", dwn.asset_attribute) + if dwn.source_type == SourceType.SOURCE_AMBIENTCG: + extract_file(dwn.file_path) + delete_zip(dwn.file_path) + dir_file = os.path.dirname(dwn.file_path) + convert_ambientcg_asset(dir_file, dwn.asset_id, dwn.asset_attribute, dwn.asset_cat_id, dwn.tags) + else: + print("finishhh ", dwn.asset_id, " | ", dwn.asset_attribute) + dir_file = os.path.dirname(dwn.file_path) + # up + # dir_file = os.path.dirname(dir_file) + mark_polyhaven_asset(dir_file, dwn.asset_id, dwn.asset_attribute, dwn.asset_cat_id, dwn.tags) + to_remove.append(index) + else: + prog = thread.progress + + dwn.progress = prog + if thread.progress >= 100: + dwn.alive = False + + if not thread.is_alive(): + del threads[thread_id] + + for i in to_remove: + downloads.remove(i) + + for window in bpy.context.window_manager.windows: + # print("==============windoss ", window) + for area in window.screen.areas: + if area.type == 'VIEW_3D': + for reg in area.regions: + # print("region",reg.type, "width", reg.width) + open_tab = reg.width > 1 + if reg.type == "UI" and open_tab: + # print("redraw area", area.type, "reg", reg.type) + reg.tag_redraw() + return 0.1 + + return 1.0 + +from .data import AssetItem +def download_asset_previews(context, overwrite_existing:bool, search_results:dict[str, AssetItem], search_items): + from .properties import get_preview_dir, load_per_material + + directory = get_preview_dir(context) + + if not os.path.exists(directory): + os.mkdir(directory) + + thread_search = threads[THREAD_SEARCHING] + + progress_initial = thread_search.progress + progress_max = 90 + span = progress_max - progress_initial + item_count = len(search_results) + + for index,ast in enumerate(search_results.keys()): + if thread_search.cancel: + break + + link = search_results[ast].thumbnail + file_name = bpy.path.basename(link) + # remove parameters + file_name = file_name.split("?")[0] + file_name = os.path.join(directory, file_name) + + if not overwrite_existing and os.path.exists(file_name): + continue + + with open(file_name, "wb") as f: + try: + print("download "+link+" to "+file_name) + response = requests.get(link, stream=True) + total_length = response.headers.get('content-length') + if not total_length: + print('Error #1 while downloading', link, ':', "Empty Response.") + return + dl = 0 + total_length = int(total_length) + for data in response.iter_content(chunk_size = 4096): + if thread_search is not None and thread_search.cancel: + response.close() + return + dl += len(data) + f.write(data) + except Exception as e: + print('Error #2 while downloading', link, ':', e) + prog = (index + 1) / item_count + # print("url = ",prog, ' | ',link,' | ', file_name) + # refresh list + load_per_material(file_name, search_items[index]) + + thread_search.progress = (int) (progress_initial + prog * span) + +def download_previews(context, overwrite_existing:bool, material_items): + from .properties import get_preview_dir, load_per_material + from .properties import last_search + + directory = get_preview_dir(context) + + if not os.path.exists(directory): + os.mkdir(directory) + thread_search = threads[THREAD_SEARCHING] + + + progress_initial = thread_search.progress + progress_max = 90 + span = progress_max - progress_initial + item_count = len(last_search) + + for index,ast in enumerate(last_search): + if thread_search.cancel: + break + link = last_search[ast]["preview"] + file_name = bpy.path.basename(link) + file_name = os.path.join(directory, file_name) + + if not overwrite_existing and os.path.exists(file_name): + continue + with open(file_name, "wb") as f: + try: + response = requests.get(link, stream=True) + total_length = response.headers.get('content-length') + if not total_length: + print('Error #1 while downloading', link, ':', "Empty Response.") + return + dl = 0 + total_length = int(total_length) + for data in response.iter_content(chunk_size = 4096): + if thread_search is not None and thread_search.cancel: + response.close() + return + dl += len(data) + f.write(data) + except Exception as e: + print('Error #2 while downloading', link, ':', e) + prog = (index + 1) / item_count + # print("url = ",prog, ' | ',link,' | ', file_name) + # refresh list + load_per_material(file_name, material_items[index]) + + thread_search.progress = (int) (progress_initial + prog * span) + +from .data import AssetItem, SourceType +def retrieve_ambientcg(keyword:str = '', page:int = 0, limit:int = 20) -> dict[str, AssetItem]: + base_link = "https://ambientCG.com/api/v2/full_json" + params = { + 'type': 'Material', + 'include':'imageData,downloadData,tagData,displayData', + 'limit': str(limit), + 'offset': str(limit * page) + } + + if keyword != '': + params['q'] = keyword + + agent = get_addon_name() + '/' + get_current_version_str() + print("user-agent ", agent) + # add user-agent + req_headers = { + 'User-Agent': agent + } + max_try = 3 + for _ in range(max_try): + try: + response = requests.get(base_link, params=params, verify=False, headers=req_headers, timeout=10) + break + except requests.exceptions.ReadTimeout: + continue + + if not response.status_code == 200: + print("Can't download, Code: " + str(response.status_code)) + return None + + assets = response.json()["foundAssets"] + + print("Found ",len(assets), "textures") + + retval:dict[str, AssetItem] = {} + + for asst in assets: + # print(json.dumps(asst, indent=4)) + asset_id = asst["assetId"] + thumbnail = asst["previewImage"]["256-PNG"] + + zip_assets = asst["downloadFolders"]["default"]["downloadFiletypeCategories"]["zip"]["downloads"] + + new_item = AssetItem() + new_item.id = asset_id + new_item.name = asst["displayName"] + new_item.thumbnail = thumbnail + new_item.source_type = SourceType.SOURCE_AMBIENTCG + new_item.tags = asst["tags"] + + for k in zip_assets: + attr = k["attribute"] + new_item.add_attribute(attr, k["downloadLink"], k["fileName"], k["size"]) + retval[new_item.id] = new_item + + return retval + +def retrieve_polyhaven_asset(id:str, asset_name:str, thumb_url:str, tags:list[str])->AssetItem: + base_link = "https://api.polyhaven.com/files/"+id + agent = get_addon_name() + '/' + get_current_version_str() + print("user-agent ", agent) + # add user-agent + req_headers = { + 'User-Agent': agent + } + max_try = 3 + + for _ in range(max_try): + try: + response = requests.get(base_link, verify=False, headers=req_headers, timeout=10) + break + except requests.exceptions.ReadTimeout: + continue + + if not response.status_code == 200: + print("Can't download, Code: " + str(response.status_code)) + return None + + retval = AssetItem() + retval.source_type = SourceType.SOURCE_POLYHAVEN + retval.id = id + retval.name = asset_name + retval.thumbnail = thumb_url + retval.tags = tags + + obj = response.json() + blend_obj = obj["blend"] + for k in blend_obj.keys(): + blend_attr = blend_obj[k]["blend"] + blnd_url = blend_attr["url"] + includes = blend_attr["include"] + file_name = blnd_url.split("/")[-1] + + new_attr = retval.add_attribute(k, blnd_url, file_name, blend_attr["size"]) + # print("k ", k, " content ", includes) + for ic in includes.keys(): + inc_itm = includes[ic] + url = inc_itm["url"] + # get file name from url + new_attr.add_texture(url, ic, inc_itm["size"]) + + return retval + +def retrieve_polyhaven(keyword:str = '', page:int = 0, limit:int = 20) -> dict[str, AssetItem]: + + base_link = "https://api.polyhaven.com/assets" + params = { + 'type': 'textures', + # 'limit': str(limit), + # 'offset': str(limit * page) + } + + if keyword != '': + params['c'] = keyword + + thread_search = get_searching_thread() + + print("retrieve_polyhaven", base_link, params) + + agent = get_addon_name() + '/' + get_current_version_str() + print("user-agent ", agent) + # add user-agent + req_headers = { + 'User-Agent': agent + } + + response = requests.get(base_link, params=params, verify=False, headers=req_headers, timeout=10) + if not response.status_code == 200: + print("Can't download, Code: " + str(response.status_code)) + return None + thread_search.progress = 35 + + progress_initial = thread_search.progress + progress_max = 60 + span = progress_max - progress_initial + + retval:dict[str, AssetItem] = {} + + obj_assets = response.json() + # print("response ", json.dumps(obj_assets)) + + item_count = len(obj_assets.keys()) + + for i, id in enumerate(obj_assets.keys()): + # print("index ", i, "id ", id) + if i >= limit: + break + + it = obj_assets[id] + # print("index ", i, "id ", id, "name ", it["name"], "thumb ", it["thumbnail_url"]) + new_item = retrieve_polyhaven_asset(id, it["name"], it["thumbnail_url"], it["tags"]) + retval[new_item.id] = new_item + + prog = (i + 1) / item_count + + thread_search.progress = (int) (progress_initial + prog * span) + + return retval + +from .properties import get_textures_dir +def texture_exist(context, asset_id:str, attribute:str) -> bool: + location = os.path.join(get_textures_dir(context), asset_id, attribute) + if os.path.exists(location): + files = os.listdir(location) + for f in files: + if asset_id in f: + return True + + return False + +def find_blend(context, asset_id:str, attribute:str) -> str: + location = os.path.join(get_textures_dir(context), asset_id, attribute) + if os.path.exists(location): + files = os.listdir(location) + for f in files: + if asset_id in f and f.endswith(".blend"): + full_path = os.path.join(location, f) + return full_path + return None + +def extract_file(my_file:str) -> bool: + dir_name = os.path.dirname(my_file) + # new_folder = os.path.basename(my_file).split('.')[0] + # dir_name = os.path.join(dir_name, new_folder) + print("extract "+my_file+" to "+dir_name) + + try: + with zipfile.ZipFile(my_file, 'r') as zObject: + zObject.extractall(path=dir_name) + return True + except zipfile.BadZipFile: + return False + +# Delete the zip file +def delete_zip(file_path): + if not os.path.exists(file_path): + return + try: + os.remove(file_path) + except Exception as e: + print('Error while deleting zip file:', e) + +def convert_ambientcg_asset(ambient_dir:str, id:str, attribute:str, cat_id:str, tags:str): + import subprocess + print("current directory: ", os.getcwd()) + print("data: ", ambient_dir, " | ", id) + addon_dir = get_addon_dir() + os.chdir(addon_dir) + print("current directory next: ", os.getcwd()) + + subprocess.call( + [ + bpy.app.binary_path, + "--background", + "--factory-startup", + "--python", + os.path.join(addon_dir, "create_blend.py"), + "--", + "--target", + ambient_dir, + "--id", + id, + "--attribute", + attribute, + "--category_id", + cat_id, + "--tags", + tags + ] + ) + +def mark_polyhaven_asset(dir:str, id:str, attribute:str, cat_id:str, tags:str): + import subprocess + + file_blend = "" + for i in os.listdir(dir): + # get blend file + if i.endswith(".blend"): + file_blend = os.path.join(dir, i) + break + print("file ", i) + print("current directory: ", os.getcwd()) + print("data: ", dir, " | ", id, " | ", file_blend) + addon_dir = get_addon_dir() + os.chdir(addon_dir) + print("current directory next: ", os.getcwd()) + + if file_blend == "": + print("failed") + return + + subprocess.call( + [ + bpy.app.binary_path, + "--background", + "--factory-startup", + file_blend, + "--python", + os.path.join(addon_dir, "mark_blend.py"), + "--", + "--id", + id, + "--attribute", + attribute, + "--category_id", + cat_id, + "--tags", + tags + ] + ) + +def get_addon_dir() -> str: + return os.path.dirname(os.path.realpath(__file__)) + + +def register(): + bpy.app.timers.register(monitor_downloads, first_interval=1, persistent=True) + +def unregister(): + if bpy.app.timers.is_registered(monitor_downloads): + bpy.app.timers.unregister(monitor_downloads) \ No newline at end of file diff --git a/textures_lib/mark_blend.py b/textures_lib/mark_blend.py new file mode 100644 index 00000000..1e2e40e7 --- /dev/null +++ b/textures_lib/mark_blend.py @@ -0,0 +1,60 @@ +import sys, bpy, os + +argv = sys.argv +argv = argv[argv.index("--") + 1 :] # get all args after "--" + +arg_dict = {} + +for i, content in enumerate(argv): + print(i, " >> ",argv[i]) + + +# parse arguments +for i in range(0, len(argv), 2): + key = argv[i] + key = key.replace("--", "") + + arg_dict[key] = argv[i + 1] + +print(arg_dict) + +id = arg_dict["id"] +cat_id = arg_dict["category_id"] + +# file path +blend_file = bpy.data.filepath +blend_dir = os.path.dirname(blend_file) +os.chdir(blend_dir) + +try: + asset = bpy.data.materials[id] +except KeyError as e: + if len(bpy.data.materials) == 1: + asset = bpy.data.materials[0] + else: + raise e + +asset.name = id + "_" + arg_dict["attribute"] +asset.asset_mark() + +if cat_id: + asset.asset_data.catalog_id = cat_id + +tags = arg_dict["tags"].split(";") + +if tags: + for t in tags: + asset.asset_data.tags.new(t, skip_if_exists=True) +asset.asset_data.description = "Material by polyhaven.com" + +override = bpy.context.copy() +override["id"] = asset + +thumbnail_file = os.path.join(blend_dir, arg_dict["id"]+".png") +print("thumbfile: ", thumbnail_file) + +with bpy.context.temp_override(**override): + bpy.ops.ed.lib_id_load_custom_preview(filepath=thumbnail_file) + +bpy.context.preferences.filepaths.save_version = 0 # Avoid .blend1 +bpy.ops.wm.save_mainfile(relative_remap=True) diff --git a/textures_lib/operators.py b/textures_lib/operators.py new file mode 100644 index 00000000..61a7d867 --- /dev/null +++ b/textures_lib/operators.py @@ -0,0 +1,384 @@ + +import bpy, threading, os, shutil + +from bpy.types import Operator +from bpy.props import StringProperty, IntProperty, BoolProperty + +from .. import Layer +from ..common import * + +from .downloader import download_stream, get_thread_id, get_thread, get_addon_dir +from .downloader import threads + +from .properties import assets_library, TexLibProps, DownloadQueue, get_textures_dir, cancel_searching, get_cat_asset_lib, get_preview_dir, retrieve_asset_library, get_os_config_dir, get_library_name + +class TexLibAddToUcupaint(Operator, Layer.BaseMultipleImagesLayer): + """Open Multiple Textures to Layer Ucupaint""" + + bl_label = "Add to Ucupaint" + bl_idname = "texlib.add_to_ucupaint" + + attribute:StringProperty() + id:StringProperty() + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() + + def invoke(self, context, event): + self.invoke_operator(context) + return context.window_manager.invoke_props_dialog(self, width=320) + + def check(self, context): + return self.check_operator(context) + + def draw(self, context): + self.draw_operator(context) + + def execute(self, context): + directory = os.path.join(get_textures_dir(context), self.id, self.attribute) + import_list = os.listdir(directory) + + if not self.open_images_to_single_layer(context, directory, import_list): + return {'CANCELLED'} + + return {'FINISHED'} + +class TexLibCancelDownload(Operator): + """Cancel downloading textures""" + + bl_label = "" + bl_idname = "texlib.cancel" + attribute:StringProperty() + id:StringProperty() + + def execute(self, context:bpy.context): + thread_id = get_thread_id(self.id, self.attribute) + thread = get_thread(thread_id) + + if thread == None: + return {'CANCELLED'} + thread.cancel = True + + texlib:TexLibProps = context.window_manager.ytexlib + dwn:DownloadQueue + for dwn in texlib.downloads: + if dwn.asset_id == self.id and dwn.asset_attribute == self.attribute: + dwn.alive = False + return {'FINISHED'} + + return {'CANCELLED'} + +class TexLibRemoveTextureAttribute(Operator): + """Remove existing textures""" + + bl_label = "Remove Textures" + bl_idname = "texlib.remove_attribute" + attribute:StringProperty() + id:StringProperty() + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.label(text="Are you sure to remove this texture?") + + def execute(self, context:bpy.context): + dir_up = os.path.join(get_textures_dir(context), self.id) + dir = os.path.join(dir_up, self.attribute) + # print("item", self.id," | attr", self.attribute, " | file ", dir) + print("remove dir: ", dir) + print("remove parent: ", dir_up) + # remove folder + if os.path.exists(dir): + for root, dirs, files in os.walk(dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(dir) + + # remove parent folder if empty + if not os.listdir(dir_up): + os.rmdir(dir_up) + my_list = context.window_manager.ytexlib.downloaded_material_items + my_list.remove(my_list.find(self.id)) + + retrieve_asset_library(context) + + return {'FINISHED'} + return {'CANCELLED'} + +class TexLibDownload(Operator): + """Download textures from source""" + + bl_label = "" + bl_idname = "texlib.download" + + attribute:StringProperty() + path_download:StringProperty() + id:StringProperty() + file_exist:BoolProperty(default=False) + + def invoke(self, context, event): + if self.file_exist: + return context.window_manager.invoke_props_dialog(self) + return self.execute(context) + + def draw(self, context:bpy.context): + layout = self.layout + + layout.label(text="Already downloaded. Overwrite?", icon="QUESTION") + + def execute(self, context): + asset_item = assets_library[self.id] + attribute_item = asset_item.attributes[self.attribute] + link = attribute_item.asset.link + directory = os.path.join( get_textures_dir(context), self.id, self.attribute) + file_name = os.path.join(directory, attribute_item.asset.file_name) + + asset_cats_file = get_cat_asset_lib(context) + print("asset cat file: ", asset_cats_file) + has_cat_id = False + + if os.path.exists(asset_cats_file): + cat_id = get_cat_id(asset_cats_file) + has_cat_id = cat_id != "" # replace existing with template cat + + if not has_cat_id: + template_asst = os.path.join(get_addon_dir(), "blender_assets.cats.txt") + shutil.copy(template_asst, asset_cats_file) + + cat_id = get_cat_id(asset_cats_file) + print("cat id: ", cat_id) + + if not os.path.exists(directory): + # print("make dir "+directory) + os.makedirs(directory) + + prev_dir = get_preview_dir(context) + thumb_file = os.path.join(prev_dir, self.id+".png") + # copy preview file + if os.path.exists(thumb_file): + dest_thumb = os.path.join(directory, self.id+".png") + print("copy file ", thumb_file, " to ", dest_thumb) + shutil.copyfile(thumb_file, dest_thumb) + + links = [link] + file_names = [file_name] + + for idx, attr in enumerate(attribute_item.textures): + txt_file = os.path.join(directory, attr.file_name) + txr_dir = os.path.dirname(txt_file) + if not os.path.exists(txr_dir): + os.makedirs(txr_dir) + + links.append(attr.link) + file_names.append(txt_file) + + thread_id = get_thread_id(self.id, self.attribute) + new_thread = threading.Thread(target=download_stream, args=(links,file_names,thread_id,)) + new_thread.progress = 0 + new_thread.cancel = False + threads[thread_id] = new_thread + + new_thread.start() + + texlib:TexLibProps = context.window_manager.ytexlib + new_dwn:DownloadQueue = texlib.downloads.add() + new_dwn.asset_cat_id = cat_id + if len(asset_item.tags) > 0: + new_dwn.tags = ";".join(asset_item.tags) + else: + new_dwn.tags = "" + + new_dwn.asset_id = self.id + new_dwn.file_path = file_name + new_dwn.source_type = asset_item.source_type + new_dwn.asset_attribute = self.attribute + new_dwn.alive = True + new_dwn.file_size = attribute_item.asset.size + new_dwn.progress = 0 + + return {'FINISHED'} + +class TexLibCancelSearch(Operator): + bl_idname = "texlib.cancel_search" + bl_label = "" + + def execute(self, context): + cancel_searching(context) + return{'FINISHED'} + +class TexLibRemoveTextureAllAttributes(Operator): + bl_idname = "texlib.remove_attributes" + bl_label = "" + id:StringProperty() + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.label(text="Are you sure to remove this textures?") + + def execute(self, context): + dir = get_textures_dir(context) + self.id + print("item", self.id, " | file ", dir) + my_list = context.window_manager.ytexlib.downloaded_material_items + my_list.remove(my_list.find(self.id)) + # remove folder + if os.path.exists(dir): + for root, dirs, files in os.walk(dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(dir) + return {'FINISHED'} + + return {'CANCELLED'} + +class ShowFilePathPreference(Operator): + bl_idname = "texlib.show_pref" + bl_label = "Show Preference" + + def execute(self, context): + bpy.ops.screen.userpref_show('INVOKE_DEFAULT') + bpy.context.preferences.active_section = 'FILE_PATHS' + return{'FINISHED'} + +class CreateAssetDirectory(Operator): + bl_idname = "texlib.create_dir" + bl_label = "Setup Asset Directory" + + def execute(self, context): + print("create asset directory "+get_os_config_dir()) + asset_library_path = get_os_config_dir() + # Create the directory if it doesn't exist + if not os.path.exists(asset_library_path): + os.makedirs(asset_library_path) + print(f"Created directory: {asset_library_path}") + else: + print(f"Directory already exists: {asset_library_path}") + + # Add a new Asset Library + bpy.ops.preferences.asset_library_add(directory=asset_library_path) + context.preferences.filepaths.asset_libraries[-1].name = get_library_name() + + print(f"Added Asset Library: {asset_library_path}") + + return{'FINISHED'} + + +class ShowLibrary(Operator): + bl_idname = "texlib.show_lib" + bl_label = "Show Asset Browser" + + area_type = "FILE_BROWSER" + area_ui_type = "ASSETS" + + @classmethod + def poll(cls, context): + existing_lib = False + for area in context.screen.areas: + if area.type == ShowLibrary.area_type and area.ui_type == ShowLibrary.area_ui_type: # 'VIEW_3D', 'CONSOLE', 'INFO' etc. + existing_lib = True + break + return not existing_lib + + def execute(self, context): + existing_lib = False + for area in context.screen.areas: + if area.type == self.area_type and area.ui_type == self.area_ui_type: # 'VIEW_3D', 'CONSOLE', 'INFO' etc. + existing_lib = True + break + + # for area in context.screen.areas: + # print(area.type, " >> ", area.ui_type) + if not existing_lib: + # with context.temp_override(area=area): + bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.3) + # Get the new area + new_area = bpy.context.screen.areas[-1] + + # Change the type of the new area to FILE_BROWSER + new_area.type = self.area_type + new_area.ui_type = self.area_ui_type + + return{'FINISHED'} + +class DebugOp(Operator): + bl_idname = "texlib.debug" + bl_label = "Check Debug" + + def execute(self, context): + lib_dir = get_cat_asset_lib(context) + + content = get_cat_id(lib_dir) + print("lib dir: ", lib_dir, "=", content) + + retrieve_asset_library(context) + return{'FINISHED'} + +def get_cat_id(file_path:str, category:str = "Materials") -> str: + retval = "" + print("read file: ", file_path) + + # read file + with open(file_path, "r") as file: + lines = file.readlines() + for line in lines: + # print(line) + line = line.strip() + if not line: + continue # Empty lines + if line.startswith("#"): + continue + if line.startswith("VERSION"): + continue + + parts = line.split(":") + uuid, path = parts[:2] + crumbs = path.split("/") + + retval = uuid # In case the asset is not in any categories, revert to top level type catalog + + if len(crumbs) == 1: + # Ignore top level type catalog from here on + continue + + # Match catalog only if asset has all categories in its tree + match = True + for cat in crumbs[1:]: + if cat.lower() != category.lower(): + match = False + + if not match: + continue + + return uuid + + return retval + +classes = [ + TexLibAddToUcupaint, + TexLibCancelDownload, + TexLibRemoveTextureAttribute, + TexLibDownload, + TexLibCancelSearch, + TexLibRemoveTextureAllAttributes, + ShowFilePathPreference, + CreateAssetDirectory, + ShowLibrary, + DebugOp +] + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/textures_lib/properties.py b/textures_lib/properties.py new file mode 100644 index 00000000..4aff8f8b --- /dev/null +++ b/textures_lib/properties.py @@ -0,0 +1,357 @@ +import bpy, threading, os, json, pathlib + +from bpy.props import StringProperty, IntProperty, BoolProperty, CollectionProperty, PointerProperty +from bpy.types import PropertyGroup, Context, Scene + +from ..preferences import * +from .. import lib + +from .downloader import get_searching_thread, set_searching_thread, retrieve_ambientcg, retrieve_polyhaven, download_asset_previews +from .data import AssetItem + +assets_library:dict[str, AssetItem] = {} +last_search = {} + + +def load_per_material(file_name:str, material_item): + item = os.path.basename(file_name) + + my_id = item.split(".")[0] + # print(">>item",item,"file",file_name, "my_id", my_id) + loaded = previews_collection.load(item, file_name, 'IMAGE', force_reload=True) + + previews_collection.preview_items[my_id] = (my_id, item, "", loaded.icon_id, len(previews_collection.preview_items)) + # material_item.thumb = loaded.icon_id + + +def cancel_searching(context): + + thread_search = get_searching_thread() + if thread_search != None: + thread_search.cancel = True + + texlib:TexLibProps = context.window_manager.ytexlib + + searching_dwn = texlib.searching_download + searching_dwn.alive = False + +def get_textures_dir(context) -> str: + file_path = get_lib_dir(context) + retval = os.path.join(file_path, "textures") + os.sep + if not os.path.exists(retval): + os.mkdir(retval) + return retval + +def get_preview_dir(context) -> str: + file_path = get_lib_dir(context) + retval = os.path.join(file_path, "previews") + os.sep + if not os.path.exists(retval): + os.mkdir(retval) + return retval + +def get_library_name(): + from ..common import get_addon_title + + return get_addon_title()+" Assets" + +def get_asset_lib(context) -> bpy.types.UserAssetLibrary: + for l in context.preferences.filepaths.asset_libraries: + if l.name == get_library_name(): + return l + return None + +def get_os_config_dir() -> str: + import platform + + # Get the operating system + os_name = platform.system() + + print("build_platform", os_name) + + if os_name == 'Linux': + default_dir = os.path.expanduser("~/.config") + elif os_name == 'Darwin': + default_dir = os.path.expanduser("~/Library/Application Support") + else: + default_dir = os.path.expanduser("~\\AppData\\Roaming") + + return os.path.join(default_dir, get_addon_title(), "Assets") + +def get_cat_asset_lib(context) -> str: + asset_lib = get_asset_lib(context) + if asset_lib == None: + return None + + retval = os.path.join(asset_lib.path, "blender_assets.cats.txt") + + return retval + +def get_lib_dir(context) -> str: + # ypup:YPaintPreferences = get_user_preferences() + asset_lib = get_asset_lib(context) + if asset_lib == None: + return None + + retval = asset_lib.path + + if not os.path.exists(retval): + os.mkdir(retval) + return retval + +def read_asset_info(context) -> bool: + dir_name = get_lib_dir(context) + if dir_name == None: + return False + + file_name = os.path.join(dir_name, "assets.json") + + if os.path.exists(file_name): + file = open(file_name, 'r') + content = file.read() + jsn = json.loads(content) + # new_dict = {k: v.to_dict() for k, v in jsn} + for key in jsn: + assets_library[key] = AssetItem.from_dict(jsn[key]) + # if assets_library[key].source_type == "polyhaven": + # print("\n>>>>>>",jsn[key]) + # assets_library.update(new_dict) + file.close() + return True + return False + +def retrieve_asset_library(context:Context): + asset_path = get_lib_dir(context) + if asset_path == None: + return None + + txlib:TexLibProps = context.window_manager.ytexlib + + print("asset_lib=", asset_path) + txlib.library_items.clear() + + library_path = pathlib.Path(asset_path) + blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] + for blend_file in blend_files: + with bpy.data.libraries.load(str(blend_file), assets_only=True) as (data_from, data_to): + for mat in data_from.materials: + new_item:MaterialItem = txlib.library_items.add() + new_item.name = mat + new_item.source_type = "" + new_item.full_path = str(blend_file) + + nama = mat + # split string and remove last index + split_name = nama.split("_")[:-1] + # combine array string into one string + new_id = "_".join(split_name) + new_item.asset_id = new_id + +def searching_material(context:Context, keyword:str, search_ambientcg:bool = True, search_polyhaven:bool = True): + + txlib:TexLibProps = context.window_manager.ytexlib + + thread_search = get_searching_thread() + + if not len(assets_library): + read_asset_info(context) + + thread_search.progress = 10 + + search_results:dict[str, AssetItem] = {} + if search_ambientcg: + search_results = retrieve_ambientcg(keyword) + thread_search.progress = 30 + + if search_polyhaven: + list_polyhaven = retrieve_polyhaven(keyword) + search_results.update(list_polyhaven) + thread_search.progress = 60 + + assets_library.update(search_results) + + save_library_to_file(context, search_results, "last-search.json") + thread_search.progress = 65 + + save_library_to_file(context, assets_library, "assets.json") + + thread_search.progress = 70 + + txlib.search_items.clear() + for key in search_results: + new_item:MaterialItem = txlib.search_items.add() + asst_item = search_results[key] + new_item.name = asst_item.name + new_item.asset_id = asst_item.id + new_item.source_type = asst_item.source_type + + download_asset_previews(context, False, search_results, txlib.search_items) + + read_asset_info(context) + + load_previews(context) + thread_search.progress = 100 + + +def save_library_to_file(context, list:dict[str, AssetItem], file_name:str): + dir_name = get_lib_dir(context) + file_name = os.path.join(dir_name, file_name) + + file = open(file_name, 'w') + + to_write = {} + for key in list: + to_write[key] = list[key].to_dict() + + file.write(json.dumps(to_write)) + file.close() + print("stored to ", file_name) + + +def load_previews(context): + print(">>>>>>>>>>>>>>>>>>>>>>> INIT TexLIB") + dir_name = get_preview_dir(context) + files = os.listdir(dir_name) + + previews_collection.clear() + preview_items = {} + for index, item in enumerate(files): + file = dir_name + item + my_id = item.split(".")[0] + sizefile = os.path.getsize(file) + if sizefile == 0: # detect corrupt image + os.remove(file) + continue + loaded = previews_collection.load(item, file, 'IMAGE', force_reload=True) + # print(">>item",item,"file",file,"size", sizefile, "id", loaded.icon_id) + + preview_items[my_id] = (my_id, item, "", loaded.icon_id, index) + + previews_collection.preview_items.update(preview_items) + + + +def update_input_search(self, context): + + # if self.input_last == self.input_search: + # print("no search:"+self.input_search) + # return + + self.input_last = self.input_search + + txlib:TexLibProps = context.window_manager.ytexlib + txlib.material_items.clear() + txlib.search_items.clear() + + if self.input_search == '': + last_search.clear() + if get_searching_thread() != None: + cancel_searching(context) + return + + # cancel previous search + if get_searching_thread() != None: + cancel_searching(context) + + thread_search = threading.Thread(target=searching_material, args=(context, self.input_search, self.check_ambiencg, self.check_polyhaven)) + thread_search.progress = 0 + thread_search.cancel = False + set_searching_thread(thread_search) + + thread_search.start() + + self.searching_download.progress = 0 + self.searching_download.alive = True + +def change_mode_asset(self, context): + + retrieve_asset_library(context) + + +class TextureItem (PropertyGroup): + texture_id: StringProperty(name="Texture ID") + location: StringProperty(name="Location", subtype="FILE_PATH") + thumbnail: StringProperty(name="Thumbnail", subtype="FILE_PATH") + blend_file: StringProperty(name="Blend File", subtype="FILE_PATH") + +class MaterialItem(PropertyGroup): + asset_id: StringProperty(name="Asset ID") + name: StringProperty( name="Name", description="Material name", default="Untitled") + source_type: StringProperty( name="Source Type", description="Source type", default="") + full_path: StringProperty( name="Full Path", description="Full path to the asset", default="") + # thumb: IntProperty( name="thumbnail", description="", default=0) + +class DownloadQueue(PropertyGroup): + asset_id : StringProperty() + asset_attribute: StringProperty() + asset_cat_id: StringProperty() + tags: StringProperty() + texture_index: IntProperty(default=-1) + file_path : StringProperty() + source_type: StringProperty() + alive : BoolProperty(default = False) + file_size:IntProperty() + progress : IntProperty( + default = 0, + min = 0, + max = 100, + description = 'Progress of the download', + subtype = 'PERCENTAGE' + ) + +class TexLibProps(PropertyGroup): + page: IntProperty(name="page", default= 0) + input_search:StringProperty(name="Search", update=update_input_search) + check_local:BoolProperty(name="Local", default=True) + check_ambiencg:BoolProperty(name="AmbientCG", default=True) + check_polyhaven:BoolProperty(name="Polyhaven", default=True) + mode_asset:EnumProperty( + items = (('SEARCH', 'Search', ''), ('LIBRARY', 'Library', '')), + name = 'Location', + default = 'SEARCH', + # description = 'Location of the PBR Texture.\n' + # ' Local: the assets that you have already downloaded.\n' + # ' Online: available for download on AmbientCG.com.\n', + update=change_mode_asset + ) + + input_last:StringProperty() + material_items:CollectionProperty(type= MaterialItem) + material_index:IntProperty(default=0, name="Material index") + downloaded_material_items:CollectionProperty(type= MaterialItem) + downloaded_material_index:IntProperty(default=0, name="Material index") + + + search_items:CollectionProperty(type= MaterialItem) + search_index:IntProperty(default=0, name="Search index") + + library_items:CollectionProperty(type= MaterialItem) + library_index:IntProperty(default=0, name="Search index") + + downloads:CollectionProperty(type=DownloadQueue) + searching_download:PointerProperty(type=DownloadQueue) + selected_download_item:IntProperty(default=0) + +classes = [ + TextureItem, MaterialItem, DownloadQueue, TexLibProps +] + +def register(): + for cl in classes: + bpy.utils.register_class(cl) + + bpy.types.WindowManager.ytexlib = PointerProperty(type= TexLibProps) + + global previews_collection + previews_collection = bpy.utils.previews.new() + previews_collection.preview_items = {} + + if read_asset_info(bpy.context): + load_previews(bpy.context) + + +def unregister(): + for cl in classes: + bpy.utils.unregister_class(cl) + + del bpy.types.WindowManager.ytexlib + bpy.utils.previews.remove(previews_collection) \ No newline at end of file diff --git a/textures_lib/ui.py b/textures_lib/ui.py new file mode 100644 index 00000000..421fe241 --- /dev/null +++ b/textures_lib/ui.py @@ -0,0 +1,255 @@ +import bpy + +from bpy.types import Panel, UIList + +from .. import lib + +from .properties import assets_library +from .properties import TexLibProps, MaterialItem, DownloadQueue, get_asset_lib, get_library_name + +from .operators import TexLibAddToUcupaint, TexLibCancelDownload, TexLibDownload, TexLibRemoveTextureAttribute + +from .downloader import texture_exist, get_thread, get_thread_id, find_blend + +from ..common import is_online, is_bl_newer_than + +class TexLibBrowser(Panel): + bl_label = "Texture Libraries" + bl_idname = "TEXLIB_PT_Browser" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Ucupaint" + + @classmethod + def poll(cls, context): + return is_online() and is_bl_newer_than(3) + + def draw(self, context): + layout = self.layout + texlib:TexLibProps = context.window_manager.ytexlib + + ass_lib = get_asset_lib(context) + if ass_lib == None: + # layout.label(text="Create an asset library named "+get_library_name()) + row = layout.row() + row.alert = True + row.operator("texlib.create_dir", icon="ERROR") + return + + layout.prop(texlib, "mode_asset", expand=True) + + search_mode = texlib.mode_asset == "SEARCH" + + if search_mode: + self.draw_search(context, texlib) + else: + self.draw_library(context, texlib) + + def draw_search(self, context, texlib:TexLibProps): + layout = self.layout + + sel_index = texlib.search_index + my_list = texlib.search_items + + # layout.operator("texlib.debug") + + layout.prop(texlib, "input_search") + source_search = layout.row() + # source_search.prop(texlib, "check_local") + source_search.prop(texlib, "check_ambiencg") + source_search.prop(texlib, "check_polyhaven") + + searching_dwn = texlib.searching_download + + if searching_dwn.alive: + prog = searching_dwn.progress + + if prog >= 0: + row_search = layout.row() + + if prog < 10: + row_search.label(text="Searching...") + else: + row_search.prop(searching_dwn, "progress", slider=True, text="Retrieving thumbnails.") + row_search.operator("texlib.cancel_search", icon="CANCEL") + + # print("list", local_files_mode, ":",sel_index,"/",len(my_list)) + # print("list", local_files_mode, ":",texlib.material_index,"/",len(texlib.material_items)," | ", texlib.downloaded_material_index,"/",len(texlib.downloaded_material_items)) + if not len(my_list) and texlib.input_search != '' and not searching_dwn.alive: + layout.label(text='No results for \''+texlib.input_search+'\'!', icon='ERROR') + elif len(my_list): + + #layout.separator() + layout.label(text=str(len(my_list))+" resuts for '"+texlib.input_search+"':") + col_lay = layout.row() + + col_lay.template_list("TEXLIB_UL_Material", "material_list", texlib, "search_items", texlib, "search_index") + + if sel_index < len(my_list): + sel_mat:MaterialItem = my_list[sel_index] + mat_id:str = sel_mat.asset_id + + thumb = _get_asset_preview(mat_id) + + + layout.separator() + layout.label(text="Preview:") + prev_box = layout.box() + selected_mat = prev_box.column(align=True) + selected_mat.alignment = "CENTER" + selected_mat.template_icon(icon_value=thumb, scale=5.0) + selected_mat.label(text=sel_mat.name) + + # print("len ", len(assets_library.keys())) + download = assets_library[mat_id] + + layout.separator() + layout.label(text="Attributes:") + for d in download.attributes: + attribute = download.attributes[d] + current_asset = attribute.asset + # row.alignment = "LEFT" + total_size = current_asset.size + for t in attribute.textures: + total_size += t.size + + ukuran = round(total_size / 1000000,2) + + ui_attr = layout.split(factor=0.7) + + row = ui_attr.row() + + row.label(text=d, ) + # rr.label(text=d, ) + row.label(text=str(ukuran)+ "MB") + + thread_id = get_thread_id(mat_id, d) + dwn_thread = get_thread(thread_id) + + btn_row = ui_attr.row() + btn_row.alignment = "RIGHT" + + if dwn_thread != None: + btn_row.label(text=str(dwn_thread.progress)+"%") + op:TexLibCancelDownload = btn_row.operator("texlib.cancel", icon="X") + op.attribute = d + op.id = mat_id + else: + check_exist:bool = texture_exist(context, mat_id, d) + if check_exist: + op = btn_row.operator("wm.y_open_images_from_material_to_single_layer", icon="ADD") + blend_path = find_blend(context, mat_id, d) + if blend_path: + op.asset_library_path = blend_path + + with bpy.data.libraries.load(str(blend_path), assets_only=True) as (data_from, data_to): + for mat in data_from.materials: + op.mat_name = mat + break + + op_remove:TexLibRemoveTextureAttribute = btn_row.operator("texlib.remove_attribute", icon="REMOVE") + op_remove.attribute = d + op_remove.id = sel_mat.asset_id + + op:TexLibDownload = btn_row.operator("texlib.download", icon="IMPORT") + op.id = sel_mat.asset_id + op.attribute = d + op.file_exist = check_exist + + if len(texlib.downloads): + layout.separator() + layout.label(text="Downloads:") + layout.template_list("TEXLIB_UL_Downloads", "download_list", texlib, "downloads", texlib, "selected_download_item") + + def draw_library(self, context, texlib:TexLibProps): + sel_index = texlib.library_index + my_list = texlib.library_items + layout = self.layout + + if sel_index >= len(my_list): + sel_index = len(my_list) - 1 + + selected:MaterialItem = my_list[sel_index] + + layout.template_list("TEXLIB_UL_Material", "material_list", texlib, "library_items", texlib, "library_index") + layout.operator("texlib.show_lib") + + layout.separator() + layout.label(text="Preview:") + mat_name = selected.name + prev_box = layout.box() + selected_mat = prev_box.column(align=True) + selected_mat.alignment = "CENTER" + thumb = _get_asset_preview(selected.asset_id) + selected_mat.template_icon(icon_value=thumb, scale=5.0) + selected_mat.label(text=mat_name) + + split_names = mat_name.split("_") + if len(split_names) > 1: + attr = split_names[-1] + + btns = layout.row() + + op = btns.operator("wm.y_open_images_from_material_to_single_layer", icon="ADD", text="Add to Ucupaint") + op.mat_name = selected.name + op.asset_library_path = selected.full_path + + remove:TexLibRemoveTextureAttribute = btns.operator("texlib.remove_attribute", icon="REMOVE") + remove.attribute = attr + remove.id = selected.asset_id + +class TEXLIB_UL_Downloads(UIList): + + def draw_item(self, context, layout, data, item:DownloadQueue, icon, active_data, active_propname, index): + """Demo UIList.""" + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row = layout.row(align=True) + if item.alive: + row.prop(item, "progress", slider=True, text=item.asset_id+" | "+item.asset_attribute) + op:TexLibCancelDownload = row.operator("texlib.cancel", icon="X") + op.attribute = item.asset_attribute + op.id = item.asset_id + else: + row.label(text="cancelling") + +class TEXLIB_UL_Material(UIList): + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + """Demo UIList.""" + + thumb = _get_asset_preview(item.asset_id) + + row = layout.row(align=True) + if self.layout_type in {'DEFAULT', 'COMPACT'}: + row.template_icon(icon_value = thumb, scale = 1.0) + row.label(text=item.name) + if item.source_type != "": + row.label(text=item.source_type) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text=item.name, icon_value = thumb) + + +classes = [ + TexLibBrowser, + TEXLIB_UL_Material, + TEXLIB_UL_Downloads +] + +def _get_asset_preview(item_id:str): + from .properties import previews_collection + + if hasattr(previews_collection, "preview_items") and item_id in previews_collection.preview_items: + thumb = previews_collection.preview_items[item_id][3] + else: + thumb = lib.custom_icons["input"].icon_id + return thumb + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/transition.py b/transition.py index 9e9d0e20..845fbc20 100644 --- a/transition.py +++ b/transition.py @@ -127,7 +127,7 @@ def show_transition(self, context, ttype): class YShowTransitionBump(bpy.types.Operator): """Use transition bump (This will affect other channels)""" - bl_idname = "node.y_show_transition_bump" + bl_idname = "wm.y_show_transition_bump" bl_label = "Show Transition Bump" bl_options = {'REGISTER', 'UNDO'} @@ -140,7 +140,7 @@ def execute(self, context): class YShowTransitionRamp(bpy.types.Operator): """Use transition ramp (Works best if there's transition bump enabled on other channel)""" - bl_idname = "node.y_show_transition_ramp" + bl_idname = "wm.y_show_transition_ramp" bl_label = "Show Transition Ramp" bl_options = {'REGISTER', 'UNDO'} @@ -153,7 +153,7 @@ def execute(self, context): class YShowTransitionAO(bpy.types.Operator): """Use transition AO (Only works if there's transition bump enabled on other channel)""" - bl_idname = "node.y_show_transition_ao" + bl_idname = "wm.y_show_transition_ao" bl_label = "Show Transition AO" bl_options = {'REGISTER', 'UNDO'} @@ -166,7 +166,7 @@ def execute(self, context): class YHideTransitionEffect(bpy.types.Operator): """Remove transition Effect""" - bl_idname = "node.y_hide_transition_effect" + bl_idname = "wm.y_hide_transition_effect" bl_label = "Hide Transition Effect" bl_options = {'REGISTER', 'UNDO'} diff --git a/transition_common.py b/transition_common.py index 8c3ce2e3..3b58c81c 100644 --- a/transition_common.py +++ b/transition_common.py @@ -177,12 +177,12 @@ def set_transition_ramp_nodes(tree, layer, ch): ((root_ch.type == 'RGB' and root_ch.enable_alpha) or layer.parent_idx != -1)): tr_ramp_blend = replace_new_node( tree, ch, 'tr_ramp_blend', 'ShaderNodeGroup', 'Transition Ramp Blend', - lib.RAMP_FLIP_STRAIGHT_OVER_BLEND + lib.RAMP_FLIP_STRAIGHT_OVER_BLEND, hard_replace=True ) else: tr_ramp_blend, dirty = replace_new_node( tree, ch, 'tr_ramp_blend', 'ShaderNodeGroup', 'Transition Ramp Blend', - lib.RAMP_FLIP_BLEND, return_status=True + lib.RAMP_FLIP_BLEND, return_status=True, hard_replace=True ) if dirty: duplicate_lib_node_tree(tr_ramp_blend) diff --git a/ui.py b/ui.py index 281cc503..a58936e4 100644 --- a/ui.py +++ b/ui.py @@ -1,10 +1,11 @@ -import bpy, re, time, os +import bpy, re, time, os, sys from bpy.props import * from bpy.app.handlers import persistent from bpy.app.translations import pgettext_iface -from . import lib, Modifier, MaskModifier, UDIM, ListItem +from . import lib, Modifier, MaskModifier, UDIM, ListItem, Decal from .common import * + RGBA_CHANNEL_PREFIX = { 'ALPHA' : 'alpha_', 'R' : 'r_', @@ -40,6 +41,7 @@ def update_yp_ui(): ypui.bake_target_idx = yp.active_bake_target_index ypui.need_update = False ypui.halt_prop_update = True + ypui.channels.clear() if len(yp.bake_targets) > 0: bt = yp.bake_targets[yp.active_bake_target_index] @@ -63,6 +65,11 @@ def update_yp_ui(): ypui.channel_ui.expand_smooth_bump_settings = channel.expand_smooth_bump_settings ypui.channel_ui.modifiers.clear() + # Construct noncontextual channel UI objects + for i, ch in enumerate(yp.channels): + c = ypui.channels.add() + c.expand_baked_data = ch.expand_baked_data + # Construct channel UI objects for i, mod in enumerate(channel.modifiers): m = ypui.channel_ui.modifiers.add() @@ -173,17 +180,22 @@ def draw_bake_info(bake_info, layout, entity): layout.label(text='List of Objects:') box = layout.box() bcol = box.column() + bcol.context_pointer_set('bake_info', bi) if num_oos > 0: for oo in bi.other_objects: if is_bl_newer_than(2,79) and not oo.object: continue brow = bcol.row() brow.context_pointer_set('other_object', oo) - brow.context_pointer_set('bake_info', bi) if is_bl_newer_than(2, 79): brow.label(text=oo.object.name, icon_value=lib.get_icon('object_index')) else: brow.label(text=oo.object_name, icon_value=lib.get_icon('object_index')) - brow.operator('node.y_remove_bake_info_other_object', text='', icon_value=lib.get_icon('close')) + brow.operator('wm.y_remove_bake_info_other_object', text='', icon_value=lib.get_icon('close')) + + if is_bl_newer_than(2, 79): + bbcol = bcol.column(align=True) + bbcol.operator('wm.y_select_all_other_objects', text='Select All', icon='RESTRICT_SELECT_OFF') + bbcol.operator('wm.y_toggle_other_objects_visibility', text='Toggle Hide', icon='RESTRICT_VIEW_OFF') else: brow = bcol.row() brow.label(text='No source objects found!', icon='ERROR') @@ -199,13 +211,42 @@ def draw_bake_info(bake_info, layout, entity): layout.context_pointer_set('bake_info', bi) if bi.bake_type == 'SELECTED_VERTICES': - c = layout.operator("node.y_try_to_select_baked_vertex", text='Try to Reselect Vertices', icon='GROUP_VERTEX') - c = layout.operator("node.y_bake_to_layer", text='Rebake ' + bake_type_labels[bi.bake_type], icon_value=lib.get_icon('bake')) + c = layout.operator("wm.y_try_to_select_baked_vertex", text='Try to Reselect Vertices', icon='GROUP_VERTEX') + c = layout.operator("wm.y_bake_to_layer", text='Rebake ' + bake_type_labels[bi.bake_type], icon_value=lib.get_icon('bake')) c.type = bi.bake_type if m1 or m3: c.target_type = 'LAYER' else: c.target_type = 'MASK' c.overwrite_current = True +class NODE_MT_copy_image_path_menu(bpy.types.Menu): + bl_label = "Copy Image Path Options" + bl_idname = "NODE_MT_copy_image_path_menu" + bl_description = get_addon_title() + " Options for copying the image path or opening the containing folder" + + def draw(self, context): + layout = self.layout + image = context.image + + full_path = os.path.normpath(image.filepath or "") + op = layout.operator("wm.copy_image_path_to_clipboard", text="Copy Image Filepath", icon="COPYDOWN") + op.clipboard_text = full_path + + # Add more branches below for different operating systems + if sys.platform in {'win32', 'darwin', 'linux'}: + + if sys.platform == 'win32': + browser_name = 'Explorer' + elif sys.platform == 'darwin': + browser_name = 'Finder' + else: browser_name = 'File Manager' + + op = layout.operator("wm.open_containing_image_folder", text="Open Image in "+browser_name, icon="FILE_FOLDER") + op.file_path = image.filepath + else: + folder_path = os.path.normpath(os.path.dirname(full_path)) if full_path else "" + op = layout.operator("wm.copy_image_path_to_clipboard", text="Copy Containing Folder Path") + op.clipboard_text = folder_path + def draw_image_props(context, source, layout, entity=None, show_flip_y=False, show_datablock=True, show_source_input=False): image = source.image @@ -217,7 +258,7 @@ def draw_image_props(context, source, layout, entity=None, show_flip_y=False, sh split.label(text='Input:') split.prop(entity, 'source_input', text='') - unlink_op = 'node.y_remove_layer' + unlink_op = 'wm.y_remove_layer' if entity: yp = entity.id_data.yp m1 = re.match(r'^yp\.layers\[(\d+)\]\.masks\[(\d+)\]$', entity.path_from_id()) @@ -226,17 +267,19 @@ def draw_image_props(context, source, layout, entity=None, show_flip_y=False, sh layer = yp.layers[int(m1.group(1))] col.context_pointer_set('layer', layer) col.context_pointer_set('mask', entity) - unlink_op = 'node.y_remove_layer_mask' + unlink_op = 'wm.y_remove_layer_mask' elif m2: layer = yp.layers[int(m2.group(1))] col.context_pointer_set('layer', layer) col.context_pointer_set('channel', entity) if show_flip_y: - unlink_op = 'node.y_remove_channel_override_1_source' - else: unlink_op = 'node.y_remove_channel_override_source' + unlink_op = 'wm.y_remove_channel_override_1_source' + else: unlink_op = 'wm.y_remove_channel_override_source' - if image.y_bake_info.is_baked and not image.y_bake_info.is_baked_channel: - bi = image.y_bake_info + bi = image.y_bake_info + if (bi.is_baked and not bi.is_baked_channel and + (not bi.is_baked_entity or bi.baked_entity_type in {'EDGE_DETECT', 'AO'}) # NOTE: Some baked type can come from entity + ): #if image.yia.is_image_atlas or image.yua.is_udim_atlas: # col.label(text=image.name + ' (Baked)', icon_value=lib.get_icon('image')) #elif show_datablock: col.template_ID(source, "image", unlink=unlink_op) @@ -304,7 +347,11 @@ def draw_image_props(context, source, layout, entity=None, show_flip_y=False, sh if not image.filepath: col.label(text='Image Path: -') else: - col.label(text='Path: ' + image.filepath) + # Create a row with two parts: one label and one dropdown button. + row = col.row(align=True) + row.label(text="Path: " + os.path.normpath(image.filepath)) + row.context_pointer_set('image', image) + row.menu("NODE_MT_copy_image_path_menu", text="", icon='DOWNARROW_HLT') image_format = 'RGBA' image_bit = int(image.depth / 4) @@ -642,6 +689,34 @@ def draw_edge_detect_props(layer, source, layout): row.label(text='Radius:') draw_input_prop(row, layer, 'edge_detect_radius') + row = col.row() + row.label(text='Use Previous Normal:') + row.prop(layer, 'hemi_use_prev_normal', text='') + +def draw_ao_props(layer, source, layout): + col = layout.column() + + row = col.row() + row.label(text='Distance:') + draw_input_prop(row, layer, 'ao_distance') + + # NOTE: AO samples is a bit irrelevant + #row = col.row() + #row.label(text='Samples:') + #row.prop(source, 'samples', text='') + + row = col.row() + row.label(text='Inside:') + row.prop(source, 'inside', text='') + + row = col.row() + row.label(text='Only Local (Cycles Only):') + row.prop(source, 'only_local', text='') + + row = col.row() + row.label(text='Use Previous Normal:') + row.prop(layer, 'hemi_use_prev_normal', text='') + def draw_inbetween_modifier_mask_props(layer, source, layout): col = layout.column() if layer.modifier_type == 'CURVE': @@ -701,7 +776,7 @@ def draw_mask_modifier_stack(layer, mask, layout, ui): box.active = m.enable MaskModifier.draw_modifier_properties(tree, m, box) -def draw_modifier_stack(context, parent, channel_type, layout, ui, layer=None, extra_blank=False, use_modifier_1=False, layout_active=True): +def draw_modifier_stack(context, parent, channel_type, layout, ui, layer=None, extra_blank=False, use_modifier_1=False, layout_active=True, is_root_ch=False): ypui = context.window_manager.ypui @@ -811,7 +886,7 @@ def draw_modifier_stack(context, parent, channel_type, layout, ui, layer=None, e row.label(text='', icon='BLANK1') box = row.box() box.active = m.enable - Modifier.draw_modifier_properties(bpy.context, channel_type, mod_tree.nodes, m, box, False) + Modifier.draw_modifier_properties(bpy.context, channel_type, mod_tree.nodes, m, parent, box, is_root_ch=is_root_ch) #row.label(text='', icon='BLANK1') @@ -890,11 +965,13 @@ def draw_bake_targets_ui(context, layout, node): #rcol.context_pointer_set('node', node) if is_bl_newer_than(2, 80): - rcol.operator("node.y_new_bake_target", icon='ADD', text='') - rcol.operator("node.y_remove_bake_target", icon='REMOVE', text='') + rcol.operator("wm.y_new_bake_target", icon='ADD', text='') + rcol.operator("wm.y_remove_bake_target", icon='REMOVE', text='') else: - rcol.operator("node.y_new_bake_target", icon='ZOOMIN', text='') - rcol.operator("node.y_remove_bake_target", icon='ZOOMOUT', text='') + rcol.operator("wm.y_new_bake_target", icon='ZOOMIN', text='') + rcol.operator("wm.y_remove_bake_target", icon='ZOOMOUT', text='') + + rcol.menu("NODE_MT_y_bake_list_special_menu", text='', icon='DOWNARROW_HLT') if len(yp.bake_targets) > 0: bt = yp.bake_targets[yp.active_bake_target_index] @@ -983,15 +1060,15 @@ def draw_root_channels_ui(context, layout, node): if is_bl_newer_than(2, 80): rcol.menu("NODE_MT_y_new_channel_menu", text='', icon='ADD') - #rcol.operator_menu_enum("node.y_add_new_ypaint_channel", 'type', icon='ADD', text='') - rcol.operator("node.y_remove_ypaint_channel", icon='REMOVE', text='') + #rcol.operator_menu_enum("wm.y_add_new_ypaint_channel", 'type', icon='ADD', text='') + rcol.operator("wm.y_remove_ypaint_channel", icon='REMOVE', text='') else: rcol.menu("NODE_MT_y_new_channel_menu", text='', icon='ZOOMIN') - #rcol.operator_menu_enum("node.y_add_new_ypaint_channel", 'type', icon='ZOOMIN', text='') - rcol.operator("node.y_remove_ypaint_channel", icon='ZOOMOUT', text='') + #rcol.operator_menu_enum("wm.y_add_new_ypaint_channel", 'type', icon='ZOOMIN', text='') + rcol.operator("wm.y_remove_ypaint_channel", icon='ZOOMOUT', text='') - rcol.operator("node.y_move_ypaint_channel", text='', icon='TRIA_UP').direction = 'UP' - rcol.operator("node.y_move_ypaint_channel", text='', icon='TRIA_DOWN').direction = 'DOWN' + rcol.operator("wm.y_move_ypaint_channel", text='', icon='TRIA_UP').direction = 'UP' + rcol.operator("wm.y_move_ypaint_channel", text='', icon='TRIA_DOWN').direction = 'DOWN' if len(yp.channels) > 0: @@ -1015,22 +1092,22 @@ def draw_root_channels_ui(context, layout, node): if is_height_input_unconnected_but_has_start_process(node, root_normal_ch): row = mcol.row(align=True) row.alert = True - row.operator('node.y_optimize_normal_process', icon='ERROR', text='Fix Height Process') + row.operator('wm.y_optimize_normal_process', icon='ERROR', text='Fix Height Process') elif is_height_input_connected_but_has_no_start_process(node, root_normal_ch): row = mcol.row(align=True) row.alert = True - row.operator('node.y_optimize_normal_process', icon='ERROR', text='Fix Height Input') + row.operator('wm.y_optimize_normal_process', icon='ERROR', text='Fix Height Input') if is_output_unconnected(node, output_index, channel): row = mcol.row(align=True) row.alert = True - row.operator('node.y_connect_ypaint_channel', icon='ERROR', text='Fix Unconnected Channel Output') + row.operator('wm.y_connect_ypaint_channel', icon='ERROR', text='Fix Unconnected Channel Output') # Fix for alpha channel missing connection elif channel.type == 'RGB' and channel.enable_alpha and is_output_unconnected(node, output_index + 1, channel): row = mcol.row(align=True) row.alert = True - row.operator('node.y_connect_ypaint_channel_alpha', icon='ERROR', text='Fix Unconnected Alpha Output') + row.operator('wm.y_connect_ypaint_channel_alpha', icon='ERROR', text='Fix Unconnected Alpha Output') row = mcol.row(align=True) @@ -1072,7 +1149,7 @@ def draw_root_channels_ui(context, layout, node): baked = nodes.get(channel.baked) layout_active = not yp.use_baked or not baked - draw_modifier_stack(context, channel, channel.type, bcol, chui, layout_active=layout_active) + draw_modifier_stack(context, channel, channel.type, bcol, chui, layout_active=layout_active, is_root_ch=True) inp = node.inputs[channel.io_index] @@ -1189,7 +1266,7 @@ def draw_root_channels_ui(context, layout, node): # brow.label(text='', icon='BLANK1') if channel.type == 'NORMAL': - if ypup.show_experimental or channel.enable_smooth_bump or not is_bl_newer_than(2, 80): + if ypup.show_experimental or channel.enable_smooth_bump: # or not is_bl_newer_than(2, 80): brow = bcol.row(align=True) if is_bl_newer_than(2, 80): @@ -1372,9 +1449,10 @@ def draw_root_channels_ui(context, layout, node): brow = bcol.row(align=True) vcols = get_vertex_colors(context.object) - if yp.use_baked and channel.bake_to_vcol_name in vcols: - label_text = 'Use Baked Vertex Color:' - else: label_text = 'Bake To Vertex Color:' + #if yp.use_baked and channel.bake_to_vcol_name in vcols: + # label_text = 'Use Baked Vertex Color:' + #else: + label_text = 'Bake To Vertex Color:' rrow = brow.row(align=True) inbox_dropdown_button(rrow, chui, 'expand_bake_to_vcol_settings', label_text, scale_override=0.95) @@ -1413,48 +1491,29 @@ def draw_layer_source(context, layout, layer, layer_tree, source, image, vcol, i label = '' #label += pgettext_iface('Layer') + ': ' if image: - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_image') - #else: icon_value = lib.get_icon('collapsed_image') icon_value = lib.get_icon('image') if image.yia.is_image_atlas or image.yua.is_udim_atlas: label += layer.name else: label += image.name elif vcol: - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_vertex_color') - #else: icon_value = lib.get_icon('collapsed_vertex_color') icon_value = lib.get_icon('vertex_color') label += vcol.name elif layer.type == 'BACKGROUND': - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_background') - #else: icon_value = lib.get_icon('collapsed_background') icon_value = lib.get_icon('background') label += layer.name elif layer.type == 'COLOR': - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_color') - #else: icon_value = lib.get_icon('collapsed_color') icon_value = lib.get_icon('color') label += layer.name elif layer.type == 'GROUP': - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_group') - #else: icon_value = lib.get_icon('collapsed_group') icon_value = lib.get_icon('group') label += layer.name elif layer.type == 'HEMI': - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_hemi') - #else: icon_value = lib.get_icon('collapsed_hemi') icon_value = lib.get_icon('hemi') label += layer.name + elif layer.type in {'EDGE_DETECT', 'AO'}: + icon_value = lib.get_icon('edge_detect') + label += layer.name else: - title = source.bl_idname.replace('ShaderNodeTex', '') - #if lui.expand_content: - # icon_value = lib.get_icon('uncollapsed_texture') - #else: icon_value = lib.get_icon('collapsed_texture') icon_value = lib.get_icon('texture') label += layer.name @@ -1468,10 +1527,6 @@ def draw_layer_source(context, layout, layer, layer_tree, source, image, vcol, i row.context_pointer_set('layer', layer) row.context_pointer_set('layer_ui', lui) - if layer.use_temp_bake: - row = row.row(align=True) - row.operator('node.y_disable_temp_image', icon='FILE_REFRESH', text='Disable Baked Temp') - if layer.type not in {'GROUP', 'PREFERENCES'}: #icon = 'PREFERENCES' if is_bl_newer_than(2, 80) else 'SCRIPTWIN' icon = 'MODIFIER_ON' if is_bl_newer_than(2, 80) else 'MODIFIER' @@ -1503,6 +1558,8 @@ def draw_layer_source(context, layout, layer, layer_tree, source, image, vcol, i suffix = 'color' elif layer.type == 'HEMI': suffix = 'hemi' + elif layer.type in {'EDGE_DETECT', 'AO'}: + suffix = 'edge_detect' elif layer.type == 'VCOL': suffix = 'vertex_color' else: suffix = 'texture' @@ -1537,6 +1594,8 @@ def draw_layer_source(context, layout, layer, layer_tree, source, image, vcol, i icon_value = lib.get_icon('group') elif layer.type == 'HEMI': icon_value = lib.get_icon('hemi') + elif layer.type in {'EDGE_DETECT', 'AO'}: + icon_value = lib.get_icon('edge_detect') else: icon_value = lib.get_icon('texture') #if layer.type == 'COLOR' and not lui.expand_source: @@ -1552,26 +1611,54 @@ def draw_layer_source(context, layout, layer, layer_tree, source, image, vcol, i #bbox = row.box() rrcol = row.column() - if layer.use_temp_bake: - rrcol.context_pointer_set('parent', layer) - rrcol.operator('node.y_disable_temp_image', icon='FILE_REFRESH', text='Disable Baked Temp') - elif image: - draw_image_props(context, source, rrcol, layer, show_flip_y=True, show_datablock=False) + ccol = rrcol.column() + ccol.active = not layer.use_baked + + if image: + draw_image_props(context, source, ccol, layer, show_flip_y=True, show_datablock=False) # NOTE: Divide rgb by alpha is mostly useless for image layer, # so it's hidden under experimental feature unless the user ever enabled it before if hasattr(layer, 'divide_rgb_by_alpha') and (layer.divide_rgb_by_alpha or ypup.show_experimental): - rrrow = rrcol.row(align=True) + rrrow = ccol.row(align=True) rrrow.label(text='Divide RGB by Alpha:') rrrow.prop(layer, 'divide_rgb_by_alpha', text='') elif layer.type == 'COLOR': - draw_solid_color_props(layer, source, rrcol) + draw_solid_color_props(layer, source, ccol) elif layer.type == 'VCOL': - draw_vcol_props(rrcol, vcol, layer) + draw_vcol_props(ccol, vcol, layer) elif layer.type == 'HEMI': - draw_hemi_props(layer, source, rrcol) - else: draw_tex_props(source, rrcol, entity=layer) + draw_hemi_props(layer, source, ccol) + elif layer.type == 'EDGE_DETECT': + draw_edge_detect_props(layer, source, ccol) + elif layer.type == 'AO': + draw_ao_props(layer, source, ccol) + else: draw_tex_props(source, ccol, entity=layer) + + if layer.baked_source == '' and layer.type in {'EDGE_DETECT', 'HEMI', 'AO'}: + rrrow = rrcol.row(align=True) + rrrow.operator("wm.y_bake_entity_to_image", text='Bake '+mask_type_labels[layer.type]+' as Image', icon_value=lib.get_icon('bake')) + + elif layer.baked_source != '': + + baked_source = layer_tree.nodes.get(layer.baked_source) + if baked_source and baked_source.image: + brow = rrcol.row(align=True) + brow.active = layer.use_baked + brow.label(text='Baked: ') + crow = brow.row(align=True) + crow.alignment = 'RIGHT' + crow.label(text=baked_source.image.name, icon='IMAGE_DATA') + + rrcol.context_pointer_set('entity', layer) + rrcol.context_pointer_set('layer', layer) + brow = rrcol.row(align=True) + brow.operator("wm.y_bake_entity_to_image", text='Rebake', icon_value=lib.get_icon('bake')) + brow.prop(layer, 'use_baked', text='Use Baked', toggle=True) + + icon = 'TRASH' if is_bl_newer_than(2, 80) else 'X' + brow.operator("wm.y_remove_baked_entity", text='', icon=icon) layout.separator() @@ -1584,8 +1671,12 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i scene = context.scene # Vector - if is_layer_using_vector(layer): - row = layout.row(align=False) + if is_layer_using_vector(layer, exclude_baked=True): + + col = layout.column() + col.active = not layer.use_baked + + row = col.row(align=False) icon_value = lib.get_icon('uv') rrow = row.row(align=True) @@ -1628,7 +1719,7 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i # rrow.menu("NODE_MT_y_uv_special_menu", icon=icon, text='') if lui.expand_vector: - row = layout.row(align=True) + row = col.row(align=True) row.label(text='', icon='BLANK1') bbox = row.box() boxcol = bbox.column() @@ -1640,7 +1731,7 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i is_using_image_atlas = image and (image.yia.is_image_atlas or image.yua.is_udim_atlas) - if layer.texcoord_type == 'UV': + if is_a_mesh and layer.texcoord_type == 'UV': rrow = boxcol.row(align=True) rrow.label(text='', icon='BLANK1') rrow.label(text='UV Map:') @@ -1673,16 +1764,35 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i splits.label(text='Decal Distance:') draw_input_prop(splits, layer, 'decal_distance_value') + if texcoord and texcoord.object: + + rrow = boxcol.row(align=True) + rrow.label(text='', icon='BLANK1') + rrrow = rrow.row() + rrrow.label(text='Decal Constraint:') + draw_input_prop(rrrow, texcoord.object.yp_decal, 'enable_shrinkwrap') + + # NOTE: Show constraint target when there's more than one material users + decal_const = Decal.get_decal_shrinkwrap_constraint(texcoord.object) + if decal_const: + mat = get_active_material() + if mat.users > 1 or decal_const.target == None: + rrow = boxcol.row(align=True) + rrow.label(text='', icon='BLANK1') + rrrow = rrow.row() + rrrow.label(text='Constraint Target:') + draw_input_prop(rrrow, decal_const, 'target') + boxcol.context_pointer_set('entity', layer) rrow = boxcol.row(align=True) rrow.label(text='', icon='BLANK1') if is_bl_newer_than(2, 80): - rrow.operator('node.y_select_decal_object', icon='EMPTY_SINGLE_ARROW') - else: rrow.operator('node.y_select_decal_object', icon='EMPTY_DATA') + rrow.operator('wm.y_select_decal_object', icon='EMPTY_SINGLE_ARROW') + else: rrow.operator('wm.y_select_decal_object', icon='EMPTY_DATA') rrow = boxcol.row(align=True) rrow.label(text='', icon='BLANK1') - rrow.operator('node.y_set_decal_object_position_to_sursor', text='Set Position to Cursor', icon='CURSOR') + rrow.operator('wm.y_set_decal_object_position_to_sursor', text='Set Position to Cursor', icon='CURSOR') if layer.texcoord_type != 'Decal' and not is_using_image_atlas: mapping = get_layer_mapping(layer) @@ -1726,7 +1836,7 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i rrow = boxcol.row(align=True) rrow.label(text='', icon='BLANK1') rrow.alert = True - rrow.operator('node.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') + rrow.operator('wm.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') # Blur row rrow = boxcol.row(align=True) @@ -1740,6 +1850,8 @@ def draw_layer_vector(context, layout, layer, layer_tree, source, image, vcol, i layout.separator() def get_layer_channel_input_label(layer, ch, source=None): + yp = layer.id_data.yp + if ch.override: if not source: source = get_channel_source(ch, layer) label = 'Custom' @@ -1756,6 +1868,9 @@ def get_layer_channel_input_label(layer, ch, source=None): # else: # #label += ' Color' # label = 'Color' + elif layer.type == 'GROUP': + root_ch = yp.channels[get_layer_channel_index(layer, ch)] + label = 'Group ' + root_ch.name else: label = 'Layer' @@ -1797,7 +1912,7 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): ch_idx = get_layer_channel_index(layer, ch) root_ch = yp.channels[ch_idx] #label = root_ch.name - if root_ch.type == 'NORMAL' and ch.normal_map_type != 'NORMAL_MAP': + if root_ch.type == 'NORMAL' and ch.normal_map_type != 'NORMAL_MAP' and layer.type != 'GROUP': if ch.normal_map_type == 'BUMP_MAP': if is_bl_newer_than(2, 80): label += ' (Bump)' @@ -1835,7 +1950,7 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): if ch and root_ch: rrow = row.row(align=True) rrow.alignment = 'RIGHT' - if root_ch.type == 'NORMAL': + if root_ch.type == 'NORMAL' and layer.type != 'GROUP': splits = split_layout(rrow, 0.5, align=True) splits.prop(ch, 'normal_blend_type', text='') if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: @@ -1891,6 +2006,9 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): row = ccol.row(align=True) + if layer.type == 'GROUP': + row.active = get_channel_enabled(ch, layer, root_ch) + if not chui.expand_content: # and ch.enable: split = split_layout(row, 0.35) rrow = split.row(align=True) @@ -1901,7 +2019,7 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): rrow.scale_x = 0.95 label = '' - if root_ch.type == 'NORMAL': + if root_ch.type == 'NORMAL' and layer.type != 'GROUP': if chui.expand_content: label += yp.channels[i].name + ' (' label += normal_type_labels[ch.normal_map_type] @@ -1909,7 +2027,7 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): label += ')' else: label += yp.channels[i].name intensity_value = get_entity_prop_value(ch, 'intensity_value') - if intensity_value != 1.0: + if intensity_value != 1.0 and layer.type != 'GROUP': label += ' (%.1f)' % intensity_value if not chui.expand_content: label += ':' @@ -1955,7 +2073,11 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): else: draw_input_prop(ssplit, ch, 'intensity_value') - if root_ch.type == 'NORMAL': + if layer.type == 'GROUP': + rrrow = ssplit.row(align=True) + draw_input_prop(rrrow, ch, 'intensity_value') + + elif root_ch.type == 'NORMAL': rrrow = ssplit.row(align=True) if ch.normal_map_type == 'NORMAL_MAP': @@ -1976,11 +2098,13 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): #if ypup.layer_list_mode in {'CLASSIC', 'BOTH'}: if ch.enable: - if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} and ch.override and ch.override_type in {'IMAGE', 'VCOL'}: + if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} and ch.override: if ch.override_type == 'IMAGE': rrrow.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('image')) elif ch.override_type == 'VCOL': rrrow.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('vertex_color')) + elif ch.override_type != 'DEFAULT': + rrrow.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('texture')) if ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'} and ch.override_1 and ch.override_1_type == 'IMAGE': rrrow.prop(ch, 'active_edit_1', text='', toggle=True, icon_value=lib.get_icon('image')) @@ -2003,6 +2127,8 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): rrrow.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('image')) elif ch.override_type == 'VCOL': rrrow.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('vertex_color')) + elif ch.override_type != 'DEFAULT': + rrrow.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('texture')) else: label = get_layer_channel_input_label(layer, ch) ssplit.menu("NODE_MT_y_layer_channel_input_menu", text=label) @@ -2033,6 +2159,14 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): #mcol = mrow.column(align=True) #mcol.use_property_split = True + if layer.type == 'GROUP': + channel_enabled = get_channel_enabled(ch, layer, root_ch) + + if ch.enable and not channel_enabled: + mbox.label(text='No children is using \''+root_ch.name+'\' channel!', icon='ERROR') + + mcol.active = channel_enabled + # Blend type if layer.type != 'BACKGROUND' or root_ch.type == 'NORMAL': row = mcol.row(align=True) @@ -2074,17 +2208,18 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): if root_ch.type == 'NORMAL': - #mcol.separator() + if layer.type != 'GROUP': - row = mcol.row(align=True) - row.label(text='', icon='BLANK1') - #split = split_layout(row, 0.4) - row.label(text='Type:') - rrow = row.row(align=True) - rrow.scale_x = 1.4 - rrow.prop(ch, 'normal_map_type', text='') + #mcol.separator() + + row = mcol.row(align=True) + row.label(text='', icon='BLANK1') + #split = split_layout(row, 0.4) + row.label(text='Type:') + rrow = row.row(align=True) + rrow.scale_x = 1.4 + rrow.prop(ch, 'normal_map_type', text='') - if layer.type != 'GROUP': if ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: # Height @@ -2159,7 +2294,7 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): brow.alert = True brow.context_pointer_set('channel', ch) brow.context_pointer_set('image', image) - brow.operator('node.y_refresh_neighbor_uv', icon='ERROR') + brow.operator('wm.y_refresh_neighbor_uv', icon='ERROR') if ch.show_transition_bump or ch.enable_transition_bump: @@ -2269,7 +2404,7 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): #row.label(text='', icon='BLANK1') # Write height - if ch.normal_map_type not in {'NORMAL_MAP', 'VECTOR_DISPLACEMENT_MAP'} or ch.enable_transition_bump: + if ch.normal_map_type not in {'NORMAL_MAP', 'VECTOR_DISPLACEMENT_MAP'} or ch.enable_transition_bump or layer.type == 'GROUP': row = mcol.row(align=True) row.label(text='', icon='BLANK1') row.label(text='Write Height:') @@ -2400,189 +2535,153 @@ def draw_layer_channels(context, layout, layer, layer_tree, image, specific_ch): split_factor = 0.375 if root_ch.type != 'NORMAL' or ch.normal_map_type != 'BUMP_NORMAL_MAP' else 0.475 - # Override settings - if root_ch.type != 'NORMAL' or ch.normal_map_type != 'NORMAL_MAP': # or (not source_1 and not cache_1): + if layer.type != 'GROUP' or root_ch.type != 'NORMAL': + # Override settings + if root_ch.type != 'NORMAL' or ch.normal_map_type != 'NORMAL_MAP': # or (not source_1 and not cache_1): + + modcol = mcol.column() + modcol.active = layer.type != 'BACKGROUND' + draw_modifier_stack(context, ch, root_ch.type, modcol, + ypui.layer_ui.channels[i], layer) + + #mcol.separator() + + if root_ch.type != 'NORMAL' or ch.normal_map_type != 'VECTOR_DISPLACEMENT_MAP' or ch.override: + + input_settings_available = has_layer_input_options(layer) and (ch.layer_input != 'ALPHA' + and root_ch.colorspace == 'SRGB' and root_ch.type != 'NORMAL' ) - modcol = mcol.column() - modcol.active = layer.type != 'BACKGROUND' - draw_modifier_stack(context, ch, root_ch.type, modcol, - ypui.layer_ui.channels[i], layer) + #row = mcol.row(align=True) + srow = split_layout(mcol, split_factor, align=False) + row = srow.row(align=True) - #mcol.separator() + label = 'Source:' if root_ch.type != 'NORMAL' or ch.normal_map_type != 'BUMP_NORMAL_MAP' else 'Bump Source:' + if ch.override or input_settings_available: + inbox_dropdown_button(row, chui, 'expand_source', label) + else: + row.label(text='', icon='BLANK1') + row.label(text=label) + + row = srow.row(align=True) + label = get_layer_channel_input_label(layer, ch, source) + row.context_pointer_set('parent', ch) + if ch.override and ch.override_type == 'DEFAULT' and not ch.expand_source: + split = split_layout(row, 0.55, align=True) + split.menu("NODE_MT_y_layer_channel_input_menu", text=label) + if root_ch.type == 'VALUE': + draw_input_prop(split, ch, 'override_value') + else: draw_input_prop(split, ch, 'override_color') + else: + rrow = row.row(align=True) + rrow.scale_x = 1.4 if ch.normal_map_type != 'BUMP_NORMAL_MAP' else 1.1 + rrow.menu("NODE_MT_y_layer_channel_input_menu", text=label) - if root_ch.type != 'NORMAL' or ch.normal_map_type != 'VECTOR_DISPLACEMENT_MAP' or ch.override: + if ch.enable and ch.override: #and ypup.layer_list_mode in {'CLASSIC', 'BOTH'}: + if ch.override_type == 'IMAGE': + row.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('image')) + elif ch.override_type == 'VCOL': + row.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('vertex_color')) + elif ch.override_type != 'DEFAULT': + row.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('texture')) - input_settings_available = has_layer_input_options(layer) and (ch.layer_input != 'ALPHA' - and root_ch.colorspace == 'SRGB' and root_ch.type != 'NORMAL' ) + ch_source = None + if ch.override: + ch_source = get_channel_source(ch, layer) + + if ch.expand_source and (ch.override or input_settings_available): # and ch.override_type != 'DEFAULT': + + rrow = mcol.row(align=True) + rrow.label(text='', icon='BLANK1') + #rrcol = rrow.box() + rrcol = rrow.column() + + if ch.override: + if ch.override_type == 'DEFAULT': + row = rrcol.row() + if root_ch.type == 'VALUE': + row.label(text='Custom Value:') + draw_input_prop(row, ch, 'override_value') + else: + row.label(text='Custom Color:') + draw_input_prop(row, ch, 'override_color') + + if ch_source: + if ch.override_type == 'IMAGE': + draw_image_props(context, ch_source, rrcol, ch, show_datablock=False) + elif ch.override_type == 'VCOL': + draw_vcol_props(rrcol) + else: + draw_tex_props(ch_source, rrcol, entity=ch) + + elif input_settings_available: + row = rrcol.row(align=True) + row.label(text='Gamma Space:') + row.prop(ch, 'gamma_space', text='') + + # Override 1 + if root_ch.type == 'NORMAL' and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: # and (source_1 or cache_1))): + + modcol = mcol.column() + modcol.active = layer.type != 'BACKGROUND' + draw_modifier_stack(context, ch, root_ch.type, modcol, + ypui.layer_ui.channels[i], layer, use_modifier_1=True) - #row = mcol.row(align=True) srow = split_layout(mcol, split_factor, align=False) row = srow.row(align=True) - - label = 'Source:' if root_ch.type != 'NORMAL' or ch.normal_map_type != 'BUMP_NORMAL_MAP' else 'Bump Source:' - if ch.override or input_settings_available: - inbox_dropdown_button(row, chui, 'expand_source', label) - else: + label = 'Source:' if ch.normal_map_type != 'BUMP_NORMAL_MAP' else 'Normal Source:' + if not ch.override_1: row.label(text='', icon='BLANK1') row.label(text=label) + else: + inbox_dropdown_button(row, chui, 'expand_source_1', label) + + if ch.override_1: + if ch.override_1_type == 'IMAGE' and source_1 and source_1.image: + label = source_1.image.name + else: label = 'Custom' + else: + label = 'Layer' + if is_bl_newer_than(2, 81) and layer.type == 'VORONOI' and layer.voronoi_feature in {'DISTANCE_TO_EDGE', 'N_SPHERE_RADIUS'}: + label += ' Distance' + else: label += ' Color' row = srow.row(align=True) - label = get_layer_channel_input_label(layer, ch, source) row.context_pointer_set('parent', ch) - if ch.override and ch.override_type == 'DEFAULT' and not ch.expand_source: + if ch.override_1 and ch.override_1_type == 'DEFAULT' and not ch.expand_source_1: split = split_layout(row, 0.55, align=True) - split.menu("NODE_MT_y_layer_channel_input_menu", text=label) - if root_ch.type == 'VALUE': - draw_input_prop(split, ch, 'override_value') - else: draw_input_prop(split, ch, 'override_color') + split.menu("NODE_MT_y_layer_channel_input_1_menu", text=label) + draw_input_prop(split, ch, 'override_1_color') else: rrow = row.row(align=True) rrow.scale_x = 1.4 if ch.normal_map_type != 'BUMP_NORMAL_MAP' else 1.1 - rrow.menu("NODE_MT_y_layer_channel_input_menu", text=label) + rrow.menu("NODE_MT_y_layer_channel_input_1_menu", text=label) - if ch.enable and ch.override: #and ypup.layer_list_mode in {'CLASSIC', 'BOTH'}: - if ch.override_type == 'IMAGE': - row.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('image')) - elif ch.override_type == 'VCOL': - row.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('vertex_color')) - elif ch.override_type != 'DEFAULT': - row.prop(ch, 'active_edit', text='', toggle=True, icon_value=lib.get_icon('texture')) + if ch.enable and ch.override_1 and ch.override_1_type == 'IMAGE': # and ypup.layer_list_mode in {'CLASSIC', 'BOTH'}: + row.prop(ch, 'active_edit_1', text='', toggle=True, icon_value=lib.get_icon('image')) - ch_source = None - if ch.override: - ch_source = get_channel_source(ch, layer) + #icon = 'PREFERENCES' if is_bl_newer_than(2, 80) else 'SCRIPTWIN' + #row.menu("NODE_MT_y_replace_channel_override_1_menu", icon=icon, text='') - if ch.expand_source and (ch.override or input_settings_available): # and ch.override_type != 'DEFAULT': + ch_source_1 = None + if ch.override_1: + ch_source_1 = layer_tree.nodes.get(ch.source_1) + elif ch.override_1_type not in {'DEFAULT'}: + #ch_source_1 = layer_tree.nodes.get(getattr(ch, 'cache_' + ch.override_1_type.lower())) + ch_source_1 = layer_tree.nodes.get(getattr(ch, 'cache_1_image')) + #if ch.expand_source_1 and ch.override_1_type == 'IMAGE' and ch_source_1: + if ch.expand_source_1 and ch.override_1: rrow = mcol.row(align=True) rrow.label(text='', icon='BLANK1') - #rrcol = rrow.box() + #rbox = rrow.box() + #rbox.active = ch.override_1 rrcol = rrow.column() - - if ch.override: - if ch.override_type == 'DEFAULT': - row = rrcol.row() - if root_ch.type == 'VALUE': - row.label(text='Custom Value:') - draw_input_prop(row, ch, 'override_value') - else: - row.label(text='Custom Color:') - draw_input_prop(row, ch, 'override_color') - - if ch_source: - if ch.override_type == 'IMAGE': - draw_image_props(context, ch_source, rrcol, ch, show_datablock=False) - elif ch.override_type == 'VCOL': - draw_vcol_props(rrcol) - else: - draw_tex_props(ch_source, rrcol, entity=ch) - - elif input_settings_available: - row = rrcol.row(align=True) - row.label(text='Gamma Space:') - row.prop(ch, 'gamma_space', text='') - - # Override 1 - if root_ch.type == 'NORMAL' and ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: # and (source_1 or cache_1))): - - modcol = mcol.column() - modcol.active = layer.type != 'BACKGROUND' - draw_modifier_stack(context, ch, root_ch.type, modcol, - ypui.layer_ui.channels[i], layer, use_modifier_1=True) - - srow = split_layout(mcol, split_factor, align=False) - row = srow.row(align=True) - label = 'Source:' if ch.normal_map_type != 'BUMP_NORMAL_MAP' else 'Normal Source:' - if not ch.override_1: - row.label(text='', icon='BLANK1') - row.label(text=label) - else: - inbox_dropdown_button(row, chui, 'expand_source_1', label) - - if ch.override_1: - if ch.override_1_type == 'IMAGE' and source_1 and source_1.image: - label = source_1.image.name - else: label = 'Custom' - else: - label = 'Layer' - if is_bl_newer_than(2, 81) and layer.type == 'VORONOI' and layer.voronoi_feature in {'DISTANCE_TO_EDGE', 'N_SPHERE_RADIUS'}: - label += ' Distance' - else: label += ' Color' - - row = srow.row(align=True) - row.context_pointer_set('parent', ch) - if ch.override_1 and ch.override_1_type == 'DEFAULT' and not ch.expand_source_1: - split = split_layout(row, 0.55, align=True) - split.menu("NODE_MT_y_layer_channel_input_1_menu", text=label) - draw_input_prop(split, ch, 'override_1_color') - else: - rrow = row.row(align=True) - rrow.scale_x = 1.4 if ch.normal_map_type != 'BUMP_NORMAL_MAP' else 1.1 - rrow.menu("NODE_MT_y_layer_channel_input_1_menu", text=label) - - if ch.enable and ch.override_1 and ch.override_1_type == 'IMAGE': # and ypup.layer_list_mode in {'CLASSIC', 'BOTH'}: - row.prop(ch, 'active_edit_1', text='', toggle=True, icon_value=lib.get_icon('image')) - - #icon = 'PREFERENCES' if is_bl_newer_than(2, 80) else 'SCRIPTWIN' - #row.menu("NODE_MT_y_replace_channel_override_1_menu", icon=icon, text='') - - ch_source_1 = None - if ch.override_1: - ch_source_1 = layer_tree.nodes.get(ch.source_1) - elif ch.override_1_type not in {'DEFAULT'}: - #ch_source_1 = layer_tree.nodes.get(getattr(ch, 'cache_' + ch.override_1_type.lower())) - ch_source_1 = layer_tree.nodes.get(getattr(ch, 'cache_1_image')) - - #if ch.expand_source_1 and ch.override_1_type == 'IMAGE' and ch_source_1: - if ch.expand_source_1 and ch.override_1: - rrow = mcol.row(align=True) - rrow.label(text='', icon='BLANK1') - #rbox = rrow.box() - #rbox.active = ch.override_1 - rrcol = rrow.column() - if ch.override_1_type == 'DEFAULT': - row = rrcol.row() - row.label(text='Custom Color:') - draw_input_prop(row, ch, 'override_1_color') - elif ch.override_1_type == 'IMAGE' and ch_source_1: - draw_image_props(context, ch_source_1, rrcol, entity=ch, show_flip_y=True, show_datablock=False) - - # Layer input - #if (layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'MUSGRAVE'} and not - # (is_bl_newer_than(2, 81) and layer.type == 'VORONOI' and layer.voronoi_feature in {'DISTANCE_TO_EDGE', 'N_SPHERE_RADIUS'}) - # ): - # row = mcol.row(align=True) - - # input_settings_available = (ch.layer_input != 'ALPHA' - # and root_ch.colorspace == 'SRGB' and root_ch.type != 'NORMAL' ) - - # if input_settings_available: - # if chui.expand_input_settings: - # icon_value = lib.get_icon('uncollapsed_input') - # else: icon_value = lib.get_icon('collapsed_input') - # row.prop(chui, 'expand_input_settings', text='', emboss=False, icon_value=icon_value) - # else: - # row.label(text='', icon_value=lib.get_icon('input')) - - # split = split_layout(row, 0.275) - - # split.label(text='Input:') - # srow = split.row(align=True) - # srow.prop(ch, 'layer_input', text='') - - # if chui.expand_input_settings and input_settings_available: - # row = mcol.row(align=True) - # row.label(text='', icon='BLANK1') - # box = row.box() - # bcol = box.column(align=False) - - # brow = bcol.row(align=True) - # brow.label(text='Gamma Space:') - # brow.prop(ch, 'gamma_space', text='') - - # #row.label(text='', icon='BLANK1') - - # extra_separator = True + if ch.override_1_type == 'DEFAULT': + row = rrcol.row() + row.label(text='Custom Color:') + draw_input_prop(row, ch, 'override_1_color') + elif ch.override_1_type == 'IMAGE' and ch_source_1: + draw_image_props(context, ch_source_1, rrcol, entity=ch, show_flip_y=True, show_datablock=False) if ypui.expand_channels: mrow.label(text='', icon='BLANK1') @@ -2736,7 +2835,7 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): mask_icon = 'hemi' elif mask.type == 'OBJECT_INDEX': mask_icon = 'object_index' - elif mask.type == 'EDGE_DETECT': + elif mask.type in {'EDGE_DETECT', 'AO'}: mask_icon = 'edge_detect' elif mask.type == 'COLOR_ID': mask_icon = 'color' @@ -2859,10 +2958,7 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): #rbcol = rbox.column() rbcol = rrow.column() rbcol.active = not mask.use_baked - if mask.use_temp_bake: - rbcol.context_pointer_set('parent', mask) - rbcol.operator('node.y_disable_temp_image', icon='FILE_REFRESH', text='Disable Baked Temp') - elif mask_image: + if mask_image: draw_image_props(context, mask_source, rbcol, mask, show_datablock=False, show_source_input=True) elif mask.type == 'HEMI': draw_hemi_props(mask, mask_source, rbcol) @@ -2872,24 +2968,47 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): draw_colorid_props(mask, mask_source, rbcol) elif mask.type == 'EDGE_DETECT': draw_edge_detect_props(mask, mask_source, rbcol) + elif mask.type == 'AO': + draw_ao_props(mask, mask_source, rbcol) elif mask.type == 'MODIFIER': draw_inbetween_modifier_mask_props(mask, mask_source, rbcol) elif mask.type == 'VCOL': draw_vcol_props(rbcol, entity=mask, show_divide_rgb_alpha=False, show_source_input=True) else: draw_tex_props(mask_source, rbcol, entity=mask, show_source_input=True) - if mask.baked_source != '': - rrcol.context_pointer_set('entity', mask) + rrcol.context_pointer_set('entity', mask) + if mask.baked_source == '' and mask.type in {'EDGE_DETECT', 'HEMI', 'AO'}: rrrow = rrcol.row(align=True) rrrow.label(text='', icon='BLANK1') - rrrow.operator("node.y_bake_entity_to_image", text='Rebake', icon_value=lib.get_icon('bake')) - rrrow.prop(mask, 'use_baked', text='Use Baked', toggle=True) + rrrow.operator("wm.y_bake_entity_to_image", text='Bake '+mask_type_labels[mask.type]+' as Image', icon_value=lib.get_icon('bake')) + + elif mask.baked_source != '': + + baked_source = mask_tree.nodes.get(mask.baked_source) + if baked_source and baked_source.image: + brow = rrcol.row(align=True) + brow.active = mask.use_baked + brow.label(text='', icon='BLANK1') + + crow = brow.row(align=True) + drow = crow.row(align=True) + drow.label(text='Baked: ') + drow = crow.row(align=True) + drow.alignment = 'RIGHT' + drow.label(text=baked_source.image.name, icon='IMAGE_DATA') - if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}: + brow = rrcol.row(align=True) + brow.label(text='', icon='BLANK1') + brow.operator("wm.y_bake_entity_to_image", text='Rebake', icon_value=lib.get_icon('bake')) + brow.prop(mask, 'use_baked', text='Use Baked', toggle=True) + icon = 'TRASH' if is_bl_newer_than(2, 80) else 'X' + brow.operator("wm.y_remove_baked_entity", text='', icon=icon) + + if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'}: rrcol.separator() # Vector row - if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'}: + if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'}: srow = split_layout(rrcol, 0.35, align=False) srow.active = not mask.use_baked @@ -2908,10 +3027,12 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): rrow = srow.row(align=True) if mask.texcoord_type == 'UV' and not maskui.expand_vector: - rrrow = split_layout(rrow, 0.35, align=True) - rrrow.prop(mask, 'texcoord_type', text='') - if not maskui.expand_vector: + if obj.type == 'MESH': + rrrow = split_layout(rrow, 0.35, align=True) + rrrow.prop(mask, 'texcoord_type', text='') rrrow.prop_search(mask, "uv_name", obj.data, "uv_layers", text='', icon='GROUP_UVS') + else: + rrow.prop(mask, 'texcoord_type', text='') #rrow.context_pointer_set('mask', mask) #icon = 'PREFERENCES' if is_bl_newer_than(2, 80) else 'SCRIPTWIN' @@ -2944,7 +3065,7 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): splits.label(text='Projection Blend:') splits.prop(mask_src, 'projection_blend', text='') - if mask.texcoord_type == 'UV': + if mask.texcoord_type == 'UV' and obj.type == 'MESH': rrow = boxcol.row(align=True) rrow.label(text='UV Map:') rrrow = rrow.row(align=True) @@ -2952,6 +3073,7 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): rrrow.prop_search(mask, "uv_name", obj.data, "uv_layers", text='', icon='GROUP_UVS') icon = 'PREFERENCES' if is_bl_newer_than(2, 80) else 'SCRIPTWIN' + rrow.context_pointer_set('entity', mask) rrow.menu("NODE_MT_y_uv_special_menu", icon=icon, text='') if mask.texcoord_type == 'Decal': @@ -2964,11 +3086,25 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): splits.label(text='Decal Distance:') draw_input_prop(splits, mask, 'decal_distance_value') + if texcoord and texcoord.object: + rrow = boxcol.row(align=True) + rrow.label(text='Decal Constraint:') + draw_input_prop(rrow, texcoord.object.yp_decal, 'enable_shrinkwrap') + + # NOTE: Show constraint target when there's more than one material users + decal_const = Decal.get_decal_shrinkwrap_constraint(texcoord.object) + if decal_const: + mat = get_active_material() + if mat.users > 1 or decal_const.target == None: + rrow = boxcol.row(align=True) + rrow.label(text='Constraint Target:') + draw_input_prop(rrow, decal_const, 'target') + boxcol.context_pointer_set('entity', mask) if is_bl_newer_than(2, 80): - boxcol.operator('node.y_select_decal_object', icon='EMPTY_SINGLE_ARROW') - else: boxcol.operator('node.y_select_decal_object', icon='EMPTY_DATA') - boxcol.operator('node.y_set_decal_object_position_to_sursor', text='Set Position to Cursor', icon='CURSOR') + boxcol.operator('wm.y_select_decal_object', icon='EMPTY_SINGLE_ARROW') + else: boxcol.operator('wm.y_select_decal_object', icon='EMPTY_DATA') + boxcol.operator('wm.y_set_decal_object_position_to_sursor', text='Set Position to Cursor', icon='CURSOR') if mask.texcoord_type != 'Decal' and not is_using_image_atlas: mapping = get_mask_mapping(mask) @@ -3010,7 +3146,7 @@ def draw_layer_masks(context, layout, layer, specific_mask=None): ): rrow = boxcol.row(align=True) rrow.alert = True - rrow.operator('node.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') + rrow.operator('wm.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') # Blur row if mask.texcoord_type != 'Layer': @@ -3047,7 +3183,7 @@ def draw_layers_ui(context, layout, node): if group_tree.users > 1: row = box.row(align=True) row.alert = True - op = row.operator("node.y_duplicate_yp_nodes", text='Fix Multi-User ' + get_addon_title() + ' Node', icon='ERROR') + op = row.operator("wm.y_duplicate_yp_nodes", text='Fix Multi-User ' + get_addon_title() + ' Node', icon='ERROR') op.duplicate_node = True op.duplicate_material = False op.only_active = True @@ -3058,8 +3194,12 @@ def draw_layers_ui(context, layout, node): if yp.use_baked: col = box.column(align=False) - if len(yp.channels) > 0: - root_ch = yp.channels[yp.active_channel_index] + for i, root_ch in enumerate(yp.channels): + + try: nchui = ypui.channels[i] + except: + ypui.need_update = True + return baked = nodes.get(root_ch.baked) baked_vcol_node = nodes.get(root_ch.baked_vcol) @@ -3067,67 +3207,100 @@ def draw_layers_ui(context, layout, node): icon_name = lib.channel_custom_icon_dict[root_ch.type] icon_value = lib.get_icon(icon_name) - if not baked or not baked.image or root_ch.no_layer_using: - col.label(text=root_ch.name + " channel hasn't been baked yet!", icon_value=icon_value) - else: - row = col.row(align=True) - title = 'Baked ' + root_ch.name - row.label(text=title, icon_value=icon_value) + no_baked_data = not baked or not baked.image or root_ch.no_layer_using + bake_disabled = root_ch.disable_global_baked and not yp.enable_baked_outside + + #if not baked or not baked.image or root_ch.no_layer_using: + # col.label(text=root_ch.name + " channel hasn't been baked yet!", icon_value=icon_value) + #else: + row = col.row(align=True) + row.context_pointer_set('root_ch', root_ch) + if baked: row.context_pointer_set('image', baked.image) - row.context_pointer_set('root_ch', root_ch) - row.context_pointer_set('image', baked.image) + rrow = row.row(align=True) + icon = get_collapse_arrow_icon(getattr(nchui, 'expand_baked_data')) + rrow.prop(nchui, 'expand_baked_data', text='', emboss=False, icon=icon) + rrow = row.row(align=True) + rrow.active = not (bake_disabled or no_baked_data) + title = 'Baked ' + root_ch.name + if bake_disabled: + title += ' (Disabled)' + if is_bl_newer_than(2, 80): + rrow.alignment = 'LEFT' + rrow.scale_x = 0.95 + rrow.prop(nchui, 'expand_baked_data', text=title, icon_value=icon_value, emboss=False) + else: + rrow.label(text=title, icon_value=icon_value) + if not no_baked_data: icon = 'PREFERENCES' if is_bl_newer_than(2, 80) else 'SCRIPTWIN' - row.menu("NODE_MT_y_baked_image_menu", text='', icon=icon) + rrow = row.row(align=True) + if is_bl_newer_than(2, 80): + rrow.alignment = 'RIGHT' + rrow.menu("NODE_MT_y_baked_image_menu", text='', icon=icon) - row = col.row(align=True) - row.active = not root_ch.disable_global_baked or yp.enable_baked_outside - row.label(text='', icon='BLANK1') - if baked.image.is_dirty: - title = baked.image.name + ' *' - else: title = baked.image.name - if root_ch.disable_global_baked and not yp.enable_baked_outside: - title += ' (Disabled)' - elif not root_ch.enable_bake_to_vcol and baked_vcol_node: - title += pgettext_iface(' (Active)') - row.label(text=title, icon_value=lib.get_icon('image')) + if not nchui.expand_baked_data: continue - if baked.image.packed_file: - row.label(text='', icon='PACKAGE') + row = col.row(align=True) + row.label(text='', icon='BLANK1') + bbox = row.box() + bcol = bbox.column(align=True) - # If enabled or a baked vertex color is found - if root_ch.enable_bake_to_vcol or baked_vcol_node: - obj = context.object - vcols = get_vertex_colors(obj) - vcol_name = root_ch.bake_to_vcol_name - vcol = vcols.get(vcol_name) + if no_baked_data: + bcol.label(text=root_ch.name + " channel hasn't been baked yet!", icon='ERROR') + continue - row = col.row(align=True) - row.label(text='', icon='BLANK1') + row = bcol.row(align=True) + row.active = not root_ch.disable_global_baked or yp.enable_baked_outside + #row.label(text='', icon='BLANK1') + if baked.image.is_dirty: + title = baked.image.name + ' *' + else: title = baked.image.name + if root_ch.disable_global_baked and not yp.enable_baked_outside: + title += ' (Disabled)' + elif not root_ch.use_baked_vcol and baked_vcol_node: + title += pgettext_iface(' (Active)') + row.label(text=title, icon_value=lib.get_icon('image')) + + if baked.image.packed_file: + row.label(text='', icon='PACKAGE') + #row.label(text='', icon='BLANK1') - row.active = not root_ch.disable_global_baked or yp.enable_baked_outside - title = '' - if root_ch.disable_global_baked and not yp.enable_baked_outside: - title += pgettext_iface(' (Disabled)') - elif root_ch.enable_bake_to_vcol and baked_vcol_node: - title += pgettext_iface(' (Active)') + # If enabled or a baked vertex color is found + if root_ch.use_baked_vcol or baked_vcol_node: + obj = context.object + vcols = get_vertex_colors(obj) + vcol_name = root_ch.bake_to_vcol_name + vcol = vcols.get(vcol_name) - if baked_vcol_node and vcol: - row.label(text=vcol_name + title, icon_value=lib.get_icon('vertex_color')) - else: - row.label(text='Baked vertex color is missing!' + title, icon='ERROR') + row = bcol.row(align=True) + #row.label(text='', icon='BLANK1') + + row.active = not root_ch.disable_global_baked or yp.enable_baked_outside + title = '' + if root_ch.disable_global_baked and not yp.enable_baked_outside: + title += pgettext_iface(' (Disabled)') + elif root_ch.use_baked_vcol and baked_vcol_node: + title += pgettext_iface(' (Active)') + + if baked_vcol_node and vcol: + row.label(text=vcol_name + title, icon_value=lib.get_icon('vertex_color')) + + icon = 'CHECKBOX_HLT' if root_ch.use_baked_vcol else 'CHECKBOX_DEHLT' + row.prop(root_ch, 'use_baked_vcol', icon=icon, text='', toggle=True, emboss=False) + else: + row.label(text='Baked vertex color is missing!' + title, icon='ERROR') if root_ch.type == 'NORMAL': baked_normal_overlay = nodes.get(root_ch.baked_normal_overlay) if baked_normal_overlay and baked_normal_overlay.image: - row = col.row(align=True) - row.active = not root_ch.disable_global_baked - row.label(text='', icon='BLANK1') + row = bcol.row(align=True) + row.active = not root_ch.disable_global_baked or yp.enable_baked_outside if baked_normal_overlay.image.is_dirty: title = baked_normal_overlay.image.name + ' *' else: title = baked_normal_overlay.image.name - if root_ch.disable_global_baked: + if root_ch.disable_global_baked and not yp.enable_baked_outside: title += ' (Disabled)' row.label(text=title, icon_value=lib.get_icon('image')) @@ -3136,22 +3309,35 @@ def draw_layers_ui(context, layout, node): baked_disp = nodes.get(root_ch.baked_disp) if baked_disp and baked_disp.image: - row = col.row(align=True) - row.active = not root_ch.disable_global_baked - row.label(text='', icon='BLANK1') + row = bcol.row(align=True) + row.active = not root_ch.disable_global_baked or yp.enable_baked_outside if baked_disp.image.is_dirty: title = baked_disp.image.name + ' *' else: title = baked_disp.image.name - if root_ch.disable_global_baked: + if root_ch.disable_global_baked and not yp.enable_baked_outside: title += ' (Disabled)' row.label(text=title, icon_value=lib.get_icon('image')) if baked_disp.image.packed_file: row.label(text='', icon='PACKAGE') + baked_vdisp = nodes.get(root_ch.baked_vdisp) + if baked_vdisp and baked_vdisp.image: + row = bcol.row(align=True) + row.active = not root_ch.disable_global_baked or yp.enable_baked_outside + if baked_vdisp.image.is_dirty: + title = baked_vdisp.image.name + ' *' + else: title = baked_vdisp.image.name + if root_ch.disable_global_baked and not yp.enable_baked_outside: + title += ' (Disabled)' + row.label(text=title, icon_value=lib.get_icon('image')) + + if baked_vdisp.image.packed_file: + row.label(text='', icon='PACKAGE') + btimages = [] for bt in yp.bake_targets: - for i, letter in enumerate(rgba_letters): + for letter in rgba_letters: btc = getattr(bt, letter) if getattr(btc, 'channel_name') == root_ch.name: bt_node = nodes.get(bt.image_node) @@ -3162,8 +3348,7 @@ def draw_layers_ui(context, layout, node): if btimg.is_dirty: title += ' *' - row = col.row(align=True) - row.label(text='', icon='BLANK1') + row = bcol.row(align=True) row.label(text=title, icon_value=lib.get_icon('image')) if btimg.packed_file: @@ -3171,13 +3356,20 @@ def draw_layers_ui(context, layout, node): btimages.append(btimg) + row = box.row(align=True) + icon = 'FILE_TICK' + row.operator('wm.y_save_all_baked_images', text='Save As All...', icon=icon).copy = False + row.operator('wm.y_save_all_baked_images', text='Save Copies All...', icon=icon).copy = True + + icon = 'TRASH' if is_bl_newer_than(2, 80) else 'CANCEL' + row.operator('wm.y_delete_baked_channel_images', text='', icon=icon) return if is_a_mesh and not uv_found: row = box.row(align=True) row.alert = True - row.operator("node.y_add_simple_uvs", icon='ERROR') + row.operator("wm.y_add_simple_uvs", icon='ERROR') row.alert = False return @@ -3189,18 +3381,18 @@ def draw_layers_ui(context, layout, node): channel_mismatch = True break - for mask in layer.masks: - if len(mask.channels) != num_channels: - channel_mismatch = True - break - - if channel_mismatch: + for mask in layer.masks: + if len(mask.channels) != num_channels: + channel_mismatch = True break + if channel_mismatch: + break + if channel_mismatch: row = box.row(align=True) row.alert = True - row.operator("node.y_fix_channel_missmatch", text='Fix Missmatched Channels!', icon='ERROR') + row.operator("wm.y_fix_channel_missmatch", text='Fix Missmatched Channels!', icon='ERROR') row.alert = False return @@ -3230,12 +3422,12 @@ def draw_layers_ui(context, layout, node): # ): # row = box.row(align=True) # row.alert = True - # row.operator("node.y_fix_duplicated_yp_nodes", text='Fix Duplicated Layers', icon='ERROR') + # row.operator("wm.y_fix_duplicated_yp_nodes", text='Fix Duplicated Layers', icon='ERROR') # row.alert = False # #box.prop(ypui, 'make_image_single_user') # return - # Check source for missing data + # Check for missing data missing_data = False for layer in yp.layers: if layer.type in {'IMAGE' , 'VCOL'}: @@ -3291,7 +3483,7 @@ def draw_layers_ui(context, layout, node): if missing_data: row = box.row(align=True) row.alert = True - row.operator("node.y_fix_missing_data", icon='ERROR') + row.operator("wm.y_fix_missing_data", icon='ERROR') row.alert = False return @@ -3316,14 +3508,14 @@ def draw_layers_ui(context, layout, node): # Check layer and mask uv for layer in yp.layers: - if layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'COLOR', 'BACKGROUND', 'EDGE_DETECT', 'MODIFIER'} and layer.uv_name != '': + if layer.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'COLOR', 'BACKGROUND', 'EDGE_DETECT', 'MODIFIER', 'AO'} and layer.uv_name != '': uv_layer = uv_layers.get(layer.uv_name) if not uv_layer and layer.uv_name not in uv_missings: uv_missings.append(layer.uv_name) #entities.append(layer.name) for mask in layer.masks: - if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER'} and mask.uv_name != '': + if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID', 'BACKFACE', 'EDGE_DETECT', 'MODIFIER', 'AO'} and mask.uv_name != '': uv_layer = uv_layers.get(mask.uv_name) if not uv_layer and mask.uv_name not in uv_missings: uv_missings.append(mask.uv_name) @@ -3333,7 +3525,7 @@ def draw_layers_ui(context, layout, node): row = box.row(align=True) row.alert = True title = 'UV ' + uv_name + ' is missing or renamed!' - row.operator("node.y_fix_missing_uv", text=title, icon='ERROR').source_uv_name = uv_name + row.operator("wm.y_fix_missing_uv", text=title, icon='ERROR').source_uv_name = uv_name #print(entities) row.alert = False @@ -3349,7 +3541,7 @@ def draw_layers_ui(context, layout, node): if need_tangent_refresh: row = box.row(align=True) row.alert = True - row.operator('node.y_refresh_tangent_sign_vcol', icon='FILE_REFRESH', text='Tangent Sign Hacks is missing!') + row.operator('wm.y_refresh_tangent_sign_vcol', icon='FILE_REFRESH', text='Tangent Sign Hacks is missing!') row.alert = False # Get active item entity @@ -3450,7 +3642,7 @@ def draw_layers_ui(context, layout, node): if ypup.layer_list_mode in {'DYNAMIC', 'BOTH'}: if ypup.layer_list_mode == 'BOTH': - rcol.operator('node.y_refresh_list_items', icon='FILE_REFRESH', text='Refresh Items') + rcol.operator('wm.y_refresh_list_items', icon='FILE_REFRESH', text='Refresh Items') rcol.template_list("NODE_UL_YPaint_list_items", "", yp, "list_items", yp, "active_item_index", rows=5, maxrows=5) @@ -3464,54 +3656,54 @@ def draw_layers_ui(context, layout, node): if has_children(layer): # or (image and not image.packed_file): if is_bl_newer_than(2, 80): - rcol.operator("node.y_remove_layer_menu", icon='REMOVE', text='') - else: rcol.operator("node.y_remove_layer_menu", icon='ZOOMOUT', text='') + rcol.operator("wm.y_remove_layer_menu", icon='REMOVE', text='') + else: rcol.operator("wm.y_remove_layer_menu", icon='ZOOMOUT', text='') else: if is_bl_newer_than(2, 80): - c = rcol.operator("node.y_remove_layer", icon='REMOVE', text='') - else: c = rcol.operator("node.y_remove_layer", icon='ZOOMOUT', text='') + c = rcol.operator("wm.y_remove_layer", icon='REMOVE', text='') + else: c = rcol.operator("wm.y_remove_layer", icon='ZOOMOUT', text='') c.remove_children = False if is_top_member(layer): - c = rcol.operator("node.y_move_in_out_layer_group_menu", text='', icon='TRIA_UP') + c = rcol.operator("wm.y_move_in_out_layer_group_menu", text='', icon='TRIA_UP') c.direction = 'UP' c.move_out = True else: upper_idx, upper_layer = get_upper_neighbor(layer) if upper_layer and (upper_layer.type == 'GROUP' or upper_layer.parent_idx != layer.parent_idx): - c = rcol.operator("node.y_move_in_out_layer_group_menu", text='', icon='TRIA_UP') + c = rcol.operator("wm.y_move_in_out_layer_group_menu", text='', icon='TRIA_UP') c.direction = 'UP' c.move_out = False else: - c = rcol.operator("node.y_move_layer", text='', icon='TRIA_UP') + c = rcol.operator("wm.y_move_layer", text='', icon='TRIA_UP') c.direction = 'UP' if is_bottom_member(layer): - c = rcol.operator("node.y_move_in_out_layer_group_menu", text='', icon='TRIA_DOWN') + c = rcol.operator("wm.y_move_in_out_layer_group_menu", text='', icon='TRIA_DOWN') c.direction = 'DOWN' c.move_out = True else: lower_idx, lower_layer = get_lower_neighbor(layer) if lower_layer and (lower_layer.type == 'GROUP' and lower_layer.parent_idx == layer.parent_idx): - c = rcol.operator("node.y_move_in_out_layer_group_menu", text='', icon='TRIA_DOWN') + c = rcol.operator("wm.y_move_in_out_layer_group_menu", text='', icon='TRIA_DOWN') c.direction = 'DOWN' c.move_out = False else: - c = rcol.operator("node.y_move_layer", text='', icon='TRIA_DOWN') + c = rcol.operator("wm.y_move_layer", text='', icon='TRIA_DOWN') c.direction = 'DOWN' else: if is_bl_newer_than(2, 80): - rcol.operator("node.y_remove_layer", icon='REMOVE', text='') - else: rcol.operator("node.y_remove_layer", icon='ZOOMOUT', text='') + rcol.operator("wm.y_remove_layer", icon='REMOVE', text='') + else: rcol.operator("wm.y_remove_layer", icon='ZOOMOUT', text='') - rcol.operator("node.y_move_layer", text='', icon='TRIA_UP').direction = 'UP' - rcol.operator("node.y_move_layer", text='', icon='TRIA_DOWN').direction = 'DOWN' + rcol.operator("wm.y_move_layer", text='', icon='TRIA_UP').direction = 'UP' + rcol.operator("wm.y_move_layer", text='', icon='TRIA_DOWN').direction = 'DOWN' rcol.menu("NODE_MT_y_layer_list_special_menu", text='', icon='DOWNARROW_HLT') @@ -3539,21 +3731,24 @@ def draw_layers_ui(context, layout, node): # Check if any images aren't using proper linear pipelines if any_linear_images_problem(yp): col.alert = True - col.operator('node.y_use_linear_color_space', text='Refresh Linear Color Space', icon='ERROR') + col.operator('wm.y_use_linear_color_space', text='Refresh Linear Color Space', icon='ERROR') col.alert = False # Check if AO is enabled or not scene = bpy.context.scene - if is_bl_newer_than(2, 93) and not scene.eevee.use_gtao and scene.render.engine != 'BLENDER_EEVEE_NEXT': - edge_detect_found = False + if is_bl_newer_than(2, 93) and not is_bl_newer_than(4, 2) and not scene.eevee.use_gtao: + ao_found = False for l in yp.layers: + if l.type in {'EDGE_DETECT', 'AO'} and l.enable: + ao_found = True + break for m in l.masks: - if m.type == 'EDGE_DETECT' and get_mask_enabled(m, l): - edge_detect_found = True + if m.type in {'EDGE_DETECT', 'AO'} and get_mask_enabled(m, l): + ao_found = True break - if edge_detect_found: + if ao_found: col.alert = True - col.operator('node.y_fix_edge_detect_ao', text='Fix EEVEE Edge Detect AO', icon='ERROR') + col.operator('wm.y_fix_edge_detect_ao', text='Fix EEVEE Edge Detect AO', icon='ERROR') col.alert = False if obj.type == 'MESH' and colorid_vcol: @@ -3614,13 +3809,23 @@ def draw_layers_ui(context, layout, node): elif obj.mode == 'VERTEX_PAINT' and is_bl_newer_than(2, 92) and ((layer.type == 'VCOL' and not mask_vcol) or (mask_vcol and mask.source_input == 'ALPHA')) and not override_vcol: bbox = col.box() row = bbox.row(align=True) - row.operator('paint.y_toggle_eraser', text='Toggle Eraser') + brush = context.tool_settings.vertex_paint.brush + label = 'Toggle Eraser' + if brush.name == eraser_names['VERTEX_PAINT']: + row.alert = True + label = 'Disable Eraser' + row.operator('paint.y_toggle_eraser', text=label) elif obj.mode == 'SCULPT' and is_bl_newer_than(3, 2) and ((layer.type == 'VCOL' and not mask_vcol) or (mask_vcol and mask.source_input == 'ALPHA')) and not override_vcol: bbox = col.box() row = bbox.row(align=True) - row.operator('paint.y_toggle_eraser', text='Toggle Eraser') + brush = context.tool_settings.sculpt.brush + label = 'Toggle Eraser' + if brush.name == eraser_names['SCULPT']: + row.alert = True + label = 'Disable Eraser' + row.operator('paint.y_toggle_eraser', text=label) # Only works with experimental sculpt texture paint is turned on in_sculpt_texture_paint_mode = obj.mode == 'SCULPT' and ( @@ -3634,20 +3839,36 @@ def draw_layers_ui(context, layout, node): if is_bl_newer_than(4, 3) and in_texture_paint_mode: brush = context.tool_settings.image_paint.brush - if brush.image_tool != 'MASK': + if brush and get_brush_image_tool(brush) != 'MASK': bbox = col.box() row = bbox.row(align=True) - row.operator('paint.y_toggle_eraser', text='Toggle Eraser') + label = 'Toggle Eraser' + if brush.name in tex_eraser_asset_names or (brush not in tex_default_brushes and brush.blend == 'ERASE_ALPHA'): + row.alert = True + label = 'Disable Eraser' + row.operator('paint.y_toggle_eraser', text=label) elif in_texture_paint_mode or in_sculpt_texture_paint_mode: bbox = col.box() row = bbox.row(align=True) - row.operator('paint.y_toggle_eraser', text='Toggle Eraser') + if in_texture_paint_mode: + brush = context.tool_settings.image_paint.brush + label = 'Toggle Eraser' + if brush.name == eraser_names['TEXTURE_PAINT']: + row.alert = True + label = 'Disable Eraser' + elif in_sculpt_texture_paint_mode: + brush = context.tool_settings.sculpt.brush + label = 'Toggle Eraser' + if brush.name == eraser_names['SCULPT']: + row.alert = True + label = 'Disable Eraser' + row.operator('paint.y_toggle_eraser', text=label) ve = context.scene.ve_edit if is_bl_newer_than(4, 3) and in_texture_paint_mode: brush = context.tool_settings.image_paint.brush - if ((mask_image and mask.source_input == 'RGB') or override_image) and (brush.name in tex_eraser_asset_names or brush.blend == 'ERASE_ALPHA'): + if brush and ((mask_image and mask.source_input == 'RGB') or override_image) and (brush.name in tex_eraser_asset_names or brush.blend == 'ERASE_ALPHA'): bbox = col.box() row = bbox.row(align=True) row.alert = True @@ -3656,7 +3877,7 @@ def draw_layers_ui(context, layout, node): elif in_texture_paint_mode or in_sculpt_texture_paint_mode: brush = context.tool_settings.image_paint.brush if in_texture_paint_mode else context.tool_settings.sculpt.brush - if ((mask_image and mask.source_input == 'RGB') or override_image) and brush.name == eraser_names[obj.mode]: + if brush and ((mask_image and mask.source_input == 'RGB') or override_image) and brush.name == eraser_names[obj.mode]: bbox = col.box() row = bbox.row(align=True) row.alert = True @@ -3665,7 +3886,7 @@ def draw_layers_ui(context, layout, node): elif obj.mode == 'VERTEX_PAINT' and is_bl_newer_than(2, 80): brush = context.tool_settings.vertex_paint.brush - if mask_vcol and mask.source_input == 'RGB' and brush.name == eraser_names[obj.mode]: + if brush and mask_vcol and mask.source_input == 'RGB' and brush.name == eraser_names[obj.mode]: bbox = col.box() row = bbox.row(align=True) row.alert = True @@ -3674,7 +3895,7 @@ def draw_layers_ui(context, layout, node): elif obj.mode == 'SCULPT' and is_bl_newer_than(3, 2): brush = context.tool_settings.sculpt.brush - if mask_vcol and mask.source_input == 'RGB' and brush.name == eraser_names[obj.mode]: + if brush and mask_vcol and mask.source_input == 'RGB' and brush.name == eraser_names[obj.mode]: bbox = col.box() row = bbox.row(align=True) row.alert = True @@ -3687,18 +3908,18 @@ def draw_layers_ui(context, layout, node): bbox = col.box() row = bbox.row(align=True) row.alert = True - row.operator('node.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') + row.operator('wm.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') elif obj.data.uv_layers.active.name == TEMP_UV: bbox = col.box() row = bbox.row(align=True) row.alert = True - row.operator('node.y_back_to_original_uv', icon='EDITMODE_HLT', text='Edit Original UV') + row.operator('wm.y_back_to_original_uv', icon='EDITMODE_HLT', text='Edit Original UV') else: if yp.need_temp_uv_refresh or is_active_uv_map_missmatch_active_entity(obj, layer): bbox = col.box() row = bbox.row(align=True) row.alert = True - row.operator('node.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') + row.operator('wm.y_refresh_transformed_uv', icon='FILE_REFRESH', text='Refresh UV') if is_a_mesh and is_bl_newer_than(3, 2): height_layer_ch = get_height_channel(layer) @@ -3724,7 +3945,7 @@ def draw_layers_ui(context, layout, node): cbox = bbox.column() row = cbox.row(align=True) row.alert = True - row.operator('node.y_refresh_list_items', icon='FILE_REFRESH', text='Refresh Layer List') + row.operator('wm.y_refresh_list_items', icon='FILE_REFRESH', text='Refresh Layer List') row.alert = False specific_ch = None @@ -3789,7 +4010,7 @@ def draw_test_ui(context, layout): col.label(text='Run test with default cube scene!') if obj and obj.name == 'Cube' and mat and mat.name == 'Material' and not node: - col.operator('node.y_run_automated_test') + col.operator('wm.y_run_automated_test') if (wmyp.test_result_run != 0): col.label(text=pgettext_iface('Test Run Count: ') + str(wmyp.test_result_run)) @@ -3803,6 +4024,7 @@ def main_draw(self, context): scene = context.scene obj = context.object mat = obj.active_material + ypup = get_user_preferences() #slot = context.material_slot #space = context.space_data @@ -3819,8 +4041,8 @@ def main_draw(self, context): layout = self.layout - #layout.operator("node.y_debug_mesh", icon='MESH_DATA') - #layout.operator("node.y_test_ray", icon='MESH_DATA') + #layout.operator("wm.y_debug_mesh", icon='MESH_DATA') + #layout.operator("wm.y_test_ray", icon='MESH_DATA') from . import addon_updater_ops @@ -3953,7 +4175,7 @@ def main_draw(self, context): if not node: layout.label(text="No active " + get_addon_title() + " node!", icon='ERROR') - layout.operator("node.y_quick_ypaint_node_setup", icon_value=lib.get_icon('nodetree')) + layout.operator("wm.y_quick_ypaint_node_setup", icon_value=lib.get_icon('nodetree')) # Test draw_test_ui(context=context, layout=layout) @@ -3968,9 +4190,34 @@ def main_draw(self, context): col = layout.column() col.alert = True col.label(text=group_tree.name + ' (' + yp.version + ')', icon_value=lib.get_icon('nodetree')) - col.operator("node.y_update_yp_trees", text='Update node to version ' + get_current_version_str(), icon='ERROR') + col.operator("wm.y_update_yp_trees", text='Update node to version ' + get_current_version_str(), icon='ERROR') return + # Message will appear when opening file with newer node version + if version_tuple(yp.version) > version_tuple(get_current_version_str()): + col = layout.column() + col.alert = True + col.label(text='This node uses newer version!', icon='ERROR') + if 'addon_updater_ops' not in dir(): + # Extension platform releases link + col.operator('wm.url_open', text='Update '+get_addon_title(), icon='ERROR').url = 'https://extensions.blender.org/add-ons/ucupaint/' + else: + if is_online(): + # Blender with online access already has the update button + col.label(text='Please update the addon!', icon='BLANK1') + else: + # Github releases link + col.operator('wm.url_open', text='Update '+get_addon_title(), icon='ERROR').url = 'https://github.com/ucupumar/ucupaint/releases' + + if ypup.developer_mode: + height_root_ch = get_root_height_channel(yp) + if height_root_ch and height_root_ch.enable_smooth_bump: + col = layout.column() + col.alert = True + col.label(text='Smooth(er) bump is no longer supported!', icon='ERROR') + col.operator("wm.y_update_remove_smooth_bump", text='Remove Smooth Bump') + #return + #layout.label(text='Active: ' + node.node_tree.name, icon_value=lib.get_icon('nodetree')) row = layout.row(align=True) row.label(text='', icon_value=lib.get_icon('nodetree')) @@ -4004,7 +4251,7 @@ def main_draw(self, context): #if (baked_found or yp.use_baked) and not group_tree.users > 1: # rrow = row.row(align=True) # rrow.alignment = 'RIGHT' - # rrow.operator('node.y_bake_channels', text='Rebake', icon_value=lib.get_icon('bake')).only_active_channel = False + # rrow.operator('wm.y_bake_channels', text='Rebake', icon_value=lib.get_icon('bake')).only_active_channel = False # rrow.separator() # rrow.prop(yp, 'use_baked', toggle=True, text='Use Baked') # rrow.prop(yp, 'enable_baked_outside', toggle=True, text='', icon='NODETREE') @@ -4033,12 +4280,13 @@ def main_draw(self, context): if scenario_1: rrow = row.row(align=True) rrow.alignment = 'RIGHT' - rrow.operator('node.y_refresh_tangent_sign_vcol', icon='FILE_REFRESH', text='Tangent') + rrow.operator('wm.y_refresh_tangent_sign_vcol', icon='FILE_REFRESH', text='Tangent') if (baked_found or yp.use_baked) and not group_tree.users > 1: rrow = row.row(align=True) - rrow.alignment = 'RIGHT' - rrow.operator('node.y_bake_channels', text='Rebake', icon_value=lib.get_icon('bake')).only_active_channel = False + if is_bl_newer_than(2, 80): + rrow.alignment = 'RIGHT' + rrow.operator('wm.y_bake_channels', text='Rebake', icon_value=lib.get_icon('bake')).only_active_channel = False rrow.separator() rrow.prop(yp, 'use_baked', toggle=True, text='Use Baked') rrow.prop(yp, 'enable_baked_outside', toggle=True, text='', icon='NODETREE') @@ -4207,9 +4455,9 @@ def main_draw(self, context): col.label(text=pgettext_iface('Number of Color Ramps: ') + str(num_ramps), icon_value=lib.get_icon('modifier')) col.label(text=pgettext_iface('Number of RGB Curves: ') + str(num_curves), icon_value=lib.get_icon('modifier')) - #col.operator('node.y_new_image_atlas_segment_test', icon_value=lib.get_icon('image')) - #col.operator('node.y_new_udim_atlas_segment_test', icon_value=lib.get_icon('image')) - #col.operator('node.y_uv_transform_test', icon_value=lib.get_icon('uv')) + #col.operator('wm.y_new_image_atlas_segment_test', icon_value=lib.get_icon('image')) + #col.operator('wm.y_new_udim_atlas_segment_test', icon_value=lib.get_icon('image')) + #col.operator('wm.y_uv_transform_test', icon_value=lib.get_icon('uv')) # Test draw_test_ui(context=context, layout=layout) @@ -4340,7 +4588,7 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn icon_value = lib.get_icon(lib.channel_custom_icon_dict[item.type]) row.prop(item, 'name', text='', emboss=False, icon_value=icon_value) - if not yp.use_baked or item.no_layer_using: + if not yp.use_baked or (item.no_layer_using and not (yp.use_baked and yp.enable_baked_outside)): if item.type == 'RGB': row = row.row(align=True) @@ -4376,7 +4624,7 @@ def any_subitem_in_layer(layer): root_ch = yp.channels[i] if (root_ch.type == 'NORMAL' and ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} - and ch.override and ch.override_type in {'IMAGE', 'VCOL'} + and ch.override and ch.override_type != 'DEFAULT' ): return True @@ -4385,7 +4633,7 @@ def any_subitem_in_layer(layer): ): return True - elif root_ch.type != 'NORMAL' and ch.override and ch.override_type in {'IMAGE', 'VCOL'}: + elif root_ch.type != 'NORMAL' and ch.override and ch.override_type != 'DEFAULT': return True return False @@ -4429,6 +4677,20 @@ def get_ch_type_icon_prefix(layer, ch): if get_layer_channel_type(layer, ch) == 'NORMAL': return 'vector_' return '' +def get_ch_override_label(layer, ch, is_normal_override=False): + yp = ch.id_data.yp + + label = channel_override_labels[ch.override_type] + + root_ch = yp.channels[get_layer_channel_index(layer, ch)] + channel_label = root_ch.name + if root_ch.type == 'NORMAL' and not is_normal_override: + channel_label = 'Bump' + + label += ' ('+channel_label+')' + + return label + def layer_listing(layout, layer, show_expand=False): yp = layer.id_data.yp layer_tree = get_tree(layer) @@ -4522,6 +4784,8 @@ def layer_listing(layout, layer, show_expand=False): row.prop(layer, 'name', text='', emboss=False, icon_value=lib.get_icon('vertex_color')) elif layer.type == 'HEMI': row.prop(layer, 'name', text='', emboss=False, icon_value=lib.get_icon('hemi')) + elif layer.type in {'EDGE_DETECT', 'AO'}: + row.prop(layer, 'name', text='', emboss=False, icon_value=lib.get_icon('edge_detect')) elif layer.type == 'COLOR': row.prop(layer, 'name', text='', emboss=False, icon='COLOR') elif layer.type == 'BACKGROUND': row.prop(layer, 'name', text='', emboss=False, icon_value=lib.get_icon('background')) @@ -4546,6 +4810,8 @@ def layer_listing(layout, layer, show_expand=False): row.prop(active_override, ae_prop, text='', emboss=False, icon='COLOR') elif layer.type == 'HEMI': row.prop(active_override, ae_prop, text='', emboss=False, icon_value=lib.get_icon('hemi')) + elif layer.type in {'EDGE_DETECT', 'AO'}: + row.prop(active_override, ae_prop, text='', emboss=False, icon_value=lib.get_icon('edge_detect')) elif layer.type == 'BACKGROUND': row.prop(active_override, ae_prop, text='', emboss=False, icon_value=lib.get_icon('background')) elif layer.type == 'GROUP': @@ -4564,6 +4830,8 @@ def layer_listing(layout, layer, show_expand=False): row.label(text='', icon='COLOR') elif layer.type == 'HEMI': row.label(text='', icon_value=lib.get_icon('hemi')) + elif layer.type in {'EDGE_DETECT', 'AO'}: + row.label(text='', icon_value=lib.get_icon('edge_detect')) elif layer.type == 'BACKGROUND': row.label(text='', icon_value=lib.get_icon('background')) elif layer.type == 'GROUP': @@ -4595,7 +4863,8 @@ def layer_listing(layout, layer, show_expand=False): icon_name = get_ch_type_icon_prefix(layer, c) + 'vertex_color' row.label(text='', icon_value=lib.get_icon(icon_name)) else: - row.label(text='', icon_value=lib.get_icon('texture')) + icon_name = get_ch_type_icon_prefix(layer, c) + 'texture' + row.label(text='', icon_value=lib.get_icon(icon_name)) else: if c.override_type == 'IMAGE': src = get_channel_source(c, layer) @@ -4610,7 +4879,8 @@ def layer_listing(layout, layer, show_expand=False): icon_name = get_ch_type_icon_prefix(layer, c) + 'vertex_color' row.prop(c, 'active_edit', text='', emboss=False, icon_value=lib.get_icon(icon_name)) else: - row.prop(c, 'active_edit', text='', emboss=False, icon_value=lib.get_icon('texture')) + icon_name = get_ch_type_icon_prefix(layer, c) + 'texture' + row.prop(c, 'active_edit', text='', emboss=False, icon_value=lib.get_icon(icon_name)) if c.override_1 and c.override_1_type != 'DEFAULT' and c.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'}: row = master.row(align=True) @@ -4663,7 +4933,7 @@ def layer_listing(layout, layer, show_expand=False): row.label(text='', icon_value=lib.get_icon('hemi')) elif m.type == 'OBJECT_INDEX': row.label(text='', icon_value=lib.get_icon('object_index')) - elif m.type == 'EDGE_DETECT': + elif m.type in {'EDGE_DETECT', 'AO'}: row.label(text='', icon_value=lib.get_icon('edge_detect')) elif m.type == 'COLOR_ID': row.label(text='', icon_value=lib.get_icon('color')) @@ -4691,7 +4961,7 @@ def layer_listing(layout, layer, show_expand=False): row.prop(m, 'active_edit', text='', emboss=False, icon_value=lib.get_icon('hemi')) elif m.type == 'OBJECT_INDEX': row.prop(m, 'active_edit', text='', emboss=False, icon_value=lib.get_icon('object_index')) - elif m.type == 'EDGE_DETECT': + elif m.type in {'EDGE_DETECT', 'AO'}: row.prop(m, 'active_edit', text='', emboss=False, icon_value=lib.get_icon('edge_detect')) elif m.type == 'COLOR_ID': row.prop(m, 'active_edit', text='', emboss=False, icon_value=lib.get_icon('color')) @@ -4719,7 +4989,7 @@ def layer_listing(layout, layer, show_expand=False): #row.label(text='Vertex Color Override') row.prop(override_ch, 'override_vcol_name', text='', emboss=False) else: - row.label(text='Channel Override') + row.label(text=get_ch_override_label(layer, override_ch, override_ch.active_edit_1)) elif active_mask_image: if active_mask_image.yia.is_image_atlas or active_mask_image.yua.is_udim_atlas: row.prop(mask, 'name', text='', emboss=False) @@ -4887,8 +5157,6 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn ch_source = get_channel_source_1(ch, layer) override_type = ch.override_1_type - channel_icon = lib.channel_custom_icon_dict[root_ch.type] - ch_image = None if override_type == 'IMAGE' and ch_source and ch_source.image: ch_image = ch_source.image @@ -4902,7 +5170,8 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn icon_name = get_ch_type_icon_prefix(layer, ch) + 'vertex_color' row.prop(ch, 'override_vcol_name', text='', emboss=False, icon_value=lib.get_icon(icon_name)) else: - row.prop(item, 'name', text='', emboss=False, icon_value=lib.get_icon(channel_icon)) + icon_name = get_ch_type_icon_prefix(layer, ch) + 'texture' + row.label(text=get_ch_override_label(layer, ch, item.is_second_member), icon_value=lib.get_icon(icon_name)) if ch_image: # Asterisk icon to indicate dirty image @@ -4913,10 +5182,6 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn if ch_image.packed_file: row.label(text='', icon='PACKAGE') - #rrow = row.row(align=True) - #rrow.alignment = 'RIGHT' - #rrow.label(text=root_ch.name) - #rrow.label(text='', icon_value=lib.get_icon(channel_icon)) row.label(text='', icon='BLANK1') # Masks @@ -4967,7 +5232,7 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn row.prop(mask, 'name', text='', emboss=False, icon_value=lib.get_icon('hemi')) elif mask.type == 'OBJECT_INDEX': row.prop(mask, 'name', text='', emboss=False, icon_value=lib.get_icon('object_index')) - elif mask.type == 'EDGE_DETECT': + elif mask.type in {'EDGE_DETECT', 'AO'}: row.prop(mask, 'name', text='', emboss=False, icon_value=lib.get_icon('edge_detect')) elif mask.type == 'COLOR_ID': row.prop(mask, 'name', text='', emboss=False, icon_value=lib.get_icon('color')) @@ -5033,9 +5298,14 @@ def poll(cls, context): def draw(self, context): obj = context.object - op = self.layout.operator("node.y_open_images_from_material_to_single_layer", icon_value=lib.get_icon('image'), text='Open Material Images to Layer') - op.mat_name = context.mat_asset.name if hasattr(context, 'mat_asset') else '' - op.asset_library_path = context.mat_asset.full_library_path if hasattr(context, 'mat_asset') else '' + + mat_asset = getattr(context, 'mat_asset', None) + mat_name = mat_asset.name if mat_asset else '' + asset_library_path = mat_asset.full_library_path if mat_asset else '' + + op = self.layout.operator("wm.y_open_images_from_material_to_single_layer", icon_value=lib.get_icon('image'), text='Open Material Images to Layer') + op.mat_name = mat_name + op.asset_library_path = asset_library_path if obj.type == 'MESH': op.texcoord_type = 'UV' @@ -5044,6 +5314,10 @@ def draw(self, context): else: op.texcoord_type = 'Generated' + op = self.layout.operator("wm.y_open_layers_from_material", icon='PASTEDOWN') + op.mat_name = mat_name + op.asset_library_path = asset_library_path + def draw_yp_asset_browser_menu(self, context): assets = context.selected_assets if is_bl_newer_than(4) else context.selected_asset_files @@ -5083,15 +5357,15 @@ def draw(self, context): self.layout.label(text='Image: ' + filename) - op = self.layout.operator("node.y_open_image_to_layer", icon_value=lib.get_icon('image'), text="Open Image as Layer") + op = self.layout.operator("wm.y_open_image_to_layer", icon_value=lib.get_icon('image'), text="Open Image as Layer") op.file_browser_filepath = filepath op.texcoord_type = 'UV' - op = self.layout.operator("node.y_open_image_as_mask", icon_value=lib.get_icon('image'), text="Open Image as Mask") + op = self.layout.operator("wm.y_open_image_as_mask", icon_value=lib.get_icon('image'), text="Open Image as Mask") op.file_browser_filepath = filepath op.texcoord_type = 'UV' - op = self.layout.operator("node.y_new_layer", icon_value=lib.get_icon('image'), text="Open Image as Mask for New Layer") + op = self.layout.operator("wm.y_new_layer", icon_value=lib.get_icon('image'), text="Open Image as Mask for New Layer") op.type = 'COLOR' op.add_mask = True op.mask_type = 'IMAGE' @@ -5100,15 +5374,15 @@ def draw(self, context): self.layout.separator() - op = self.layout.operator("node.y_open_image_to_layer", icon_value=lib.get_icon('image'), text="Open Image as Decal Layer") + op = self.layout.operator("wm.y_open_image_to_layer", icon_value=lib.get_icon('image'), text="Open Image as Decal Layer") op.file_browser_filepath = filepath op.texcoord_type = 'Decal' - op = self.layout.operator("node.y_open_image_as_mask", icon_value=lib.get_icon('image'), text="Open Image as Decal Mask") + op = self.layout.operator("wm.y_open_image_as_mask", icon_value=lib.get_icon('image'), text="Open Image as Decal Mask") op.file_browser_filepath = filepath op.texcoord_type = 'Decal' - op = self.layout.operator("node.y_new_layer", icon_value=lib.get_icon('image'), text="Open Image as Decal Mask for New Layer") + op = self.layout.operator("wm.y_new_layer", icon_value=lib.get_icon('image'), text="Open Image as Decal Mask for New Layer") op.type = 'COLOR' op.add_mask = True op.mask_type = 'IMAGE' @@ -5140,6 +5414,8 @@ def draw_ypaint_about(self, context): col.operator('wm.url_open', text='morirain', icon=icon_name).url = 'https://github.com/morirain' col.operator('wm.url_open', text='kareemov03', icon=icon_name).url = 'https://www.artstation.com/kareem' col.operator('wm.url_open', text='passivestar', icon=icon_name).url = 'https://github.com/passivestar' + col.operator('wm.url_open', text='bappity', icon=icon_name).url = 'https://github.com/bappitybup' + col.operator('wm.url_open', text='bittie', icon=icon_name).url = 'https://github.com/BittieByte' col.separator() col.label(text='Documentation:') @@ -5226,20 +5502,20 @@ def draw(self, context): col = row.column() - col.operator('node.y_bake_channels', text='Bake All Channels', icon_value=lib.get_icon('bake')).only_active_channel = False - col.operator('node.y_rename_ypaint_tree', text='Rename Tree', icon_value=lib.get_icon('rename')) + col.operator('wm.y_bake_channels', text='Bake All Channels', icon_value=lib.get_icon('bake')).only_active_channel = False + col.operator('wm.y_rename_ypaint_tree', text='Rename Tree', icon_value=lib.get_icon('rename')) col.separator() - col.operator('node.y_remove_yp_node', icon_value=lib.get_icon('close')) + col.operator('wm.y_remove_yp_node', icon_value=lib.get_icon('close')) col.separator() - col.operator('node.y_clean_yp_caches', icon_value=lib.get_icon('clean')) + col.operator('wm.y_clean_yp_caches', icon_value=lib.get_icon('clean')) col.separator() - op = col.operator('node.y_duplicate_yp_nodes', text='Duplicate Material and ' + get_addon_title() + ' nodes', icon='COPY_ID') + op = col.operator('wm.y_duplicate_yp_nodes', text='Duplicate Material and ' + get_addon_title() + ' nodes', icon='COPY_ID') op.duplicate_material = True col.separator() @@ -5251,7 +5527,7 @@ def draw(self, context): else: icon = 'RADIOBUT_OFF' #row = col.row() - col.operator('node.y_change_active_ypaint_node', text=n.node_tree.name, icon=icon).name = n.name + col.operator('wm.y_change_active_ypaint_node', text=n.node_tree.name, icon=icon).name = n.name col.separator() col.label(text='Option:') @@ -5267,6 +5543,25 @@ def draw(self, context): #col.prop(ypui, 'disable_auto_temp_uv_update') #col.prop(yp, 'disable_quick_toggle') +class YBakeListSpecialMenu(bpy.types.Menu): + bl_idname = "NODE_MT_y_bake_list_special_menu" + bl_label = "Bake Special Menu" + bl_description = "Bake Special Menu" + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() + + def draw(self, context): + node = get_active_ypaint_node() + + row = self.layout.row() + col = row.column() + + col.operator('wm.y_copy_bake_target', icon='COPYDOWN') + col.operator('wm.y_paste_bake_target', icon='PASTEDOWN').paste_as_new = True + col.operator('wm.y_paste_bake_target', text="Paste Bake Target Values", icon='PASTEDOWN').paste_as_new = False + class YBakeTargetMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_bake_target_menu" bl_description = 'Bake Target Menu' @@ -5279,14 +5574,14 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() - col.operator('node.y_pack_image', icon='PACKAGE') - col.operator('node.y_save_image', icon='FILE_TICK') + col.operator('wm.y_pack_image', icon='PACKAGE') + col.operator('wm.y_save_image', icon='FILE_TICK') if context.image: if context.image.packed_file: - col.operator('node.y_save_as_image', text='Unpack As Image', icon='UGLYPACKAGE').copy = False - else: col.operator('node.y_save_as_image', text='Save As Image').copy = False - col.operator('node.y_save_as_image', text='Save an Image Copy...', icon='FILE_TICK').copy = True + col.operator('wm.y_save_as_image', text='Unpack As Image', icon='UGLYPACKAGE').copy = False + else: col.operator('wm.y_save_as_image', text='Save As Image').copy = False + col.operator('wm.y_save_as_image', text='Save an Image Copy...', icon='FILE_TICK').copy = True class YNewChannelMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_new_channel_menu" @@ -5302,13 +5597,13 @@ def draw(self, context): col.label(text='Add New Channel') icon_value = lib.get_icon(lib.channel_custom_icon_dict['VALUE']) - col.operator("node.y_add_new_ypaint_channel", icon_value=icon_value, text='Value').type = 'VALUE' + col.operator("wm.y_add_new_ypaint_channel", icon_value=icon_value, text='Value').type = 'VALUE' icon_value = lib.get_icon(lib.channel_custom_icon_dict['RGB']) - col.operator("node.y_add_new_ypaint_channel", icon_value=icon_value, text='RGB').type = 'RGB' + col.operator("wm.y_add_new_ypaint_channel", icon_value=icon_value, text='RGB').type = 'RGB' icon_value = lib.get_icon(lib.channel_custom_icon_dict['NORMAL']) - col.operator("node.y_add_new_ypaint_channel", icon_value=icon_value, text='Normal').type = 'NORMAL' + col.operator("wm.y_add_new_ypaint_channel", icon_value=icon_value, text='Normal').type = 'NORMAL' class YNewLayerMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_new_layer_menu" @@ -5330,56 +5625,56 @@ def draw(self, context): #col = self.layout.column(align=True) #col.context_pointer_set('group_node', context.group_node) #col.label(text='Image:') - col.operator("node.y_new_layer", text='New Image', icon_value=lib.get_icon('image')).type = 'IMAGE' + col.operator("wm.y_new_layer", text='New Image', icon_value=lib.get_icon('image')).type = 'IMAGE' #col.separator() - op = col.operator("node.y_open_image_to_layer", text='Open Image...') + op = col.operator("wm.y_open_image_to_layer", text='Open Image...') op.texcoord_type = 'UV' op.file_browser_filepath = '' - col.operator("node.y_open_available_data_to_layer", text='Open Available Image').type = 'IMAGE' + col.operator("wm.y_open_available_data_to_layer", text='Open Available Image').type = 'IMAGE' - col.operator("node.y_open_images_to_single_layer", text='Open Images to Single Layer...') - col.operator("node.y_open_images_from_material_to_single_layer", text='Open Images from Material').asset_library_path = '' + col.operator("wm.y_open_images_to_single_layer", text='Open Images to Single Layer...') + col.operator("wm.y_open_images_from_material_to_single_layer", text='Open Images from Material').asset_library_path = '' # NOTE: Dedicated menu for opening images to single layer is kinda hard to see, so it's probably better be hidden for now #col.menu("NODE_MT_y_open_images_to_single_layer_menu", text='Open Images to Single Layer') col.separator() - col.operator("node.y_new_layer", icon_value=lib.get_icon('group'), text='Layer Group').type = 'GROUP' + col.operator("wm.y_new_layer", icon_value=lib.get_icon('group'), text='Layer Group').type = 'GROUP' col.separator() #col.label(text='Vertex Color:') - #col.operator("node.y_new_layer", icon='GROUP_VCOL', text='New Vertex Color').type = 'VCOL' - col.operator("node.y_new_layer", icon_value=lib.get_icon('vertex_color'), text='New Vertex Color').type = 'VCOL' - col.operator("node.y_open_available_data_to_layer", text='Open Available Vertex Color').type = 'VCOL' + #col.operator("wm.y_new_layer", icon='GROUP_VCOL', text='New Vertex Color').type = 'VCOL' + col.operator("wm.y_new_layer", icon_value=lib.get_icon('vertex_color'), text='New Vertex Color').type = 'VCOL' + col.operator("wm.y_open_available_data_to_layer", text='Open Available Vertex Color').type = 'VCOL' col.separator() #col.menu("NODE_MT_y_new_solid_color_layer_menu", text='Solid Color', icon_value=lib.get_icon('color')) icon_value = lib.get_icon("color") - c = col.operator("node.y_new_layer", icon_value=icon_value, text='Solid Color') + c = col.operator("wm.y_new_layer", icon_value=icon_value, text='Solid Color') c.type = 'COLOR' c.add_mask = False - c = col.operator("node.y_new_layer", text='Solid Color w/ Image Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Image Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'IMAGE' - c = col.operator("node.y_new_layer", text='Solid Color w/ Vertex Color Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Vertex Color Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'VCOL' - c = col.operator("node.y_new_layer", text='Solid Color w/ Color ID Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Color ID Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'COLOR_ID' if is_bl_newer_than(2, 93): - c = col.operator("node.y_new_layer", text='Solid Color w/ Edge Detect Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Edge Detect Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'EDGE_DETECT' @@ -5387,15 +5682,15 @@ def draw(self, context): col.separator() #col.label(text='Background:') - c = col.operator("node.y_new_layer", icon_value=lib.get_icon('background'), text='Background w/ Image Mask') + c = col.operator("wm.y_new_layer", icon_value=lib.get_icon('background'), text='Background w/ Image Mask') c.type = 'BACKGROUND' c.add_mask = True c.mask_type = 'IMAGE' #if is_bl_newer_than(2, 80): - # c = col.operator("node.y_new_layer", text='Background w/ Vertex Color Mask') - #else: c = col.operator("node.y_new_layer", text='Background w/ Vertex Color Mask') - c = col.operator("node.y_new_layer", text='Background w/ Vertex Color Mask') + # c = col.operator("wm.y_new_layer", text='Background w/ Vertex Color Mask') + #else: c = col.operator("wm.y_new_layer", text='Background w/ Vertex Color Mask') + c = col.operator("wm.y_new_layer", text='Background w/ Vertex Color Mask') c.type = 'BACKGROUND' c.add_mask = True @@ -5403,64 +5698,69 @@ def draw(self, context): if is_bl_newer_than(3, 2): col.separator() - col.operator("node.y_new_vdm_layer", text='Vector Displacement Image', icon='SCULPTMODE_HLT') + col.operator("wm.y_new_vdm_layer", text='Vector Displacement Image', icon='SCULPTMODE_HLT') col = row.column() col.label(text='Generated Layer:') - #col.operator("node.y_new_layer", icon='TEXTURE', text='Brick').type = 'BRICK' - col.operator("node.y_new_layer", icon_value=lib.get_icon('texture'), text='Brick').type = 'BRICK' - col.operator("node.y_new_layer", text='Checker').type = 'CHECKER' - col.operator("node.y_new_layer", text='Gradient').type = 'GRADIENT' - col.operator("node.y_new_layer", text='Magic').type = 'MAGIC' - if not is_bl_newer_than(4, 1): col.operator("node.y_new_layer", text='Musgrave').type = 'MUSGRAVE' - col.operator("node.y_new_layer", text='Noise').type = 'NOISE' - if is_bl_newer_than(4, 3): col.operator("node.y_new_layer", text='Gabor').type = 'GABOR' - col.operator("node.y_new_layer", text='Voronoi').type = 'VORONOI' - col.operator("node.y_new_layer", text='Wave').type = 'WAVE' + #col.operator("wm.y_new_layer", icon='TEXTURE', text='Brick').type = 'BRICK' + col.operator("wm.y_new_layer", icon_value=lib.get_icon('texture'), text='Brick').type = 'BRICK' + col.operator("wm.y_new_layer", text='Checker').type = 'CHECKER' + col.operator("wm.y_new_layer", text='Gradient').type = 'GRADIENT' + col.operator("wm.y_new_layer", text='Magic').type = 'MAGIC' + if not is_bl_newer_than(4, 1): col.operator("wm.y_new_layer", text='Musgrave').type = 'MUSGRAVE' + col.operator("wm.y_new_layer", text='Noise').type = 'NOISE' + if is_bl_newer_than(4, 3): col.operator("wm.y_new_layer", text='Gabor').type = 'GABOR' + col.operator("wm.y_new_layer", text='Voronoi').type = 'VORONOI' + col.operator("wm.y_new_layer", text='Wave').type = 'WAVE' col.separator() - col.operator("node.y_new_layer", icon_value=lib.get_icon('hemi'), text='Fake Lighting').type = 'HEMI' + col.operator("wm.y_new_layer", icon_value=lib.get_icon('hemi'), text='Fake Lighting').type = 'HEMI' + if is_bl_newer_than(2, 93): + col.separator() + col.operator("wm.y_new_layer", icon_value=lib.get_icon('edge_detect'), text='Ambient Occlusion').type = 'AO' + col.operator("wm.y_new_layer", text='Edge Detect').type = 'EDGE_DETECT' col = row.column() col.label(text='Bake as Layer:') - c = col.operator("node.y_bake_to_layer", icon_value=lib.get_icon('bake'), text='Ambient Occlusion') + c = col.operator("wm.y_bake_to_layer", icon_value=lib.get_icon('bake'), text='Ambient Occlusion') c.type = 'AO' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Pointiness') + c = col.operator("wm.y_bake_to_layer", text='Pointiness') c.type = 'POINTINESS' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Cavity') + c = col.operator("wm.y_bake_to_layer", text='Cavity') c.type = 'CAVITY' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Dust') + c = col.operator("wm.y_bake_to_layer", text='Dust') c.type = 'DUST' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Paint Base') + c = col.operator("wm.y_bake_to_layer", text='Paint Base') c.type = 'PAINT_BASE' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Bevel Normal') - c.type = 'BEVEL_NORMAL' - c.target_type = 'LAYER' - c.overwrite_current = False + if is_bl_newer_than(2, 80): + c = col.operator("wm.y_bake_to_layer", text='Bevel Normal') + c.type = 'BEVEL_NORMAL' + c.target_type = 'LAYER' + c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Bevel Grayscale') - c.type = 'BEVEL_MASK' - c.target_type = 'LAYER' - c.overwrite_current = False + c = col.operator("wm.y_bake_to_layer", text='Bevel Grayscale') + c.type = 'BEVEL_MASK' + c.target_type = 'LAYER' + c.overwrite_current = False # NOTE: Blender 2.76 does not bake to object space normal correctly if is_bl_newer_than(2, 77): - c = col.operator("node.y_bake_to_layer", text='Object Space Normal') + c = col.operator("wm.y_bake_to_layer", text='Object Space Normal') c.type = 'OBJECT_SPACE_NORMAL' c.target_type = 'LAYER' c.overwrite_current = False @@ -5468,12 +5768,12 @@ def draw(self, context): if is_bl_newer_than(2, 80): col.separator() - c = col.operator("node.y_bake_to_layer", text='Multires Normal') + c = col.operator("wm.y_bake_to_layer", text='Multires Normal') c.type = 'MULTIRES_NORMAL' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Multires Displacement') + c = col.operator("wm.y_bake_to_layer", text='Multires Displacement') c.type = 'MULTIRES_DISPLACEMENT' c.target_type = 'LAYER' c.overwrite_current = False @@ -5482,24 +5782,24 @@ def draw(self, context): if is_bl_newer_than(2, 77): col.separator() - c = col.operator("node.y_bake_to_layer", text='Other Objects Emission') + c = col.operator("wm.y_bake_to_layer", text='Other Objects Color') c.type = 'OTHER_OBJECT_EMISSION' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Other Objects Normal') + c = col.operator("wm.y_bake_to_layer", text='Other Objects Normal') c.type = 'OTHER_OBJECT_NORMAL' c.target_type = 'LAYER' c.overwrite_current = False - c = col.operator("node.y_bake_to_layer", text='Other Objects Channels') + c = col.operator("wm.y_bake_to_layer", text='Other Objects Channels') c.type = 'OTHER_OBJECT_CHANNELS' c.target_type = 'LAYER' c.overwrite_current = False col.separator() - c = col.operator("node.y_bake_to_layer", text='Selected Vertices') + c = col.operator("wm.y_bake_to_layer", text='Selected Vertices') c.type = 'SELECTED_VERTICES' c.target_type = 'LAYER' c.overwrite_current = False @@ -5507,7 +5807,7 @@ def draw(self, context): if ypup.show_experimental: col.separator() - c = col.operator("node.y_bake_to_layer", text='Flow') + c = col.operator("wm.y_bake_to_layer", text='Flow') c.type = 'FLOW' c.target_type = 'LAYER' c.overwrite_current = False @@ -5528,29 +5828,23 @@ def draw(self, context): yp = node.node_tree.yp #try: # root_ch = yp.channels[yp.active_channel_index] + root_ch = context.root_ch + row = col.row() row.active = not yp.enable_baked_outside - row.prop(context.root_ch, 'disable_global_baked', text='Disable Baked Image(s)', icon='RESTRICT_RENDER_ON') + label = 'Disable Baked ' + root_ch.name + row.prop(context.root_ch, 'disable_global_baked', text=label, icon='RESTRICT_RENDER_ON') col.separator() #except Exception as e: # print(e) - col.operator('node.y_pack_image', icon='PACKAGE') - col.operator('node.y_save_image', icon='FILE_TICK') + col.operator('wm.y_pack_image', icon='PACKAGE') + col.operator('wm.y_save_image', icon='FILE_TICK') if context.image.packed_file: - col.operator('node.y_save_as_image', text='Unpack As Image', icon='UGLYPACKAGE').copy = False - else: col.operator('node.y_save_as_image', text='Save As Image').copy = False - col.operator('node.y_save_as_image', text='Save an Image Copy...', icon='FILE_TICK').copy = True - - col.separator() - - icon = 'FILEBROWSER' if is_bl_newer_than(2, 80) else 'FILE_FOLDER' - col.operator('node.y_save_all_baked_images', text='Save All Baked Images to...', icon=icon).copy = False - col.operator('node.y_save_all_baked_images', text='Save All Baked Image Copies to...').copy = True - - col.separator() - col.operator('node.y_delete_baked_channel_images', text='Delete All Baked Images', icon='ERROR') + col.operator('wm.y_save_as_image', text='Unpack As Image', icon='UGLYPACKAGE').copy = False + else: col.operator('wm.y_save_as_image', text='Save As Image').copy = False + col.operator('wm.y_save_as_image', text='Save an Image Copy...', icon='FILE_TICK').copy = True class YLayerChannelNormalBlendMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_layer_channel_normal_blend_menu" @@ -5564,7 +5858,7 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() #align=True) for key, val in normal_blend_labels.items(): - col.operator('node.y_set_layer_channel_normal_blend_type', text=val).normal_blend_type = key + col.operator('wm.y_set_layer_channel_normal_blend_type', text=val).normal_blend_type = key class YLayerChannelBlendMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_layer_channel_blend_menu" @@ -5578,7 +5872,7 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() #align=True) for key, val in blend_type_labels.items(): - col.operator('node.y_set_layer_channel_blend_type', text=val).blend_type = key + col.operator('wm.y_set_layer_channel_blend_type', text=val).blend_type = key class YLayerChannelBlendPopover(bpy.types.Panel): bl_idname = "NODE_PT_y_layer_channel_blend_popover" @@ -5689,7 +5983,7 @@ def draw(self, context): draw_input_prop(col, ch, 'intensity_value', text='') def has_layer_input_options(layer): - return (layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'MUSGRAVE'} and not + return (layer.type not in {'IMAGE', 'VCOL', 'BACKGROUND', 'COLOR', 'GROUP', 'HEMI', 'MUSGRAVE', 'EDGE_DETECT', 'AO'} and not (is_bl_newer_than(2, 81) and layer.type == 'VORONOI' and layer.voronoi_feature in {'DISTANCE_TO_EDGE', 'N_SPHERE_RADIUS'})) class YLayerChannelInputMenu(bpy.types.Menu): @@ -5723,21 +6017,24 @@ def draw(self, context): col.separator() # Layer Color - label = 'Layer' - if is_bl_newer_than(2, 81) and layer.type == 'VORONOI': - if layer.voronoi_feature == 'DISTANCE_TO_EDGE': - label += ' Distance' - elif layer.voronoi_feature == 'N_SPHERE_RADIUS': - label += ' Radius' + if layer.type == 'GROUP': + label = 'Group ' + root_ch.name + else: + label = 'Layer' + if is_bl_newer_than(2, 81) and layer.type == 'VORONOI': + if layer.voronoi_feature == 'DISTANCE_TO_EDGE': + label += ' Distance' + elif layer.voronoi_feature == 'N_SPHERE_RADIUS': + label += ' Radius' + else: + label += ' Color' else: label += ' Color' - else: - label += ' Color' - if layer.type not in {'IMAGE', 'VCOL'}: - label += ' ('+layer_type_labels[layer.type]+')' + if layer.type not in {'IMAGE', 'VCOL'}: + label += ' ('+layer_type_labels[layer.type]+')' icon = 'RADIOBUT_ON' if not ch.override and (ch.layer_input == 'RGB' or not has_layer_input_options(layer)) else 'RADIOBUT_OFF' - op = col.operator('node.y_set_layer_channel_input', text=label, icon=icon) + op = col.operator('wm.y_set_layer_channel_input', text=label, icon=icon) op.type = 'RGB' op.set_normal_input = False @@ -5755,7 +6052,7 @@ def draw(self, context): label += ' ('+layer_type_labels[layer.type]+')' icon = 'RADIOBUT_ON' if ch.layer_input == 'ALPHA' and not ch.override else 'RADIOBUT_OFF' - op = col.operator('node.y_set_layer_channel_input', text=label, icon=icon) + op = col.operator('wm.y_set_layer_channel_input', text=label, icon=icon) op.type = 'ALPHA' op.set_normal_input = False @@ -5768,7 +6065,7 @@ def draw(self, context): else: label += ' Color' icon = 'RADIOBUT_ON' if ch.override and ch.override_type == 'DEFAULT' else 'RADIOBUT_OFF' - op = col.operator('node.y_set_layer_channel_input', text=label, icon=icon) + op = col.operator('wm.y_set_layer_channel_input', text=label, icon=icon) op.type = 'CUSTOM' op.set_normal_input = False @@ -5827,7 +6124,7 @@ def draw(self, context): label += ' ('+layer_type_labels[layer.type]+')' icon = 'RADIOBUT_ON' if not ch.override_1 else 'RADIOBUT_OFF' - op = col.operator('node.y_set_layer_channel_input', text=label, icon=icon) + op = col.operator('wm.y_set_layer_channel_input', text=label, icon=icon) op.type = 'RGB' op.set_normal_input = True @@ -5835,7 +6132,7 @@ def draw(self, context): # Custom/Override Default icon = 'RADIOBUT_ON' if ch.override_1 and ch.override_1_type == 'DEFAULT' else 'RADIOBUT_OFF' - op = col.operator('node.y_set_layer_channel_input', text='Custom Color', icon=icon) + op = col.operator('wm.y_set_layer_channel_input', text='Custom Color', icon=icon) op.type = 'CUSTOM' op.set_normal_input = True @@ -5869,23 +6166,25 @@ def draw(self, context): row = self.layout.row() col = row.column() - col.operator('node.y_merge_layer', text='Merge Layer Up', icon='TRIA_UP').direction = 'UP' - col.operator('node.y_merge_layer', text='Merge Layer Down', icon='TRIA_DOWN').direction = 'DOWN' + col.operator('wm.y_merge_layer', text='Merge Layer Up', icon='TRIA_UP').direction = 'UP' + col.operator('wm.y_merge_layer', text='Merge Layer Down', icon='TRIA_DOWN').direction = 'DOWN' col.separator() - c = col.operator("node.y_duplicate_layer", icon='COPY_ID', text='Duplicate Layer').duplicate_blank = False - c = col.operator("node.y_duplicate_layer", icon='COPY_ID', text='Duplicate Blank Layer').duplicate_blank = True + c = col.operator("wm.y_duplicate_layer", icon='COPY_ID', text='Duplicate Layer').duplicate_blank = False + c = col.operator("wm.y_duplicate_layer", icon='COPY_ID', text='Duplicate Blank Layer').duplicate_blank = True col.separator() - col.operator('node.y_copy_layer', text='Copy Layer', icon='COPYDOWN').all_layers = False - col.operator('node.y_copy_layer', text='Copy All Layers', icon='COPYDOWN').all_layers = True - col.operator('node.y_paste_layer', text='Paste Layer(s)', icon='PASTEDOWN') + col.operator('wm.y_copy_layer', text='Copy Layer', icon='COPYDOWN').all_layers = False + col.operator('wm.y_copy_layer', text='Copy All Layers', icon='COPYDOWN').all_layers = True + col.operator('wm.y_paste_layer', text='Paste Layer(s)', icon='PASTEDOWN') + + col.separator() + col.operator('wm.y_rebake_baked_images', text='Rebake All Baked Images', icon_value=lib.get_icon('bake')) if UDIM.is_udim_supported(): - col.separator() - col.operator('node.y_refill_udim_tiles', text='Refill UDIM Tiles', icon_value=lib.get_icon('uv')) + col.operator('wm.y_refill_udim_tiles', text='Refill UDIM Tiles', icon_value=lib.get_icon('uv')) #col.prop(yp, 'layer_preview_mode', text='Layer Only Viewer') @@ -5899,39 +6198,39 @@ def draw(self, context): col.label(text='No active image') #col.separator() - #col.operator('node.y_transfer_layer_uv', text='Transfer Active Layer UV', icon_value=lib.get_icon('uv')) - #col.operator('node.y_transfer_some_layer_uv', text='Transfer All Layers & Masks UV', icon_value=lib.get_icon('uv')) + #col.operator('wm.y_transfer_layer_uv', text='Transfer Active Layer UV', icon_value=lib.get_icon('uv')) + #col.operator('wm.y_transfer_some_layer_uv', text='Transfer All Layers & Masks UV', icon_value=lib.get_icon('uv')) #if hasattr(context, 'image') and context.image: col.separator() - op = col.operator('node.y_resize_image', text='Resize Image', icon='FULLSCREEN_ENTER') + op = col.operator('wm.y_resize_image', text='Resize Image', icon='FULLSCREEN_ENTER') if hasattr(context, 'layer'): op.layer_name = context.layer.name if hasattr(context, 'image'): op.image_name = context.image.name - col.operator("node.y_invert_image", icon='IMAGE_ALPHA') + col.operator("wm.y_invert_image", icon='IMAGE_ALPHA') col.separator() - col.operator('node.y_pack_image', icon='PACKAGE') - col.operator('node.y_save_image', icon='FILE_TICK') + col.operator('wm.y_pack_image', icon='PACKAGE') + col.operator('wm.y_save_image', icon='FILE_TICK') if hasattr(context, 'image') and context.image.packed_file: - col.operator('node.y_save_as_image', text='Unpack As Image...', icon='UGLYPACKAGE').copy = False + col.operator('wm.y_save_as_image', text='Unpack As Image...', icon='UGLYPACKAGE').copy = False else: if is_bl_newer_than(2, 80): - col.operator('node.y_save_as_image', text='Save As Image...').copy = False - else: col.operator('node.y_save_as_image', text='Save As Image...', icon='SAVE_AS').copy = False - col.operator('node.y_save_as_image', text='Save an Image Copy...', icon='FILE_TICK').copy = True + col.operator('wm.y_save_as_image', text='Save As Image...').copy = False + else: col.operator('wm.y_save_as_image', text='Save As Image...', icon='SAVE_AS').copy = False + col.operator('wm.y_save_as_image', text='Save an Image Copy...', icon='FILE_TICK').copy = True col.separator() - col.operator("node.y_reload_image", icon='FILE_REFRESH') + col.operator("wm.y_reload_image", icon='FILE_REFRESH') col.separator() if hasattr(context, 'image'): col.menu("NODE_MT_y_image_convert_menu", text='Convert Image') - if is_bl_newer_than(2, 80): col.operator('node.y_save_pack_all', text='Save/Pack All Images') - else: col.operator('node.y_save_pack_all', text='Save/Pack All Images', icon='FILE_TICK') + if is_bl_newer_than(2, 80): col.operator('wm.y_save_pack_all', text='Save/Pack All Images') + else: col.operator('wm.y_save_pack_all', text='Save/Pack All Images', icon='FILE_TICK') class YOpenImagesToSingleLayerMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_open_images_to_single_layer_menu" @@ -5945,8 +6244,8 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() - col.operator("node.y_open_images_to_single_layer", icon='FILE_FOLDER', text='From Directory') - col.operator("node.y_open_images_from_material_to_single_layer", icon='MATERIAL_DATA', text='From Material').asset_library_path = '' + col.operator("wm.y_open_images_to_single_layer", icon='FILE_FOLDER', text='From Directory') + col.operator("wm.y_open_images_from_material_to_single_layer", icon='MATERIAL_DATA', text='From Material').asset_library_path = '' class YNewSolidColorLayerMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_new_solid_color_layer_menu" @@ -5961,27 +6260,27 @@ def draw(self, context): col = self.layout.column() icon_value = lib.get_icon("color") - c = col.operator("node.y_new_layer", icon_value=icon_value, text='Solid Color') + c = col.operator("wm.y_new_layer", icon_value=icon_value, text='Solid Color') c.type = 'COLOR' c.add_mask = False - c = col.operator("node.y_new_layer", text='Solid Color w/ Image Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Image Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'IMAGE' - c = col.operator("node.y_new_layer", text='Solid Color w/ Vertex Color Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Vertex Color Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'VCOL' - c = col.operator("node.y_new_layer", text='Solid Color w/ Color ID Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Color ID Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'COLOR_ID' if is_bl_newer_than(2, 93): - c = col.operator("node.y_new_layer", text='Solid Color w/ Edge Detect Mask') + c = col.operator("wm.y_new_layer", text='Solid Color w/ Edge Detect Mask') c.type = 'COLOR' c.add_mask = True c.mask_type = 'EDGE_DETECT' @@ -6008,11 +6307,11 @@ def draw(self, context): col.separator() if context.image.yia.is_image_atlas or context.image.yua.is_udim_atlas: - col.operator("node.y_convert_to_standard_image", icon='IMAGE_DATA', text='Convert to standard Image').all_images = False - col.operator("node.y_convert_to_standard_image", icon='IMAGE_DATA', text='Convert All Image Atlas to standard Images').all_images = True + col.operator("wm.y_convert_to_standard_image", icon='IMAGE_DATA', text='Convert to standard Image').all_images = False + col.operator("wm.y_convert_to_standard_image", icon='IMAGE_DATA', text='Convert All Image Atlas to standard Images').all_images = True else: - col.operator("node.y_convert_to_image_atlas", icon='IMAGE_DATA', text='Convert to Image Atlas').all_images = False - col.operator("node.y_convert_to_image_atlas", icon='IMAGE_DATA', text='Convert All Images to Image Atlas').all_images = True + col.operator("wm.y_convert_to_image_atlas", icon='IMAGE_DATA', text='Convert to Image Atlas').all_images = False + col.operator("wm.y_convert_to_image_atlas", icon='IMAGE_DATA', text='Convert All Images to Image Atlas').all_images = True class YUVSpecialMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_uv_special_menu" @@ -6026,8 +6325,8 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() - col.operator('node.y_transfer_layer_uv', text='Transfer UV', icon_value=lib.get_icon('uv')) - col.operator('node.y_transfer_some_layer_uv', text='Transfer All Layers & Masks UV', icon_value=lib.get_icon('uv')) + col.operator('wm.y_transfer_layer_uv', text='Transfer UV', icon_value=lib.get_icon('uv')) + col.operator('wm.y_transfer_some_layer_uv', text='Transfer All Layers & Masks UV', icon_value=lib.get_icon('uv')) class YModifierMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_modifier_menu" @@ -6047,16 +6346,16 @@ def draw(self, context): col.label(text='ERROR: Context has no parent or modifier!', icon='ERROR') return - op = col.operator('node.y_move_ypaint_modifier', icon='TRIA_UP', text='Move Modifier Up') + op = col.operator('wm.y_move_ypaint_modifier', icon='TRIA_UP', text='Move Modifier Up') op.direction = 'UP' - op = col.operator('node.y_move_ypaint_modifier', icon='TRIA_DOWN', text='Move Modifier Down') + op = col.operator('wm.y_move_ypaint_modifier', icon='TRIA_DOWN', text='Move Modifier Down') op.direction = 'DOWN' col.separator() if is_bl_newer_than(2, 80): - op = col.operator('node.y_remove_ypaint_modifier', icon='REMOVE', text='Remove Modifier') - else: op = col.operator('node.y_remove_ypaint_modifier', icon='ZOOMOUT', text='Remove Modifier') + op = col.operator('wm.y_remove_ypaint_modifier', icon='REMOVE', text='Remove Modifier') + else: op = col.operator('wm.y_remove_ypaint_modifier', icon='ZOOMOUT', text='Remove Modifier') #if hasattr(context, 'layer') and context.modifier.type in {'RGB_TO_INTENSITY', 'OVERRIDE_COLOR'}: # col.separator() @@ -6080,16 +6379,16 @@ def draw(self, context): col.label(text='ERROR: Context has no parent or modifier!', icon='ERROR') return - op = col.operator('node.y_move_normalmap_modifier', icon='TRIA_UP', text='Move Modifier Up') + op = col.operator('wm.y_move_normalmap_modifier', icon='TRIA_UP', text='Move Modifier Up') op.direction = 'UP' - op = col.operator('node.y_move_normalmap_modifier', icon='TRIA_DOWN', text='Move Modifier Down') + op = col.operator('wm.y_move_normalmap_modifier', icon='TRIA_DOWN', text='Move Modifier Down') op.direction = 'DOWN' col.separator() if is_bl_newer_than(2, 80): - op = col.operator('node.y_remove_normalmap_modifier', icon='REMOVE', text='Remove Modifier') - else: op = col.operator('node.y_remove_normalmap_modifier', icon='ZOOMOUT', text='Remove Modifier') + op = col.operator('wm.y_remove_normalmap_modifier', icon='REMOVE', text='Remove Modifier') + else: op = col.operator('wm.y_remove_normalmap_modifier', icon='ZOOMOUT', text='Remove Modifier') class YMaskModifierMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_mask_modifier_menu" @@ -6109,17 +6408,17 @@ def draw(self, context): col.label(text='ERROR: Context has no mask, modifier, or layer!', icon='ERROR') return - op = col.operator('node.y_move_mask_modifier', icon='TRIA_UP', text='Move Modifier Up') + op = col.operator('wm.y_move_mask_modifier', icon='TRIA_UP', text='Move Modifier Up') op.direction = 'UP' - op = col.operator('node.y_move_mask_modifier', icon='TRIA_DOWN', text='Move Modifier Down') + op = col.operator('wm.y_move_mask_modifier', icon='TRIA_DOWN', text='Move Modifier Down') op.direction = 'DOWN' col.separator() if is_bl_newer_than(2, 80): - op = col.operator('node.y_remove_mask_modifier', icon='REMOVE', text='Remove Modifier') - else: op = col.operator('node.y_remove_mask_modifier', icon='ZOOMOUT', text='Remove Modifier') + op = col.operator('wm.y_remove_mask_modifier', icon='REMOVE', text='Remove Modifier') + else: op = col.operator('wm.y_remove_mask_modifier', icon='ZOOMOUT', text='Remove Modifier') class YTransitionBumpMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_transition_bump_menu" @@ -6141,8 +6440,8 @@ def draw(self, context): return if is_bl_newer_than(2, 80): - col.operator('node.y_hide_transition_effect', text='Remove Transition Bump', icon='REMOVE').type = 'BUMP' - else: col.operator('node.y_hide_transition_effect', text='Remove Transition Bump', icon='ZOOMOUT').type = 'BUMP' + col.operator('wm.y_hide_transition_effect', text='Remove Transition Bump', icon='REMOVE').type = 'BUMP' + else: col.operator('wm.y_hide_transition_effect', text='Remove Transition Bump', icon='ZOOMOUT').type = 'BUMP' class YTransitionRampMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_transition_ramp_menu" @@ -6167,8 +6466,8 @@ def draw(self, context): col.separator() if is_bl_newer_than(2, 80): - col.operator('node.y_hide_transition_effect', text='Remove Transition Ramp', icon='REMOVE').type = 'RAMP' - else: col.operator('node.y_hide_transition_effect', text='Remove Transition Ramp', icon='ZOOMOUT').type = 'RAMP' + col.operator('wm.y_hide_transition_effect', text='Remove Transition Ramp', icon='REMOVE').type = 'RAMP' + else: col.operator('wm.y_hide_transition_effect', text='Remove Transition Ramp', icon='ZOOMOUT').type = 'RAMP' class YTransitionAOMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_transition_ao_menu" @@ -6200,8 +6499,8 @@ def draw(self, context): col = layout.column() if is_bl_newer_than(2, 80): - col.operator('node.y_hide_transition_effect', text='Remove Transition AO', icon='REMOVE').type = 'AO' - else: col.operator('node.y_hide_transition_effect', text='Remove Transition AO', icon='ZOOMOUT').type = 'AO' + col.operator('wm.y_hide_transition_effect', text='Remove Transition AO', icon='REMOVE').type = 'AO' + else: col.operator('wm.y_hide_transition_effect', text='Remove Transition AO', icon='ZOOMOUT').type = 'AO' def new_mask_button(layout, operator, text, lib_icon='', otype='', target_type='', modifier_type='', overwrite_current=None): if lib_icon: @@ -6238,56 +6537,61 @@ def draw(self, context): col.context_pointer_set('layer', context.layer) col.label(text='Image Mask:') - new_mask_button(col, 'node.y_new_layer_mask', 'New Image Mask', lib_icon='image', otype='IMAGE') - op = new_mask_button(col, 'node.y_open_image_as_mask', 'Open Image as Mask...', lib_icon='open_image') + new_mask_button(col, 'wm.y_new_layer_mask', 'New Image Mask', lib_icon='image', otype='IMAGE') + op = new_mask_button(col, 'wm.y_open_image_as_mask', 'Open Image as Mask...', lib_icon='open_image') op.texcoord_type = 'UV' op.file_browser_filepath = '' - new_mask_button(col, 'node.y_open_available_data_as_mask', 'Open Available Image as Mask', lib_icon='open_image', otype='IMAGE') + new_mask_button(col, 'wm.y_open_available_data_as_mask', 'Open Available Image as Mask', lib_icon='open_image', otype='IMAGE') col.separator() col.label(text='Vertex Color Mask:') - new_mask_button(col, 'node.y_new_layer_mask', 'New Vertex Color Mask', lib_icon='vertex_color', otype='VCOL') - new_mask_button(col, 'node.y_open_available_data_as_mask', 'Open Available Vertex Color as Mask', lib_icon='vertex_color', otype='VCOL') + new_mask_button(col, 'wm.y_new_layer_mask', 'New Vertex Color Mask', lib_icon='vertex_color', otype='VCOL') + new_mask_button(col, 'wm.y_open_available_data_as_mask', 'Open Available Vertex Color as Mask', lib_icon='vertex_color', otype='VCOL') - new_mask_button(col, 'node.y_new_layer_mask', 'Color ID', lib_icon='color', otype='COLOR_ID') + new_mask_button(col, 'wm.y_new_layer_mask', 'Color ID', lib_icon='color', otype='COLOR_ID') col = row.column(align=True) col.label(text='Generated Mask:') - new_mask_button(col, 'node.y_new_layer_mask', 'Brick', otype='BRICK', lib_icon='texture') - new_mask_button(col, 'node.y_new_layer_mask', 'Checker', otype='CHECKER') - new_mask_button(col, 'node.y_new_layer_mask', 'Gradient', otype='GRADIENT') - new_mask_button(col, 'node.y_new_layer_mask', 'Magic', otype='MAGIC') - if not is_bl_newer_than(4, 1): new_mask_button(col, 'node.y_new_layer_mask', 'Musgrave', otype='MUSGRAVE') - new_mask_button(col, 'node.y_new_layer_mask', 'Noise', otype='NOISE') - if is_bl_newer_than(4, 3): new_mask_button(col, 'node.y_new_layer_mask', 'Gabor', otype='GABOR') - new_mask_button(col, 'node.y_new_layer_mask', 'Voronoi', otype='VORONOI') - new_mask_button(col, 'node.y_new_layer_mask', 'Wave', otype='WAVE') + new_mask_button(col, 'wm.y_new_layer_mask', 'Brick', otype='BRICK', lib_icon='texture') + new_mask_button(col, 'wm.y_new_layer_mask', 'Checker', otype='CHECKER') + new_mask_button(col, 'wm.y_new_layer_mask', 'Gradient', otype='GRADIENT') + new_mask_button(col, 'wm.y_new_layer_mask', 'Magic', otype='MAGIC') + if not is_bl_newer_than(4, 1): new_mask_button(col, 'wm.y_new_layer_mask', 'Musgrave', otype='MUSGRAVE') + new_mask_button(col, 'wm.y_new_layer_mask', 'Noise', otype='NOISE') + if is_bl_newer_than(4, 3): new_mask_button(col, 'wm.y_new_layer_mask', 'Gabor', otype='GABOR') + new_mask_button(col, 'wm.y_new_layer_mask', 'Voronoi', otype='VORONOI') + new_mask_button(col, 'wm.y_new_layer_mask', 'Wave', otype='WAVE') col.separator() - new_mask_button(col, 'node.y_new_layer_mask', 'Fake Lighting', lib_icon='hemi', otype='HEMI') + new_mask_button(col, 'wm.y_new_layer_mask', 'Fake Lighting', lib_icon='hemi', otype='HEMI') col.separator() - new_mask_button(col, 'node.y_new_layer_mask', 'Object Index', otype='OBJECT_INDEX', lib_icon='object_index') - new_mask_button(col, 'node.y_new_layer_mask', 'Backface', otype='BACKFACE', lib_icon='backface') + new_mask_button(col, 'wm.y_new_layer_mask', 'Object Index', otype='OBJECT_INDEX', lib_icon='object_index') + new_mask_button(col, 'wm.y_new_layer_mask', 'Backface', otype='BACKFACE', lib_icon='backface') if is_bl_newer_than(2, 93): - new_mask_button(col, 'node.y_new_layer_mask', 'Edge Detect', otype='EDGE_DETECT', lib_icon='edge_detect') + col.separator() + new_mask_button(col, 'wm.y_new_layer_mask', 'Ambient Occlusion', otype='AO', lib_icon='edge_detect') + new_mask_button(col, 'wm.y_new_layer_mask', 'Edge Detect', otype='EDGE_DETECT') col = row.column(align=True) col.label(text='Bake as Mask:') - new_mask_button(col, 'node.y_bake_to_layer', 'Ambient Occlusion', lib_icon='bake', otype='AO', target_type='MASK', overwrite_current=False) - new_mask_button(col, 'node.y_bake_to_layer', 'Pointiness', otype='POINTINESS', target_type='MASK', overwrite_current=False) - new_mask_button(col, 'node.y_bake_to_layer', 'Cavity', otype='CAVITY', target_type='MASK', overwrite_current=False) - new_mask_button(col, 'node.y_bake_to_layer', 'Dust', otype='DUST', target_type='MASK', overwrite_current=False) - new_mask_button(col, 'node.y_bake_to_layer', 'Paint Base', otype='PAINT_BASE', target_type='MASK', overwrite_current=False) - new_mask_button(col, 'node.y_bake_to_layer', 'Bevel Grayscale', otype='BEVEL_MASK', target_type='MASK', overwrite_current=False) - new_mask_button(col, 'node.y_bake_to_layer', 'Selected Vertices', otype='SELECTED_VERTICES', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Ambient Occlusion', lib_icon='bake', otype='AO', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Pointiness', otype='POINTINESS', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Cavity', otype='CAVITY', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Dust', otype='DUST', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Paint Base', otype='PAINT_BASE', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Bevel Grayscale', otype='BEVEL_MASK', target_type='MASK', overwrite_current=False) + new_mask_button(col, 'wm.y_bake_to_layer', 'Selected Vertices', otype='SELECTED_VERTICES', target_type='MASK', overwrite_current=False) + if is_bl_newer_than(2, 77): + col.separator() + new_mask_button(col, 'wm.y_bake_to_layer', 'Other Objects Color', otype='OTHER_OBJECT_EMISSION', target_type='MASK', overwrite_current=False) col.separator() col.label(text='Inbetween Modifier Mask:') - new_mask_button(col, 'node.y_new_layer_mask', 'Invert', otype='MODIFIER', modifier_type='INVERT', lib_icon='modifier') - new_mask_button(col, 'node.y_new_layer_mask', 'Ramp', otype='MODIFIER', modifier_type='RAMP', lib_icon='modifier') - new_mask_button(col, 'node.y_new_layer_mask', 'Curve', otype='MODIFIER', modifier_type='CURVE', lib_icon='modifier') + new_mask_button(col, 'wm.y_new_layer_mask', 'Invert', otype='MODIFIER', modifier_type='INVERT', lib_icon='modifier') + new_mask_button(col, 'wm.y_new_layer_mask', 'Ramp', otype='MODIFIER', modifier_type='RAMP', lib_icon='modifier') + new_mask_button(col, 'wm.y_new_layer_mask', 'Curve', otype='MODIFIER', modifier_type='CURVE', lib_icon='modifier') class YLayerMaskMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_layer_mask_menu" @@ -6317,63 +6621,48 @@ def draw(self, context): mask_tree = get_mask_tree(mask) source = mask_tree.nodes.get(mask.source) col.context_pointer_set('image', source.image) - col.operator('node.y_invert_image', text='Invert Image', icon='IMAGE_ALPHA') + col.operator('wm.y_invert_image', text='Invert Image', icon='IMAGE_ALPHA') col.separator() - op = col.operator('node.y_move_layer_mask', icon='TRIA_UP', text='Move Mask Up') + op = col.operator('wm.y_move_layer_mask', icon='TRIA_UP', text='Move Mask Up') op.direction = 'UP' - op = col.operator('node.y_move_layer_mask', icon='TRIA_DOWN', text='Move Mask Down') + op = col.operator('wm.y_move_layer_mask', icon='TRIA_DOWN', text='Move Mask Down') op.direction = 'DOWN' col.separator() - op = col.operator('node.y_merge_mask', icon='TRIA_UP', text='Merge Mask Up') + op = col.operator('wm.y_merge_mask', icon='TRIA_UP', text='Merge Mask Up') op.direction = 'UP' - op = col.operator('node.y_merge_mask', icon='TRIA_DOWN', text='Merge Mask Down') + op = col.operator('wm.y_merge_mask', icon='TRIA_DOWN', text='Merge Mask Down') op.direction = 'DOWN' col.separator() col.context_pointer_set('entity', mask) - col.operator('node.y_bake_entity_to_image', icon_value=lib.get_icon('bake'), text='Bake as Image') - if mask.baked_source != '': - if is_bl_newer_than(2, 80): - col.operator('node.y_remove_baked_entity', text='Remove Baked Image', icon='REMOVE') - else: col.operator('node.y_remove_baked_entity', text='Remove Baked Image', icon='ZOOMOUT') + col.operator('wm.y_bake_entity_to_image', icon_value=lib.get_icon('bake'), text='Bake as Image') col.separator() - #op = col.operator('node.y_transfer_layer_uv', icon_value=lib.get_icon('uv'), text='Transfer UV') + #op = col.operator('wm.y_transfer_layer_uv', icon_value=lib.get_icon('uv'), text='Transfer UV') #col.separator() if is_bl_newer_than(2, 80): - col.operator('node.y_remove_layer_mask', text='Remove Mask', icon='REMOVE') - else: col.operator('node.y_remove_layer_mask', text='Remove Mask', icon='ZOOMOUT') + col.operator('wm.y_remove_layer_mask', text='Remove Mask', icon='REMOVE') + else: col.operator('wm.y_remove_layer_mask', text='Remove Mask', icon='ZOOMOUT') col = row.column(align=True) col.label(text='Add Modifier') - col.operator('node.y_new_mask_modifier', text='Invert', icon_value=lib.get_icon('modifier')).type = 'INVERT' - col.operator('node.y_new_mask_modifier', text='Ramp', icon_value=lib.get_icon('modifier')).type = 'RAMP' - col.operator('node.y_new_mask_modifier', text='Curve', icon_value=lib.get_icon('modifier')).type = 'CURVE' + col.operator('wm.y_new_mask_modifier', text='Invert', icon_value=lib.get_icon('modifier')).type = 'INVERT' + col.operator('wm.y_new_mask_modifier', text='Ramp', icon_value=lib.get_icon('modifier')).type = 'RAMP' + col.operator('wm.y_new_mask_modifier', text='Curve', icon_value=lib.get_icon('modifier')).type = 'CURVE' #if mask.type not in {'VCOL', 'HEMI', 'OBJECT_INDEX', 'COLOR_ID'}: # col.separator() # col.prop(mask, 'enable_blur_vector', text='Blur Vector') - ypup = get_user_preferences() - - if ypup.developer_mode: - col = row.column(align=True) - col.context_pointer_set('parent', mask) - col.label(text='Advanced') - if not mask.use_temp_bake: - col.operator('node.y_bake_temp_image', text='Bake Temp Image', icon_value=lib.get_icon('bake')) - else: - col.operator('node.y_disable_temp_image', text='Disable Baked Temp Image', icon='FILE_REFRESH') - class YMaterialSpecialMenu(bpy.types.Menu): bl_idname = "MATERIAL_MT_y_special_menu" bl_label = "Material Special Menu" @@ -6385,8 +6674,8 @@ def poll(cls, context): def draw(self, context): col = self.layout.column() - col.operator('material.y_select_all_material_polygons', icon='FACESEL') - col.operator('material.y_rename_uv_using_the_same_material', icon='GROUP_UVS') + col.operator('wm.y_select_all_material_polygons', icon='FACESEL') + col.operator('wm.y_rename_uv_using_the_same_material', icon='GROUP_UVS') class YReplaceChannelOverrideMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_replace_channel_override_menu" @@ -6422,8 +6711,8 @@ def draw(self, context): #icon = 'RADIOBUT_ON' if ch.override_type == 'DEFAULT' else 'RADIOBUT_OFF' #if root_ch.type == 'VALUE': - # col.operator('node.y_replace_layer_channel_override', text='Value', icon=icon).type = 'DEFAULT' - #else: col.operator('node.y_replace_layer_channel_override', text='Color', icon=icon).type = 'DEFAULT' + # col.operator('wm.y_replace_layer_channel_override', text='Value', icon=icon).type = 'DEFAULT' + #else: col.operator('wm.y_replace_layer_channel_override', text='Color', icon=icon).type = 'DEFAULT' col.separator() @@ -6438,15 +6727,15 @@ def draw(self, context): icon = 'RADIOBUT_ON' if ch.override and ch.override_type == 'IMAGE' else 'RADIOBUT_OFF' if cache_image and (ch.override_type != 'IMAGE' or not ch.override): - col.operator('node.y_replace_layer_channel_override', text=label, icon=icon).type = 'IMAGE' + col.operator('wm.y_replace_layer_channel_override', text=label, icon=icon).type = 'IMAGE' else: col.label(text=label, icon=icon) row = col.row(align=True) ccol = row.column(align=True) - ccol.operator('node.y_open_image_to_override_layer_channel', text='Open Image...', icon_value=lib.get_icon('open_image')) - ccol.operator('node.y_open_available_data_to_override_channel', text='Open Available Image', icon_value=lib.get_icon('open_image')).type = 'IMAGE' + ccol.operator('wm.y_open_image_to_override_layer_channel', text='Open Image...', icon_value=lib.get_icon('open_image')) + ccol.operator('wm.y_open_available_data_to_override_channel', text='Open Available Image', icon_value=lib.get_icon('open_image')).type = 'IMAGE' col.separator() @@ -6460,16 +6749,16 @@ def draw(self, context): icon = 'RADIOBUT_ON' if ch.override and ch.override_type == 'VCOL' else 'RADIOBUT_OFF' if cache_vcol and (ch.override_type != 'VCOL' or not ch.override): - col.operator('node.y_replace_layer_channel_override', text=label, icon=icon).type = 'VCOL' + col.operator('wm.y_replace_layer_channel_override', text=label, icon=icon).type = 'VCOL' else: col.label(text=label, icon=icon) row = col.row(align=True) ccol = row.column(align=True) - #ccol.operator('node.y_replace_layer_channel_override', text='New Vertex Color', icon_value=lib.get_icon('vertex_color')).type = 'VCOL' - ccol.operator('node.y_new_vcol_to_override_channel', text='New Vertex Color', icon_value=lib.get_icon('vertex_color')) - ccol.operator('node.y_open_available_data_to_override_channel', text='Use Available Vertex Color', icon_value=lib.get_icon('vertex_color')).type = 'VCOL' + #ccol.operator('wm.y_replace_layer_channel_override', text='New Vertex Color', icon_value=lib.get_icon('vertex_color')).type = 'VCOL' + ccol.operator('wm.y_new_vcol_to_override_channel', text='New Vertex Color', icon_value=lib.get_icon('vertex_color')) + ccol.operator('wm.y_open_available_data_to_override_channel', text='Use Available Vertex Color', icon_value=lib.get_icon('vertex_color')).type = 'VCOL' col.separator() @@ -6477,13 +6766,13 @@ def draw(self, context): if item[0] == 'MUSGRAVE' and is_bl_newer_than(4, 1): continue if item[0] == 'GABOR' and not is_bl_newer_than(4, 3): continue - if item[0] == ch.override_type: + if ch.override and item[0] == ch.override_type: icon = 'RADIOBUT_ON' else: icon = 'RADIOBUT_OFF' if item[0] in {'DEFAULT', 'IMAGE', 'VCOL'}: continue - col.operator('node.y_replace_layer_channel_override', text=item[1], icon=icon).type = item[0] + col.operator('wm.y_replace_layer_channel_override', text=item[1], icon=icon).type = item[0] class YReplaceChannelOverride1Menu(bpy.types.Menu): bl_idname = "NODE_MT_y_replace_channel_override_1_menu" @@ -6518,9 +6807,9 @@ def draw(self, context): #icon = 'RADIOBUT_ON' if ch.override_1_type == 'DEFAULT' else 'RADIOBUT_OFF' ##if root_ch.type == 'VALUE': - ## col.operator('node.y_replace_layer_channel_override_1', text='Value', icon=icon).type = 'DEFAULT' + ## col.operator('wm.y_replace_layer_channel_override_1', text='Value', icon=icon).type = 'DEFAULT' ##else: - #col.operator('node.y_replace_layer_channel_override_1', text='Color', icon=icon).type = 'DEFAULT' + #col.operator('wm.y_replace_layer_channel_override_1', text='Color', icon=icon).type = 'DEFAULT' #col.separator() @@ -6535,7 +6824,7 @@ def draw(self, context): icon = 'RADIOBUT_ON' if ch.override_1 and ch.override_1_type == 'IMAGE' else 'RADIOBUT_OFF' if cache_1_image and (ch.override_1_type != 'IMAGE' or not ch.override_1): - col.operator('node.y_replace_layer_channel_override_1', text=label, icon=icon).type = 'IMAGE' + col.operator('wm.y_replace_layer_channel_override_1', text=label, icon=icon).type = 'IMAGE' else: col.label(text=label, icon=icon) @@ -6544,8 +6833,8 @@ def draw(self, context): #ccol.label(text='', icon='BLANK1') ccol = row.column(align=True) - ccol.operator('node.y_open_image_to_override_1_layer_channel', text='Open Image...', icon_value=lib.get_icon('open_image')) - ccol.operator('node.y_open_available_data_to_override_1_channel', text='Open Available Image', icon_value=lib.get_icon('open_image')) + ccol.operator('wm.y_open_image_to_override_1_layer_channel', text='Open Image...', icon_value=lib.get_icon('open_image')) + ccol.operator('wm.y_open_available_data_to_override_1_channel', text='Open Available Image', icon_value=lib.get_icon('open_image')) class YChannelSpecialMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_channel_special_menu" @@ -6566,7 +6855,7 @@ def draw(self, context): col.label(text='ERROR: Context has no parent!', icon='ERROR') return - col.operator('node.y_bake_channels', text="Bake " + context.parent.name + " Channel", icon_value=lib.get_icon('bake')).only_active_channel = True + col.operator('wm.y_bake_channels', text="Bake " + context.parent.name + " Channel", icon_value=lib.get_icon('bake')).only_active_channel = True if context.parent.type != 'NORMAL': col.separator() @@ -6577,15 +6866,15 @@ def draw(self, context): # Override color and multiplier modifier are deprecated if mt[0] == 'OVERRIDE_COLOR': continue if mt[0] == 'MULTIPLIER': continue - col.operator('node.y_new_ypaint_modifier', text=mt[1], icon_value=lib.get_icon('modifier')).type = mt[0] + col.operator('wm.y_new_ypaint_modifier', text=mt[1], icon_value=lib.get_icon('modifier')).type = mt[0] ypup = get_user_preferences() if ypup.show_experimental: col = row.column() col.label(text='Experimental') - col.operator('node.y_bake_channel_to_vcol', text='Bake Channel to Vertex Color', icon_value=lib.get_icon('vertex_color')).all_materials = False - col.operator('node.y_bake_channel_to_vcol', text='Bake Channel to Vertex Color (Batch All Materials)', icon_value=lib.get_icon('vertex_color')).all_materials = True + col.operator('wm.y_bake_channel_to_vcol', text='Bake Channel to Vertex Color', icon_value=lib.get_icon('vertex_color')).all_materials = False + col.operator('wm.y_bake_channel_to_vcol', text='Bake Channel to Vertex Color (Batch All Materials)', icon_value=lib.get_icon('vertex_color')).all_materials = True class YLayerChannelSpecialMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_layer_channel_special_menu" @@ -6637,23 +6926,23 @@ def draw(self, context): # Override color and multiplier modifier are deprecated if mt[0] == 'OVERRIDE_COLOR': continue if mt[0] == 'MULTIPLIER': continue - col.operator('node.y_new_ypaint_modifier', text=mt[1], icon_value=lib.get_icon('modifier')).type = mt[0] + col.operator('wm.y_new_ypaint_modifier', text=mt[1], icon_value=lib.get_icon('modifier')).type = mt[0] if is_bump_normal_layer_channel: col.separator() col.label(text='Add Modifier (Normal)') if is_normal_layer_channel or is_bump_normal_layer_channel: - col.operator('node.y_new_normalmap_modifier', text='Invert', icon_value=lib.get_icon('modifier')).type = 'INVERT' - col.operator('node.y_new_normalmap_modifier', text='Math', icon_value=lib.get_icon('modifier')).type = 'MATH' + col.operator('wm.y_new_normalmap_modifier', text='Invert', icon_value=lib.get_icon('modifier')).type = 'INVERT' + col.operator('wm.y_new_normalmap_modifier', text='Math', icon_value=lib.get_icon('modifier')).type = 'MATH' col = row.column() col.label(text='Transition Effects') if root_ch.type == 'NORMAL': - col.operator('node.y_show_transition_bump', text='Transition Bump', icon_value=lib.get_icon('background')) + col.operator('wm.y_show_transition_bump', text='Transition Bump', icon_value=lib.get_icon('background')) else: - col.operator('node.y_show_transition_ramp', text='Transition Ramp', icon_value=lib.get_icon('background')) - col.operator('node.y_show_transition_ao', text='Transition AO', icon_value=lib.get_icon('background')) + col.operator('wm.y_show_transition_ramp', text='Transition Ramp', icon_value=lib.get_icon('background')) + col.operator('wm.y_show_transition_ao', text='Transition AO', icon_value=lib.get_icon('background')) class YLayerTypeMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_layer_type_menu" @@ -6677,7 +6966,7 @@ def draw(self, context): cache_image = tree.nodes.get(layer.cache_image) if layer.type != 'IMAGE' and cache_image and cache_image.image: - op = col.operator('node.y_replace_layer_type', text='Image: ' + cache_image.image.name, icon='RADIOBUT_OFF') + op = col.operator('wm.y_replace_layer_type', text='Image: ' + cache_image.image.name, icon='RADIOBUT_OFF') op.type = 'IMAGE' op.load_item = False op.item_name = '' @@ -6689,8 +6978,12 @@ def draw(self, context): icon = 'RADIOBUT_ON' if layer.type == 'IMAGE' else 'RADIOBUT_OFF' col.label(text='Image' + suffix, icon=icon) - label = 'Open Available Image' if layer.type != 'IMAGE' else 'Replace Image' - op = col.operator('node.y_replace_layer_type', text=folder_emoji+label) #, icon_value=lib.get_icon('open_image')) + label = 'Open Image' if layer.type != 'IMAGE' else 'Replace Image' + op = col.operator('wm.y_open_image_to_replace_layer', text=folder_emoji+label) + + label = 'Open Available Image' if layer.type != 'IMAGE' else 'Replace with Available Image' + #label = 'Open Available Image' + op = col.operator('wm.y_replace_layer_type', text=folder_emoji+label) op.type = 'IMAGE' op.load_item = True @@ -6698,7 +6991,7 @@ def draw(self, context): cache_vcol = tree.nodes.get(layer.cache_vcol) if layer.type != 'VCOL' and cache_vcol and cache_vcol.attribute_name != '': - op = col.operator('node.y_replace_layer_type', text='Vertex Color: ' + cache_vcol.attribute_name, icon='RADIOBUT_OFF') + op = col.operator('wm.y_replace_layer_type', text='Vertex Color: ' + cache_vcol.attribute_name, icon='RADIOBUT_OFF') op.type = 'VCOL' op.load_item = False op.item_name = '' @@ -6711,55 +7004,61 @@ def draw(self, context): col.label(text='Vertex Color' + suffix, icon=icon) label = 'Open Available Vertex Color' if layer.type != 'VCOL' else 'Replace Vertex Color' - op = col.operator('node.y_replace_layer_type', text=folder_emoji+label) #, icon_value=lib.get_icon('vertex_color')) + op = col.operator('wm.y_replace_layer_type', text=folder_emoji+label) #, icon_value=lib.get_icon('vertex_color')) op.type = 'VCOL' op.load_item = True col.separator() icon = 'RADIOBUT_ON' if layer.type == 'COLOR' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Solid Color', icon=icon).type = 'COLOR' + col.operator('wm.y_replace_layer_type', text='Solid Color', icon=icon).type = 'COLOR' icon = 'RADIOBUT_ON' if layer.type == 'BACKGROUND' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Background', icon=icon).type = 'BACKGROUND' + col.operator('wm.y_replace_layer_type', text='Background', icon=icon).type = 'BACKGROUND' icon = 'RADIOBUT_ON' if layer.type == 'GROUP' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Group', icon=icon).type = 'GROUP' + col.operator('wm.y_replace_layer_type', text='Group', icon=icon).type = 'GROUP' col.separator() icon = 'RADIOBUT_ON' if layer.type == 'BRICK' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Brick', icon=icon).type = 'BRICK' + col.operator('wm.y_replace_layer_type', text='Brick', icon=icon).type = 'BRICK' icon = 'RADIOBUT_ON' if layer.type == 'CHECKER' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Checker', icon=icon).type = 'CHECKER' + col.operator('wm.y_replace_layer_type', text='Checker', icon=icon).type = 'CHECKER' icon = 'RADIOBUT_ON' if layer.type == 'GRADIENT' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Gradient', icon=icon).type = 'GRADIENT' + col.operator('wm.y_replace_layer_type', text='Gradient', icon=icon).type = 'GRADIENT' icon = 'RADIOBUT_ON' if layer.type == 'MAGIC' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Magic', icon=icon).type = 'MAGIC' + col.operator('wm.y_replace_layer_type', text='Magic', icon=icon).type = 'MAGIC' if not is_bl_newer_than(4, 1): icon = 'RADIOBUT_ON' if layer.type == 'MUSGRAVE' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Musgrave', icon=icon).type = 'MUSGRAVE' + col.operator('wm.y_replace_layer_type', text='Musgrave', icon=icon).type = 'MUSGRAVE' icon = 'RADIOBUT_ON' if layer.type == 'NOISE' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Noise', icon=icon).type = 'NOISE' + col.operator('wm.y_replace_layer_type', text='Noise', icon=icon).type = 'NOISE' if is_bl_newer_than(4, 3): icon = 'RADIOBUT_ON' if layer.type == 'GABOR' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Gabor', icon=icon).type = 'GABOR' + col.operator('wm.y_replace_layer_type', text='Gabor', icon=icon).type = 'GABOR' icon = 'RADIOBUT_ON' if layer.type == 'VORONOI' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Voronoi', icon=icon).type = 'VORONOI' + col.operator('wm.y_replace_layer_type', text='Voronoi', icon=icon).type = 'VORONOI' icon = 'RADIOBUT_ON' if layer.type == 'WAVE' else 'RADIOBUT_OFF' - col.operator('node.y_replace_layer_type', text='Wave', icon=icon).type = 'WAVE' + col.operator('wm.y_replace_layer_type', text='Wave', icon=icon).type = 'WAVE' col.separator() icon = 'RADIOBUT_ON' if layer.type == 'HEMI' else 'RADIOBUT_OFF' - col.operator("node.y_replace_layer_type", icon=icon, text='Fake Lighting').type = 'HEMI' + col.operator("wm.y_replace_layer_type", icon=icon, text='Fake Lighting').type = 'HEMI' + + icon = 'RADIOBUT_ON' if layer.type == 'AO' else 'RADIOBUT_OFF' + col.operator("wm.y_replace_layer_type", icon=icon, text='Ambient Occlusion').type = 'AO' + + icon = 'RADIOBUT_ON' if layer.type == 'EDGE_DETECT' else 'RADIOBUT_OFF' + col.operator("wm.y_replace_layer_type", icon=icon, text='Edge Detect').type = 'EDGE_DETECT' class YMaskTypeMenu(bpy.types.Menu): bl_idname = "NODE_MT_y_mask_type_menu" @@ -6784,7 +7083,7 @@ def draw(self, context): cache_image = tree.nodes.get(mask.cache_image) if mask.type != 'IMAGE' and cache_image and cache_image.image: - op = col.operator('node.y_replace_mask_type', text='Image: ' + cache_image.image.name, icon='RADIOBUT_OFF') + op = col.operator('wm.y_replace_mask_type', text='Image: ' + cache_image.image.name, icon='RADIOBUT_OFF') op.type = 'IMAGE' op.load_item = False op.item_name = '' @@ -6796,8 +7095,11 @@ def draw(self, context): icon = 'RADIOBUT_ON' if mask.type == 'IMAGE' else 'RADIOBUT_OFF' col.label(text='Image' + suffix, icon=icon) - label = 'Open Available Image' if mask.type != 'IMAGE' else 'Replace Image' - op = col.operator('node.y_replace_mask_type', text=folder_emoji+label) #, icon_value=lib.get_icon('open_image')) + label = 'Open Image' if mask.type != 'IMAGE' else 'Replace Image' + op = col.operator('wm.y_open_image_to_replace_mask', text=folder_emoji+label) + + label = 'Open Available Image' if mask.type != 'IMAGE' else 'Replace with Available Image' + op = col.operator('wm.y_replace_mask_type', text=folder_emoji+label) op.type = 'IMAGE' op.load_item = True @@ -6805,7 +7107,7 @@ def draw(self, context): cache_vcol = tree.nodes.get(mask.cache_vcol) if mask.type != 'VCOL' and cache_vcol and cache_vcol.attribute_name != '': - op = col.operator('node.y_replace_mask_type', text='Vertex Color: ' + cache_vcol.attribute_name, icon='RADIOBUT_OFF') + op = col.operator('wm.y_replace_mask_type', text='Vertex Color: ' + cache_vcol.attribute_name, icon='RADIOBUT_OFF') op.type = 'VCOL' op.load_item = False op.item_name = '' @@ -6818,84 +7120,87 @@ def draw(self, context): col.label(text='Vertex Color' + suffix, icon=icon) label = 'Open Available Vertex Color' if mask.type != 'VCOL' else 'Replace Vertex Color' - op = col.operator('node.y_replace_mask_type', text=folder_emoji+label) #, icon_value=lib.get_icon('vertex_color')) + op = col.operator('wm.y_replace_mask_type', text=folder_emoji+label) #, icon_value=lib.get_icon('vertex_color')) op.type = 'VCOL' op.load_item = True col.separator() #icon = 'RADIOBUT_ON' if mask.type == 'COLOR' else 'RADIOBUT_OFF' - #col.operator('node.y_replace_mask_type', text='Solid Color', icon=icon).type = 'COLOR' + #col.operator('wm.y_replace_mask_type', text='Solid Color', icon=icon).type = 'COLOR' #icon = 'RADIOBUT_ON' if mask.type == 'BACKGROUND' else 'RADIOBUT_OFF' - #col.operator('node.y_replace_mask_type', text='Background', icon=icon).type = 'BACKGROUND' + #col.operator('wm.y_replace_mask_type', text='Background', icon=icon).type = 'BACKGROUND' #icon = 'RADIOBUT_ON' if mask.type == 'GROUP' else 'RADIOBUT_OFF' - #col.operator('node.y_replace_mask_type', text='Group', icon=icon).type = 'GROUP' + #col.operator('wm.y_replace_mask_type', text='Group', icon=icon).type = 'GROUP' #col.separator() icon = 'RADIOBUT_ON' if mask.type == 'BRICK' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Brick', icon=icon).type = 'BRICK' + col.operator('wm.y_replace_mask_type', text='Brick', icon=icon).type = 'BRICK' icon = 'RADIOBUT_ON' if mask.type == 'CHECKER' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Checker', icon=icon).type = 'CHECKER' + col.operator('wm.y_replace_mask_type', text='Checker', icon=icon).type = 'CHECKER' icon = 'RADIOBUT_ON' if mask.type == 'GRADIENT' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Gradient', icon=icon).type = 'GRADIENT' + col.operator('wm.y_replace_mask_type', text='Gradient', icon=icon).type = 'GRADIENT' icon = 'RADIOBUT_ON' if mask.type == 'MAGIC' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Magic', icon=icon).type = 'MAGIC' + col.operator('wm.y_replace_mask_type', text='Magic', icon=icon).type = 'MAGIC' if not is_bl_newer_than(4, 1): icon = 'RADIOBUT_ON' if mask.type == 'MUSGRAVE' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Musgrave', icon=icon).type = 'MUSGRAVE' + col.operator('wm.y_replace_mask_type', text='Musgrave', icon=icon).type = 'MUSGRAVE' icon = 'RADIOBUT_ON' if mask.type == 'NOISE' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Noise', icon=icon).type = 'NOISE' + col.operator('wm.y_replace_mask_type', text='Noise', icon=icon).type = 'NOISE' if is_bl_newer_than(4, 3): icon = 'RADIOBUT_ON' if mask.type == 'GABOR' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Gabor', icon=icon).type = 'GABOR' + col.operator('wm.y_replace_mask_type', text='Gabor', icon=icon).type = 'GABOR' icon = 'RADIOBUT_ON' if mask.type == 'VORONOI' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Voronoi', icon=icon).type = 'VORONOI' + col.operator('wm.y_replace_mask_type', text='Voronoi', icon=icon).type = 'VORONOI' icon = 'RADIOBUT_ON' if mask.type == 'WAVE' else 'RADIOBUT_OFF' - col.operator('node.y_replace_mask_type', text='Wave', icon=icon).type = 'WAVE' + col.operator('wm.y_replace_mask_type', text='Wave', icon=icon).type = 'WAVE' col.separator() icon = 'RADIOBUT_ON' if mask.type == 'HEMI' else 'RADIOBUT_OFF' - col.operator("node.y_replace_mask_type", icon=icon, text='Fake Lighting').type = 'HEMI' + col.operator("wm.y_replace_mask_type", icon=icon, text='Fake Lighting').type = 'HEMI' col.separator() icon = 'RADIOBUT_ON' if mask.type == 'COLOR_ID' else 'RADIOBUT_OFF' - col.operator("node.y_replace_mask_type", icon=icon, text='Color ID').type = 'COLOR_ID' + col.operator("wm.y_replace_mask_type", icon=icon, text='Color ID').type = 'COLOR_ID' icon = 'RADIOBUT_ON' if mask.type == 'OBJECT_INDEX' else 'RADIOBUT_OFF' - col.operator("node.y_replace_mask_type", icon=icon, text='Object Index').type = 'OBJECT_INDEX' + col.operator("wm.y_replace_mask_type", icon=icon, text='Object Index').type = 'OBJECT_INDEX' icon = 'RADIOBUT_ON' if mask.type == 'BACKFACE' else 'RADIOBUT_OFF' - col.operator("node.y_replace_mask_type", icon=icon, text='Backface').type = 'BACKFACE' + col.operator("wm.y_replace_mask_type", icon=icon, text='Backface').type = 'BACKFACE' + + icon = 'RADIOBUT_ON' if mask.type == 'AO' else 'RADIOBUT_OFF' + col.operator("wm.y_replace_mask_type", icon=icon, text='Ambient Occlusion').type = 'AO' icon = 'RADIOBUT_ON' if mask.type == 'EDGE_DETECT' else 'RADIOBUT_OFF' - col.operator("node.y_replace_mask_type", icon=icon, text='Edge Detect').type = 'EDGE_DETECT' + col.operator("wm.y_replace_mask_type", icon=icon, text='Edge Detect').type = 'EDGE_DETECT' col.separator() icon = 'RADIOBUT_ON' if mask.type == 'MODIFIER' and mask.modifier_type == 'INVERT' else 'RADIOBUT_OFF' - op = col.operator("node.y_replace_mask_type", icon=icon, text='Invert Modifier') + op = col.operator("wm.y_replace_mask_type", icon=icon, text='Invert Modifier') op.type = 'MODIFIER' op.modifier_type = 'INVERT' icon = 'RADIOBUT_ON' if mask.type == 'MODIFIER' and mask.modifier_type == 'RAMP' else 'RADIOBUT_OFF' - op = col.operator("node.y_replace_mask_type", icon=icon, text='Ramp Modifier') + op = col.operator("wm.y_replace_mask_type", icon=icon, text='Ramp Modifier') op.type = 'MODIFIER' op.modifier_type = 'RAMP' icon = 'RADIOBUT_ON' if mask.type == 'MODIFIER' and mask.modifier_type == 'CURVE' else 'RADIOBUT_OFF' - op = col.operator("node.y_replace_mask_type", icon=icon, text='Curve Modifier') + op = col.operator("wm.y_replace_mask_type", icon=icon, text='Curve Modifier') op.type = 'MODIFIER' op.modifier_type = 'CURVE' @@ -6929,16 +7234,15 @@ def draw(self, context): # Override color modifier is deprecated if mt[0] == 'OVERRIDE_COLOR': continue if mt[0] == 'MULTIPLIER': continue - col.operator('node.y_new_ypaint_modifier', text=mt[1], icon_value=lib.get_icon('modifier')).type = mt[0] + col.operator('wm.y_new_ypaint_modifier', text=mt[1], icon_value=lib.get_icon('modifier')).type = mt[0] if ypup.developer_mode: - #if context.parent.type == 'HEMI': col = row.column() col.label(text='Advanced') - if context.parent.use_temp_bake: - col.operator('node.y_disable_temp_image', text='Disable Baked Temp Image', icon='FILE_REFRESH') - else: - col.operator('node.y_bake_temp_image', text='Bake Temp Image', icon_value=lib.get_icon('bake')) + + col.context_pointer_set('entity', context.parent) + col.context_pointer_set('layer', context.parent) + col.operator('wm.y_bake_entity_to_image', icon_value=lib.get_icon('bake'), text='Bake Layer as Image') #col = row.column() #col.label(text='Options:') @@ -7004,6 +7308,20 @@ def update_layer_ui(self, context): # # ListItem.refresh_list_items(yp) +def update_noncontextual_channel_ui(self, context): + group_node = get_active_ypaint_node() + if not group_node: return + yp = group_node.node_tree.yp + if len(yp.channels) == 0: return + + m = re.match(r'ypui\.channels\[(\d+)\]', self.path_from_id()) + + if m: ch = yp.channels[int(m.group(1))] + else: return + + if hasattr(ch, 'expand_baked_data'): + ch.expand_baked_data = self.expand_baked_data + def update_channel_ui(self, context): ypui = context.window_manager.ypui if ypui.halt_prop_update: return @@ -7260,6 +7578,13 @@ class YChannelUI(bpy.types.PropertyGroup): update = update_channel_ui ) + expand_baked_data : BoolProperty( + name = 'Baked Channel Data', + description = 'Expand baked channel data', + default = False, + update = update_noncontextual_channel_ui + ) + modifiers : CollectionProperty(type=YModifierUI) modifiers_1 : CollectionProperty(type=YModifierUI) @@ -7442,6 +7767,7 @@ class YPaintUI(bpy.types.PropertyGroup): # Group channel related UI channel_idx : IntProperty(default=0) channel_ui : PointerProperty(type=YChannelUI) + channels : CollectionProperty(type=YChannelUI) modifiers : CollectionProperty(type=YModifierUI) # Bake target related UI @@ -7475,7 +7801,7 @@ def add_new_ypaint_node_menu(self, context): l = self.layout l.operator_context = 'INVOKE_REGION_WIN' l.separator() - l.operator('node.y_add_new_ypaint_node', text=get_addon_title(), icon_value=lib.get_icon('nodetree')) + l.operator('wm.y_add_new_ypaint_node', text=get_addon_title(), icon_value=lib.get_icon('nodetree')) def copy_ui_settings(source, dest): for attr in dir(source): @@ -7532,6 +7858,7 @@ def register(): bpy.utils.register_class(YNewChannelMenu) bpy.utils.register_class(YNewLayerMenu) bpy.utils.register_class(YBakeTargetMenu) + bpy.utils.register_class(YBakeListSpecialMenu) bpy.utils.register_class(YBakedImageMenu) bpy.utils.register_class(YLayerListSpecialMenu) bpy.utils.register_class(YLayerChannelBlendMenu) @@ -7574,6 +7901,7 @@ def register(): bpy.utils.register_class(NODE_UL_YPaint_list_items) bpy.utils.register_class(YPAssetBrowserMenu) bpy.utils.register_class(YPFileBrowserMenu) + bpy.utils.register_class(NODE_MT_copy_image_path_menu) if not is_bl_newer_than(2, 80): bpy.utils.register_class(VIEW3D_PT_YPaint_tools) @@ -7613,6 +7941,7 @@ def unregister(): bpy.utils.unregister_class(YNewChannelMenu) bpy.utils.unregister_class(YNewLayerMenu) bpy.utils.unregister_class(YBakeTargetMenu) + bpy.utils.unregister_class(YBakeListSpecialMenu) bpy.utils.unregister_class(YBakedImageMenu) bpy.utils.unregister_class(YLayerListSpecialMenu) bpy.utils.unregister_class(YLayerChannelBlendMenu) @@ -7655,6 +7984,7 @@ def unregister(): bpy.utils.unregister_class(NODE_UL_YPaint_list_items) bpy.utils.unregister_class(YPAssetBrowserMenu) bpy.utils.unregister_class(YPFileBrowserMenu) + bpy.utils.unregister_class(NODE_MT_copy_image_path_menu) if not is_bl_newer_than(2, 80): bpy.utils.unregister_class(VIEW3D_PT_YPaint_tools) @@ -7671,6 +8001,9 @@ def unregister(): if is_bl_newer_than(3): bpy.types.ASSETBROWSER_MT_context_menu.remove(draw_yp_asset_browser_menu) + if is_bl_newer_than(2, 81): + bpy.types.FILEBROWSER_MT_context_menu.remove(draw_yp_file_browser_menu) + # Remove Handlers bpy.app.handlers.load_post.remove(yp_load_ui_settings) bpy.app.handlers.save_pre.remove(yp_save_ui_settings) diff --git a/vcol_editor.py b/vcol_editor.py index 7f08517b..61d54711 100644 --- a/vcol_editor.py +++ b/vcol_editor.py @@ -1,9 +1,23 @@ import bpy, bmesh, numpy, time, os +from bpy.app.handlers import persistent from mathutils import * from pathlib import Path from bpy.props import * from .common import * +tex_default_brush_eraser_pairs = { + 'Paint Hard' : 'Erase Hard', + 'Paint Soft' : 'Erase Soft', + 'Paint Hard Pressure' : 'Erase Hard Pressure', + 'Smear' : 'Erase Soft', + 'Airbrush' : 'Erase Soft', + 'Paint Soft Pressure' : 'Erase Soft', + 'Clone' : 'Erase Hard', + 'Blur' : 'Erase Soft', + 'Fill' : 'Erase Hard', + 'Mask' : 'Erase Soft', +} + class YSetActiveVcol(bpy.types.Operator): bl_idname = "mesh.y_set_active_vcol" bl_label = "Set Active Vertex Color" @@ -28,72 +42,56 @@ def execute(self, context): self.report({'ERROR'}, "There's no vertex color named " + self.vcol_name + '!') return {'CANCELLED'} -def set_brush_asset(brush_name, mode='TEXTURE_PAINT'): - - wmyp = bpy.context.window_manager.ypprops - - # Check asset brush caches first - bac = wmyp.brush_asset_caches.get(brush_name) - if bac: - blend_path = bac.blend_path - if blend_path != '': blend_path += os.sep - try: - bpy.ops.brush.asset_activate( - asset_library_type = bac.library_type, - asset_library_identifier = bac.library_name, - relative_asset_identifier = blend_path + "Brush\\" + brush_name - ) - return - except Exception as e: print(e) - - # Try local +def activate_essential_brush(brush_name, essential_asset_identifier): try: bpy.ops.brush.asset_activate( - asset_library_type = 'LOCAL', - asset_library_identifier = "", - relative_asset_identifier = 'Brush\\' + brush_name + asset_library_type = 'ESSENTIALS', + asset_library_identifier = '', + relative_asset_identifier = essential_asset_identifier + os.sep + "Brush" + os.sep + brush_name ) + return True + except Exception as e: print('EXCEPTIION:', e) - # Set up the cache for faster loading next time - bac = wmyp.brush_asset_caches.add() - bac.name = brush_name - bac.library_type = 'LOCAL' - bac.library_name = '' - bac.blend_path = '' - - return - except Exception as e: print(e) + return False - # Try essential - if mode == 'TEXTURE_PAINT': mode_type = 'texture' - elif mode == 'VERTEX_PAINT': mode_type = 'vertex' - elif mode == 'SCULPT': mode_type = 'sculpt' +def activate_local_brush(brush_name): try: - asset_identifier = "brushes\\essentials_brushes-mesh_" + mode_type + ".blend" bpy.ops.brush.asset_activate( - asset_library_type = 'ESSENTIALS', - asset_library_identifier = "", - relative_asset_identifier = asset_identifier + "\\Brush\\" + brush_name + asset_library_type = 'LOCAL', + asset_library_identifier = '', + relative_asset_identifier = 'Brush' + os.sep + brush_name ) + return True + except Exception as e: print('EXCEPTIION:', e) + + return False + +def activate_custom_brush(brush_name, filepath): + # Get asset library name and blend path + asset_libraries = bpy.context.preferences.filepaths.asset_libraries + library_name = '' + blend_path = '' + for al in asset_libraries: + if filepath.startswith(al.path): + library_name = al.name + blend_path = str(filepath).replace(str(al.path) + os.sep, '') + break - # Set up the cache for faster loading next time - bac = wmyp.brush_asset_caches.add() - bac.name = brush_name - bac.library_type = 'ESSENTIALS' - bac.library_name = '' - bac.blend_path = asset_identifier - - return - except Exception as e: print(e) + try: + bpy.ops.brush.asset_activate( + asset_library_type = 'CUSTOM', + asset_library_identifier = library_name, + relative_asset_identifier = blend_path + os.sep + "Brush" + os.sep + brush_name + ) + return True + except Exception as e: print('EXCEPTIION:', e) - # Try other libraries + return False - # NOTE: This is insanely slow since it scans all asset library blend files for a single brush - # but I dunno any other way :( - prefs = bpy.context.preferences - filepaths = prefs.filepaths - asset_libraries = filepaths.asset_libraries +def activate_unknown_custom_brush(brush_name): + asset_libraries = bpy.context.preferences.filepaths.asset_libraries + # Look for brush in all asset libraries for asset_library in asset_libraries: library_name = asset_library.name library_path = Path(asset_library.path) @@ -110,28 +108,45 @@ def set_brush_asset(brush_name, mode='TEXTURE_PAINT'): relative_asset_identifier = blend_path + "\\Brush\\" + brush_name ) - # Set up the cache for faster loading next time - bac = wmyp.brush_asset_caches.add() - bac.name = brush_name - bac.library_type = 'CUSTOM' - bac.library_name = library_name - bac.blend_path = blend_path - - return + return True except Exception as e: print(e) -tex_default_brush_eraser_pairs = { - 'Paint Hard' : 'Erase Hard', - 'Paint Soft' : 'Erase Soft', - 'Paint Hard Pressure' : 'Erase Hard Pressure', - 'Smear' : 'Erase Soft', - 'Airbrush' : 'Erase Soft', - 'Paint Soft Pressure' : 'Erase Soft', - 'Clone' : 'Erase Hard', - 'Blur' : 'Erase Soft', - 'Fill' : 'Erase Hard', - 'Mask' : 'Erase Soft', -} + return False + +def set_brush_asset(brush_name, mode='TEXTURE_PAINT'): + # Get essential asset identifier path + essential_asset_prefix = "brushes\\essentials_brushes-mesh_" + mode_type = '' + if mode == 'TEXTURE_PAINT': mode_type = 'texture' + elif mode == 'VERTEX_PAINT': mode_type = 'vertex' + elif mode == 'SCULPT': mode_type = 'sculpt' + essential_asset_identifier = essential_asset_prefix + mode_type + ".blend" + + # Try to get brush from local data + brush = bpy.data.brushes.get(brush_name) + + if not brush: + # Look for essential brush + activated = activate_essential_brush(brush_name, essential_asset_identifier) + + # Look for local brush + if not activated: + activated = activate_local_brush(brush_name) + + # Look for custom brush + if not activated: + activated = activate_unknown_custom_brush(brush_name) + else: + + if brush.library and brush.library.name.startswith('essentials_'): + # Essential brush + activate_essential_brush(brush_name, essential_asset_identifier) + elif not brush.library: + # Local brush + activate_local_brush(brush_name) + else: + # Custom brush + activate_custom_brush(brush_name, brush.library.filepath) def set_custom_eraser_brush_icon(eraser_brush): eraser_icon = 'eraser.png' #if is_bl_newer_than(2, 92) else 'eraser_small.png' @@ -165,6 +180,7 @@ def execute(self, context): ve = context.scene.ve_edit mode = context.object.mode + wmyp = context.window_manager.ypprops # Blender 4.3+ texture paint will switch between available brush asset if mode == 'TEXTURE_PAINT' and is_bl_newer_than(4, 3): @@ -172,7 +188,7 @@ def execute(self, context): brush = context.tool_settings.image_paint.brush brush_name = brush.name - image_tool = brush.image_tool + image_tool = get_brush_image_tool(brush) use_pressure_strength = brush.use_pressure_strength use_pressure_size = brush.use_pressure_size @@ -182,7 +198,7 @@ def execute(self, context): if brush.name in tex_default_brush_eraser_pairs: # Get eraser name based on dictionary new_brush_name = tex_default_brush_eraser_pairs[brush.name] - elif brush.image_tool == 'DRAW': + elif get_brush_image_tool(brush) == 'DRAW': # Only toggle erase alpha if the draw brush is custom new_brush_name = brush.name else: @@ -194,12 +210,31 @@ def execute(self, context): # Get original brush name else: - new_brush_name = ve.ori_texpaint_brush + if ve.ori_texpaint_brush != '': + new_brush_name = ve.ori_texpaint_brush + else: new_brush_name = 'Paint Soft' # Toggle 'Erase Alpha' if new brush is the same if brush.name == new_brush_name: brush.blend = ve.ori_texpaint_blending_mode if brush.blend == 'ERASE_ALPHA' else 'ERASE_ALPHA' else: + + # HACK: Set brush to 'builtin.brush' brush first before using non-draw brush + # This is to avoid confusion since by default, if user go back to use 'Brush' tool, it will point to eraser brush + new_brush = bpy.data.brushes.get(new_brush_name) + if new_brush and get_brush_image_tool(new_brush) != 'DRAW': + if wmyp.default_builtin_brush != '': + set_brush_asset(wmyp.default_builtin_brush, mode) + else: set_brush_asset('Paint Soft', mode) + + # HACK: Remember what brush the last time used if user use non-draw brush + if get_brush_image_tool(brush) != 'DRAW': + try: bpy.ops.wm.tool_set_by_id(name="builtin.brush") + except Exception as e: print('EXCEPTIION:', e) + brush = context.tool_settings.image_paint.brush + if brush and brush.name not in tex_eraser_asset_names and get_brush_image_tool(brush) == 'DRAW': + wmyp.default_builtin_brush = brush.name + # Set brush asset set_brush_asset(new_brush_name, mode) @@ -237,7 +272,7 @@ def execute(self, context): draw_brush = bpy.data.brushes.get(draw_brush_name) if not draw_brush: - draw_brushes = [d for d in bpy.data.brushes if d.use_paint_sculpt and d.sculpt_tool == 'PAINT'] + draw_brushes = [d for d in bpy.data.brushes if d.use_paint_sculpt and get_brush_sculpt_tool(d) == 'PAINT'] if draw_brushes: draw_brush = draw_brushes[0] else: self.report({'ERROR'}, "Cannot find a paint brush!") @@ -775,6 +810,7 @@ class YVcolEditorProps(bpy.types.PropertyGroup): ori_texpaint_blending_mode : StringProperty(default='') ori_texpaint_brush : StringProperty(default='') + ori_texpaint_builtin_brush : StringProperty(default='') ori_sculpt_blending_mode : StringProperty(default='') ori_sculpt_brush : StringProperty(default='') diff --git a/vector_displacement.py b/vector_displacement.py index 4b16c37d..d8ca4bdc 100644 --- a/vector_displacement.py +++ b/vector_displacement.py @@ -37,14 +37,15 @@ def _remember_before_bake(obj): book['ori_material_override'] = bpy.context.view_layer.material_override # Multires related - book['ori_use_bake_multires'] = scene.render.use_bake_multires - book['ori_use_bake_clear'] = scene.render.use_bake_clear - book['ori_render_bake_type'] = scene.render.bake_type - book['ori_bake_margin'] = scene.render.bake_margin + book['ori_use_bake_multires'] = get_scene_bake_multires(scene) + book['ori_use_bake_clear'] = get_scene_bake_clear(scene) + book['ori_render_bake_type'] = get_scene_render_bake_type(scene) + book['ori_bake_margin'] = get_scene_bake_margin(scene) book['ori_view_transform'] = scene.view_settings.view_transform # Remember world settings - book['ori_distance'] = scene.world.light_settings.distance + if scene.world: + book['ori_distance'] = scene.world.light_settings.distance # Remember image editor images book['editor_images'] = [a.spaces[0].image for a in bpy.context.screen.areas if a.type == 'IMAGE_EDITOR'] @@ -62,6 +63,10 @@ def _prepare_bake_settings(book, obj, uv_map='', samples=1, margin=15, bake_devi scene = bpy.context.scene ypui = bpy.context.window_manager.ypui + wmyp = bpy.context.window_manager.ypprops + + # Hack function on depsgraph update can cause crash, so halt it before baking + wmyp.halt_hacks = True scene.render.engine = 'CYCLES' scene.render.threads_mode = 'AUTO' @@ -74,9 +79,9 @@ def _prepare_bake_settings(book, obj, uv_map='', samples=1, margin=15, bake_devi scene.render.bake.use_cage = False scene.render.use_simplify = False scene.render.bake.target = 'IMAGE_TEXTURES' - scene.render.use_bake_multires = False - scene.render.bake_margin = margin - scene.render.use_bake_clear = False + set_scene_bake_multires(scene, False) + set_scene_bake_margin(scene, margin) + set_scene_bake_clear(scene, False) scene.cycles.samples = samples scene.cycles.use_denoising = False scene.cycles.bake_type = 'EMIT' @@ -115,6 +120,7 @@ def _recover_bake_settings(book, recover_active_uv=False): obj = book['obj'] uv_layers = obj.data.uv_layers ypui = bpy.context.window_manager.ypui + wmyp = bpy.context.window_manager.ypprops scene.render.engine = book['ori_engine'] scene.cycles.samples = book['ori_samples'] @@ -135,13 +141,14 @@ def _recover_bake_settings(book, recover_active_uv=False): bpy.context.view_layer.material_override = book['ori_material_override'] # Multires related - scene.render.use_bake_multires = book['ori_use_bake_multires'] - scene.render.use_bake_clear = book['ori_use_bake_clear'] - scene.render.bake_type = book['ori_render_bake_type'] - scene.render.bake_margin = book['ori_bake_margin'] + set_scene_bake_multires(scene, book['ori_use_bake_multires']) + set_scene_bake_clear(scene, book['ori_use_bake_clear']) + set_scene_render_bake_type(scene, book['ori_render_bake_type']) + set_scene_bake_margin(scene, book['ori_bake_margin']) # Recover world settings - scene.world.light_settings.distance = book['ori_distance'] + if scene.world: + scene.world.light_settings.distance = book['ori_distance'] # Recover image editors for i, area in enumerate([a for a in bpy.context.screen.areas if a.type == 'IMAGE_EDITOR']): @@ -159,6 +166,9 @@ def _recover_bake_settings(book, recover_active_uv=False): uvl = uv_layers.get(book['ori_active_render_uv']) if uvl: uvl.active_render = True + # Bring back the hack functions + wmyp.halt_hacks = False + def get_offset_attributes(base, sclupted_mesh, layer_disabled_mesh=None, intensity=1.0): print('INFO: Getting offset attributes...') @@ -706,7 +716,7 @@ def execute(self, context): height_ch = get_height_channel(layer) intensity = get_vdm_intensity(layer, height_ch) if height_ch else 1.0 - if mapping and is_transformed(mapping): + if mapping and is_transformed(mapping, layer): self.report({'ERROR'}, "Cannot sculpt VDM with transformed mapping!") return {'CANCELLED'} @@ -835,7 +845,7 @@ def execute(self, context): bpy.ops.object.mode_set(mode='SCULPT') - print('INFO: Sculpt mode is entered in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') + self.report({'INFO'}, 'Sculpt mode is entered in '+'{:0.2f}'.format(time.time() - T)+' seconds!') return {'FINISHED'} @@ -898,7 +908,7 @@ def execute(self, context): if space.type == 'VIEW_3D' and space.shading.type not in {'MATERIAL', 'RENDERED'}: space.shading.type = 'MATERIAL' - print('INFO: Applying sculpt to VDM is done in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') + self.report({'INFO'}, 'Applying sculpt to VDM is done in '+'{:0.2f}'.format(time.time() - T)+' seconds!') return {'FINISHED'} diff --git a/versioning.py b/versioning.py index 50e1c761..59c84177 100644 --- a/versioning.py +++ b/versioning.py @@ -7,7 +7,7 @@ from .node_arrangements import * from .node_connections import * from .input_outputs import * -from . import Bake, ListItem +from . import Bake, ListItem, Modifier def flip_tangent_sign(): meshes = [] @@ -135,6 +135,87 @@ def check_list_items_then_refresh(yp): ch.override = False ch.override = True +def update_bake_info_baked_entity_props(yp): + + entities = [] + bis = [] + images = [] + + for layer in yp.layers: + source = get_layer_source(layer, get_baked=True) + if source and source.image: + image = source.image + if not image.yia.is_image_atlas and not image.yua.is_udim_atlas: + entities.append(layer) + bis.append(image.y_bake_info) + images.append(image) + + else: + if image.yia.is_image_atlas: + segment = image.yia.segments.get(layer.baked_segment_name) + elif image.yua.is_udim_atlas: + segment = image.yua.segments.get(layer.baked_segment_name) + else: segment = None + + if segment: + entities.append(layer) + bis.append(segment.bake_info) + images.append(image) + + for mask in layer.masks: + source = get_mask_source(mask, get_baked=True) + if source and source.image: + image = source.image + if not image.yia.is_image_atlas and not image.yua.is_udim_atlas: + entities.append(mask) + bis.append(image.y_bake_info) + images.append(image) + + else: + if image.yia.is_image_atlas: + segment = image.yia.segments.get(mask.baked_segment_name) + elif image.yua.is_udim_atlas: + segment = image.yua.segments.get(mask.baked_segment_name) + else: segment = None + + if segment: + entities.append(mask) + bis.append(segment.bake_info) + images.append(image) + + for i, entity in enumerate(entities): + bi = bis[i] + image = images[i] + + if not bi.is_baked_entity: + bi.is_baked_entity = True + print('INFO: Image '+image.name+' is marked as baked entity image!') + + if bi.baked_entity_type != entity.type: + bi.baked_entity_type = entity.type + print('INFO: Image '+image.name+' is updated with baked entity type info!') + + if entity.type == 'AO': + osource = get_entity_source(entity) + bi.bake_type = 'AO' + bi.ao_distance = get_entity_prop_value(entity, 'ao_distance') + bi.only_local = osource.only_local + + elif entity.type == 'EDGE_DETECT': + bi.bake_type = 'BEVEL_MASK' + bi.bevel_radius = get_entity_prop_value(entity, 'edge_detect_radius') + +def update_bake_info_use_cages(yp): + + images = get_yp_images(yp) + + for i, image in enumerate(images): + bi = image.y_bake_info + + if bi.is_baked and bi.bake_type.startswith('OTHER_OBJECT_'): + if bi.cage_object_name != '': + bi.use_cage = True + def update_yp_tree(tree): cur_version = get_current_version_str() yp = tree.yp @@ -144,6 +225,53 @@ def update_yp_tree(tree): # SECTION I: Update based on yp version + ## EARLY UPDATE + # NOTE: These update should happen earlier since it affects how the node connections + + # Version 2.3.2 has ramp modifier affect option + if version_tuple(yp.version) < (2, 3, 2): + yp.halt_update = True + + for ch in yp.channels: + for mod in ch.modifiers: + if mod.type == 'COLOR_RAMP': + mod.affect_alpha = True + + for layer in yp.layers: + for mod in layer.modifiers: + if mod.type == 'COLOR_RAMP': + mod.affect_alpha = True + + for ch in layer.channels: + for mod in ch.modifiers: + if mod.type == 'COLOR_RAMP': + mod.affect_alpha = True + + for mod in ch.modifiers_1: + if mod.type == 'COLOR_RAMP': + mod.affect_alpha = True + + yp.halt_update = False + + # Version 2.3.3 has new vdisp_blend node + if version_tuple(yp.version) < (2, 3, 3): + height_root_ch = get_root_height_channel(yp) + if height_root_ch: + for layer in yp.layers: + height_ch = get_height_channel(layer) + if height_ch: + # Convert standard blend node to vdisp_blend + if height_ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP' and layer.type != 'GROUP': + height_ch.vdisp_blend = height_ch.blend + height_ch.blend = '' + layer_tree = get_tree(layer) + if layer_tree: + vdisp_blend = layer_tree.nodes.get(height_ch.vdisp_blend) + if vdisp_blend: vdisp_blend.label = 'VDisp Blend' + + ## LATER UPDATE + # NOTE: These updates probably can be run after the above + # Version 0.9.1 and above will fix wrong bake type stored on images bake type if version_tuple(yp.version) < (0, 9, 1): #print(cur_version) @@ -204,9 +332,18 @@ def update_yp_tree(tree): for i, ch in enumerate(layer.channels): root_ch = yp.channels[i] mod_ids = [] + multp_ids = [] for j, mod in enumerate(ch.modifiers): if mod.type == 'OVERRIDE_COLOR': mod_ids.append(j) + elif mod.type == 'MULTIPLIER': + multp_ids.append(j) + + # HACK: Disable multiply modifiers if override color is found because some old blend file has wrong color + if mod_ids and multp_ids: + for j in multp_ids: + mod = ch.modifiers[j] + mod.enable = False for j in reversed(mod_ids): mod = ch.modifiers[j] @@ -236,7 +373,7 @@ def update_yp_tree(tree): reconnect_layer_nodes(layer) rearrange_layer_nodes(layer) - # Version 0.9.4 and above will replace multipier modifier with math modifier + # Version 0.9.4 and above will replace multiplier modifier with math modifier if version_tuple(yp.version) < (0, 9, 4): mods = [] @@ -274,6 +411,20 @@ def update_yp_tree(tree): mtree = get_mod_tree(parent) + # Get original values + r_val = mod.multiplier_r_val + g_val = mod.multiplier_g_val + b_val = mod.multiplier_b_val + a_val = mod.multiplier_a_val + use_clamp = mod.use_clamp + multp = mtree.nodes.get(mod.multiplier) + if multp: + if 'Clamp' in multp.inputs: use_clamp = multp.inputs['Clamp'].default_value > 0.5 + if 'Multiply R' in multp.inputs: r_val = multp.inputs['Multiply R'].default_value + if 'Multiply G' in multp.inputs: g_val = multp.inputs['Multiply G'].default_value + if 'Multiply B' in multp.inputs: b_val = multp.inputs['Multiply B'].default_value + if 'Multiply A' in multp.inputs: a_val = multp.inputs['Multiply A'].default_value + mod.name = 'Math' mod.type = 'MATH' remove_node(mtree, mod, 'multiplier') @@ -289,15 +440,19 @@ def update_yp_tree(tree): mod.affect_alpha = True math.node_tree.nodes.get('Mix.A').mute = False - mod.math_a_val = mod.multiplier_a_val - mod.math_r_val = mod.multiplier_r_val - math.node_tree.nodes.get('Math.R').use_clamp = mod.use_clamp - math.node_tree.nodes.get('Math.A').use_clamp = mod.use_clamp + mod.math_r_val = math.inputs[2].default_value = r_val + + math.node_tree.nodes.get('Math.R').use_clamp = use_clamp + math.node_tree.nodes.get('Math.A').use_clamp = use_clamp if ch_type != 'VALUE': - mod.math_g_val = mod.multiplier_g_val - mod.math_b_val = mod.multiplier_b_val - math.node_tree.nodes.get('Math.G').use_clamp = mod.use_clamp - math.node_tree.nodes.get('Math.B').use_clamp = mod.use_clamp + mod.math_g_val = math.inputs[3].default_value = g_val + mod.math_b_val = math.inputs[4].default_value = b_val + mod.math_a_val = math.inputs[5].default_value = a_val + + math.node_tree.nodes.get('Math.G').use_clamp = use_clamp + math.node_tree.nodes.get('Math.B').use_clamp = use_clamp + else: + mod.math_a_val = math.inputs[3].default_value = a_val if mods: for layer in yp.layers: @@ -335,7 +490,7 @@ def update_yp_tree(tree): if layer.type == 'IMAGE': source = get_layer_source(layer) - if source and source.image and not source.image.is_float: + if source and source.image and source.image.users == 1 and not source.image.is_float: if source.image.colorspace_settings.name != get_srgb_name(): source.image.colorspace_settings.name = get_srgb_name() print('INFO:', source.image.name, 'image is now using sRGB!') @@ -348,7 +503,7 @@ def update_yp_tree(tree): if ch.override_type == 'IMAGE': source = get_channel_source(ch) - if source and source.image and not source.image.is_float: + if source and source.image and source.image.users == 1 and not source.image.is_float: if source.image.colorspace_settings.name != get_srgb_name(): source.image.colorspace_settings.name = get_srgb_name() print('INFO:', source.image.name, 'image is now using sRGB!') @@ -360,7 +515,7 @@ def update_yp_tree(tree): if mask.type == 'IMAGE': source = get_mask_source(mask) - if source and source.image and not source.image.is_float: + if source and source.image and source.image.users == 1 and not source.image.is_float: if source.image.colorspace_settings.name != get_srgb_name(): source.image.colorspace_settings.name = get_srgb_name() print('INFO:', source.image.name, 'image is now using sRGB!') @@ -738,6 +893,153 @@ def update_yp_tree(tree): if len(ch.modifiers) == 0 and not ch.enable_transition_bump and not ch.enable_transition_ramp and not ch.enable_transition_ao: ch.expand_content = False + # Version 2.2.1 has flag prop for baked entity + if version_tuple(yp.version) < (2, 2, 1): + update_bake_info_baked_entity_props(yp) + + for ch in yp.channels: + + # Use baked vcol is now has it's own property + if ch.enable_bake_to_vcol: + ch.use_baked_vcol = True + + # Now baked channel data can be expanded + baked = tree.nodes.get(ch.baked) + if baked: ch.expand_baked_data = True + + # Version 2.2.2 has more flag props for baked entity + if version_tuple(yp.version) < (2, 2, 2): + update_bake_info_baked_entity_props(yp) + + # Version 2.2.3 use premultiplied alpha for float image atlas and new linear gamma system + if version_tuple(yp.version) < (2, 2, 3): + + if is_bl_newer_than(2, 80): + # Update float image atlas to use premultiplied alpha + for image in bpy.data.images: + if not image.is_float: continue + if image.yia.is_image_atlas or image.yua.is_udim_atlas: + if not image.is_dirty and image.alpha_mode != 'PREMUL': + image.alpha_mode = 'PREMUL' + print("INFO: Image atlas named '"+image.name+"' is now using premultiplied alpha!") + + if yp.use_linear_blending: + # Non color layer used to be have linear node mistakenly added + for layer in yp.layers: + if layer.type == 'IMAGE': + source = get_layer_source(layer) + if source and source.image: + image = source.image + if not is_image_source_srgb(image, source): + + # Check if layer is used as normal map + used_as_normal_map = False + for i, ch in enumerate(layer.channels): + if not ch.enable: continue + root_ch = yp.channels[i] + if root_ch.type == 'NORMAL' and ((ch.normal_map_type in {'NORMAL_MAP', 'BUMP_NORMAL_MAP'} and not ch.override_1) or ch.normal_map_type == 'VECTOR_DISPLACEMENT_MAP'): + used_as_normal_map = True + break + + # Do not add gamma modifier if layer is used as normal map + if not used_as_normal_map: + + # In some cases (like in linked blend file context), adding new data is causing an error + try: mod = Modifier.add_new_modifier(layer, 'MATH') + except Exception as e: + mod = None + print('EXCEPTIION:', e) + + if mod: + mod.math_meth = 'POWER' + mod_tree = get_mod_tree(mod) + math = mod_tree.nodes.get(mod.math) + gamma = 2.2 + if math: + math.inputs[2].default_value = gamma + math.inputs[3].default_value = gamma + math.inputs[4].default_value = gamma + else: + mod.math_r_val = gamma + mod.math_g_val = gamma + mod.math_b_val = gamma + + # Move modifier to the first index + if len(layer.modifiers) > 1: + for i in reversed(range(len(layer.modifiers))): + if i == 0: break + index = i + new_index = i-1 + layer.modifiers.move(index, new_index) + swap_modifier_fcurves(layer, index, new_index) + + print('INFO: Gamma modifier added to \''+image.name+'\' layer') + + # Update linear nodes since it gets refactored + check_yp_linear_nodes(yp, reconnect=True) + + # Version 2.3.2 has bake cage option + if version_tuple(yp.version) < (2, 3, 2): + + # Mark use cage object for older bake info + update_bake_info_use_cages(yp) + + # Version 2.3.3 has scale correction on normal from unwritten height bump map + if version_tuple(yp.version) < (2, 3, 3): + height_root_ch = get_root_height_channel(yp) + if height_root_ch and not height_root_ch.enable_smooth_bump: + for layer in yp.layers: + height_ch = get_height_channel(layer) + if height_ch: + if height_ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'} and not height_ch.write_height: + height = get_entity_prop_value(height_ch, 'bump_distance') + set_entity_prop_value(height_ch, 'bump_distance', height * 5) + + # Version 2.3.5 has blur bake type, and no longer use get/set prop for Blender 5.0 compatibility + if version_tuple(yp.version) < (2, 3, 5): + images = get_yp_images(yp) + + for i, image in enumerate(images): + bi = image.y_bake_info + + if bi.is_baked_entity: + if bi.blur: + bi.blur_type = 'NOISE' + + # Update original name for channel + for ch in yp.channels: + ch.original_name = ch.name + + if ch.bake_to_vcol_name == '': + ch.bake_to_vcol_name = 'Baked ' + ch.name + + height_root_ch = get_root_height_channel(yp) + if height_root_ch: + for layer in yp.layers: + height_ch = get_height_channel(layer) + if height_ch: + + # Update to proper 'Add' max height calculation node + if height_ch.normal_blend_type == 'OVERLAY' and height_ch.normal_map_type in {'BUMP_MAP', 'BUMP_NORMAL_MAP'}: + check_all_layer_channel_io_and_nodes(layer, specific_ch=height_ch) + reconnect_layer_nodes(layer) + rearrange_layer_nodes(layer) + + # Transition bump distance now can do negative value, so reset the value by reenabling and enabling back + if height_ch.enable_transition_bump: + height_ch.enable_transition_bump = False + height_ch.enable_transition_bump = True + + # Version 2.3.6 will remove the remains of mistakenly baked fake lighting layers/masks + # NOTE: The function won't run because version 2.3.6 is not finalized yet + if False and version_tuple(yp.version) < (2, 3, 6): + for layer in yp.layers: + ltree = get_tree(layer) + for n in ltree.nodes: + if n.type == 'TEX_IMAGE' and n.image and len(n.outputs[0].links) == 0 and 'Fake Lighting' in n.image.name and ' Temp' in n.image.name: + print('INFO: Unused image named \''+n.image.name+'\' is removed!') + simple_remove_node(ltree, n) + # SECTION II: Updates based on the blender version # Blender 2.92 can finally access it's vertex color alpha @@ -1349,7 +1651,7 @@ def update_node_tree_libs(name): print('INFO: ' + get_addon_title() + ' Node group libraries are checked in', '{:0.2f}'.format((time.time() - T) * 1000), 'ms!') class YUpdateYPTrees(bpy.types.Operator): - bl_idname = "node.y_update_yp_trees" + bl_idname = "wm.y_update_yp_trees" bl_label = "Update " + get_addon_title() + " Node Groups" bl_description = "Update " + get_addon_title() + " node groups to newest version" bl_options = {'REGISTER', 'UNDO'} @@ -1363,14 +1665,90 @@ def execute(self, context): update_routine('') return {'FINISHED'} +class YUpdateRemoveSmoothBump(bpy.types.Operator): + bl_idname = "wm.y_update_remove_smooth_bump" + bl_label = "Remove Smooth Bump" + bl_description = "Smooth(er) bump is no longer supported, remove it to continue" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return get_active_ypaint_node() + + def execute(self, context): + #update_node_tree_libs('') + #update_routine('') + + for ng in bpy.data.node_groups: + if not hasattr(ng, 'yp'): continue + if not ng.yp.is_ypaint_node: continue + + yp = ng.yp + + height_root_ch = get_root_height_channel(yp) + if height_root_ch and height_root_ch.enable_smooth_bump: + + # Get object dimension and volume + dimension = None + volume = None + mats = get_materials_using_yp(yp) + mat = mats[0] if len(mats) > 0 else None + if mat: + objs = get_all_objects_with_same_materials(mat) + if objs: + dimensions = 0 + volumes = 0 + for obj in objs: + volumes += (obj.dimensions.x + obj.dimensions.y + obj.dimensions.z) / 3 + dimensions += obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + dimension = dimensions / len(objs) + volume = volumes / len(objs) + + # Check if material use subsurface scattering + sss_enabled = False + outp = get_material_output(mat) + bsdf = get_closest_bsdf_backward(outp, ['BSDF_PRINCIPLED']) + if bsdf: + inp = bsdf.inputs.get('Subsurface Weight') if is_bl_newer_than(4) else bsdf.inputs.get('Subsurface') + if inp and (inp.default_value > 0.0 or len(inp.links) > 0): + sss_enabled = True + + for layer in yp.layers: + height_ch = get_height_channel(layer) + if height_ch and dimension != None and volume != None: + + # NOTE: Smooth bump is originally tested on default cube, which has volume of 8 blender units and dimension of 2 + # These values are fine tuned to closer results based on old models + multiplier = 1 + if layer.type == 'COLOR' and height_ch.enable_transition_bump and sss_enabled: + multiplier = volume / 16 + elif layer.type == 'IMAGE' and not height_root_ch.enable_subdiv_setup: + multiplier = dimension / 2 + elif layer.type == 'NOISE' and sss_enabled: + multiplier = volume / 32 + elif layer.type != 'IMAGE': + multiplier = volume / 8 + + height = get_entity_prop_value(height_ch, 'bump_distance') + set_entity_prop_value(height_ch, 'bump_distance', height * multiplier) + height = get_entity_prop_value(height_ch, 'transition_bump_distance') + set_entity_prop_value(height_ch, 'transition_bump_distance', height * multiplier) + + height_root_ch.enable_smooth_bump = False + print("INFO: Smooth bump on "+ng.name+" is now disabled!") + + return {'FINISHED'} + def register(): bpy.utils.register_class(YUpdateYPTrees) + bpy.utils.register_class(YUpdateRemoveSmoothBump) bpy.app.handlers.load_post.append(update_node_tree_libs) bpy.app.handlers.load_post.append(update_routine) def unregister(): bpy.utils.unregister_class(YUpdateYPTrees) + bpy.utils.unregister_class(YUpdateRemoveSmoothBump) bpy.app.handlers.load_post.remove(update_node_tree_libs) bpy.app.handlers.load_post.remove(update_routine)