diff --git a/__init__.py b/__init__.py index c6afc5b..6f4a805 100644 --- a/__init__.py +++ b/__init__.py @@ -6,7 +6,7 @@ 'author': 'GiveMeAllYourCats & Hotox, Unofficial version maintained by Yusarina', 'location': 'View 3D > Tool Shelf > CATS', 'description': 'A tool designed to shorten steps needed to import and optimize models into VRChat', - 'version': (3, 6, 5, 2), # Has to be (x, x, x) not [x, x, x]!! Only change this version and the dev branch var right before publishing the new update! + 'version': (3, 6, 6, 0), # Has to be (x, x, x) not [x, x, x]!! Only change this version and the dev branch var right before publishing the new update! 'blender': (3, 6, 0), 'wiki_url': 'https://github.com/Yusarina/Cats-Blender-Plugin-Unofficial-/wiki', 'tracker_url': 'https://github.com/Yusarina/Cats-Blender-Plugin-Unofficial-/issues', @@ -295,17 +295,11 @@ def register(): bpy.context.window_manager.addon_support = {'OFFICIAL', 'COMMUNITY'} # Add shapekey button to shapekey menu - if hasattr(bpy.types, 'MESH_MT_shape_key_specials'): # pre 2.80 - bpy.types.MESH_MT_shape_key_specials.append(tools.shapekey.addToShapekeyMenu) - else: - bpy.types.MESH_MT_shape_key_context_menu.append(tools.shapekey.addToShapekeyMenu) + bpy.types.MESH_MT_shape_key_context_menu.append(tools.shapekey.addToShapekeyMenu) # Disable request warning when using google translate requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) - # Monkey patch fbx exporter to include empty shapekeys - tools.fbx_patch.start_patch_fbx_exporter_timer() - # Apply the settings after a short time, because you can't change checkboxes during register process tools.settings.start_apply_settings_timer() diff --git a/extentions.py b/extentions.py index 5fb2960..ac97222 100644 --- a/extentions.py +++ b/extentions.py @@ -153,6 +153,12 @@ def register(): default=True ) + Scene.delete_zero_weight_keep_twists = BoolProperty( + name=t('Scene.delete_zero_weight_keep_twists.label'), + description=t('Scene.delete_zero_weight_keep_twists.desc'), + default=False + ) + Scene.merge_armatures_cleanup_shape_keys = BoolProperty( name=t('Scene.merge_armatures_cleanup_shape_keys.label'), description=t('Scene.merge_armatures_cleanup_shape_keys.desc'), diff --git a/globs.py b/globs.py index 795e2ab..60e7f96 100644 --- a/globs.py +++ b/globs.py @@ -31,16 +31,3 @@ ICON_PROTECT = 'LOCKED' ICON_UNPROTECT = 'UNLOCKED' ICON_EXPORT = 'EXPORT' -if bpy.app.version < (2, 79, 9): - ICON_ADD, ICON_REMOVE = 'ZOOMIN', 'ZOOMOUT' - ICON_URL = 'LOAD_FACTORY' - ICON_SETTINGS = 'SCRIPTPLUGINS' - ICON_ALL = 'META_BALL' - ICON_MOD_ARMATURE = 'OUTLINER_OB_ARMATURE' - ICON_FIX_MODEL = 'BONE_DATA' - ICON_EYE_ROTATION = 'MAN_ROT' - ICON_POSE_MODE = 'POSE_DATA' - ICON_SHADING_TEXTURE = 'TEXTURE_SHADED' - ICON_PROTECT = 'KEY_HLT' - ICON_UNPROTECT = 'KEY_DEHLT' - ICON_EXPORT = 'LOAD_FACTORY' diff --git a/resources/translations.csv b/resources/translations.csv index ac0bebc..18e1b13 100644 --- a/resources/translations.csv +++ b/resources/translations.csv @@ -198,7 +198,7 @@ MMDOptions.info5,these on models already set up for Unity,これらはすでに MMDOptions.info6,and VRChat or it may break your model.,VRChat を使用しないと、モデルが壊れる可能性があります。,VRChat을 사용하지 않으면 모델이 손상될 수 있습니다. MMDOptions.FixMaterialinfo1,This will apply some vrchat fixes,これにより、いくつかの vrchat 修正が適用されます,이것은 일부 vrchat 수정 사항을 적용합니다. MMDOptions.FixMaterialinfo2,to materials,材料に,재료에. -OtherOptionsPanel.fbtFix1,Not needed for vrchat anymore!,, +OtherOptionsPanel.fbtFix1,Not needed for vrchat anymore!,VRChat では不要になったので、不要です!,VRChat에서 더 이상 필요하지 않습니다! OptimizePanel.label,Optimization,最適化,최적화 OptimizePanel.atlasDesc,A greatly improved Atlas Generator.,アトラスジェネレータを大幅に改良。,텍스처들을 결합시켜주는 더욱 향상된 아틀라스 생성기입니다. OptimizePanel.atlasAuthor,Made by Shotariya,shotaryiaによって作られた,제작자: Shotariya @@ -213,10 +213,6 @@ OptimizePanel.matCombOutdated5_2.8,right.,右側にあります。。,오른쪽 OptimizePanel.matCombOutdated6,Or download and install it manually:,または手動でダウンロードしてインストールする:,혹은 직접 다운로드와 설치하기: OptimizePanel.matCombOutdated6_alt,Download and install it manually:,手動でダウンロードしてインストールする:,직접 다운로드와 설치하기: OptimizePanel.matCombNotInstalled,Material Combiner is not installed!,マテリアル コンバイナーがインストールされていません!,매테리얼 통합기가 설치되지 않았습니다! -CopyProtectionPanel.label,Copy Protection,コピープロテクション,복사핵 방지 -CopyProtectionPanel.desc1,Tries to protect your avatar from Unity cache ripping.,Unity キャッシュリッピングからアバターを保護しようとします。,유니티 캐쉬뜨기로부터 당신의 아바타를 보호하세요. -CopyProtectionPanel.desc2,This protection is not 100% safe!,この保護は100%安全ではありません!,이 방법도 100% 안전하지는 않습니다! -CopyProtectionPanel.desc3,Before use: Read the documentation!,使用前: ドキュメントを読んでください!,사용 전에: 문서를 읽어주세요! ScalingPanel.label,Model Scaling,, ScalingPanel.imscaleDisabled1,Immersive Scaler is not enabled!,, ScalingPanel.imscaleDisabled2,Enable it in your user preferences:,, @@ -361,15 +357,6 @@ Very useful for manual decimation",,"선택된 메쉬를 두 부분으로 나누 수동으로 데시메이션을 할 시 매우 유용합니다" SeparateByShapekeys.success,Successfully separated by shape keys.,,성공적으로 쉐이프키별로 분리됐습니다. -SeparateByCopyProtection.label,Separate by Copy Protection,コピープロテクションに分離する,복사핵 방지로 분리하기 -SeparateByCopyProtection.desc,"Separates selected mesh into two parts, -depending on whether it is effected by the Cats Copy Protection or not. - -Useful if you have the Copy Protection enabled on multiple selected parts of your model",,"선택된 메쉬를 두 부분으로 나누는데, -Cats의 복사핵 방지의 영향을 받는지 여부에 따라 결정됩니다. - -모델의 여러 부분에서 복사 방지를 활성화 한 경우 유용합니다." -SeparateByCopyProtection.success,Successfully separated by shape keys.,,성공적으로 쉐이프키별로 분리됐습니다. SeparateByX.error.noMesh,No meshes found!,,메쉬가 발견되지 않음! SeparateByX.error.multipleMesh,"Multiple meshes found! Please select the mesh you want to separate!",,"여러 메쉬들이 발견됨! @@ -519,16 +506,6 @@ BoneMergeButton.desc,"Merges the given percentage of bones together. This is useful to reduce the amount of bones used by Dynamic Bones.",, BoneMergeButton.success,Merged bones.,, ShowError.label,Report: Error,レポート: エラー, -CopyProtectionEnable.label,Enable Protection,保護を有効にする,보호 활성화 -CopyProtectionEnable.desc,"Protects your model from piracy. NOT a 100% safe protection! -Read the documentation before use",, -CopyProtectionEnable.success,Model secured!,, -CopyProtectionDisable.label,Disable Protection,保護を無効にする,보호 비활성화 -CopyProtectionDisable.desc,Removes the copy protections from this model.,, -CopyProtectionDisable.success,Model un-secured!,, -ProtectionTutorialButton.label,Go to Documentation,ドキュメントに移動,문서로 이동 -ProtectionTutorialButton.URL,https://github.com/michaeldegroot/cats-blender-plugin#copy-protection,, -ProtectionTutorialButton.success,Documentation,, ForumButton.label,Go to the Forums,フォーラムに移動する,포럼으로 가기 ForumButton.URL,https://vrcat.club/threads/cats-blender-plugin.6/,, ForumButton.success,Forum opened.,, @@ -587,9 +564,13 @@ Supported types: - FBX: .fbx - DAE: .dae - ZIP: .zip",, -DecimationLegacy.info1,Decimation will be made a legacy,デシメーションは遺産となるだろう,학살은 유산이 될 것입니다 -DecimationLegacy.info2,Compoment Soon we recomend using,すぐに使用することをお勧めします,곧 우리는 다음을 사용하는 것이 좋습니다 -DecimationLegacy.info3,Tuxedo instead!,代わりにタキシード!,대신 턱시도! +DecimationLegacy.info1,Decimation will be removed in,デシメーションは次の期間に削除されます,다음 기간에 Decimation이 제거됩니다. +DecimationLegacy.info2,Blender 3.1 please use Tuxdo,Blender 3.1 では Tuxdo を使用してください,블렌더 3.1은 Tuxdo를 사용하세요 +DecimationLegacy.info3,Blender Plugin instead!,代わりに Blender プラグインを使用してください。,대신 블렌더 플러그인을 사용하세요! +DecimationMoved1.info1,Decimation has been moved to,, +DecimationMoved1.info2,to legacy features please start,, +DecimationMoved1.info3,using tuxedo instead as this is,, +DecimationMoved1.info4,an obslolete system in cats,, LegacyDecimationButton.label,Get Tuxedo!,タキシードをゲット!,턱시도 획득! LegacyDecimationButton.URL,https://github.com/feilen/tuxedo-blender-plugin/releases,, DecimationHelpButton.success,Opened Github,, @@ -668,18 +649,6 @@ ErrorDisplay.eyes3,"If you want Eye Tracking to work, rename this mesh to 'Body' ErrorDisplay.eyes3_alt,Make sure that the mesh containing the eyes is named 'Body' in order,, ErrorDisplay.eyes4_alt,to get Eye Tracking to work.,, ErrorDisplay.continue,Continue to Export,, -OneTexPerMatButton.label,One Material Texture,1つのマテリアルテクスチャ,매테리얼당 하나의 텍스처 -OneTexPerMatButton.desc,Have all material slots ignore extra texture slots as these are not used by VRChat.,, -OneTexPerMatOnlyButton.label,One Material Texture,1つのマテリアルテクスチャ,매테리얼당 하나의 텍스처 -OneTexPerMatOnlyButton.desc,"Have all material slots ignore extra texture slots as these are not used by VRChat. -Also removes the textures from the material instead of disabling it. -This makes no difference, but cleans the list for the perfectionists",, -ToolsMaterial.error.notCompatible,This function is not yet compatible with Blender 2.8!,, -OneTexPerXButton.success,All materials have one texture now.,, -StandardizeTextures.label,Standardize Textures,テクスチャを標準化する,텍스처들 표준화 -StandardizeTextures.desc,"Enables Color and Alpha on every texture, sets the blend method to Multiply -and changes the materials transparency to Z-Transparency",, -StandardizeTextures.success,All textures are now standardized.,, CombineMaterialsButton.label,Combine Same Materials,同じマテリアルを組み合わせる,같은 매테리얼들을 통합 CombineMaterialsButton.desc,"Combines similar materials into one, reducing draw calls. Your avatar should visibly look the same after this operation. @@ -839,6 +808,8 @@ This will not change how the bones function in any way, it just improves the aes 뼈의 기능에는 영향을 주지 않으며 뼈대(armature)의 미관을 개선할 뿐입니다." Scene.fix_materials.label,Fix Materials,マテリアルを修正する,매테리얼 고치기 Scene.fix_materials.desc,This will apply some VRChat related fixes to materials,,매테리얼에 일부 VRChat 관련 픽스가 적용됩니다. +Scene.delete_zero_weight_keep_twists.label,Keep bones with _twist,_twist でボーンを維持する,_twist를 사용하여 뼈 유지 +Scene.delete_zero_weight_keep_twists.desc,When checked any bones with _twist or Twist in it's name will be kept. This is too ensure twist bones are kept,チェックすると、名前に _twist または Twist が含まれるボーンが保持されます。これにより、ツイストボーンが確実に保持されます,선택하면 이름에 _twist 또는 Twist가 포함된 뼈대가 유지됩니다. 이는 트위스트 뼈가 유지되도록 보장하는 것이기도 합니다. Scene.remove_rigidbodies_joints.label,Remove Rigidbodies and Joints,リジッドボディとジョイントを除去,Rigidbodies와 Joints 제거 Scene.remove_rigidbodies_joints.desc,"Rigidbodies and joints are used by MMD software to simulate physics. They are completely useless for VRChat, so removing them is recommended for VRChat users!",,"Rigidbodies와 joints는 MMD software에서 물리연산을 위해 사용되는 것들입니다. diff --git a/tools/__init__.py b/tools/__init__.py index 5d10cd1..5b36486 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -14,7 +14,6 @@ from . import credits from . import decimation from . import eyetracking - from . import fbx_patch from . import importer from . import material from . import rootbone @@ -39,7 +38,6 @@ importlib.reload(credits) importlib.reload(decimation) importlib.reload(eyetracking) - importlib.reload(fbx_patch) importlib.reload(importer) importlib.reload(material) importlib.reload(rootbone) diff --git a/tools/armature.py b/tools/armature.py index df07e22..45e57d1 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -37,19 +37,6 @@ def poll(cls, context): return True def execute(self, context): - # Todo: Remove this - # armature = Common.get_armature() - # Common.switch('EDIT') - # - # for bone in armature.data.edit_bones: - # bone.tail = bone.head - # bone.tail[2] += 0.1 - # - # Common.switch('OBJECT') - # - # - # return {'FINISHED'} - saved_data = Common.SavedData() is_vrm = False @@ -241,35 +228,14 @@ def execute(self, context): Common.switch('OBJECT') Common.delete_hierarchy(bpy.data.objects[obj_name]) - # Remove objects from different layers and things that are not meshes - get_current_layers = [] - if hasattr(bpy.context.scene, 'layers'): - for i, layer in enumerate(bpy.context.scene.layers): - if layer: - get_current_layers.append(i) - if len(armature.children) > 1: for child in armature.children: for child2 in child.children: if child2.type != 'MESH': Common.delete(child2) - continue - in_layer = False - for i in get_current_layers: - if child2.layers[i]: - in_layer = True - if not in_layer: - Common.delete(child2) if child.type != 'MESH': Common.delete(child) - continue - in_layer = False - for i in get_current_layers: - if child.layers[i]: - in_layer = True - if not in_layer and hasattr(bpy.context.scene, 'layers'): - Common.delete(child) # Unlock all transforms for i in range(0, 3): @@ -900,7 +866,7 @@ def add_eye_children(eye_bone, parent_name): hips = armature.pose.bones.get('Hips') obj = hips.id_data - matrix_final = Common.matmul(obj.matrix_world, hips.matrix) + matrix_final = obj.matrix_world @ hips.matrix # print(matrix_final) # print(matrix_final[2][3]) # print(fbx) @@ -909,7 +875,7 @@ def add_eye_children(eye_bone, parent_name): # print(hips.head[0], hips.head[1], hips.head[2]) # Rotation of -180 around the X-axis rot_x_neg180 = Matrix.Rotation(-math.pi, 4, 'X') - armature.matrix_world = Common.matmul(rot_x_neg180, armature.matrix_world) + armature.matrix_world = rot_x_neg180 @ armature.matrix_world for mesh in meshes: mesh.rotation_euler = (math.radians(180), 0, 0) @@ -1201,6 +1167,7 @@ def add_eye_children(eye_bone, parent_name): except RuntimeError: pass + armature.show_in_front = False wm.progress_end() if not hierarchy_check_hips['result']: diff --git a/tools/armature_manual.py b/tools/armature_manual.py index 572ae86..72decf6 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -715,51 +715,6 @@ def execute(self, context): self.report({'INFO'}, done_message) return {'FINISHED'} - -@register_wrap -class SeparateByCopyProtection(bpy.types.Operator): - bl_idname = 'cats_manual.separate_by_copy_protection' - bl_label = t('SeparateByCopyProtection.label') - bl_description = t('SeparateByCopyProtection.desc') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - @classmethod - def poll(cls, context): - obj = context.active_object - - if obj and obj.type == 'MESH': - return True - - meshes = Common.get_meshes_objects(check=False) - return meshes - - def execute(self, context): - saved_data = Common.SavedData() - obj = context.active_object - - if not obj or (obj and obj.type != 'MESH'): - Common.unselect_all() - meshes = Common.get_meshes_objects() - if len(meshes) == 0: - saved_data.load() - self.report({'ERROR'}, t('SeparateByX.error.noMesh')) - return {'FINISHED'} - if len(meshes) > 1: - saved_data.load() - self.report({'ERROR'}, t('SeparateByX.error.multipleMesh')) - return {'FINISHED'} - obj = meshes[0] - obj_name = obj.name - - done_message = t('SeparateByCopyProtection.success') - if not Common.separate_by_cats_protection(context, obj): - done_message = t('SeparateByX.warn.noSeparation') - - saved_data.load(ignore=[obj_name]) - self.report({'INFO'}, done_message) - return {'FINISHED'} - - @register_wrap class MergeWeights(bpy.types.Operator): bl_idname = 'cats_manual.merge_weights' diff --git a/tools/common.py b/tools/common.py index 7743f9d..d2dafbd 100644 --- a/tools/common.py +++ b/tools/common.py @@ -23,6 +23,7 @@ from mathutils import Vector from datetime import datetime from html.parser import HTMLParser +from functools import lru_cache from html.entities import name2codepoint from typing import Optional, Set, Dict, Any @@ -55,20 +56,21 @@ class SavedData: __active_object = None def __init__(self): + context = bpy.context # initialize as instance attributes rather than class attributes self.__object_properties = {} self.__active_object = None for obj in get_objects(): mode = obj.mode - selected = is_selected(obj) + selected = obj.select_get() hidden = is_hidden(obj) pose = None if obj.type == 'ARMATURE': pose = obj.data.pose_position self.__object_properties[obj.name] = [mode, selected, hidden, pose] - active = get_active() + active = context.view_layer.objects.active if active: self.__active_object = active.name @@ -106,7 +108,8 @@ def load(self, ignore=None, load_mode=True, load_select=True, load_hide=True, lo # Set the active object if load_active and self.__active_object and get_objects().get(self.__active_object): - if self.__active_object not in ignore and self.__active_object != get_active(): + context = bpy.context + if self.__active_object not in ignore and self.__active_object != context.view_layer.objects.active: set_active(get_objects().get(self.__active_object), skip_sel=True) @@ -183,24 +186,12 @@ def set_active(obj, skip_sel=False): bpy.context.view_layer.objects.active = obj -def get_active(): - return bpy.context.view_layer.objects.active - - def select(obj, sel=True): if obj is not None: hide(obj, False) obj.select_set(sel) -def is_selected(obj): - return obj.select_get() - - -def is_selected(obj): - return obj.select_get() - - def hide(obj, val=True): if hasattr(obj, 'hide_set'): obj.hide_set(val) @@ -221,7 +212,9 @@ def set_unselectable(obj, val=True): def switch(new_mode, check_mode=True): - if check_mode and get_active() and get_active().mode == new_mode: + context = bpy.context + active = context.view_layer.objects.active + if check_mode and active and active.mode == new_mode: return if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode=new_mode, toggle=False) @@ -664,16 +657,8 @@ def fix_armature_names(armature_name=None): pass -def get_texture_sizes(self, context): - # Format is (identifier, name, description) - return [ - ("1024", "1024 (low)", "1024"), - ("2048", "2048 (medium)", "2048"), - ("4096", "4096 (high)", "4096") - ] - - def get_meshes_objects(armature_name=None, mode=0, check=True, visible_only=False): + context = bpy.context # Modes: # 0 = With armatures only # 1 = Top level only @@ -708,7 +693,7 @@ def get_meshes_objects(armature_name=None, mode=0, check=True, visible_only=Fals meshes.append(ob) elif mode == 3: - if is_selected(ob): + if ob.select_get(): meshes.append(ob) if visible_only: @@ -718,14 +703,14 @@ def get_meshes_objects(armature_name=None, mode=0, check=True, visible_only=Fals # Check for broken meshes and delete them if check: - current_active = get_active() + current_active = context.view_layer.objects.active to_remove = [] for mesh in meshes: - selected = is_selected(mesh) + selected = mesh.select_get() # print(mesh.name, mesh.users) set_active(mesh) - if not get_active(): + if not context.view_layer.objects.active: to_remove.append(mesh) if not selected: @@ -746,6 +731,7 @@ def join_meshes(armature_name=None, mode=0, apply_transformations=True, repair_s # Modes: # 0 - Join all meshes # 1 - Join selected only + context = bpy.context if not armature_name: armature_name = bpy.context.scene.armature @@ -792,7 +778,7 @@ def join_meshes(armature_name=None, mode=0, apply_transformations=True, repair_s # Get the name of the active mesh in order to check if it was deleted later - active_mesh_name = get_active().name + active_mesh_name = context.view_layer.objects.active.name # Join the meshes if bpy.ops.object.join.poll(): @@ -810,7 +796,7 @@ def join_meshes(armature_name=None, mode=0, apply_transformations=True, repair_s print('DELETED', mesh.name, mesh.users) # Rename result to Body and correct modifiers - mesh = get_active() + mesh = context.view_layer.objects.active if mesh: # If its the only mesh in the armature left, rename it to Body if len(get_meshes_objects(armature_name=armature_name)) == 1: @@ -882,25 +868,6 @@ def apply_transforms_with_children(parent): apply_transforms_with_children(obj) -def reset_transforms(armature_name=None): - if not armature_name: - armature_name = bpy.context.scene.armature - armature = get_armature(armature_name=armature_name) - - # Reset transforms on armature - for i in range(0, 3): - armature.location[i] = 0 - armature.rotation_euler[i] = 0 - armature.scale[i] = 1 - - # Apply transforms of meshes - for mesh in get_meshes_objects(armature_name=armature_name): - for i in range(0, 3): - mesh.location[i] = 0 - mesh.rotation_euler[i] = 0 - mesh.scale[i] = 1 - - def separate_by_materials(context, mesh): prepare_separation(mesh) @@ -997,58 +964,9 @@ def separate_by_shape_keys(context, mesh): for ob in context.selected_objects: if ob.type == 'MESH': - if ob != get_active(): - print('not active', ob.name) - active_tmp = get_active() - ob.name = ob.name.replace('.001', '') + '.no_shapes' - set_active(ob) - bpy.ops.object.shape_key_remove(all=True) - set_active(active_tmp) - select(ob, False) - else: - print('active', ob.name) - clean_shapekeys(ob) - switch('OBJECT') - - utils.clearUnusedMeshes() - - # Update the material list of the Material Combiner - update_material_list() - return True - - -def separate_by_cats_protection(context, mesh): - prepare_separation(mesh) - - switch('EDIT') - bpy.ops.mesh.select_mode(type="VERT") - bpy.ops.mesh.select_all(action='DESELECT') - - switch('OBJECT') - selected_count = 0 - max_count = 0 - if has_shapekeys(mesh): - for kb in mesh.data.shape_keys.key_blocks: - if kb.name == 'Basis Original': - for i, (v0, v1) in enumerate(zip(kb.relative_key.data, kb.data)): - max_count += 1 - if v0.co != v1.co: - mesh.data.vertices[i].select = True - selected_count += 1 - - if not selected_count or selected_count == max_count: - return False - - switch('EDIT') - bpy.ops.mesh.select_all(action='INVERT') - - bpy.ops.mesh.separate(type='SELECTED') - - for ob in context.selected_objects: - if ob.type == 'MESH': - if ob != get_active(): + active_tmp = context.view_layer.objects.active + if ob != active_tmp: print('not active', ob.name) - active_tmp = get_active() ob.name = ob.name.replace('.001', '') + '.no_shapes' set_active(ob) bpy.ops.object.shape_key_remove(all=True) @@ -1109,20 +1027,6 @@ def can_remove_shapekey(key_block): return True -def separate_by_verts(): - for obj in bpy.context.selected_objects: - if obj.type == 'MESH' and len(obj.vertex_groups) > 0: - Common.set_active(obj) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_mode(type='VERT') - for vgroup in obj.vertex_groups: - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.object.vertex_group_set_active(group=vgroup.name) - bpy.ops.object.vertex_group_select() - bpy.ops.mesh.separate(type='SELECTED') - bpy.ops.object.mode_set(mode='OBJECT') - - def save_shapekey_order(mesh_name): mesh = get_objects()[mesh_name] armature = get_armature() @@ -1304,44 +1208,6 @@ def sort_shape_keys(mesh_name, shape_key_order=None): wm.progress_end() -def isEmptyGroup(group_name): - mesh = get_objects().get('Body') - if mesh is None: - return True - vgroup = mesh.vertex_groups.get(group_name) - if vgroup is None: - return True - - for vert in mesh.data.vertices: - for group in vert.groups: - if group.group == vgroup.index: - if group.weight > 0: - return False - - return True - - -def removeEmptyGroups(obj, thres=0): - z = [] - for v in obj.data.vertices: - for g in v.groups: - if g.weight > thres: - if g not in z: - z.append(obj.vertex_groups[g.group]) - for r in obj.vertex_groups: - if r not in z: - obj.vertex_groups.remove(r) - - -def removeZeroVerts(obj, thres=0): - for v in obj.data.vertices: - z = [] - for g in v.groups: - if not g.weight > thres: - z.append(g) - for r in z: - obj.vertex_groups[g.group].remove([v.index]) - def delete_hierarchy(parent): unselect_all() to_delete = [] @@ -1420,7 +1286,12 @@ def delete_zero_weight(armature_name=None, ignore=''): not_used_bone_names = bone_names_to_work_on - vertex_group_names_used count = 0 + keep_twists = bpy.context.scene.delete_zero_weight_keep_twists + for bone_name in not_used_bone_names: + if keep_twists and ("_twist" in bone_name.lower() or "Twist" in bone_name): + continue + if not bpy.context.scene.keep_end_bones or not is_end_bone(bone_name, armature_name): if bone_name not in Bones.dont_delete_these_bones and 'Root_' not in bone_name and bone_name != ignore: armature.data.edit_bones.remove(bone_name_to_edit_bone[bone_name]) # delete bone @@ -1634,39 +1505,79 @@ def draw(self, context): row.label(text=line, icon_value=Iconloader.preview_collections["custom_icons"]["empty"].icon_id) -def remove_doubles(mesh, threshold, save_shapes=True): - if not mesh: - return 0 +def has_shapekeys(mesh_obj: Object) -> bool: + return mesh_obj.data.shape_keys is not None + + +@lru_cache(maxsize=None) +def _get_shape_key_co(shape_key: ShapeKey) -> np.ndarray: + return np.array([v.co for v in shape_key.data]) + + +def remove_doubles(mesh_obj: Object, threshold: float, save_shapes: bool = True) -> int: + if not isinstance(mesh_obj, Object): + raise TypeError("mesh_obj must be an instance of Object") + if threshold <= 0: + raise ValueError("Threshold must be positive") + + mesh = mesh_obj.data - # If the mesh has no shapekeys, don't remove doubles - if not has_shapekeys(mesh) or len(mesh.data.shape_keys.key_blocks) == 1: + if not has_shapekeys(mesh_obj) or len(mesh.shape_keys.key_blocks) == 1: return 0 - pre_tris = len(mesh.data.polygons) + pre_polygons = len(mesh.polygons) - set_active(mesh) - switch('EDIT') - bpy.ops.mesh.select_mode(type="VERT") - bpy.ops.mesh.select_all(action='DESELECT') + if save_shapes: + vertex_selection = np.full(len(mesh.vertices), True, dtype=bool) + cached_co_getter = lru_cache(maxsize=None)(_get_shape_key_co) + for kb in mesh.shape_keys.key_blocks[1:]: + relative_key = kb.relative_key + if kb == relative_key: + continue + same = cached_co_getter(kb) == cached_co_getter(relative_key) + vertex_not_moved_by_shape_key = np.all(same.reshape(-1, 3), axis=1) + vertex_selection &= vertex_not_moved_by_shape_key + del cached_co_getter - if save_shapes and has_shapekeys(mesh): - switch('OBJECT') - for kb in mesh.data.shape_keys.key_blocks: - i = 0 - for v0, v1 in zip(kb.relative_key.data, kb.data): - if v0.co != v1.co: - mesh.data.vertices[i].select = True - i += 1 - switch('EDIT') - bpy.ops.mesh.select_all(action='INVERT') + if not vertex_selection.any(): + return 0 + + if vertex_selection.all(): + save_shapes = False + else: + bpy.context.view_layer.objects.active = mesh_obj + bpy.ops.object.mode_set(mode='EDIT') + verts = list(bmesh.from_edit_mesh(mesh).verts) + for v in verts: + v.select = vertex_selection[v.index] + bpy.ops.object.mode_set(mode='OBJECT') + bpy.context.view_layer.update() else: - bpy.ops.mesh.select_all(action='SELECT') + bmesh.ops.select_all(bm, action='DESELECT') - bpy.ops.mesh.remove_doubles(threshold=threshold) - bpy.ops.mesh.select_all(action='DESELECT') - switch('OBJECT') + bm = bmesh.new() + bm.from_mesh(mesh) + if save_shapes: + verts = [v for v in bm.verts if v.select] + else: + verts = bm.verts + + total_verts = len(verts) + progress_steps = 100 + progress_step_size = total_verts // progress_steps + progress = 0 - return pre_tris - len(mesh.data.polygons) + bmesh.ops.remove_doubles(bm, verts=verts, dist=threshold) + + while progress < total_verts: + bm.select_flush(True) + bpy.context.view_layer.update() + bpy.context.window_manager.progress_update(progress / total_verts) + progress += progress_step_size + + bm.to_mesh(mesh) + + return pre_polygons - len(mesh.polygons) def get_tricount(obj): @@ -1680,12 +1591,9 @@ def get_tricount(obj): def clean_material_names(mesh): for j, mat in enumerate(mesh.material_slots): - if mat.name.endswith('.001'): - mesh.active_material_index = j - mesh.active_material.name = mat.name[:-4] - if mat.name.endswith(('. 001', ' .001')): + if mat.name.endswith(('.0+', ' 0+')): mesh.active_material_index = j - mesh.active_material.name = mat.name[:-5] + mesh.active_material.name = mat.name[:-len(mat.name.rstrip('0')) - 1] def mix_weights(mesh, vg_from, vg_to, mix_strength=1.0, mix_mode='ADD', delete_old_vg=True): @@ -1717,10 +1625,6 @@ def has_shapekeys(mesh): return hasattr(mesh.data.shape_keys, 'key_blocks') -def matmul(a, b): - return a @ b - - def ui_refresh(): # A way to refresh the ui refreshed = False @@ -1769,79 +1673,6 @@ def update_material_list(self=None, context=None): print('Material Combiner not found') -def unify_materials(): - textures = [] # TODO - - for ob in get_objects(): - if ob.type == "MESH": - for mat_slot in ob.material_slots: - if mat_slot.material: - mat_slot.material.blend_method = 'HASHED' - # mat_slot.material.blend_method = 'BLEND' # Use this for transparent textures only - print('MAT: ', mat_slot.material.name) - if mat_slot.material.node_tree: - nodes = mat_slot.material.node_tree.nodes - image = None - for node in nodes: - # print(' ' + node.name + ', ' + node.type + ', ' + node.label) - if node.type == 'TEX_IMAGE' and 'toon' not in node.name and 'sphere' not in node.name: - image = node.image - # textures.append(node.image.name) - mat_slot.material.node_tree.nodes.remove(node) - - # Create Image node - node_texture = nodes.new(type='ShaderNodeTexImage') - node_texture.location = 0, 0 - node_texture.image = image - node_texture.label = 'Cats Texture' - - # Create Principled BSDF node - node_prinipled = nodes.new(type='ShaderNodeBsdfPrincipled') - node_prinipled.location = 300, -220 - node_prinipled.label = 'Cats Emission' - node_prinipled.inputs['Specular'].default_value = 0 - node_prinipled.inputs['Roughness'].default_value = 0 - node_prinipled.inputs['Sheen Tint'].default_value = 0 - node_prinipled.inputs['Clearcoat Roughness'].default_value = 0 - node_prinipled.inputs['IOR'].default_value = 0 - - # Create Transparency BSDF node - node_transparent = nodes.new(type='ShaderNodeBsdfTransparent') - node_transparent.location = 325, -100 - node_transparent.label = 'Cats Transparency' - - # Create Mix Shader node - node_mix = nodes.new(type='ShaderNodeMixShader') - node_mix.location = 600, 0 - node_mix.label = 'Cats Mix' - - # Create Output node - node_output = nodes.new(type='ShaderNodeOutputMaterial') - node_output.location = 800, 0 - node_output.label = 'Cats Output' - - # Create 2nd Output node - node_output2 = nodes.new(type='ShaderNodeOutputMaterial') - node_output2.location = 800, -200 - node_output2.label = 'Cats Export' - - # Link nodes together - mat_slot.material.node_tree.links.new(node_texture.outputs['Color'], node_prinipled.inputs['Base Color']) - mat_slot.material.node_tree.links.new(node_texture.outputs['Alpha'], node_mix.inputs['Fac']) - - mat_slot.material.node_tree.links.new(node_prinipled.outputs['BSDF'], node_mix.inputs[2]) - mat_slot.material.node_tree.links.new(node_transparent.outputs['BSDF'], node_mix.inputs[1]) - - mat_slot.material.node_tree.links.new(node_mix.outputs['Shader'], node_output.inputs['Surface']) - - mat_slot.material.node_tree.links.new(node_prinipled.outputs['BSDF'], node_output2.inputs['Surface']) - - # break - - print(textures, len(textures)) - return {'FINISHED'} - - def bake_mmd_colors(node_base_tex: ShaderNodeTexImage, node_mmd_shader: ShaderNodeGroup): """Bake the mmd ambient color and diffuse color into the base tex or return the combined color if there is no base tex. This process follows the same steps that the mmd_shader group node follows.""" @@ -2094,59 +1925,72 @@ def remove_toon_shader(mesh): # nodes.remove(node) -def fix_mmd_shader(mesh): - for mat_slot in mesh.material_slots: - if mat_slot.material and mat_slot.material.node_tree: - nodes = mat_slot.material.node_tree.nodes - for node in nodes: - if node.name == 'mmd_shader': - node.inputs['Reflect'].default_value = 1 +def fix_mmd_shader(mesh_obj: bpy.types.Object): + # Iterate through each material slot in the mesh object + for mat_slot in mesh_obj.material_slots: + # Skip the current iteration if the material or its node tree is missing + material = mat_slot.material + if not material or not material.node_tree: + continue + + # Get the nodes of the material's node tree + mmd_shader_node = material.node_tree.nodes.get("mmd_shader") + # If the 'mmd_shader' node doesn't exist, skip the current iteration + if not mmd_shader_node: + continue + + # Get the 'Reflect' input of the 'mmd_shader' node + reflect_input = mmd_shader_node.inputs.get("Reflect") + # If the 'Reflect' input doesn't exist, skip the current iteration + if not reflect_input: + continue + + # Set the default value of the 'Reflect' input to 1 + reflect_input.default_value = 1 + # Exit the loop once the 'mmd_shader' node is found + break -def fix_vrm_shader(mesh): +def fix_vrm_shader(mesh: bpy.types.Mesh): + # Iterate through each material slot in the mesh for mat_slot in mesh.material_slots: - if mat_slot.material and mat_slot.material.node_tree: - is_vrm_mat = False - nodes = mat_slot.material.node_tree.nodes - for node in nodes: - if hasattr(node, 'node_tree') and 'MToon_unversioned' in node.node_tree.name: - node.location[0] = 200 - node.inputs['ReceiveShadow_Texture_alpha'].default_value = -10000 - node.inputs['ShadeTexture'].default_value = (1.0, 1.0, 1.0, 1.0) - node.inputs['Emission_Texture'].default_value = (0.0, 0.0, 0.0, 0.0) - node.inputs['SphereAddTexture'].default_value = (0.0, 0.0, 0.0, 0.0) - - # Support typo in old vrm importer - node_input = node.inputs.get('NomalmapTexture') or node.inputs.get('NormalmapTexture') - node_input.default_value = (1.0, 1.0, 1.0, 1.0) - - is_vrm_mat = True - break - if not is_vrm_mat: - continue + # Skip this iteration if the material or its node tree is missing + if not mat_slot.material or not mat_slot.material.node_tree: + continue - nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture'] - if 'HAIR' in mat_slot.material.name: - nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture', 'SphereAddTexture'] + is_vrm_mat = False + nodes = mat_slot.material.node_tree.nodes + # Iterate through each node in the material's node tree + for node in nodes: + # Check if the node has a node tree and its name contains 'MToon_unversioned' + if hasattr(node, 'node_tree') and 'MToon_unversioned' in node.node_tree.name: + # Set the node's location and default values for certain inputs + node.location[0] = 200 + node.inputs['ReceiveShadow_Texture_alpha'].default_value = -10000 + node.inputs['ShadeTexture'].default_value = (1.0, 1.0, 1.0, 1.0) + node.inputs['Emission_Texture'].default_value = (0.0, 0.0, 0.0, 0.0) + node.inputs['SphereAddTexture'].default_value = (0.0, 0.0, 0.0, 0.0) + + node_input = node.inputs.get('NomalmapTexture') or node.inputs.get('NormalmapTexture') + node_input.default_value = (1.0, 1.0, 1.0, 1.0) + + is_vrm_mat = True + break - for node in nodes: - # Delete all unneccessary nodes - if 'RGB' in node.name \ - or 'Value' in node.name \ - or 'Image Texture' in node.name \ - or 'UV Map' in node.name \ - or 'Mapping' in node.name: - if node.label not in nodes_to_keep: - for output in node.outputs: - for link in output.links: - mat_slot.material.node_tree.links.remove(link) - continue - - # if hasattr(node, 'node_tree') and 'matcap_vector' in node.node_tree.name: - # for output in node.outputs: - # for link in output.links: - # mat_slot.material.node_tree.links.remove(link) - # continue + if not is_vrm_mat: + continue + + nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture'] + if 'HAIR' in mat_slot.material.name: + nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture', 'SphereAddTexture'] + + # Iterate through each node in the material's node tree again + for node in nodes: + # Check if the node's name contains certain keywords and its label is not in the list of nodes to keep + if 'RGB' in node.name or 'Value' in node.name or 'Image Texture' in node.name or 'UV Map' in node.name or 'Mapping' in node.name: + if node.label not in nodes_to_keep: + # Remove all links connected to the node + mat_slot.material.node_tree.links = [link for link in mat_slot.material.node_tree.links if not (link.from_node == node or link.to_node == node)] def fix_twist_bones(mesh, bones_to_delete): @@ -2598,77 +2442,3 @@ def op_override(operator, context_override: Dict[str, Any], context: Optional[bp args.append(undo) return operator(*args, **operator_args) - -""" === THIS CODE COULD BE USEFUL === """ - -# def addvertex(meshname, shapekey_name): -# mesh = get_objects()[meshname].data -# bm = bmesh.new() -# bm.from_mesh(mesh) -# bm.verts.ensure_lookup_table() -# -# print(" ") -# if shapekey_name in bm.verts.layers.shape.keys(): -# val = bm.verts.layers.shape.get(shapekey_name) -# print("%s = %s" % (shapekey_name, val)) -# sk = mesh.shape_keys.key_blocks[shapekey_name] -# print("v=%f, f=%f" % (sk.value, sk.frame)) -# for i in range(len(bm.verts)): -# v = bm.verts[i] -# delta = v[val] - v.co -# if (delta.length > 0): -# print("v[%d]+%s" % (i, delta)) -# -# print(" ") - -# === THIS CODE COULD BE USEFUL === - -# Check which shape keys will be deleted on export by Blender -# def checkshapekeys(): -# for ob in get_objects(): -# if ob.type == 'MESH': -# mesh = ob -# bm = bmesh.new() -# bm.from_mesh(mesh.data) -# bm.verts.ensure_lookup_table() -# -# deleted_shapes = [] -# for key in bm.verts.layers.shape.keys(): -# if key == 'Basis': -# continue -# val = bm.verts.layers.shape.get(key) -# delete = True -# for vert in bm.verts: -# delta = vert[val] - vert.co -# if delta.length > 0: -# delete = False -# break -# if delete: -# deleted_shapes.append(key) -# -# return deleted_shapes - -# # Repair vrc shape keys old -# def repair_shapekeys(): -# for ob in get_objects(): -# if ob.type == 'MESH': -# mesh = ob -# bm = bmesh.new() -# bm.from_mesh(mesh.data) -# bm.verts.ensure_lookup_table() -# -# for key in bm.verts.layers.shape.keys(): -# if not key.startswith('vrc'): -# continue -# -# value = bm.verts.layers.shape.get(key) -# for vert in bm.verts: -# shapekey = vert -# shapekey_coords = mesh.matrix_world * shapekey[value] -# shapekey_coords[2] -= 0.00001 -# shapekey[value] = mesh.matrix_world.inverted() * shapekey_coords -# break -# -# bm.to_mesh(mesh.data) - -# === THIS CODE COULD BE USEFUL === diff --git a/tools/fbx_patch.py b/tools/fbx_patch.py deleted file mode 100644 index 838e52b..0000000 --- a/tools/fbx_patch.py +++ /dev/null @@ -1,493 +0,0 @@ -# MIT License - -# This is directly taken from the export_fbx_bin.py to change it via monkey patching -import bpy -import threading -from . import common as Common -from io_scene_fbx import fbx_utils - -def start_patch_fbx_exporter_timer(): - thread = threading.Thread(target=time_patch_fbx_exporter, args=[]) - thread.start() - -def time_patch_fbx_exporter(): - import time - found_scene = False - while not found_scene: - if hasattr(bpy.context, 'scene'): - found_scene = True - else: - time.sleep(0.5) - - patch_fbx_exporter() - -def patch_fbx_exporter(): - fbx_utils.get_bid_name = get_bid_name - -# Blender-specific key generators - monkeypatched to force name if present -def get_bid_name(bid): - if isinstance(bid, bpy.types.ID) and 'catsForcedExportName' in bid: - return bid['catsForcedExportName'] - library = getattr(bid, "library", None) - if library is not None: - return "%s_L_%s" % (bid.name, library.name) - else: - return bid.name - - -def fbx_data_from_scene_v279(scene, settings): - """ - Do some pre-processing over scene's data... - """ - objtypes = settings.object_types - dp_objtypes = objtypes - {'ARMATURE'} # Armatures are not supported as dupli instances currently... - perfmon = PerfMon() - perfmon.level_up() - - # ##### Gathering data... - - perfmon.step("FBX export prepare: Wrapping Objects...") - - # This is rather simple for now, maybe we could end generating templates with most-used values - # instead of default ones? - objects = OrderedDict() # Because we do not have any ordered set... - for ob in settings.context_objects: - if ob.type not in objtypes: - continue - ob_obj = ObjectWrapper(ob) - objects[ob_obj] = None - # Duplis... - ob_obj.dupli_list_create(scene, 'RENDER') - for dp_obj in ob_obj.dupli_list: - if dp_obj.type not in dp_objtypes: - continue - objects[dp_obj] = None - ob_obj.dupli_list_clear() - - perfmon.step("FBX export prepare: Wrapping Data (lamps, cameras, empties)...") - - data_lamps = OrderedDict((ob_obj.bdata.data, get_blenderID_key(ob_obj.bdata.data)) - for ob_obj in objects if ob_obj.type == 'LAMP') - # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)... - data_cameras = OrderedDict((ob_obj, get_blenderID_key(ob_obj.bdata.data)) - for ob_obj in objects if ob_obj.type == 'CAMERA') - # Yep! Contains nothing, but needed! - data_empties = OrderedDict((ob_obj, get_blender_empty_key(ob_obj.bdata)) - for ob_obj in objects if ob_obj.type == 'EMPTY') - - perfmon.step("FBX export prepare: Wrapping Meshes...") - - data_meshes = OrderedDict() - for ob_obj in objects: - if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE: - continue - ob = ob_obj.bdata - use_org_data = True - org_ob_obj = None - - # Do not want to systematically recreate a new mesh for dupliobject instances, kind of break purpose of those. - if ob_obj.is_dupli: - org_ob_obj = ObjectWrapper(ob) # We get the "real" object wrapper from that dupli instance. - if org_ob_obj in data_meshes: - data_meshes[ob_obj] = data_meshes[org_ob_obj] - continue - - is_ob_material = any(ms.link == 'OBJECT' for ms in ob.material_slots) - - if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES or is_ob_material: - # We cannot use default mesh in that case, or material would not be the right ones... - use_org_data = not (is_ob_material or ob.type in BLENDER_OTHER_OBJECT_TYPES) - tmp_mods = [] - if use_org_data and ob.type == 'MESH': - # No need to create a new mesh in this case, if no modifier is active! - for mod in ob.modifiers: - # For meshes, when armature export is enabled, disable Armature modifiers here! - if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types: - tmp_mods.append((mod, mod.show_render)) - mod.show_render = False - if mod.show_render: - use_org_data = False - if not use_org_data: - tmp_me = ob.to_mesh(scene, apply_modifiers=True, - settings='RENDER' if settings.use_mesh_modifiers_render else 'PREVIEW') - data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True) - # Re-enable temporary disabled modifiers. - for mod, show_render in tmp_mods: - mod.show_render = show_render - if use_org_data: - data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False) - - # In case "real" source object of that dupli did not yet still existed in data_meshes, create it now! - if org_ob_obj is not None: - data_meshes[org_ob_obj] = data_meshes[ob_obj] - - perfmon.step("FBX export prepare: Wrapping ShapeKeys...") - - # ShapeKeys. - print('Modified Shapekey export by Cats') - data_deformers_shape = OrderedDict() - geom_mat_co = settings.global_matrix if settings.bake_space_transform else None - for me_key, me, _free in data_meshes.values(): - if not (me.shape_keys and len(me.shape_keys.key_blocks) > 1): # We do not want basis-only relative skeys... - continue - if me in data_deformers_shape: - continue - - shapes_key = get_blender_mesh_shape_key(me) - # We gather all vcos first, since some skeys may be based on others... - _cos = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3 - me.vertices.foreach_get("co", _cos) - v_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co)) - sk_cos = {} - for shape in me.shape_keys.key_blocks[1:]: - shape.data.foreach_get("co", _cos) - sk_cos[shape] = tuple(vcos_transformed_gen(_cos, geom_mat_co)) - sk_base = me.shape_keys.key_blocks[0] - - for shape in me.shape_keys.key_blocks[1:]: - # Only write vertices really different from org coordinates! - shape_verts_co = [] - shape_verts_idx = [] - - sv_cos = sk_cos[shape] - ref_cos = v_cos if shape.relative_key == sk_base else sk_cos[shape.relative_key] - for idx, (sv_co, ref_co) in enumerate(zip(sv_cos, ref_cos)): - if similar_values_iter(sv_co, ref_co): - # Note: Maybe this is a bit too simplistic, should we use real shape base here? Though FBX does not - # have this at all... Anyway, this should cover most common cases imho. - continue - shape_verts_co.extend(Vector(sv_co) - Vector(ref_co)) - shape_verts_idx.append(idx) - - # FBX does not like empty shapes (makes Unity crash e.g.). - # To prevent this, we add a vertex that does nothing, but it keeps the shape key intact - if not shape_verts_co: - shape_verts_co.extend((0, 0, 0)) - shape_verts_idx.append(0) - - channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape) - data = (channel_key, geom_key, shape_verts_co, shape_verts_idx) - data_deformers_shape.setdefault(me, (me_key, shapes_key, OrderedDict()))[2][shape] = data - - perfmon.step("FBX export prepare: Wrapping Armatures...") - - # Armatures! - data_deformers_skin = OrderedDict() - data_bones = OrderedDict() - arm_parents = set() - for ob_obj in tuple(objects): - if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}): - continue - fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes, - data_bones, data_deformers_skin, data_empties, arm_parents) - - # Generate leaf bones - data_leaf_bones = [] - if settings.add_leaf_bones: - data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones) - - perfmon.step("FBX export prepare: Wrapping World...") - - # Some world settings are embedded in FBX materials... - if scene.world: - data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),)) - else: - data_world = OrderedDict() - - perfmon.step("FBX export prepare: Wrapping Materials...") - - # TODO: Check all the mat stuff works even when mats are linked to Objects - # (we can then have the same mesh used with different materials...). - # *Should* work, as FBX always links its materials to Models (i.e. objects). - # XXX However, material indices would probably break... - data_materials = OrderedDict() - for ob_obj in objects: - # If obj is not a valid object for materials, wrapper will just return an empty tuple... - for mat_s in ob_obj.material_slots: - mat = mat_s.material - if mat is None: - continue # Empty slots! - # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc. - # However, I doubt anything else than Lambert/Phong is really portable! - # We support any kind of 'surface' shader though, better to have some kind of default Lambert than nothing. - # Note we want to keep a 'dummy' empty mat even when we can't really support it, see T41396. - mat_data = data_materials.get(mat) - if mat_data is not None: - mat_data[1].append(ob_obj) - else: - data_materials[mat] = (get_blenderID_key(mat), [ob_obj]) - - perfmon.step("FBX export prepare: Wrapping Textures...") - - # Note FBX textures also hold their mapping info. - # TODO: Support layers? - data_textures = OrderedDict() - # FbxVideo also used to store static images... - data_videos = OrderedDict() - # For now, do not use world textures, don't think they can be linked to anything FBX wise... - for mat in data_materials.keys(): - if check_skip_material(mat): - continue - for tex, use_tex in zip(mat.texture_slots, mat.use_textures): - if tex is None or tex.texture is None or not use_tex: - continue - # For now, only consider image textures. - # Note FBX does has support for procedural, but this is not portable at all (opaque blob), - # so not useful for us. - # TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside. - # if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}: - if tex.texture.type not in {'IMAGE'}: - continue - img = tex.texture.image - if img is None: - continue - # Find out whether we can actually use this texture for this material, in FBX context. - tex_fbx_props = fbx_mat_properties_from_texture(tex) - if not tex_fbx_props: - continue - tex_data = data_textures.get(tex) - if tex_data is not None: - tex_data[1][mat] = tex_fbx_props - else: - data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),))) - vid_data = data_videos.get(img) - if vid_data is not None: - vid_data[1].append(tex) - else: - data_videos[img] = (get_blenderID_key(img), [tex]) - - perfmon.step("FBX export prepare: Wrapping Animations...") - - # Animation... - animations = () - animated = set() - frame_start = scene.frame_start - frame_end = scene.frame_end - if settings.bake_anim: - # From objects & bones only for a start. - # Kind of hack, we need a temp scene_data for object's space handling to bake animations... - tmp_scdata = FBXExportData( - None, None, None, - settings, scene, objects, None, None, 0.0, 0.0, - data_empties, data_lamps, data_cameras, data_meshes, None, - data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape, - data_world, data_materials, data_textures, data_videos, - ) - animations, animated, frame_start, frame_end = fbx_animations(tmp_scdata) - - # ##### Creation of templates... - - perfmon.step("FBX export prepare: Generating templates...") - - templates = OrderedDict() - templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1) - - if data_empties: - templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties)) - - if data_lamps: - templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps)) - - if data_cameras: - templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras)) - - if data_bones: - templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones)) - - if data_meshes: - nbr = len({me_key for me_key, _me, _free in data_meshes.values()}) - if data_deformers_shape: - nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values()) - templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr) - - if objects: - templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects)) - - if arm_parents: - # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures - templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents)) - - if data_deformers_skin or data_deformers_shape: - nbr = 0 - if data_deformers_skin: - nbr += len(data_deformers_skin) - nbr += sum(len(clusters) for def_me in data_deformers_skin.values() for a, b, clusters in def_me.values()) - if data_deformers_shape: - nbr += len(data_deformers_shape) - nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values()) - assert(nbr != 0) - templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr) - - # No world support in FBX... - """ - if data_world: - templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world)) - """ - - if data_materials: - templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials)) - - if data_textures: - templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures)) - - if data_videos: - templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos)) - - if animations: - nbr_astacks = len(animations) - nbr_acnodes = 0 - nbr_acurves = 0 - for _astack_key, astack, _al, _n, _fs, _fe in animations: - for _alayer_key, alayer in astack.values(): - for _acnode_key, acnode, _acnode_name in alayer.values(): - nbr_acnodes += 1 - for _acurve_key, _dval, acurve, acurve_valid in acnode.values(): - if acurve: - nbr_acurves += 1 - - templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks) - # Would be nice to have one layer per animated object, but this seems tricky and not that well supported. - # So for now, only one layer per anim stack. - templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks) - templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes) - templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves) - - templates_users = sum(tmpl.nbr_users for tmpl in templates.values()) - - # ##### Creation of connections... - - perfmon.step("FBX export prepare: Generating Connections...") - - connections = [] - - # Objects (with classical parenting). - for ob_obj in objects: - # Bones are handled later. - if not ob_obj.is_bone: - par_obj = ob_obj.parent - # Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0). - if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents: - connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None)) - else: - connections.append((b"OO", ob_obj.fbx_uuid, 0, None)) - - # Armature & Bone chains. - for bo_obj in data_bones.keys(): - par_obj = bo_obj.parent - if par_obj not in objects: - continue - connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None)) - - # Object data. - for ob_obj in objects: - if ob_obj.is_bone: - bo_data_key = data_bones[ob_obj] - connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None)) - else: - if ob_obj.type == 'LAMP': - lamp_key = data_lamps[ob_obj.bdata.data] - connections.append((b"OO", get_fbx_uuid_from_key(lamp_key), ob_obj.fbx_uuid, None)) - elif ob_obj.type == 'CAMERA': - cam_key = data_cameras[ob_obj] - connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None)) - elif ob_obj.type == 'EMPTY' or ob_obj.type == 'ARMATURE': - empty_key = data_empties[ob_obj] - connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None)) - elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE: - mesh_key, _me, _free = data_meshes[ob_obj] - connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None)) - - # Leaf Bones - for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones: - connections.append((b"OO", node_uuid, par_uuid, None)) - connections.append((b"OO", attr_uuid, node_uuid, None)) - - # 'Shape' deformers (shape keys, only for meshes currently)... - for me_key, shapes_key, shapes in data_deformers_shape.values(): - # shape -> geometry - connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None)) - for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values(): - # shape channel -> shape - connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None)) - # geometry (keys) -> shape channel - connections.append((b"OO", get_fbx_uuid_from_key(geom_key), get_fbx_uuid_from_key(channel_key), None)) - - # 'Skin' deformers (armature-to-geometry, only for meshes currently)... - for arm, deformed_meshes in data_deformers_skin.items(): - for me, (skin_key, ob_obj, clusters) in deformed_meshes.items(): - # skin -> geometry - mesh_key, _me, _free = data_meshes[ob_obj] - assert(me == _me) - connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None)) - for bo_obj, clstr_key in clusters.items(): - # cluster -> skin - connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None)) - # bone -> cluster - connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None)) - - # Materials - mesh_mat_indices = OrderedDict() - _objs_indices = {} - for mat, (mat_key, ob_objs) in data_materials.items(): - for ob_obj in ob_objs: - connections.append((b"OO", get_fbx_uuid_from_key(mat_key), ob_obj.fbx_uuid, None)) - # Get index of this mat for this object (or dupliobject). - # Mat indices for mesh faces are determined by their order in 'mat to ob' connections. - # Only mats for meshes currently... - # Note in case of dupliobjects a same me/mat idx will be generated several times... - # Should not be an issue in practice, and it's needed in case we export duplis but not the original! - if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE: - continue - _mesh_key, me, _free = data_meshes[ob_obj] - idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1 - mesh_mat_indices.setdefault(me, OrderedDict())[mat] = idx - del _objs_indices - - # Textures - for tex, (tex_key, mats) in data_textures.items(): - for mat, fbx_mat_props in mats.items(): - mat_key, _ob_objs = data_materials[mat] - for fbx_prop in fbx_mat_props: - # texture -> material properties - connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_from_key(mat_key), fbx_prop)) - - # Images - for vid, (vid_key, texs) in data_videos.items(): - for tex in texs: - tex_key, _texs = data_textures[tex] - connections.append((b"OO", get_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None)) - - # Animations - for astack_key, astack, alayer_key, _name, _fstart, _fend in animations: - # Animstack itself is linked nowhere! - astack_id = get_fbx_uuid_from_key(astack_key) - # For now, only one layer! - alayer_id = get_fbx_uuid_from_key(alayer_key) - connections.append((b"OO", alayer_id, astack_id, None)) - for elem_key, (alayer_key, acurvenodes) in astack.items(): - elem_id = get_fbx_uuid_from_key(elem_key) - # Animlayer -> animstack. - # alayer_id = get_fbx_uuid_from_key(alayer_key) - # connections.append((b"OO", alayer_id, astack_id, None)) - for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items(): - # Animcurvenode -> animalayer. - acurvenode_id = get_fbx_uuid_from_key(acurvenode_key) - connections.append((b"OO", acurvenode_id, alayer_id, None)) - # Animcurvenode -> object property. - connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode())) - for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items(): - if acurve: - # Animcurve -> Animcurvenode. - connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode())) - - perfmon.level_down() - - # ##### And pack all this! - - return FBXExportData( - templates, templates_users, connections, - settings, scene, objects, animations, animated, frame_start, frame_end, - data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices, - data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape, - data_world, data_materials, data_textures, data_videos, - ) diff --git a/tools/importer.py b/tools/importer.py index 0806eb7..73938cc 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -17,7 +17,6 @@ from . import armature_manual from . import common as Common from . import settings as Settings -from . import fbx_patch as Fbx_patch from .register import register_wrap from .translations import t @@ -30,10 +29,6 @@ current_blender_version = str(bpy.app.version[:2])[1:-1].replace(', ', '.') -# In blender 2.79 this string gets cut off after char 63, so don't go over that limit -# Bug Report: https://blender.stackexchange.com/questions/110788/file-browser-filter-not-working-correctly -# < > Don't go outside these brackets -formats_279 = '*.pm*;*.xps;*.mesh;*.ascii;*.smd;*.qc;*.fbx;*.dae;*.vrm;*.zip' formats = '*.pmx;*.pmd;*.xps;*.mesh;*.ascii;*.smd;*.qc;*.qci;*.vta;*.dmx;*.fbx;*.dae;*.vrm;*.zip' format_list = formats.replace('*.', '').split(';') zip_files = {} @@ -63,10 +58,6 @@ def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - # Save all current objects to check which armatures got added by the importer pre_import_objects = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE'] @@ -392,10 +383,6 @@ class ImportMMD(bpy.types.Operator): def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - if not mmd_tools_local_installed: bpy.ops.cats_importer.enable_mmd('INVOKE_DEFAULT') return {'FINISHED'} @@ -437,10 +424,6 @@ def poll(cls, context): def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - if not mmd_tools_local_installed: bpy.ops.cats_importer.enable_mmd('INVOKE_DEFAULT') return {'FINISHED'} @@ -541,10 +524,6 @@ class ImportXPS(bpy.types.Operator): def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - try: bpy.ops.xps_tools.import_model('INVOKE_DEFAULT') except AttributeError: @@ -563,10 +542,6 @@ class ImportSource(bpy.types.Operator): def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - try: bpy.ops.import_scene.smd('INVOKE_DEFAULT') except AttributeError: @@ -585,10 +560,6 @@ class ImportFBX(bpy.types.Operator): def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - # Enable fbx if it isn't enabled yet fbx_is_enabled = addon_utils.check('io_scene_fbx')[1] if not fbx_is_enabled: @@ -615,10 +586,6 @@ class ImportVRM(bpy.types.Operator): def execute(self, context): Common.remove_unused_objects() - # Make sure that the first layer is visible - if hasattr(context.scene, 'layers'): - context.scene.layers[0] = True - try: bpy.ops.import_scene.vrm('INVOKE_DEFAULT') except AttributeError: @@ -2075,23 +2042,6 @@ def execute(self, context): # Continue if there are no errors or the check was skipped - # Monkey patch FBX exporter again to import empty shape keys - Fbx_patch.patch_fbx_exporter() - - # Check if copy protection is enabled - mesh_smooth_type = 'OFF' - protected_export = False - for mesh in meshes: - if protected_export: - break - if Common.has_shapekeys(mesh): - for shapekey in mesh.data.shape_keys.key_blocks: - if shapekey.name == 'Basis Original': - protected_export = True - break - if protected_export: - mesh_smooth_type = 'FACE' - # Check if textures are found and if they should be embedded path_mode = 'AUTO' if _textures_found and Settings.get_embed_textures(): @@ -2108,8 +2058,7 @@ def execute(self, context): bake_anim=False, apply_scale_options='FBX_SCALE_ALL', path_mode=path_mode, - embed_textures=True, - mesh_smooth_type=mesh_smooth_type) + embed_textures=True,) else: bpy.ops.export_scene.fbx('INVOKE_DEFAULT', object_types={'EMPTY', 'ARMATURE', 'MESH', 'OTHER'}, @@ -2118,8 +2067,7 @@ def execute(self, context): bake_anim=False, apply_scale_options='FBX_SCALE_ALL', path_mode=path_mode, - embed_textures=True, - mesh_smooth_type=mesh_smooth_type) + embed_textures=True,) except (TypeError, ValueError): bpy.ops.export_scene.fbx('INVOKE_DEFAULT') except AttributeError: diff --git a/tools/material.py b/tools/material.py index 088f916..747f249 100644 --- a/tools/material.py +++ b/tools/material.py @@ -8,109 +8,6 @@ from . import armature as Armature mmd_tools_local_installed = True - -@register_wrap -class OneTexPerMatButton(bpy.types.Operator): - bl_idname = 'cats_material.one_tex' - bl_label = t('OneTexPerMatButton.label') - bl_description = t('OneTexPerMatButton.desc') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - @classmethod - def poll(cls, context): - if Common.get_armature() is None: - return False - return len(Common.get_meshes_objects(check=False)) > 0 - - def execute(self, context): - self.report({'ERROR'}, t('ToolsMaterial.error.notCompatible')) - return {'CANCELLED'} - - saved_data = Common.SavedData() - - Common.set_default_stage() - - for mesh in Common.get_meshes_objects(): - for mat_slot in mesh.material_slots: - for i, tex_slot in enumerate(mat_slot.material.texture_slots): - if i > 0 and tex_slot: - mat_slot.material.use_textures[i] = False - - saved_data.load() - - self.report({'INFO'}, t('OneTexPerMatButton.success')) - return {'FINISHED'} - -@register_wrap -class OneTexPerMatOnlyButton(bpy.types.Operator): - bl_idname = 'cats_material.one_tex_only' - bl_label = t('OneTexPerMatOnlyButton.label') - bl_description = t('OneTexPerMatOnlyButton.desc') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - @classmethod - def poll(cls, context): - if Common.get_armature() is None: - return False - return len(Common.get_meshes_objects(check=False)) > 0 - - def execute(self, context): - self.report({'ERROR'}, t('ToolsMaterial.error.notCompatible')) - return {'CANCELLED'} - - saved_data = Common.SavedData() - - Common.set_default_stage() - - for mesh in Common.get_meshes_objects(): - for mat_slot in mesh.material_slots: - for i, tex_slot in enumerate(mat_slot.material.texture_slots): - if i > 0 and tex_slot: - tex_slot.texture = None - - saved_data.load() - - self.report({'INFO'}, t('OneTexPerXButton.success')) - return {'FINISHED'} - -@register_wrap -class StandardizeTextures(bpy.types.Operator): - bl_idname = 'cats_material.standardize_textures' - bl_label = t('StandardizeTextures.label') - bl_description = t('StandardizeTextures.desc') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - @classmethod - def poll(cls, context): - if Common.get_armature() is None: - return False - return len(Common.get_meshes_objects(check=False)) > 0 - - def execute(self, context): - self.report({'ERROR'}, t('ToolsMaterial.error.notCompatible')) - return {'CANCELLED'} - - saved_data = Common.SavedData() - - Common.set_default_stage() - - for mesh in Common.get_meshes_objects(): - for mat_slot in mesh.material_slots: - - mat_slot.material.transparency_method = 'Z_TRANSPARENCY' - mat_slot.material.alpha = 1 - - for tex_slot in mat_slot.material.texture_slots: - if tex_slot: - tex_slot.use_map_alpha = True - tex_slot.use_map_color_diffuse = True - tex_slot.blend_type = 'MULTIPLY' - - saved_data.load() - - self.report({'INFO'}, t('StandardizeTextures.success')) - return {'FINISHED'} - @register_wrap class CombineMaterialsButton(bpy.types.Operator): bl_idname = 'cats_material.combine_mats' @@ -127,8 +24,9 @@ def poll(cls, context): return len(Common.get_meshes_objects(check=False)) > 0 def assignmatslots(self, ob, matlist): + context = bpy.context scn = bpy.context.scene - ob_active = Common.get_active() + ob_active = context.view_layer.objects.active Common.set_active(ob) for s in ob.material_slots: diff --git a/tools/register.py b/tools/register.py index 4f70075..57f6ccc 100644 --- a/tools/register.py +++ b/tools/register.py @@ -27,17 +27,14 @@ def register_wrap(cls): def make_annotations(cls): if __make_annotations: - if bpy.app.version < (2, 93, 0): - bl_props = {k:v for k, v in cls.__dict__.items() if isinstance(v, tuple)} - else: bl_props = {k:v for k, v in cls.__dict__.items() if isinstance(v, bpy.props._PropertyDeferred)} - if bl_props: - if '__annotations__' not in cls.__dict__: - setattr(cls, '__annotations__', {}) - annotations = cls.__dict__['__annotations__'] - for k, v in bl_props.items(): - annotations[k] = v - delattr(cls, k) + if bl_props: + if '__annotations__' not in cls.__dict__: + setattr(cls, '__annotations__', {}) + annotations = cls.__dict__['__annotations__'] + for k, v in bl_props.items(): + annotations[k] = v + delattr(cls, k) return cls diff --git a/ui/decimation.py b/ui/decimation.py index e82e86e..19073ed 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -5,46 +5,11 @@ from .. import globs from .main import ToolPanel -from .main import layout_split from ..tools import common as Common from ..tools import iconloader as Iconloader -from ..tools import decimation as Decimation -from ..tools import armature_manual as Armature_manual from ..tools.register import register_wrap from ..tools.translations import t - -@register_wrap -class AutoDecimatePresetGoodCats(bpy.types.Operator): - bl_idname = 'cats_decimation.preset_good' - bl_label = t('DecimationPanel.preset.good.label') - bl_description = t('DecimationPanel.preset.good.description') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - def execute(self, context): - context.scene.max_tris = 70000 - return {'FINISHED'} - -@register_wrap -class AutoDecimatePresetExcellentCats(bpy.types.Operator): - bl_idname = 'cats_decimation.preset_excellent' - bl_label = t('DecimationPanel.preset.excellent.label') - bl_description = t('DecimationPanel.preset.excellent.description') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - def execute(self, context): - context.scene.max_tris = 32000 - return {'FINISHED'} - -@register_wrap -class AutoDecimatePresetQuestCats(bpy.types.Operator): - bl_idname = 'cats_decimation.preset_quest' - bl_label = t('DecimationPanel.preset.quest.label') - bl_description = t('DecimationPanel.preset.quest.description') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - def execute(self, context): - context.scene.max_tris = 5000 - return {'FINISHED'} +from ..ui.legacy import LegacyDecimationButton @register_wrap class DecimationPanel(ToolPanel, bpy.types.Panel): @@ -53,152 +18,23 @@ class DecimationPanel(ToolPanel, bpy.types.Panel): bl_options = {'DEFAULT_CLOSED'} def draw(self, context): + + scene = context.scene layout = self.layout box = layout.box() col = box.column(align=True) - row = col.column(align=True) - row.scale_y = 0.75 - row.label(text=t('DecimationLegacy.info1'), icon='INFO') - row.label(text=t('DecimationLegacy.info2'), icon='BLANK1') - row.label(text=t('DecimationLegacy.info3'), icon='BLANK1') + sub = col.column(align=True) + sub.label(text=t("DecimationMoved1.info1"), icon='INFO') + sub.label(text=t("DecimationMoved1.info2"), icon='BLANK1') + sub.label(text=t("DecimationMoved1.info3"), icon='BLANK1') + sub.label(text=t("DecimationMoved1.info4"), icon='BLANK1') + col.separator() row = col.row(align=True) row.scale_y = 1.5 - row.operator(LegacyDecimationButton.bl_idname, icon_value=Iconloader.preview_collections['custom_icons']['help1'].icon_id) - col.separator() - col.separator() - row = col.row(align=True) - row.label(text=t('DecimationPanel.decimationMode')) - row = col.row(align=True) - row.prop(context.scene, 'decimation_mode', expand=True) - row = col.row(align=True) - row.scale_y = 0.7 - if context.scene.decimation_mode == 'SAFE': - row.label(text=t('DecimationPanel.safeModeDesc')) - elif context.scene.decimation_mode == 'HALF': - row.label(text=t('DecimationPanel.halfModeDesc')) - elif context.scene.decimation_mode == 'FULL': - row.label(text=t('DecimationPanel.fullModeDesc')) - - elif context.scene.decimation_mode == 'CUSTOM': - col.separator() - - if len(Common.get_meshes_objects(check=False)) <= 1: - row = col.row(align=True) - row.label(text=t('DecimationPanel.customSeparateMaterials')) - row = col.row(align=True) - row.scale_y = 1.2 - row.operator(Armature_manual.SeparateByMaterials.bl_idname, text=t('DecimationPanel.SeparateByMaterials.label'), icon='PLAY') - return - else: - row = col.row(align=True) - row.label(text=t('DecimationPanel.customJoinMeshes')) - row = col.row(align=True) - row.scale_y = 1.2 - row.operator(Armature_manual.JoinMeshes.bl_idname, icon='PAUSE') - - col.separator() - col.separator() - row = col.row(align=True) - row.label(text=t('DecimationPanel.customWhitelist')) - row = col.row(align=True) - row.prop(context.scene, 'selection_mode', expand=True) - col.separator() - col.separator() - - if context.scene.selection_mode == 'SHAPES': - row = layout_split(col, factor=0.7, align=False) - row.prop(context.scene, 'add_shape_key', icon='SHAPEKEY_DATA') - row.operator(Decimation.AddShapeButton.bl_idname, icon=globs.ICON_ADD) - col.separator() - - box2 = col.box() - col = box2.column(align=True) - - if len(Decimation.ignore_shapes) == 0: - col.label(text=t('DecimationPanel.warn.noShapekeySelected')) - - for shape in Decimation.ignore_shapes: - row = layout_split(col, factor=0.8, align=False) - row.label(text=shape, icon='SHAPEKEY_DATA') - op = row.operator(Decimation.RemoveShapeButton.bl_idname, text='', icon=globs.ICON_REMOVE) - op.shape_name = shape - elif context.scene.selection_mode == 'MESHES': - row = layout_split(col, factor=0.7, align=False) - row.prop(context.scene, 'add_mesh', icon='MESH_DATA') - row.operator(Decimation.AddMeshButton.bl_idname, icon=globs.ICON_ADD) - col.separator() - - if Common.is_enum_empty(context.scene.add_mesh): - row = col.row(align=True) - col.label(text=t('DecimationPanel.warn.noDecimation'), icon='ERROR') - - box2 = col.box() - col = box2.column(align=True) - if len(Decimation.ignore_meshes) == 0: - col.label(text=t('DecimationPanel.warn.noMeshSelected')) - - for mesh in Decimation.ignore_meshes: - row = layout_split(col, factor=0.8, align=False) - row.label(text=mesh, icon='MESH_DATA') - op = row.operator(Decimation.RemoveMeshButton.bl_idname, text='', icon=globs.ICON_REMOVE) - op.mesh_name = mesh - - col = box.column(align=True) - - if len(Decimation.ignore_shapes) == 0 and len(Decimation.ignore_meshes) == 0: - col.label(text=t('DecimationPanel.warn.emptyList'), icon='ERROR') - row = col.row(align=True) - else: - col.label(text=t('DecimationPanel.warn.correctWhitelist'), icon='INFO') - row = col.row(align=True) + row.operator(LegacyDecimationButton.bl_idname, icon_value=Iconloader.preview_collections['custom_icons']['help1'].icon_id) col.separator() - col.separator() - row = col.row(align=True) - row.prop(context.scene, 'decimate_fingers') - row = col.row(align=True) - row.prop(context.scene, 'decimation_remove_doubles') - row = col.row(align=True) - row.prop(context.scene, 'decimation_retain_separated_meshes', expand=True) - row = col.row(align=True) - row.operator(Decimation.AutoDecimatePresetGoodCats.bl_idname) - row.operator(Decimation.AutoDecimatePresetExcellentCats.bl_idname) - row.operator(Decimation.AutoDecimatePresetQuestCats.bl_idname) - row = col.row(align=True) - row.prop(context.scene, 'max_tris') - col.separator() - row = col.row(align=True) - row.scale_y = 1.2 - row.operator(Decimation.AutoDecimateButtonCats.bl_idname, icon='MOD_DECIM') - col.separator() - row = col.row(align=True) - row.scale_y = 1.5 - row.operator(DecimationHelpButton.bl_idname, icon_value=Iconloader.preview_collections['custom_icons']['help1'].icon_id) - col.separator() - -@register_wrap -class LegacyDecimationButton(bpy.types.Operator): - bl_idname = 'legacy_decimation.help' - bl_label = t('LegacyDecimationButton.label') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - def execute(self, context): - webbrowser.open(t('LegacyDecimationButton.URL')) - - self.report({'INFO'}, t('LegacyDecimationButton.success')) - return {'FINISHED'} - -@register_wrap -class DecimationHelpButton(bpy.types.Operator): - bl_idname = 'legacy_helpdecimation.help' - bl_label = t('DecimationHelpButton.label') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - def execute(self, context): - webbrowser.open(t('DecimationHelpButton.URL')) - - self.report({'INFO'}, t('DecimationHelpButton.success')) - return {'FINISHED'} \ No newline at end of file + col.separator() \ No newline at end of file diff --git a/ui/legacy.py b/ui/legacy.py index 7becec1..3f74fee 100644 --- a/ui/legacy.py +++ b/ui/legacy.py @@ -13,7 +13,43 @@ from ..tools import eyetracking as Eyetracking from ..tools import armature_manual as Armature_manual from ..tools.register import register_wrap +from .main import layout_split from ..tools.translations import t +from ..tools import decimation as Decimation + + +@register_wrap +class AutoDecimatePresetGoodCats(bpy.types.Operator): + bl_idname = 'cats_decimation.preset_good' + bl_label = t('DecimationPanel.preset.good.label') + bl_description = t('DecimationPanel.preset.good.description') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + context.scene.max_tris = 70000 + return {'FINISHED'} + +@register_wrap +class AutoDecimatePresetExcellentCats(bpy.types.Operator): + bl_idname = 'cats_decimation.preset_excellent' + bl_label = t('DecimationPanel.preset.excellent.label') + bl_description = t('DecimationPanel.preset.excellent.description') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + context.scene.max_tris = 32000 + return {'FINISHED'} + +@register_wrap +class AutoDecimatePresetQuestCats(bpy.types.Operator): + bl_idname = 'cats_decimation.preset_quest' + bl_label = t('DecimationPanel.preset.quest.label') + bl_description = t('DecimationPanel.preset.quest.description') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + context.scene.max_tris = 5000 + return {'FINISHED'} @register_wrap class LegacyStuff(ToolPanel, bpy.types.Panel): @@ -47,6 +83,7 @@ def draw(self, context): sub = col.column(align=True) row.label(text=t('OtherOptionsPanel.fbtFix1'), icon='BLANK1') col.separator() + col.separator() split = col.row(align=True) row = split.row(align=True) row.scale_y = 1.5 @@ -54,6 +91,128 @@ def draw(self, context): row = split.row(align=True) row.scale_y = 1.5 row.operator(Armature_manual.RemoveFBTButton.bl_idname, text=t('OtherOptionsPanel.RemoveFBTButton.label')) + col.separator() + col = box.column(align=True) + row = col.column(align=True) + row.scale_y = 0.75 + row.label(text=t('DecimationLegacy.info1'), icon='INFO') + row.label(text=t('DecimationLegacy.info2'), icon='BLANK1') + row.label(text=t('DecimationLegacy.info3'), icon='BLANK1') + col.separator() + row = col.row(align=True) + row.scale_y = 1.5 + row.operator(LegacyDecimationButton.bl_idname, icon_value=Iconloader.preview_collections['custom_icons']['help1'].icon_id) + col.separator() + col.separator() + row = col.row(align=True) + row.label(text=t('DecimationPanel.decimationMode')) + row = col.row(align=True) + row.prop(context.scene, 'decimation_mode', expand=True) + row = col.row(align=True) + row.scale_y = 0.7 + if context.scene.decimation_mode == 'SAFE': + row.label(text=t('DecimationPanel.safeModeDesc')) + elif context.scene.decimation_mode == 'HALF': + row.label(text=t('DecimationPanel.halfModeDesc')) + elif context.scene.decimation_mode == 'FULL': + row.label(text=t('DecimationPanel.fullModeDesc')) + + elif context.scene.decimation_mode == 'CUSTOM': + col.separator() + + if len(Common.get_meshes_objects(check=False)) <= 1: + row = col.row(align=True) + row.label(text=t('DecimationPanel.customSeparateMaterials')) + row = col.row(align=True) + row.scale_y = 1.2 + row.operator(Armature_manual.SeparateByMaterials.bl_idname, text=t('DecimationPanel.SeparateByMaterials.label'), icon='PLAY') + return + else: + row = col.row(align=True) + row.label(text=t('DecimationPanel.customJoinMeshes')) + row = col.row(align=True) + row.scale_y = 1.2 + row.operator(Armature_manual.JoinMeshes.bl_idname, icon='PAUSE') + + col.separator() + col.separator() + row = col.row(align=True) + row.label(text=t('DecimationPanel.customWhitelist')) + row = col.row(align=True) + row.prop(context.scene, 'selection_mode', expand=True) + col.separator() + col.separator() + + if context.scene.selection_mode == 'SHAPES': + row = layout_split(col, factor=0.7, align=False) + row.prop(context.scene, 'add_shape_key', icon='SHAPEKEY_DATA') + row.operator(Decimation.AddShapeButton.bl_idname, icon=globs.ICON_ADD) + col.separator() + + box2 = col.box() + col = box2.column(align=True) + + if len(Decimation.ignore_shapes) == 0: + col.label(text=t('DecimationPanel.warn.noShapekeySelected')) + + for shape in Decimation.ignore_shapes: + row = layout_split(col, factor=0.8, align=False) + row.label(text=shape, icon='SHAPEKEY_DATA') + op = row.operator(Decimation.RemoveShapeButton.bl_idname, text='', icon=globs.ICON_REMOVE) + op.shape_name = shape + elif context.scene.selection_mode == 'MESHES': + row = layout_split(col, factor=0.7, align=False) + row.prop(context.scene, 'add_mesh', icon='MESH_DATA') + row.operator(Decimation.AddMeshButton.bl_idname, icon=globs.ICON_ADD) + col.separator() + + if Common.is_enum_empty(context.scene.add_mesh): + row = col.row(align=True) + col.label(text=t('DecimationPanel.warn.noDecimation'), icon='ERROR') + + box2 = col.box() + col = box2.column(align=True) + + if len(Decimation.ignore_meshes) == 0: + col.label(text=t('DecimationPanel.warn.noMeshSelected')) + + for mesh in Decimation.ignore_meshes: + row = layout_split(col, factor=0.8, align=False) + row.label(text=mesh, icon='MESH_DATA') + op = row.operator(Decimation.RemoveMeshButton.bl_idname, text='', icon=globs.ICON_REMOVE) + op.mesh_name = mesh + + col = box.column(align=True) + + if len(Decimation.ignore_shapes) == 0 and len(Decimation.ignore_meshes) == 0: + col.label(text=t('DecimationPanel.warn.emptyList'), icon='ERROR') + row = col.row(align=True) + else: + col.label(text=t('DecimationPanel.warn.correctWhitelist'), icon='INFO') + row = col.row(align=True) + + col.separator() + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'decimate_fingers') + row = col.row(align=True) + row.prop(context.scene, 'decimation_remove_doubles') + row = col.row(align=True) + row.prop(context.scene, 'decimation_retain_separated_meshes', expand=True) + row = col.row(align=True) + row.operator(Decimation.AutoDecimatePresetGoodCats.bl_idname) + row.operator(Decimation.AutoDecimatePresetExcellentCats.bl_idname) + row.operator(Decimation.AutoDecimatePresetQuestCats.bl_idname) + row = col.row(align=True) + row.prop(context.scene, 'max_tris') + col.separator() + row = col.row(align=True) + row.scale_y = 1.2 + row.operator(Decimation.AutoDecimateButtonCats.bl_idname, icon='MOD_DECIM') + col.separator() + row = col.row(align=True) + row.scale_y = 1.5 + row.operator(DecimationHelpButton.bl_idname, icon_value=Iconloader.preview_collections['custom_icons']['help1'].icon_id) col.separator() col.separator() @@ -79,3 +238,27 @@ def execute(self, context): self.report({'INFO'}, t('LegacyReadButton.success')) return {'FINISHED'} + +@register_wrap +class LegacyDecimationButton(bpy.types.Operator): + bl_idname = 'legacy_decimation.help' + bl_label = t('LegacyDecimationButton.label') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + webbrowser.open(t('LegacyDecimationButton.URL')) + + self.report({'INFO'}, t('LegacyDecimationButton.success')) + return {'FINISHED'} + +@register_wrap +class DecimationHelpButton(bpy.types.Operator): + bl_idname = 'legacy_helpdecimation.help' + bl_label = t('DecimationHelpButton.label') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + webbrowser.open(t('DecimationHelpButton.URL')) + + self.report({'INFO'}, t('DecimationHelpButton.success')) + return {'FINISHED'} diff --git a/ui/mmdoptions.py b/ui/mmdoptions.py index ae2c2da..662059f 100644 --- a/ui/mmdoptions.py +++ b/ui/mmdoptions.py @@ -49,6 +49,12 @@ def draw(self, context): row.operator(ModelSettings.bl_idname, text="", icon='MODIFIER') col.separator() col.separator() + split = col.row(align=True) + row = split.row(align=True) + row.scale_y = 1.5 + row.operator(Material.CombineMaterialsButton.bl_idname, icon='MATERIAL') + col.separator() + col.separator() sub = col.column(align=True) sub.scale_y = 0.75 sub.label(text=t("MMDOptions.FixMaterialinfo1"), icon='INFO') diff --git a/ui/optimization.py b/ui/optimization.py index eff9390..be1cb72 100644 --- a/ui/optimization.py +++ b/ui/optimization.py @@ -88,11 +88,6 @@ def draw(self, context): row.alignment = 'RIGHT' row.scale_y = 0.9 row.operator(Atlas.AtlasHelpButton.bl_idname, text="", icon='QUESTION') - # row.separator() - # row = split.row(align=False) - # row.alignment = 'RIGHT' - # row.scale_y = 0.9 - # row.operator(Atlas.AtlasHelpButton.bl_idname, text="", icon='QUESTION') col.separator() # If supported version is outdated @@ -265,6 +260,11 @@ def draw(self, context): row.operator(Armature_manual.RemoveZeroWeightBones.bl_idname, text=t('OtherOptionsPanel.RemoveZeroWeightBones.label')) row.operator(Armature_manual.RemoveConstraints.bl_idname, text=t('OtherOptionsPanel.RemoveConstraints')) row.operator(Armature_manual.RemoveZeroWeightGroups.bl_idname, text=t('OtherOptionsPanel.RemoveZeroWeightGroups')) + col = box.column(align=True) + row = col.row(align=True) + row.scale_y = 1.1 + col.separator() + row.prop(context.scene, "delete_zero_weight_keep_twists") col.separator() col = box.column(align=True) diff --git a/ui/otheroptions.py b/ui/otheroptions.py index 28db577..a21047a 100644 --- a/ui/otheroptions.py +++ b/ui/otheroptions.py @@ -26,7 +26,7 @@ def draw(self, context): col = box.column(align=True) row = layout_split(col, factor=0.32, align=True) row.scale_y = button_height - row.label(text="Separate by:", icon='MESH_DATA') + row.label(text=t('OtherOptionsPanel.separateBy'), icon='MESH_DATA') row.operator(Armature_manual.SeparateByMaterials.bl_idname, text=t('OtherOptionsPanel.SeparateByMaterials.label')) row.operator(Armature_manual.SeparateByLooseParts.bl_idname, text=t('OtherOptionsPanel.SeparateByLooseParts.label')) row.operator(Armature_manual.SeparateByShapekeys.bl_idname, text=t('OtherOptionsPanel.SeparateByShapekeys.label')) @@ -103,7 +103,3 @@ def draw(self, context): row = col.row(align=True) row.scale_y = button_height row.operator(Armature_manual.TestButton.bl_idname) - - # row = col.row(align=True) - # row.scale_y = button_height - # row.operator(Armature_manual.SeparateByCopyProtection.bl_idname, icon='SHAPEKEY_DATA') diff --git a/updater.py b/updater.py index 8e53507..354e8a4 100644 --- a/updater.py +++ b/updater.py @@ -89,8 +89,8 @@ def execute(self, context): class UpdateToSelectedButton(bpy.types.Operator): bl_idname = 'cats_updater.update_selected' - bl_label = 'Update to Selected version' - bl_description = 'Updates CATS to the selected version' + bl_label = t('UpdateToSelectedButton.label') + bl_description = t('UpdateToSelectedButton.desc') bl_options = {'INTERNAL'} @classmethod @@ -757,8 +757,6 @@ def get_user_preferences(): def layout_split(layout, factor=0.0, align=False): - if bpy.app.version < (2, 79, 9): - return layout.split(percentage=factor, align=align) return layout.split(factor=factor, align=align)