From d1c1867a3a7e68ff17e4df6dad28f7745b90fb69 Mon Sep 17 00:00:00 2001 From: Yan Hnylytskyi Date: Mon, 24 Jul 2023 01:08:55 +0300 Subject: [PATCH] Pressing Save Atlas To re-selects all materials #76 - Fixed material reselection on an object without selected materials - Fixed missing materials in the combine list if they are the same - Slightly redesigned the diffuse color in the property menu - Fixed error when packing files that are missing on the drive - Fixed texture packing error for Blender versions ranging from 2.8 to 3.0 - Code refactored --- __init__.py | 2 +- addon_updater.py | 2 +- extend_types.py | 45 ++++++++++----- operators/combiner/combiner_ops.py | 84 ++++++++++++++------------- operators/get-pip.py | 2 +- operators/ui/combine_list.py | 92 ++++++++++++++++-------------- type_annotations.py | 6 +- ui/property_menu.py | 25 +++++--- utils/images.py | 8 ++- utils/materials.py | 14 ++--- 10 files changed, 161 insertions(+), 119 deletions(-) diff --git a/__init__.py b/__init__.py index 6b006f8..dbaf496 100644 --- a/__init__.py +++ b/__init__.py @@ -24,7 +24,7 @@ 'name': "Shotariya's Material Combiner", 'description': 'Public Release Material Combiner 2', 'author': 'shotariya', - 'version': (2, 1, 2, 7), + 'version': (2, 1, 2, 8), 'blender': (2, 80, 0), 'location': 'View3D', # 'warning': '', diff --git a/addon_updater.py b/addon_updater.py index 1fbd4ab..9b85a3d 100644 --- a/addon_updater.py +++ b/addon_updater.py @@ -121,7 +121,7 @@ def __init__(self): self._error_msg = None self._prefiltered_tag_count = 0 - # UI code only, ie not used within this module but still useful + # UI code only, i.e., not used within this module but still useful # properties to have # to verify a valid import, in place of placeholder import diff --git a/extend_types.py b/extend_types.py index 29af236..22c3841 100644 --- a/extend_types.py +++ b/extend_types.py @@ -7,18 +7,22 @@ class CombineList(bpy.types.PropertyGroup): ob = PointerProperty( name='Current Object', - type=bpy.types.Object) + type=bpy.types.Object, + ) ob_id = IntProperty(default=0) mat = PointerProperty( name='Current Object Material', - type=bpy.types.Material) + type=bpy.types.Material, + ) layer = IntProperty( + name='Material Layers', description='Materials with the same number will be merged together.' '\nUse this to create multiple materials linked to the same atlas file', min=1, max=99, step=1, - default=1) + default=1, + ) used = BoolProperty(default=True) type = IntProperty(default=0) @@ -75,25 +79,28 @@ def register() -> None: ('STRICTCUST', 'Strict Custom', 'Combined image has exact custom width and height'), ], description='Select atlas size', - default='QUAD') + default='QUAD', + ) bpy.types.Scene.smc_size_width = IntProperty( name='Max width (px)', description='Select max width for combined image', min=8, max=8192, step=1, - default=4096) + default=4096, + ) bpy.types.Scene.smc_size_height = IntProperty( name='Max height (px)', description='Select max height for combined image', min=8, max=8192, step=1, - default=4096) + default=4096, + ) bpy.types.Scene.smc_crop = BoolProperty( name='Crop outside images by UV', description='Crop images by UV if materials UV outside of bounds', - default=True + default=True, ) bpy.types.Scene.smc_diffuse_size = IntProperty( name='Size of materials without image', @@ -101,7 +108,8 @@ def register() -> None: min=8, max=256, step=1, - default=32) + default=32, + ) bpy.types.Scene.smc_gaps = IntProperty( name='Size of gaps between images', description='Select size of gaps between images', @@ -109,37 +117,44 @@ def register() -> None: max=32, step=200, default=0, - options={'HIDDEN'}) + options={'HIDDEN'}, + ) bpy.types.Scene.smc_save_path = StringProperty( description='Select the directory in which the generated texture atlas will be saved', - default='') + default='', + ) bpy.types.Material.root_mat = PointerProperty( name='Material Root', - type=bpy.types.Material) + type=bpy.types.Material, + ) bpy.types.Material.smc_diffuse = BoolProperty( name='Multiply image with diffuse color', description='Multiply the materials image with its diffuse color.' '\nINFO: If this color is white the final image will be the same', - default=True) + default=True, + ) bpy.types.Material.smc_size = BoolProperty( name='Custom image size', description='Select the max size for this materials image in the texture atlas', - default=False) + default=False, + ) bpy.types.Material.smc_size_width = IntProperty( name='Max width (px)', description='Select max width for material image', min=8, max=8192, step=1, - default=2048) + default=2048, + ) bpy.types.Material.smc_size_height = IntProperty( name='Max height (px)', description='Select max height for material image', min=8, max=8192, step=1, - default=2048) + default=2048, + ) def unregister() -> None: diff --git a/operators/combiner/combiner_ops.py b/operators/combiner/combiner_ops.py index fa9f6fd..cb08382 100644 --- a/operators/combiner/combiner_ops.py +++ b/operators/combiner/combiner_ops.py @@ -88,15 +88,15 @@ def get_data(data: Sequence[bpy.types.PropertyGroup]) -> SMCObData: def get_mats_uv(scn: Scene, data: SMCObData) -> MatsUV: - mats_uv = {} + mats_uv = defaultdict(lambda: defaultdict(list)) for ob_n, item in data.items(): ob = scn.objects[ob_n] - mats_uv[ob_n] = defaultdict(list) for idx, polys in get_polys(ob).items(): mat = ob.data.materials[idx] - if mat in item: - for poly in polys: - mats_uv[ob_n][mat].extend(align_uv(get_uv(ob, poly))) + if mat not in item: + continue + for poly in polys: + mats_uv[ob_n][mat].extend(align_uv(get_uv(ob, poly))) return mats_uv @@ -119,7 +119,7 @@ def _delete_material(ob: bpy.types.Object, mat_name: str) -> None: def get_duplicates(mats_uv: MatsUV) -> None: - mat_list = set(chain.from_iterable(mats_uv.values())) + mat_list = list(chain.from_iterable(mats_uv.values())) sorted_mat_list = sort_materials(mat_list) for mats in sorted_mat_list: root_mat = mats[0] @@ -128,28 +128,28 @@ def get_duplicates(mats_uv: MatsUV) -> None: def get_structure(scn: Scene, data: SMCObData, mats_uv: MatsUV) -> Structure: - structure = {} + structure = defaultdict(lambda: { + 'gfx': { + 'img_or_color': None, + 'size': (), + 'uv_size': () + }, + 'dup': [], + 'ob': [], + 'uv': [] + }) + for ob_n, item in data.items(): ob = scn.objects[ob_n] for mat in item: - if mat.name in ob.data.materials: - root_mat = mat.root_mat or mat - if root_mat not in structure: - structure[root_mat] = { - 'gfx': { - 'img_or_color': None, - 'size': (), - 'uv_size': () - }, - 'dup': [], - 'ob': [], - 'uv': [] - } - if mat.root_mat and mat.name not in structure[root_mat]['dup']: - structure[root_mat]['dup'].append(mat.name) - if ob.name not in structure[root_mat]['ob']: - structure[root_mat]['ob'].append(ob.name) - structure[root_mat]['uv'].extend(mats_uv[ob_n][mat]) + if mat.name not in ob.data.materials: + continue + root_mat = mat.root_mat or mat + if mat.root_mat and mat.name not in structure[root_mat]['dup']: + structure[root_mat]['dup'].append(mat.name) + if ob.name not in structure[root_mat]['ob']: + structure[root_mat]['ob'].append(ob.name) + structure[root_mat]['uv'].extend(mats_uv[ob_n][mat]) return structure @@ -283,11 +283,13 @@ def _set_image_or_color(item: StructureItem, mat: bpy.types.Material) -> None: def _paste_gfx(scn: Scene, item: StructureItem, mat: bpy.types.Material, img: ImageType, half_gaps: int) -> None: - if item['gfx']['fit']: - img.paste( - _get_gfx(scn, mat, item, item['gfx']['img_or_color']), - (int(item['gfx']['fit']['x'] + half_gaps), int(item['gfx']['fit']['y'] + half_gaps)) - ) + if not item['gfx']['fit']: + return + + img.paste( + _get_gfx(scn, mat, item, item['gfx']['img_or_color']), + (int(item['gfx']['fit']['x'] + half_gaps), int(item['gfx']['fit']['y'] + half_gaps)) + ) def _get_gfx(scn: Scene, mat: bpy.types.Material, item: StructureItem, @@ -403,10 +405,12 @@ def _get_unique_id(scn: Scene) -> str: def _add_its_from_existing_materials(scn: Scene, existed_ids: Set[int]) -> None: atlas_material_pattern = re.compile(r'{0}(\d+)_\d+'.format(atlas_material_prefix)) for item in scn.smc_ob_data: - if item.type == globs.CL_MATERIAL: - match = atlas_material_pattern.fullmatch(item.mat.name) - if match: - existed_ids.add(int(match.group(1))) + if item.type != globs.CL_MATERIAL: + continue + + match = atlas_material_pattern.fullmatch(item.mat.name) + if match: + existed_ids.add(int(match.group(1))) def _generate_random_unique_id(existed_ids: Set[int]) -> str: @@ -487,11 +491,13 @@ def _assign_mats(item: SMCObDataItem, comb_mats: CombMats, ob_materials: ObMats) def _assign_mats_to_polys(item: SMCObDataItem, comb_mats: CombMats, ob: bpy.types.Object, ob_materials: ObMats) -> None: for idx, polys in get_polys(ob).items(): - if ob_materials[idx] in item: - mat_name = comb_mats[item[ob_materials[idx]]].name - mat_idx = ob_materials.find(mat_name) - for poly in polys: - poly.material_index = mat_idx + if ob_materials[idx] not in item: + continue + + mat_name = comb_mats[item[ob_materials[idx]]].name + mat_idx = ob_materials.find(mat_name) + for poly in polys: + poly.material_index = mat_idx def clear_mats(scn: Scene, mats_uv: MatsUV) -> None: diff --git a/operators/get-pip.py b/operators/get-pip.py index 072d53d..1154967 100644 --- a/operators/get-pip.py +++ b/operators/get-pip.py @@ -131,7 +131,7 @@ def cert_parse_args(self, args): # We want to support people passing things like 'pip<8' to get-pip.py which # will let them install a specific version. However, because of the dreaded - # DoubleRequirement error if any of the args look like they might be a + # DoubleRequirement error, if any of the args look like they might be a # specific for one of our packages, then we'll turn off the implicit # installation of them. for arg in args: diff --git a/operators/ui/combine_list.py b/operators/ui/combine_list.py index e57e275..b613917 100644 --- a/operators/ui/combine_list.py +++ b/operators/ui/combine_list.py @@ -1,20 +1,14 @@ from collections import defaultdict -from typing import DefaultDict -from typing import Dict from typing import List from typing import Set -from typing import Tuple import bpy from bpy.props import * from ... import globs -from ...type_annotations import ObMats -from ...type_annotations import SMCObData -from ...type_annotations import SMCObDataItem +from ...type_annotations import CombineListData from ...type_annotations import Scene from ...utils.materials import get_materials -from ...utils.materials import sort_materials class RefreshObData(bpy.types.Operator): @@ -27,49 +21,55 @@ def execute(self, context: bpy.types.Context) -> Set[str]: scn = context.scene ob_list = [ob for ob in context.visible_objects if ob.type == 'MESH' and ob.data.uv_layers.active and ob.data.materials] - combine_list, layers = self._cache_previous_values(scn) - self._rebuild_items_list(scn, ob_list, combine_list, layers) + combine_list_data = self._cache_previous_values(scn) + self._rebuild_items_list(scn, ob_list, combine_list_data) return {'FINISHED'} @staticmethod - def _cache_previous_values(scn: Scene) -> Tuple[ - DefaultDict[bpy.types.Object, ObMats], - DefaultDict[bpy.types.Object, Dict[ObMats, int]] - ]: + def _cache_previous_values(scn: Scene) -> CombineListData: + combine_list_data = defaultdict(lambda: { + 'used': True, + 'mats': defaultdict(lambda: { + 'used': True, + 'layer': 1, + }), + }) - combine_list = defaultdict(list) - layers = defaultdict(dict) for item in scn.smc_ob_data: + if item.type == globs.CL_OBJECT: + combine_list_data[item.ob]['used'] = item.used if item.type == globs.CL_MATERIAL: - if item.used: - combine_list[item.ob].append(item.mat) - layers[item.ob][item.mat] = item.layer - return combine_list, layers - - def _rebuild_items_list(self, scn: Scene, ob_list: List[bpy.types.Object], - combine_list: DefaultDict[bpy.types.Object, ObMats], - layers: DefaultDict[bpy.types.Object, Dict[ObMats, int]]) -> None: + mat_data = combine_list_data[item.ob]['mats'][item.mat] + mat_data['used'] = item.used + mat_data['layer'] = item.layer + return combine_list_data + + def _rebuild_items_list(self, scn: Scene, ob_list: Set[bpy.types.Object], + combine_list_data: CombineListData) -> None: scn.smc_ob_data.clear() for ob_id, ob in enumerate(ob_list): - mat_dict = sort_materials(get_materials(ob)) - self._create_ob_item(scn, ob, ob_id) - for mats in mat_dict: - for mat in mats: - if mat: - if globs.is_blender_3_or_newer and not mat.preview: - mat.preview_ensure() - used = ob not in combine_list or mat in combine_list[ob] - layer = layers[ob][mat] if mat in layers[ob] else 1 - self._create_mat_item(scn, ob, ob_id, mat, used, layer) + ob_data = combine_list_data[ob] + ob_used = ob_data['used'] + self._create_ob_item(scn, ob, ob_id, ob_used) + + for mat in get_materials(ob): + if globs.is_blender_3_or_newer and not mat.preview: + mat.preview_ensure() + + mat_data = ob_data['mats'][mat] + mat_used = ob_used and mat_data['used'] + mat_layer = mat_data['layer'] + self._create_mat_item(scn, ob, ob_id, mat, mat_used, mat_layer) self._create_separator_item(scn) @staticmethod - def _create_ob_item(scn: Scene, ob: bpy.types.Object, ob_id: int) -> None: + def _create_ob_item(scn: Scene, ob: bpy.types.Object, ob_id: int, used: bool) -> None: item = scn.smc_ob_data.add() item.ob = ob item.ob_id = ob_id item.type = 0 + item.used = used @staticmethod def _create_mat_item(scn: Scene, ob: bpy.types.Object, ob_id: int, mat: bpy.types.Material, used: bool, @@ -106,17 +106,21 @@ def execute(self, context: bpy.types.Context) -> Set[str]: return {'FINISHED'} @staticmethod - def _switch_ob_state(data: SMCObData, item: SMCObDataItem) -> None: + def _switch_ob_state(data: List[bpy.types.PropertyGroup], item: bpy.types.PropertyGroup) -> None: mat_list = [mat for mat in data if mat.ob_id == item.ob_id and mat.type == globs.CL_MATERIAL] - if mat_list: - item.used = not item.used - for mat in mat_list: - mat.used = item.used + if not mat_list: + return + + item.used = not item.used + for mat in mat_list: + mat.used = item.used @staticmethod - def _switch_mat_state(data: SMCObData, item: SMCObDataItem) -> None: + def _switch_mat_state(data: List[bpy.types.PropertyGroup], item: bpy.types.PropertyGroup) -> None: ob = next((ob for ob in data if ob.ob_id == item.ob_id and ob.type == globs.CL_OBJECT), None) - if ob: - if not item.used: - ob.used = True - item.used = not item.used + if not ob: + return + + if not item.used: + ob.used = True + item.used = not item.used diff --git a/type_annotations.py b/type_annotations.py index 0ab1a49..3f9c324 100644 --- a/type_annotations.py +++ b/type_annotations.py @@ -13,7 +13,7 @@ bpy.types.Panel, bpy.types.Operator, bpy.types.PropertyGroup, bpy.types.AddonPreferences, bpy.types.UIList ] -SMCIcons = Union[bpy.utils.previews.ImagePreviewCollection, None] +SMCIcons = Union[bpy.utils.previews.ImagePreviewCollection, Dict[str, bpy.types.ImagePreview], None] Scene = bpy.types.ViewLayer if globs.is_blender_2_80_or_newer else bpy.types.Scene @@ -32,4 +32,8 @@ MatDictItem = List[bpy.types.Material] MatDict = DefaultDict[Tuple, MatDictItem] +CombineListDataMat = Dict[str, Union[int, bool]] +CombineListDataItem = Dict[str, Union[Dict[bpy.types.Material, CombineListDataMat], bool]] +CombineListData = Dict[bpy.types.Object, CombineListDataItem] + Diffuse = Union[bpy.types.bpy_prop_collection, Tuple[float, float, float], Tuple[int, int, int]] diff --git a/ui/property_menu.py b/ui/property_menu.py index a544d44..efe563f 100644 --- a/ui/property_menu.py +++ b/ui/property_menu.py @@ -70,11 +70,13 @@ def _show_diffuse_color(col: bpy.types.UILayout, item: bpy.types.PropertyGroup, image: Optional[bpy.types.Image] = None) -> None: if globs.is_blender_2_79_or_older: col.prop(item.mat, 'smc_diffuse') - if item.mat.smc_diffuse: - split = col.row().split(factor=0.1) if globs.is_blender_2_80_or_newer else col.row().split( - percentage=0.1) - split.separator() - split.prop(item.mat, 'diffuse_color', text='') + if not item.mat.smc_diffuse: + return + + split = col.row().split(factor=0.1) if globs.is_blender_2_80_or_newer else col.row().split( + percentage=0.1) + split.separator() + split.prop(item.mat, 'diffuse_color', text='') return shader = get_shader_type(item.mat) @@ -82,12 +84,17 @@ def _show_diffuse_color(col: bpy.types.UILayout, item: bpy.types.PropertyGroup, if shader in ['principled', 'xnalara'] and image: return - col.prop(item.mat, 'smc_diffuse') - if not item.mat.smc_diffuse: - return + if image: + col.prop(item.mat, 'smc_diffuse') + if not item.mat.smc_diffuse: + return - split = col.row().split(factor=0.1) if globs.is_blender_2_80_or_newer else col.row().split(percentage=0.1) + split = ( + col.row().split(factor=0.1) if globs.is_blender_2_80_or_newer and image else + col.row().split(percentage=0.1) if image else col + ) split.separator() + if shader in ['mmd', 'mmdCol']: split.prop(item.mat.node_tree.nodes['mmd_shader'].inputs['Diffuse Color'], 'default_value', text='') if shader in ['mtoon', 'mtoonCol']: diff --git a/utils/images.py b/utils/images.py index 0e04bdd..b7e7c14 100644 --- a/utils/images.py +++ b/utils/images.py @@ -1,3 +1,4 @@ +import os from typing import Union import bpy @@ -8,6 +9,11 @@ def get_image(tex: bpy.types.Texture) -> bpy.types.Image: def get_packed_file(image: Union[bpy.types.Image, None]) -> Union[bpy.types.PackedFile, None]: - if image and not image.packed_file: + if image and not image.packed_file and _get_image_path(image): image.pack() return image.packed_file if image and image.packed_file else None + + +def _get_image_path(img: Union[bpy.types.Image, None]) -> Union[str, None]: + path = os.path.abspath(bpy.path.abspath(img.filepath)) if img else '' + return path if os.path.isfile(path) and not path.lower().endswith(('.spa', '.sph')) else None diff --git a/utils/materials.py b/utils/materials.py index c15c64c..8abbb82 100644 --- a/utils/materials.py +++ b/utils/materials.py @@ -1,6 +1,6 @@ from collections import OrderedDict from collections import defaultdict -from typing import Set +from typing import List from typing import Union from typing import ValuesView from typing import cast @@ -40,8 +40,8 @@ } -def get_materials(ob: bpy.types.Object) -> Set[bpy.types.Material]: - return {mat_slot.material for mat_slot in ob.material_slots} +def get_materials(ob: bpy.types.Object) -> List[bpy.types.Material]: + return [mat_slot.material for mat_slot in ob.material_slots if mat_slot.material] def get_shader_type(mat: bpy.types.Material) -> Union[str, None]: @@ -70,7 +70,7 @@ def get_shader_type(mat: bpy.types.Material) -> Union[str, None]: ) -def sort_materials(mat_list: Set[bpy.types.Material]) -> ValuesView[MatDictItem]: +def sort_materials(mat_list: List[bpy.types.Material]) -> ValuesView[MatDictItem]: for mat in bpy.data.materials: mat.root_mat = None @@ -80,13 +80,13 @@ def sort_materials(mat_list: Set[bpy.types.Material]) -> ValuesView[MatDictItem] packed_file = None - if globs.is_blender_2_80_or_newer and node_tree: + if globs.is_blender_2_79_or_older: + packed_file = get_packed_file(get_image(get_texture(mat))) + elif node_tree: shader = get_shader_type(mat) node_name = shader_image_nodes.get(shader) if node_name: packed_file = get_packed_file(node_tree.nodes[node_name].image) - else: - packed_file = get_packed_file(get_image(get_texture(mat))) if packed_file: mat_dict[(packed_file, get_diffuse(mat) if mat.smc_diffuse else None)].append(mat)