From da162fa5b6ef02806098461950e1612699035e50 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 3 Jun 2020 23:53:30 +0200 Subject: [PATCH 01/64] Changed link to a new vrm importer since the old one dropped support --- README.md | 3 +++ __init__.py | 2 +- tools/importer.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 760ae722..cb8621b0 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,9 @@ It checks for a new version automatically once every day. ## Changelog +#### 0.17.1 +- Changed link to a new vrm importer since the old one dropped support + #### 0.17.0 - **Cats is now fully compatible with Blender 2.83!** - *It was compatible with 2.82 all long* diff --git a/__init__.py b/__init__.py index b8ecdad3..a620288b 100644 --- a/__init__.py +++ b/__init__.py @@ -36,7 +36,7 @@ 'tracker_url': 'https://github.com/michaeldegroot/cats-blender-plugin/issues', 'warning': '', } -dev_branch = False +dev_branch = True import os import sys diff --git a/tools/importer.py b/tools/importer.py index 35df5b5d..31787d1b 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -783,7 +783,7 @@ def execute(self, context): if Common.version_2_79_or_older(): webbrowser.open('https://github.com/iCyP/VRM_IMPORTER_for_Blender2_79') else: - webbrowser.open('https://github.com/iCyP/VRM_IMPORTER_for_Blender2_8') + webbrowser.open('https://github.com/saturday06/VRM_IMPORTER_for_Blender2_8') self.report({'INFO'}, 'VRM Importer link opened') return {'FINISHED'} From 950370a34935206d753873582d248f5bdd10bcc0 Mon Sep 17 00:00:00 2001 From: Jordo Date: Sun, 12 Jul 2020 17:43:38 +0200 Subject: [PATCH 02/64] Added translation files for CATS --- __init__.py | 30 +- extentions.py | 361 +++++------- tools/armature.py | 37 +- tools/armature_custom.py | 55 +- tools/armature_manual.py | 242 +++----- tools/atlas.py | 41 +- tools/bonemerge.py | 8 +- tools/common.py | 5 +- tools/copy_protection.py | 20 +- tools/credits.py | 19 +- tools/decimation.py | 50 +- tools/eyetracking.py | 78 +-- tools/importer.py | 186 +++--- tools/material.py | 47 +- tools/rootbone.py | 14 +- tools/settings.py | 22 +- tools/shapekey.py | 21 +- tools/supporter.py | 17 +- tools/translate.py | 71 +-- tools/viseme.py | 12 +- translations/__init__.py | 25 + translations/en_US.py | 1170 ++++++++++++++++++++++++++++++++++++++ translations/ja_JP.py | 1170 ++++++++++++++++++++++++++++++++++++++ ui/armature.py | 27 +- ui/bone_root.py | 3 +- ui/copy_protection.py | 9 +- ui/credits.py | 13 +- ui/custom.py | 29 +- ui/decimation.py | 29 +- ui/eye_tracking.py | 24 +- ui/main.py | 6 +- ui/manual.py | 54 +- ui/optimization.py | 31 +- ui/settings_updates.py | 9 +- ui/supporter.py | 11 +- ui/visemes.py | 5 +- updater.py | 121 ++-- 37 files changed, 3105 insertions(+), 967 deletions(-) create mode 100644 translations/__init__.py create mode 100644 translations/en_US.py create mode 100644 translations/ja_JP.py diff --git a/__init__.py b/__init__.py index a620288b..2a6a0c0e 100644 --- a/__init__.py +++ b/__init__.py @@ -51,6 +51,7 @@ import requests from . import globs +from .translations import t # Check if cats is reloading or started fresh if "bpy" not in locals(): @@ -167,18 +168,12 @@ def remove_corrupted_files(): if no_perm: unregister() sys.tracebacklimit = 0 - raise ImportError('\n\nFaulty CATS installation found!' - '\nTo fix this restart Blender as admin! ' - '\n') + raise ImportError(t('Main.error.restartAdmin')) if os_error: unregister() sys.tracebacklimit = 0 - message = ' ' \ - ' '\ - '\n\nFaulty CATS installation found!' \ - '\nTo fix this delete the following files and folders inside your addons folder:' \ - '\n' + message = t('Main.error.deleteFollowing') for folder in folders: if folder in to_remove: @@ -193,16 +188,12 @@ def remove_corrupted_files(): if wrong_path: unregister() sys.tracebacklimit = 0 - raise ImportError('\n\nFaulty CATS installation found!' - '\nPlease install CATS via User Preferences and restart Blender!' - '\n') + raise ImportError(t('Main.error.installViaPreferences')) if faulty_installation: unregister() sys.tracebacklimit = 0 - raise ImportError('\n\nFaulty CATS installation was found and fixed!' - '\nPlease restart Blender and enable CATS again!' - '\n') + raise ImportError(t('Main.error.restartAndEnable')) def check_unsupported_blender_versions(): @@ -210,17 +201,13 @@ def check_unsupported_blender_versions(): if bpy.app.version < (2, 79): unregister() sys.tracebacklimit = 0 - raise ImportError('\n\nBlender versions older than 2.79 are not supported by Cats. ' - '\nPlease use Blender 2.79 or later.' - '\n') + raise ImportError(t('Main.error.unsupportedVersion')) # Versions 2.80.0 to 2.80.74 are beta versions, stable is 2.80.75 if (2, 80, 0) <= bpy.app.version < (2, 80, 75): unregister() sys.tracebacklimit = 0 - raise ImportError('\n\nYou are still on the beta version of Blender 2.80!' - '\nPlease update to the release version of Blender 2.80.' - '\n') + raise ImportError(t('Main.error.beta2.80')) def set_cats_version_string(): @@ -277,8 +264,7 @@ def register(): show_error = True if show_error: sys.tracebacklimit = 0 - raise ImportError('\n\nPlease restart Blender and enable CATS again!' - '\n') + raise ImportError(t('Main.error.restartAndEnable_alt')) # if not tools.settings.use_custom_mmd_tools(): # bpy.utils.unregister_module("mmd_tools") diff --git a/extentions.py b/extentions.py index d976b744..868283cb 100644 --- a/extentions.py +++ b/extentions.py @@ -4,6 +4,7 @@ from .tools import rootbone as Rootbone from .tools import settings as Settings from .tools import importer as Importer +from .translations import t from bpy.types import Scene, Material from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, CollectionProperty @@ -11,273 +12,203 @@ def register(): Scene.armature = EnumProperty( - name='Armature', - description='Select the armature which will be used by Cats', + name=t('Scene.armature.label'), + description=t('Scene.armature.desc'), items=Common.get_armature_list, update=Common.update_material_list ) Scene.zip_content = EnumProperty( - name='Zip Content', - description='Select the model you want to import', + name=t('Scene.zip_content.label'), + description=t('Scene.zip_content.desc'), items=Importer.get_zip_content ) Scene.keep_upper_chest = BoolProperty( - name='Keep Upper Chest', - description="VrChat now partially supports the Upper Chest bone, so deleting it is no longer necessary." - "\n\nWARNING: Currently this breaks Eye Tracking, so don't check this if you want Eye Tracking", + name=t('Scene.keep_upper_chest.label'), + description=t('Scene.keep_upper_chest.desc'), default=False ) Scene.combine_mats = BoolProperty( - name='Combine Same Materials', - description="Combines similar materials into one, reducing draw calls.\n\n" - 'Your avatar should visibly look the same after this operation.\n' - 'This is a very important step for optimizing your avatar.\n' - 'If you have problems with this, uncheck this option and tell us!\n', + name=t('Scene.combine_mats.label'), + description=t('Scene.combine_mats.desc'), default=True ) Scene.remove_zero_weight = BoolProperty( - name='Remove Zero Weight Bones', - description="Cleans up the bones hierarchy, deleting all bones that don't directly affect any vertices." - '\nUncheck this if bones or vertex groups that you want to keep got deleted', + name=t('Scene.remove_zero_weight.label'), + description=t('Scene.remove_zero_weight.desc'), default=True ) Scene.keep_end_bones = BoolProperty( - name='Keep End Bones', - description="Saves end bones from deletion." - '\n\nThis can improve skirt movement for dynamic bones, but increases the bone count.' - '\nThis can also fix issues with crumbled finger bones in Unity.' - '\nMake sure to always uncheck "Add Leaf Bones" when exporting or use the CATS export button', + name=t('Scene.keep_end_bones.label'), + description=t('Scene.keep_end_bones.desc'), default=False ) Scene.keep_twist_bones = BoolProperty( - name='Keep Twist Bones', - description='This will keep any bone with "Twist" in the name.' - '\nSo if there are certain bones that you want to keep, you can add "Twist" to them and they won\'t get deleted.' - '\n\nVRChat can now make use of twist bones, so you can use this option to keep them', + name=t('Scene.keep_twist_bones.label'), + description=t('Scene.keep_twist_bones.desc'), default=False ) Scene.fix_twist_bones = BoolProperty( - name='Fix MMD Twist Bones', - description='This will make MMD arm twist bones usable in VRChat.' - '\nWIll only work if the twist bones are properly named.' - '\nRequired names:' - '\n - ArmTwist[1-3]_[L/R]' - '\n - HandTwist[1-3]_[L/R]' - '\n\nYou don\'t need to enable "Keep Twist Bones" for this to work', + name=t('Scene.fix_twist_bones.label'), + description=t('Scene.fix_twist_bones.desc'), default=True ) Scene.join_meshes = BoolProperty( - name='Join Meshes', - description='Joins all meshes of this model together.' - '\nIt also:' - '\n - Applies all transformations' - '\n - Repairs broken armature modifiers' - '\n - Applies all decimation and mirror modifiers' - '\n - Merges UV maps correctly' - '\n' - '\nINFO: You should always join your meshes', + name=t('Scene.join_meshes.label'), + description=t('Scene.join_meshes.desc'), default=True ) Scene.connect_bones = BoolProperty( - name='Connect Bones', - description="This connects all bones to their child bone if they have exactly one child bone.\n" - "This will not change how the bones function in any way, it just improves the aesthetic of the armature", + name=t('Scene.connect_bones.label'), + description=t('Scene.connect_bones.desc'), default=True ) Scene.fix_materials = BoolProperty( - name='Fix Materials', - description="This will apply some VRChat related fixes to materials", + name=t('Scene.fix_materials.label'), + description=t('Scene.fix_materials.desc'), default=True ) Scene.remove_rigidbodies_joints = BoolProperty( - name='Remove Rigidbodies and Joints', - description="Rigidbodies and joints are used by MMD software to simulate physics." - "\nThey are completely useless for VRChat, so removing them is recommended for VRChat users!", + name=t('Scene.remove_rigidbodies_joints.label'), + description=t('Scene.remove_rigidbodies_joints.desc'), default=True ) Scene.use_google_only = BoolProperty( - name='Use Old Translations (not recommended)', - description="Ignores the internal dictionary and only uses the Google Translator for shape key translations." - "\n" - '\nThis will result in slower translation speed and worse translations, but the translations will be like in CATS version 0.9.0 and older.' - "\nOnly use this if you have animations which rely on the old translations and you don't want to convert them to the new ones", + name=t('Scene.use_google_only.label'), + description=t('Scene.use_google_only.desc'), default=False ) Scene.show_more_options = BoolProperty( - name='Show More Options', - description="Shows more model options", + name=t('Scene.show_more_options.label'), + description=t('Scene.show_more_options.desc'), default=False ) Scene.merge_mode = EnumProperty( - name="Merge Mode", - description="Mode", + name=t('Scene.merge_mode.label'), + description=t('Scene.merge_mode.desc'), items=[ - ("ARMATURE", "Merge Armatures", "Here you can merge two armatures together."), - ("MESH", "Attach Mesh", "Here you can attach a mesh to an armature.") + ("ARMATURE", t('Scene.merge_mode.armature.label'), t('Scene.merge_mode.armature.desc')), + ("MESH", t('Scene.merge_mode.mesh.label'), t('Scene.merge_mode.mesh.desc')) ] ) Scene.merge_armature_into = EnumProperty( - name='Base Armature', - description='Select the armature into which the other armature will be merged\n', + name=t('Scene.merge_armature_into.label'), + description=t('Scene.merge_armature_into.desc'), items=Common.get_armature_list ) Scene.merge_armature = EnumProperty( - name='Merge Armature', - description='Select the armature which will be merged into the selected armature above\n', + name=t('Scene.merge_armature.label'), + description=t('Scene.merge_armature.desc'), items=Common.get_armature_merge_list ) Scene.attach_to_bone = EnumProperty( - name='Attach to Bone', - description='Select the bone to which the armature will be attached to\n', + name=t('Scene.attach_to_bone.label'), + description=t('Scene.attach_to_bone.desc'), items=Common.get_bones_merge ) Scene.attach_mesh = EnumProperty( - name='Attach Mesh', - description='Select the mesh which will be attached to the selected bone in the selected armature\n', + name=t('Scene.attach_mesh.label'), + description=t('Scene.attach_mesh.desc'), items=Common.get_top_meshes ) Scene.merge_same_bones = BoolProperty( - name='Merge All Bones', - description='Merges all bones together that have the same name instead of only the base bones (Hips, Spine, etc).' - '\nYou will have to make sure that all the bones you want to merge have the same name.' - '\n' - "\nIf this is checked, you won't need to fix the model with CATS beforehand but it is still advised to do so." - "\nIf this is unchecked, CATS will only merge the base bones (Hips, Spine, etc)." - "\n" - "\nThis can have unintended side effects, so check your model afterwards!" - "\n", + name=t('Scene.merge_same_bones.label'), + description=t('Scene.merge_same_bones.desc'), default=False ) Scene.apply_transforms = BoolProperty( - name='Apply Transforms', - description='Check this if both armatures and meshes are already at their correct positions.' - '\nThis will cause them to stay exactly where they are when merging', + name=t('Scene.apply_transforms.label'), + description=t('Scene.apply_transforms.desc'), default=False ) Scene.merge_armatures_join_meshes = BoolProperty( - name='Join Meshes', - description='This will join all meshes.' - '\nNot checking this will always apply transforms', + name=t('Scene.merge_armatures_join_meshes.label'), + description=t('Scene.merge_armatures_join_meshes.desc'), default=True ) Scene.merge_armatures_remove_zero_weight_bones = BoolProperty( - name='Remove Zero Weight Bones', - description="Cleans up the bones hierarchy, deleting all bones that don't directly affect any vertices." - '\nUncheck this if bones or vertex groups that you want to keep got deleted', + name=t('Scene.merge_armatures_remove_zero_weight_bones.label'), + description=t('Scene.merge_armatures_remove_zero_weight_bones.desc'), default=True ) # Decimation Scene.decimation_mode = EnumProperty( - name="Decimation Mode", - description="Decimation Mode", + name=t('Scene.decimation_mode.label'), + description=t('Scene.decimation_mode.desc'), items=[ - ("SAFE", "Safe", 'Decent results - no shape key loss\n' - '\n' - "This will only decimate meshes with no shape keys.\n" - "The results are decent and you won't lose any shape keys.\n" - 'Eye Tracking and Lip Syncing will be fully preserved.'), - - ("HALF", "Half", 'Good results - minimal shape key loss\n' - "\n" - "This will only decimate meshes with less than 4 shape keys as those are often not used.\n" - 'The results are better but you will lose the shape keys in some meshes.\n' - 'Eye Tracking and Lip Syncing should still work.'), - - ("FULL", "Full", 'Best results - full shape key loss\n' - '\n' - "This will decimate your whole model deleting all shape keys in the process.\n" - 'This will give the best results but you will lose the ability to add blinking and Lip Syncing.\n' - 'Eye Tracking will still work if you disable Eye Blinking.'), - - ("CUSTOM", "Custom", 'Custom results - custom shape key loss\n' - '\n' - "This will let you choose which meshes and shape keys should not be decimated.\n") + ("SAFE", t('Scene.decimation_mode.safe.label'), t('Scene.decimation_mode.safe.desc')), + + ("HALF", t('Scene.decimation_mode.half.label'), t('Scene.decimation_mode.half.desc')), + + ("FULL", t('Scene.decimation_mode.full.label'), t('Scene.decimation_mode.full.desc')), + + ("CUSTOM", t('Scene.decimation_mode.custom.label'), t('Scene.decimation_mode.custom.desc')) ], default='HALF' ) Scene.selection_mode = EnumProperty( - name="Selection Mode", - description="Selection Mode", + name=t('Scene.selection_mode.label'), + description=t('Scene.selection_mode.desc'), items=[ - ("SHAPES", "Shape Keys", 'Select all the shape keys you want to preserve here.'), - ("MESHES", "Meshes", "Select all the meshes you don't want to decimate here.") + ("SHAPES", t('Scene.selection_mode.shapekeys.label'), t('Scene.selection_mode.shapekeys.desc')), + ("MESHES", t('Scene.selection_mode.meshes.label'), t('Scene.selection_mode.meshes.desc')) ] ) Scene.add_shape_key = EnumProperty( - name='Shape', - description='The shape key you want to keep', + name=t('Scene.add_shape_key.label'), + description=t('Scene.add_shape_key.desc'), items=Common.get_shapekeys_decimation ) Scene.add_mesh = EnumProperty( - name='Mesh', - description='The mesh you want to leave untouched by the decimation', + name=t('Scene.add_mesh.label'), + description=t('Scene.add_mesh.desc'), items=Common.get_meshes_decimation ) Scene.decimate_fingers = BoolProperty( - name="Save Fingers", - description="Check this if you don't want to decimate your fingers!\n" - "Results will be worse but there will be no issues with finger movement.\n" - "This is probably only useful if you have a VR headset.\n" - "\n" - "This operation requires the finger bones to be named specifically:\n" - "Thumb(0-2)_(L/R)\n" - "IndexFinger(1-3)_(L/R)\n" - "MiddleFinger(1-3)_(L/R)\n" - "RingFinger(1-3)_(L/R)\n" - "LittleFinger(1-3)_(L/R)" + name=t('Scene.decimate_fingers.label'), + description=t('Scene.decimate_fingers.desc') ) Scene.decimate_hands = BoolProperty( - name="Save Hands", - description="Check this if you don't want to decimate your full hands!\n" - "Results will be worse but there will be no issues with hand movement.\n" - "This is probably only useful if you have a VR headset.\n" - "\n" - "This operation requires the finger and hand bones to be named specifically:\n" - "Left/Right wrist\n" - "Thumb(0-2)_(L/R)\n" - "IndexFinger(1-3)_(L/R)\n" - "MiddleFinger(1-3)_(L/R)\n" - "RingFinger(1-3)_(L/R)\n" - "LittleFinger(1-3)_(L/R)" + name=t('Scene.decimate_hands.label'), + description=t('Scene.decimate_hands.desc') ) Scene.decimation_remove_doubles = BoolProperty( - name="Remove Doubles", - description="Uncheck this if you got issues with with this checked", + name=t('Scene.decimation_remove_doubles.label'), + description=t('Scene.decimation_remove_doubles.desc'), default=True ) Scene.max_tris = IntProperty( - name='Tris', - description="The target amount of tris after decimation", + name=t('Scene.max_tris.label'), + description=t('Scene.max_tris.desc'), default=70000, min=1, max=200000 @@ -285,88 +216,78 @@ def register(): # Eye Tracking Scene.eye_mode = EnumProperty( - name="Eye Mode", - description="Mode", + name=t('Scene.eye_mode.label'), + description=t('Scene.eye_mode.desc'), items=[ - ("CREATION", "Creation", "Here you can create eye tracking."), - ("TESTING", "Testing", "Here you can test how eye tracking will look ingame.") + ("CREATION", t('Scene.eye_mode.creation.label'), t('Scene.eye_mode.creation.desc')), + ("TESTING", t('Scene.eye_mode.testing.label'), t('Scene.eye_mode.testing.desc')) ], update=Eyetracking.stop_testing ) Scene.mesh_name_eye = EnumProperty( - name='Mesh', - description='The mesh with the eyes vertex groups', + name=t('Scene.mesh_name_eye.label'), + description=t('Scene.mesh_name_eye.desc'), items=Common.get_meshes ) Scene.head = EnumProperty( - name='Head', - description='The head bone containing the eye bones', + name=t('Scene.head.label'), + description=t('Scene.head.desc'), items=Common.get_bones_head ) Scene.eye_left = EnumProperty( - name='Left Eye', - description='The models left eye bone', + name=t('Scene.eye_left.label'), + description=t('Scene.eye_left.desc'), items=Common.get_bones_eye_l ) Scene.eye_right = EnumProperty( - name='Right Eye', - description='The models right eye bone', + name=t('Scene.eye_right.label'), + description=t('Scene.eye_right.desc'), items=Common.get_bones_eye_r ) Scene.wink_left = EnumProperty( - name='Blink Left', - description='The shape key containing a blink with the left eye', + name=t('Scene.wink_left.label'), + description=t('Scene.wink_left.desc'), items=Common.get_shapekeys_eye_blink_l ) Scene.wink_right = EnumProperty( - name='Blink Right', - description='The shape key containing a blink with the right eye', + name=t('Scene.wink_right.label'), + description=t('Scene.wink_right.desc'), items=Common.get_shapekeys_eye_blink_r ) Scene.lowerlid_left = EnumProperty( - name='Lowerlid Left', - description='The shape key containing a slightly raised left lower lid.\n' - 'Can be set to "Basis" to disable lower lid movement', + name=t('Scene.lowerlid_left.label'), + description=t('Scene.lowerlid_left.desc'), items=Common.get_shapekeys_eye_low_l ) Scene.lowerlid_right = EnumProperty( - name='Lowerlid Right', - description='The shape key containing a slightly raised right lower lid.\n' - 'Can be set to "Basis" to disable lower lid movement', + name=t('Scene.lowerlid_right.label'), + description=t('Scene.lowerlid_right.desc'), items=Common.get_shapekeys_eye_low_r ) Scene.disable_eye_movement = BoolProperty( - name='Disable Eye Movement', - description='IMPORTANT: Do your decimation first if you check this!\n' - '\n' - 'Disables eye movement. Useful if you only want blinking.\n' - 'This creates eye bones with no movement bound to them.\n' - 'You still have to assign "LeftEye" and "RightEye" to the eyes in Unity', + name=t('Scene.disable_eye_movement.label'), + description=t('Scene.disable_eye_movement.desc'), subtype='DISTANCE' ) Scene.disable_eye_blinking = BoolProperty( - name='Disable Eye Blinking', - description='Disables eye blinking. Useful if you only want eye movement.\n' - 'This will create the necessary shape keys but leaves them empty', + name=t('Scene.disable_eye_blinking.label'), + description=t('Scene.disable_eye_blinking.desc'), subtype='NONE' ) Scene.eye_distance = FloatProperty( - name='Eye Movement Range', - description='Higher = more eye movement\n' - 'Lower = less eye movement\n' - 'Warning: Too little or too much range can glitch the eyes.\n' - 'Test your results in the "Eye Testing"-Tab!\n', + name=t('Scene.eye_distance.label'), + description=t('Scene.eye_distance.desc'), default=0.8, min=0.0, max=2.0, @@ -376,8 +297,8 @@ def register(): ) Scene.eye_rotation_x = IntProperty( - name='Up - Down', - description='Rotate the eye bones on the vertical axis', + name=t('Scene.eye_rotation_x.label'), + description=t('Scene.eye_rotation_x.desc'), default=0, min=-19, max=25, @@ -387,9 +308,8 @@ def register(): ) Scene.eye_rotation_y = IntProperty( - name='Left - Right', - description='Rotate the eye bones on the horizontal axis.' - '\nThis is from your own point of view', + name=t('Scene.eye_rotation_y.label'), + description=t('Scene.eye_rotation_y.desc'), default=0, min=-19, max=19, @@ -399,8 +319,8 @@ def register(): ) Scene.iris_height = IntProperty( - name='Iris Height', - description='Moves the iris away from the eye ball', + name=t('Scene.iris_height.label'), + description=t('Scene.iris_height.desc'), default=0, min=0, max=100, @@ -409,8 +329,8 @@ def register(): ) Scene.eye_blink_shape = FloatProperty( - name='Blink Strength', - description='Test the blinking of the eye', + name=t('Scene.eye_blink_shape.label'), + description=t('Scene.eye_blink_shape.desc'), default=1.0, min=0.0, max=1.0, @@ -420,8 +340,8 @@ def register(): ) Scene.eye_lowerlid_shape = FloatProperty( - name='Lowerlid Strength', - description='Test the lowerlid blinking of the eye', + name=t('Scene.eye_lowerlid_shape.label'), + description=t('Scene.eye_lowerlid_shape.desc'), default=1.0, min=0.0, max=1.0, @@ -432,32 +352,32 @@ def register(): # Visemes Scene.mesh_name_viseme = EnumProperty( - name='Mesh', - description='The mesh with the mouth shape keys', + name=t('Scene.mesh_name_viseme.label'), + description=t('Scene.mesh_name_viseme.desc'), items=Common.get_meshes ) Scene.mouth_a = EnumProperty( - name='Viseme AA', - description='Shape key containing mouth movement that looks like someone is saying "aa".\nDo not put empty shape keys like "Basis" in here', + name=t('Scene.mouth_a.label'), + description=t('Scene.mouth_a.desc'), items=Common.get_shapekeys_mouth_ah, ) Scene.mouth_o = EnumProperty( - name='Viseme OH', - description='Shape key containing mouth movement that looks like someone is saying "oh".\nDo not put empty shape keys like "Basis" in here', + name=t('Scene.mouth_o.label'), + description=t('Scene.mouth_o.desc'), items=Common.get_shapekeys_mouth_oh, ) Scene.mouth_ch = EnumProperty( - name='Viseme CH', - description='Shape key containing mouth movement that looks like someone is saying "ch". Opened lips and clenched teeth.\nDo not put empty shape keys like "Basis" in here', + name=t('Scene.mouth_ch.label'), + description=t('Scene.mouth_ch.desc'), items=Common.get_shapekeys_mouth_ch, ) Scene.shape_intensity = FloatProperty( - name='Shape Key Mix Intensity', - description='Controls the strength in the creation of the shape keys. Lower for less mouth movement strength', + name=t('Scene.shape_intensity.label'), + description=t('Scene.shape_intensity.desc'), default=1.0, min=0.0, max=10.0, @@ -468,19 +388,19 @@ def register(): # Bone Parenting Scene.root_bone = EnumProperty( - name='To Parent', - description='List of bones that look like they could be parented together to a root bone', + name=t('Scene.root_bone.label'), + description=t('Scene.root_bone.desc'), items=Rootbone.get_parent_root_bones, ) # Optimize Scene.optimize_mode = EnumProperty( - name="Optimize Mode", - description="Mode", + name=t('Scene.optimize_mode.label'), + description=t('Scene.optimize_mode.desc'), items=[ - ("ATLAS", "Atlas", "Allows you to make a texture atlas."), - ("MATERIAL", "Material", "Some various options on material manipulation."), - ("BONEMERGING", "Bone Merging", "Allows child bones to be merged into their parents."), + ("ATLAS", t('Scene.optimize_mode.atlas.label'), t('Scene.optimize_mode.atlas.desc')), + ("MATERIAL", t('Scene.optimize_mode.material.label'), t('Scene.optimize_mode.material.desc')), + ("BONEMERGING", t('Scene.optimize_mode.bonemerging.label'), t('Scene.optimize_mode.bonemerging.desc')), ] ) @@ -505,9 +425,8 @@ def register(): # Bone Merging Scene.merge_ratio = FloatProperty( - name='Merge Ratio', - description='Higher = more bones will be merged\n' - 'Lower = less bones will be merged\n', + name=t('Scene.merge_ratio.label'), + description=t('Scene.merge_ratio.desc'), default=50, min=1, max=100, @@ -517,36 +436,34 @@ def register(): ) Scene.merge_mesh = EnumProperty( - name='Mesh', - description='The mesh with the bones vertex groups', + name=t('Scene.merge_mesh.label'), + description=t('Scene.merge_mesh.desc'), items=Common.get_meshes ) Scene.merge_bone = EnumProperty( - name='To Merge', - description='List of bones that look like they could be merged together to reduce overall bones', + name=t('Scene.merge_bone.label'), + description=t('Scene.merge_bone.desc'), items=Rootbone.get_parent_root_bones, ) # Settings Scene.embed_textures = BoolProperty( - name='Embed Textures on Export', - description='Enable this to embed the texture files into the FBX file upon export.' - '\nUnity will automatically extract these textures and put them into a separate folder.' - '\nThis might not work for everyone and it increases the file size of the exported FBX file', + name=t('Scene.embed_textures.label'), + description=t('Scene.embed_textures.desc'), default=False, update=Settings.update_settings ) Scene.use_custom_mmd_tools = BoolProperty( - name='Use Custom mmd_tools', - description='Enable this to use your own version of mmd_tools. This will disable the internal cats mmd_tools', + name=t('Scene.use_custom_mmd_tools.label'), + description=t('Scene.use_custom_mmd_tools.desc'), default=False, update=Settings.update_settings ) Scene.debug_translations = BoolProperty( - name='Debug Google Translations', - description='Tests the Google Translations and prints the Google response in case of error', + name=t('Scene.debug_translations.label'), + description=t('Scene.debug_translations.desc'), default=False ) diff --git a/tools/armature.py b/tools/armature.py index 5b5efb29..86c944a5 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -38,6 +38,7 @@ from .common import version_2_79_or_older from .register import register_wrap from mmd_tools_local.operators import morph as Morph +from ..translations import t mmd_tools_installed = True @@ -46,16 +47,8 @@ @register_wrap class FixArmature(bpy.types.Operator): bl_idname = 'cats_armature.fix' - bl_label = 'Fix Model' - bl_description = 'Automatically:\n' \ - '- Reparents bones\n' \ - '- Removes unnecessary bones, objects, groups & constraints\n' \ - '- Translates and renames bones & objects\n' \ - '- Merges weight paints\n' \ - '- Corrects the hips\n' \ - '- Joins meshes\n' \ - '- Converts morphs into shapes\n' \ - '- Corrects shading' + bl_label = t('FixArmature.label') + bl_description = t('FixArmature.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @@ -91,9 +84,7 @@ def execute(self, context): if mesh.name.endswith(('.baked', '.baked0')): is_vrm = True # TODO if not is_vrm: - Common.show_error(3.8, ['No mesh inside the armature found!', - 'If there are meshes outside of the armature,', - 'set the armature as the parent of the meshes.']) + Common.show_error(3.8, t('FixArmature.error.noMesh')) return {'CANCELLED'} print('\nFixing Model:\n') @@ -1225,14 +1216,14 @@ def add_eye_children(eye_bone, parent_name): if fixed_uv_coords: saved_data.load() - Common.show_error(6.2, ['The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', - 'This could result in broken textures and you might have to fix them manually.', - 'This issue is often caused by edits in PMX editor.']) + Common.show_error(6.2, [t('FixArmature.error.faultyUV1', uvcoord=str(fixed_uv_coords)), + t('FixArmature.error.faultyUV2'), + t('FixArmature.error.faultyUV3')]) return {'FINISHED'} saved_data.load() - self.report({'INFO'}, 'Model successfully fixed.') + self.report({'INFO'}, t('FixArmature.fixedSuccess')) return {'FINISHED'} @@ -1240,7 +1231,7 @@ def check_hierarchy(check_parenting, correct_hierarchy_array): armature = Common.set_default_stage() missing_bones = [] - missing2 = ['The following bones were not found:', ''] + missing2 = [t('FixArmature.bonesNotFound'), ''] for correct_hierarchy in correct_hierarchy_array: # For each hierarchy array line = ' - ' @@ -1257,9 +1248,9 @@ def check_hierarchy(check_parenting, correct_hierarchy_array): if len(missing2) > 2 and not check_parenting: missing2.append('') - missing2.append('Looks like you found a model which Cats could not fix!') - missing2.append('If this is a non modified model we would love to make it compatible.') - missing2.append('Report it to us in the forum or in our discord, links can be found in the Credits panel.') + missing2.append(t('FixArmature.cantFix1')) + missing2.append(t('FixArmature.cantFix2')) + missing2.append(t('FixArmature.cantFix3')) Common.show_error(6.4, missing2) return {'result': True, 'message': ''} @@ -1278,10 +1269,10 @@ def check_hierarchy(check_parenting, correct_hierarchy_array): if previous is not None: # And there is no parent, then we have a problem mkay if bone.parent is None: - return {'result': False, 'message': bone.name + ' is not parented at all, this will cause problems!'} + return {'result': False, 'message': bone.name + t('FixArmature.notParent')} # Previous needs to be the parent of the current item if previous != bone.parent.name: - return {'result': False, 'message': bone.name + ' is not parented to ' + previous + ', this will cause problems!'} + return {'result': False, 'message': bone.name + t('FixArmature.notParentTo1') + previous + t('FixArmature.notParentTo2')} return {'result': True} diff --git a/tools/armature_custom.py b/tools/armature_custom.py index 3fb9ca3e..b29ae4a1 100644 --- a/tools/armature_custom.py +++ b/tools/armature_custom.py @@ -30,15 +30,14 @@ from . import common as Common from . import armature_bones as Bones from .register import register_wrap +from ..translations import t @register_wrap class MergeArmature(bpy.types.Operator): bl_idname = 'cats_custom.merge_armatures' - bl_label = 'Merge Armatures' - bl_description = "Merges the selected merge armature into the base armature." \ - "\nYou should fix both armatures with Cats first." \ - "\nOnly move the mesh of the merge armature to the desired position, the bones will be moved automatically" + bl_label = t('MergeArmature.label') + bl_description = t('MergeArmature.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -60,11 +59,11 @@ def execute(self, context): if not merge_armature: saved_data.load() - Common.show_error(5.2, ['The armature "' + merge_armature_name + '" could not be found.']) + Common.show_error(5.2, [t('MergeArmature.error.notFound', name=merge_armature_name)]) return {'CANCELLED'} if not base_armature: saved_data.load() - Common.show_error(5.2, ['The armature "' + base_armature_name + '" could not be found.']) + Common.show_error(5.2, [t('MergeArmature.error.notFound', name=base_armature_name)]) return {'CANCELLED'} merge_parent = merge_armature.parent @@ -75,11 +74,7 @@ def execute(self, context): for i in [0, 1, 2]: if merge_parent.scale[i] != 1 or merge_parent.location[i] != 0 or merge_parent.rotation_euler[i] != 0: saved_data.load() - Common.show_error(6.5, [ - 'Please make sure that the parent of the merge armature has the following transforms:', - ' - Location at 0', - ' - Rotation at 0', - ' - Scale at 1']) + Common.show_error(6.5, t('MergeArmature.error.checkTransforms')) return {'CANCELLED'} Common.delete(merge_armature.parent) @@ -87,19 +82,12 @@ def execute(self, context): for i in [0, 1, 2]: if base_parent.scale[i] != 1 or base_parent.location[i] != 0 or base_parent.rotation_euler[i] != 0: saved_data.load() - Common.show_error(6.5, [ - 'Please make sure that the parent of the base armature has the following transforms:', - ' - Location at 0', - ' - Rotation at 0', - ' - Scale at 1']) + Common.show_error(6.5, t('MergeArmature.error.checkTransforms')) return {'CANCELLED'} Common.delete(base_armature.parent) else: saved_data.load() - Common.show_error(6.2, - ['Please use the "Fix Model" feature on the selected armatures first!', - 'Make sure to select the armature you want to fix above the "Fix Model" button!', - 'After that please only move the mesh (not the armature!) to the desired position.']) + Common.show_error(6.2, t('MergeArmature.error.pleaseFix')) return {'CANCELLED'} # if len(Common.get_meshes_objects(armature_name=merge_armature_name)) == 0: @@ -116,18 +104,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Armatures successfully joined.') + self.report({'INFO'}, t('MergeArmature.success')) return {'FINISHED'} @register_wrap class AttachMesh(bpy.types.Operator): bl_idname = 'cats_custom.attach_mesh' - bl_label = 'Attach Mesh' - bl_description = "Attaches the selected mesh to the selected bone of the selected armature." \ - "\n" \ - "\nINFO: The mesh will only be assigned to the selected bone." \ - "\nE.g.: A jacket won't work, because it requires multiple bones" + bl_label = t('AttachMesh.label') + bl_description = t('AttachMesh.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -183,20 +168,20 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Mesh successfully attached to armature.') + self.report({'INFO'}, t('AttachMesh.success')) return {'FINISHED'} @register_wrap class CustomModelTutorialButton(bpy.types.Operator): bl_idname = 'cats_custom.tutorial' - bl_label = 'Go to Documentation' + bl_label = t('CustomModelTutorialButton.label') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation') + webbrowser.open(t('CustomModelTutorialButton.URL')) - self.report({'INFO'}, 'Documentation') + self.report({'INFO'}, t('CustomModelTutorialButton.success')) return {'FINISHED'} @@ -249,13 +234,7 @@ def merge_armatures(base_armature_name, merge_armature_name, mesh_only, mesh_nam Common.unselect_all() Common.set_active(mesh_merge) - Common.show_error(7.5, - ['If you want to rotate the new part, only modify the mesh instead of the armature,', - 'or select "Apply Transforms"!', - '', - 'The transforms of the merge armature got reset and the mesh you have to modify got selected.', - 'Now place this selected mesh where and how you want it to be and then merge the armatures again.', - "If you don't want that, undo this operation."]) + Common.show_error(7.5, t('merge_armatures.error.transformReset')) return # Save the transforms of the merge armature @@ -432,7 +411,7 @@ def merge_armatures(base_armature_name, merge_armature_name, mesh_only, mesh_nam elif mesh_name: bone = armature.pose.bones.get(mesh_only_bone_name) if not bone: - Common.show_error(5.8, ['Something went wrong! Please undo, check your selections and try again.']) + Common.show_error(5.8, t('merge_armatures.error.pleaseUndo')) return armature.pose.bones.get(mesh_only_bone_name).name = mesh_name diff --git a/tools/armature_manual.py b/tools/armature_manual.py index 0e5f1a0b..55943de1 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -30,6 +30,7 @@ from . import eyetracking as Eyetracking from .common import version_2_79_or_older from .register import register_wrap +from ..translations import t mmd_tools_installed = False try: @@ -42,9 +43,8 @@ @register_wrap class StartPoseMode(bpy.types.Operator): bl_idname = 'cats_manual.start_pose_mode' - bl_label = 'Start Pose Mode' - bl_description = 'Starts the pose mode.\n' \ - 'This lets you test how your model will move' + bl_label = t('StartPoseMode.label') + bl_description = t('StartPoseMode.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -107,8 +107,8 @@ def execute(self, context): @register_wrap class StopPoseMode(bpy.types.Operator): bl_idname = 'cats_manual.stop_pose_mode' - bl_label = 'Stop Pose Mode' - bl_description = 'Stops the pose mode and resets the pose to normal' + bl_label = t('StopPoseMode.label') + bl_description = t('StopPoseMode.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -158,9 +158,8 @@ def execute(self, context): @register_wrap class PoseToShape(bpy.types.Operator): bl_idname = 'cats_manual.pose_to_shape' - bl_label = 'Pose to Shape Key' - bl_description = 'This saves your current pose as a new shape key.' \ - '\nThe new shape key will be at the bottom of your shape key list of the mesh' + bl_label = t('PoseToShape.label') + bl_description = t('PoseToShape.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -203,8 +202,8 @@ def pose_to_shapekey(name): @register_wrap class PoseNamePopup(bpy.types.Operator): bl_idname = "cats_manual.pose_name_popup" - bl_label = "Give this shapekey a name:" - bl_description = 'Sets the shapekey name. Press anywhere outside to skip' + bl_label = t('PoseNamePopup.label') + bl_description = t('PoseNamePopup.desc') bl_options = {'INTERNAL'} bpy.types.Scene.pose_to_shapekey_name = bpy.props.StringProperty(name="Pose Name") @@ -214,7 +213,7 @@ def execute(self, context): if not name: name = 'Pose' pose_to_shapekey(name) - self.report({'INFO'}, 'Pose successfully saved as shape key.') + self.report({'INFO'}, t('PoseNamePopup.success')) return {'FINISHED'} def invoke(self, context, event): @@ -238,11 +237,8 @@ def draw(self, context): @register_wrap class PoseToRest(bpy.types.Operator): bl_idname = 'cats_manual.pose_to_rest' - bl_label = 'Apply as Rest Pose' - bl_description = 'This applies the current pose position as the new rest position.' \ - '\n' \ - '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' \ - '\nWARNING: This can have unwanted effects on shape keys, so be careful when modifying the head with this' + bl_label = t('PoseToRest.label') + bl_description = t('PoseToRest.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -356,21 +352,15 @@ def check_parent(child, scale_x_tmp, scale_y_tmp, scale_z_tmp): saved_data.load(hide_only=True) - self.report({'INFO'}, 'Pose successfully applied as rest pose.') + self.report({'INFO'}, t('PoseToRest.success')) return {'FINISHED'} @register_wrap class JoinMeshes(bpy.types.Operator): bl_idname = 'cats_manual.join_meshes' - bl_label = 'Join Meshes' - bl_description = 'Joins all meshes of this model together.' \ - '\nIt also:' \ - '\n - Reorders all shape keys correctly' \ - '\n - Applies all transforms' \ - '\n - Repairs broken armature modifiers' \ - '\n - Applies all decimation and mirror modifiers' \ - '\n - Merges UV maps correctly' + bl_label = t('JoinMeshes.label') + bl_description = t('JoinMeshes.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -383,27 +373,21 @@ def execute(self, context): mesh = Common.join_meshes() if not mesh: saved_data.load() - self.report({'ERROR'}, 'Meshes could not be joined!') + self.report({'ERROR'}, t('JoinMeshes.failure')) return {'CANCELLED'} saved_data.load() Common.unselect_all() Common.set_active(mesh) - self.report({'INFO'}, 'Meshes joined.') + self.report({'INFO'}, t('JoinMeshes.success')) return {'FINISHED'} @register_wrap class JoinMeshesSelected(bpy.types.Operator): bl_idname = 'cats_manual.join_meshes_selected' - bl_label = 'Join Selected Meshes' - bl_description = 'Joins all selected meshes of this model together.' \ - '\nIt also:' \ - '\n - Reorders all shape keys correctly' \ - '\n - Applies all transforms' \ - '\n - Repairs broken armature modifiers' \ - '\n - Applies all decimation and mirror modifiers' \ - '\n - Merges UV maps correctly' + bl_label = t('JoinMeshesSelected.label') + bl_description = t('JoinMeshesSelected.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -416,29 +400,27 @@ def execute(self, context): if not Common.get_meshes_objects(mode=3): saved_data.load() - self.report({'ERROR'}, 'No meshes selected! Please select the meshes you want to join in the hierarchy!') + self.report({'ERROR'}, t('JoinMeshesSelected.error.noSelect')) return {'FINISHED'} mesh = Common.join_meshes(mode=1) if not mesh: saved_data.load() - self.report({'ERROR'}, 'Selected meshes could not be joined!') + self.report({'ERROR'}, t('JoinMeshesSelected.error.cantJoin')) return {'CANCELLED'} saved_data.load() Common.unselect_all() Common.set_active(mesh) - self.report({'INFO'}, 'Selected meshes joined.') + self.report({'INFO'}, t('JoinMeshesSelected.success')) return {'FINISHED'} @register_wrap class SeparateByMaterials(bpy.types.Operator): bl_idname = 'cats_manual.separate_by_materials' - bl_label = 'Separate by Materials' - bl_description = 'Separates selected mesh by materials.\n' \ - '\n' \ - 'Warning: Never decimate something where you might need the shape keys later (face, mouth, eyes..)' + bl_label = t('SeparateByMaterials.label') + bl_description = t('SeparateByMaterials.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -460,12 +442,11 @@ def execute(self, context): meshes = Common.get_meshes_objects() if len(meshes) == 0: saved_data.load() - self.report({'ERROR'}, 'No meshes found!') + self.report({'ERROR'}, t('SeparateByX.error.noMesh')) return {'FINISHED'} if len(meshes) > 1: saved_data.load() - self.report({'ERROR'}, 'Multiple meshes found!' - '\nPlease select the mesh you want to separate!') + self.report({'ERROR'}, t('SeparateByX.error.multipleMesh')) return {'FINISHED'} obj = meshes[0] @@ -474,16 +455,15 @@ def execute(self, context): Common.separate_by_materials(context, obj) saved_data.load(ignore=[obj_name]) - self.report({'INFO'}, 'Successfully separated by materials.') + self.report({'INFO'}, t('SeparateByMaterials.success')) return {'FINISHED'} @register_wrap class SeparateByLooseParts(bpy.types.Operator): bl_idname = 'cats_manual.separate_by_loose_parts' - bl_label = 'Separate by Loose Parts' - bl_description = 'Separates selected mesh by loose parts.\n' \ - 'This acts like separating by materials but creates more meshes for more precision' + bl_label = t('SeparateByLooseParts.label') + bl_description = t('SeparateByLooseParts.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -505,12 +485,11 @@ def execute(self, context): meshes = Common.get_meshes_objects() if len(meshes) == 0: saved_data.load() - self.report({'ERROR'}, 'No meshes found!') + self.report({'ERROR'}, t('SeparateByX.error.noMesh')) return {'FINISHED'} if len(meshes) > 1: saved_data.load() - self.report({'ERROR'}, 'Multiple meshes found!' - '\nPlease select the mesh you want to separate!') + self.report({'ERROR'}, t('SeparateByX.error.multipleMesh')) return {'FINISHED'} obj = meshes[0] obj_name = obj.name @@ -518,18 +497,15 @@ def execute(self, context): Common.separate_by_loose_parts(context, obj) saved_data.load(ignore=[obj_name]) - self.report({'INFO'}, 'Successfully separated by loose parts.') + self.report({'INFO'}, t('SeparateByLooseParts.success')) return {'FINISHED'} @register_wrap class SeparateByShapekeys(bpy.types.Operator): bl_idname = 'cats_manual.separate_by_shape_keys' - bl_label = 'Separate by Shape Keys' - bl_description = 'Separates selected mesh into two parts,' \ - '\ndepending on whether it is effected by a shape key or not.' \ - '\n' \ - '\nVery useful for manual decimation' + bl_label = t('SeparateByShapekeys.label') + bl_description = t('SeparateByShapekeys.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -551,19 +527,18 @@ def execute(self, context): meshes = Common.get_meshes_objects() if len(meshes) == 0: saved_data.load() - self.report({'ERROR'}, 'No meshes found!') + self.report({'ERROR'}, t('SeparateByX.error.noMesh')) return {'FINISHED'} if len(meshes) > 1: saved_data.load() - self.report({'ERROR'}, 'Multiple meshes found!' - '\nPlease select the mesh you want to separate!') + self.report({'ERROR'}, t('SeparateByX.error.multipleMesh')) return {'FINISHED'} obj = meshes[0] obj_name = obj.name - done_message = 'Successfully separated by shape keys.' + done_message = t('SeparateByShapekeys.success') if not Common.separate_by_shape_keys(context, obj): - done_message = 'No meshes had to be separated!' + done_message = t('SeparateByX.warn.noSeparation') saved_data.load(ignore=[obj_name]) self.report({'INFO'}, done_message) @@ -573,11 +548,8 @@ def execute(self, context): @register_wrap class SeparateByCopyProtection(bpy.types.Operator): bl_idname = 'cats_manual.separate_by_copy_protection' - bl_label = 'Separate by Copy Protection' - bl_description = 'Separates selected mesh into two parts,' \ - '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ - '\n' \ - '\nUseful if you have the Copy Protection enabled on multiple selected parts of your model' + bl_label = t('SeparateByCopyProtection.label') + bl_description = t('SeparateByCopyProtection.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -599,19 +571,18 @@ def execute(self, context): meshes = Common.get_meshes_objects() if len(meshes) == 0: saved_data.load() - self.report({'ERROR'}, 'No meshes found!') + self.report({'ERROR'}, t('SeparateByX.error.noMesh')) return {'FINISHED'} if len(meshes) > 1: saved_data.load() - self.report({'ERROR'}, 'Multiple meshes found!' - '\nPlease select the mesh you want to separate!') + self.report({'ERROR'}, t('SeparateByX.error.multipleMesh')) return {'FINISHED'} obj = meshes[0] obj_name = obj.name - done_message = 'Successfully separated by copy protection.' + done_message = t('SeparateByCopyProtection.success') if not Common.separate_by_cats_protection(context, obj): - done_message = 'No meshes had to be separated!' + done_message = t('SeparateByX.warn.noSeparation') saved_data.load(ignore=[obj_name]) self.report({'INFO'}, done_message) @@ -621,10 +592,8 @@ def execute(self, context): @register_wrap class MergeWeights(bpy.types.Operator): bl_idname = 'cats_manual.merge_weights' - bl_label = 'Merge Weights to Parent' - bl_description = 'Deletes the selected bones and adds their weight to their respective parents.' \ - '\n' \ - '\nOnly available in Edit or Pose Mode with bones selected' + bl_label = t('MergeWeights.label') + bl_description = t('MergeWeights.desc') bl_options = {'REGISTER', 'UNDO'} @classmethod @@ -661,18 +630,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Deleted ' + str(len(parenting_list)) + ' bones and added their weights to their parents.') + self.report({'INFO'}, t('MergeWeights.success', number=str(len(parenting_list)))) return {'FINISHED'} @register_wrap class MergeWeightsToActive(bpy.types.Operator): bl_idname = 'cats_manual.merge_weights_to_active' - bl_label = 'Merge Weights to Active' - bl_description = 'Deletes the selected bones except the active one and adds their weights to the active bone.' \ - '\nThe active bone is the one you selected last.' \ - '\n' \ - '\nOnly available in Edit or Pose Mode with bones selected' + bl_label = t('MergeWeightsToActive.label') + bl_description = t('MergeWeightsToActive.desc') bl_options = {'REGISTER', 'UNDO'} @classmethod @@ -710,7 +676,7 @@ def execute(self, context): # Load original modes saved_data.load() - self.report({'INFO'}, 'Deleted ' + str(len(parenting_list)) + ' bones and added their weights to the active bone.') + self.report({'INFO'}, t('MergeWeightsToActive.success', number=str(len(parenting_list)))) return {'FINISHED'} @@ -740,8 +706,8 @@ def merge_weights(armature, parenting_list): @register_wrap class ApplyTransformations(bpy.types.Operator): bl_idname = 'cats_manual.apply_transformations' - bl_label = 'Apply Transformations' - bl_description = "Applies the position, rotation and scale to the armature and it's meshes" + bl_label = t('ApplyTransformations.label') + bl_description = t('ApplyTransformations.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -756,15 +722,15 @@ def execute(self, context): Common.apply_transforms() saved_data.load() - self.report({'INFO'}, 'Transformations applied.') + self.report({'INFO'}, t('ApplyTransformations.success')) return {'FINISHED'} @register_wrap class ApplyAllTransformations(bpy.types.Operator): bl_idname = 'cats_manual.apply_all_transformations' - bl_label = 'Apply All Transformations' - bl_description = "Applies the position, rotation and scale of all objects" + bl_label = t('ApplyAllTransformations.label') + bl_description = t('ApplyAllTransformations.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -773,16 +739,15 @@ def execute(self, context): Common.apply_all_transforms() saved_data.load() - self.report({'INFO'}, 'Transformations applied.') + self.report({'INFO'}, t('ApplyAllTransformations.success')) return {'FINISHED'} @register_wrap class RemoveZeroWeightBones(bpy.types.Operator): bl_idname = 'cats_manual.remove_zero_weight_bones' - bl_label = 'Remove Zero Weight Bones' - bl_description = "Cleans up the bones hierarchy, deleting all bones that don't directly affect any vertices\n" \ - "Don't use this if you plan to use 'Fix Model'" + bl_label = t('RemoveZeroWeightBones.label') + bl_description = t('RemoveZeroWeightBones.desc') bl_options = {'REGISTER', 'UNDO'} @classmethod @@ -799,15 +764,15 @@ def execute(self, context): Common.set_default_stage() saved_data.load() - self.report({'INFO'}, 'Deleted ' + str(count) + ' zero weight bones.') + self.report({'INFO'}, t('RemoveZeroWeightBones.success', number=str(count))) return {'FINISHED'} @register_wrap class RemoveZeroWeightGroups(bpy.types.Operator): bl_idname = 'cats_manual.remove_zero_weight_groups' - bl_label = 'Remove Zero Weight Vertex Groups' - bl_description = "Cleans up the vertex groups of all meshes, deleting all groups that don't directly affect any vertices" + bl_label = t('RemoveZeroWeightGroups.label') + bl_description = t('RemoveZeroWeightGroups.desc') bl_options = {'REGISTER', 'UNDO'} @classmethod @@ -821,7 +786,7 @@ def execute(self, context): count = Common.remove_unused_vertex_groups() saved_data.load() - self.report({'INFO'}, 'Removed ' + str(count) + ' zero weight vertex groups.') + self.report({'INFO'}, t('RemoveZeroWeightGroups.success', number=str(count))) return {'FINISHED'} # Maybe only remove groups from selected meshes instead of from all of them @@ -846,8 +811,8 @@ def execute(self, context): @register_wrap class RemoveConstraints(bpy.types.Operator): bl_idname = 'cats_manual.remove_constraints' - bl_label = 'Remove Bone Constraints' - bl_description = "Removes constrains between bones causing specific bone movement as these are not used by VRChat" + bl_label = t('RemoveConstraints.label') + bl_description = t('RemoveConstraints.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -864,17 +829,15 @@ def execute(self, context): Common.set_default_stage() saved_data.load() - self.report({'INFO'}, 'Removed all bone constraints.') + self.report({'INFO'}, t('RemoveConstraints.success')) return {'FINISHED'} @register_wrap class RecalculateNormals(bpy.types.Operator): bl_idname = 'cats_manual.recalculate_normals' - bl_label = 'Recalculate Normals' - bl_description = "Makes normals point inside of the selected mesh.\n\n" \ - "Don't use this on good looking meshes as this can screw them up.\n" \ - "Use this if there are random inverted or darker faces on the mesh" + bl_label = t('RecalculateNormals.label') + bl_description = t('RecalculateNormals.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -910,16 +873,15 @@ def execute(self, context): Common.set_default_stage() saved_data.load() - self.report({'INFO'}, 'Recalculated all normals.') + self.report({'INFO'}, t('RecalculateNormals.success')) return {'FINISHED'} @register_wrap class FlipNormals(bpy.types.Operator): bl_idname = 'cats_manual.flip_normals' - bl_label = 'Flip Normals' - bl_description = "Flips the direction of the faces' normals of the selected mesh.\n" \ - "Use this if all normals are inverted" + bl_label = t('FlipNormals.label') + bl_description = t('FlipNormals.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -956,18 +918,15 @@ def execute(self, context): Common.set_default_stage() saved_data.load() - self.report({'INFO'}, 'Flipped all normals.') + self.report({'INFO'}, t('FlipNormals.success')) return {'FINISHED'} @register_wrap class RemoveDoubles(bpy.types.Operator): bl_idname = 'cats_manual.remove_doubles' - bl_label = 'Remove Doubles' - bl_description = "Merges duplicated faces and vertices of the selected meshes." \ - "\nThis is more save than doing it manually:" \ - "\n - leaves shape keys completely untouched" \ - "\n - but removes less doubles overall" + bl_label = t('RemoveDoubles.label') + bl_description = t('RemoveDoubles.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -996,16 +955,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Removed ' + str(removed_tris) + ' vertices.') + self.report({'INFO'}, t('RemoveDoubles.success', number=str(removed_tris))) return {'FINISHED'} @register_wrap class RemoveDoublesNormal(bpy.types.Operator): bl_idname = 'cats_manual.remove_doubles_normal' - bl_label = 'Remove Doubles Normally' - bl_description = "Merges duplicated faces and vertices of the selected meshes." \ - "\nThis is exactly like doing it manually" + bl_label = t('RemoveDoublesNormal.label') + bl_description = t('RemoveDoublesNormal.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -1034,15 +992,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Removed ' + str(removed_tris) + ' vertices.') + self.report({'INFO'}, t('RemoveDoublesNormal.success', number=str(removed_tris))) return {'FINISHED'} @register_wrap class FixVRMShapesButton(bpy.types.Operator): bl_idname = 'cats_manual.fix_vrm_shapes' - bl_label = 'Fix Koikatsu Shapekeys' - bl_description = "Fixes the shapekeys of Koikatsu models" + bl_label = t('FixVRMShapesButton.label') + bl_description = t('FixVRMShapesButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -1057,7 +1015,7 @@ def execute(self, context): slider_max_mouth = 0.94 if not Common.has_shapekeys(mesh): - self.report({'INFO'}, 'No shapekeys detected!') + self.report({'INFO'}, t('FixVRMShapesButton.warn.notDetected')) saved_data.load() return {'CANCELLED'} @@ -1148,18 +1106,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Fixed VRM shapekeys.') + self.report({'INFO'}, t('FixVRMShapesButton.success')) return {'FINISHED'} @register_wrap class FixFBTButton(bpy.types.Operator): bl_idname = 'cats_manual.fix_fbt' - bl_label = 'Fix Full Body Tracking' - bl_description = "WARNING: This fix is no longer needed for VRChat, you should not use it!" \ - "\n" \ - "\nApplies a general fix for Full Body Tracking." \ - '\nIgnore the "Spine length zero" warning in Unity' + bl_label = t('FixFBTButton.label') + bl_description = t('FixFBTButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -1184,15 +1139,12 @@ def execute(self, context): right_leg_new_alt = armature.data.edit_bones.get('Right_Leg_2') if not hips or not spine or not left_leg or not right_leg: - self.report({'ERROR'}, 'Required bones could not be found!' - '\nPlease make sure that your armature contains the following bones:' - '\n - Hips, Spine, Left leg, Right leg' - '\nExact names are required!') + self.report({'ERROR'}, t('FixFBTButton.error.bonesNotFound')) saved_data.load() return {'CANCELLED'} if left_leg_new or right_leg_new or left_leg_new_alt or right_leg_new_alt: - self.report({'ERROR'}, 'Full Body Tracking Fix already applied!') + self.report({'ERROR'}, t('FixFBTButton.error.alreadyApplied')) saved_data.load() return {'CANCELLED'} @@ -1257,18 +1209,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Successfully applied the Full Body Tracking fix.') + self.report({'INFO'}, t('FixFBTButton.success')) return {'FINISHED'} @register_wrap class RemoveFBTButton(bpy.types.Operator): bl_idname = 'cats_manual.remove_fbt' - bl_label = 'Remove Full Body Tracking Fix' - bl_description = "Removes the fix for Full Body Tracking, since it is no longer advised to use it." \ - '\n' \ - '\nRequires bones:' \ - '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2' + bl_label = t('RemoveFBTButton.label') + bl_description = t('RemoveFBTButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -1292,16 +1241,13 @@ def execute(self, context): if not hips or not spine or not left_leg or not right_leg: saved_data.load() - self.report({'ERROR'}, 'Required bones could not be found!' - '\nPlease make sure that your armature contains the following bones:' - '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2' - '\nExact names are required!') + self.report({'ERROR'}, t('RemoveFBTButton.error.bonesNotFound')) saved_data.load() return {'CANCELLED'} if not left_leg_new or not right_leg_new: saved_data.load() - self.report({'ERROR'}, 'The Full Body Tracking Fix is not applied!') + self.report({'ERROR'}, t('RemoveFBTButton.error.notApplied')) return {'CANCELLED'} # Remove FBT Fix @@ -1347,15 +1293,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Successfully removed the Full Body Tracking fix.') + self.report({'INFO'}, t('RemoveFBTButton.success')) return {'FINISHED'} @register_wrap class DuplicateBonesButton(bpy.types.Operator): bl_idname = 'cats_manual.duplicate_bones' - bl_label = 'Duplicate Bones' - bl_description = "Duplicates the selected bones including their weight and renames them to _L and _R" + bl_label = t('DuplicateBonesButton.label') + bl_description = t('DuplicateBonesButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -1409,5 +1355,5 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Successfully duplicated ' + str(bone_count) + ' bones.') + self.report({'INFO'}, t('DuplicateBonesButton.success', number=str(bone_count))) return {'FINISHED'} diff --git a/tools/atlas.py b/tools/atlas.py index 80124867..cdecff38 100644 --- a/tools/atlas.py +++ b/tools/atlas.py @@ -31,6 +31,7 @@ from . import common as Common from .register import register_wrap from .. import globs +from ..translations import t # addon_name = "Shotariya-don" @@ -40,8 +41,8 @@ @register_wrap class EnableSMC(bpy.types.Operator): bl_idname = 'cats_atlas.enable_smc' - bl_label = 'Enable Material Combiner' - bl_description = 'Enables Material Combiner' + bl_label = t('EnableSMC.label') + bl_description = t('EnableSMC.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -70,7 +71,7 @@ def execute(self, context): if not addon_utils.check(mod.__name__)[0]: bpy.ops.wm.addon_enable(module=mod.__name__) break - self.report({'INFO'}, 'Enabled Material Combiner!') + self.report({'INFO'}, t('EnableSMC.success')) return {'FINISHED'} @@ -354,13 +355,13 @@ def execute(self, context): @register_wrap class AtlasHelpButton(bpy.types.Operator): bl_idname = 'cats_atlas.help' - bl_label = 'Generate Material List' - bl_description = 'Open Useful Atlas Tips' + bl_label = t('AtlasHelpButton.label') + bl_description = t('AtlasHelpButton.desc') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://github.com/michaeldegroot/cats-blender-plugin/#texture-atlas') - self.report({'INFO'}, 'Atlas Help opened.') + webbrowser.open(t('AtlasHelpButton.URL')) + self.report({'INFO'}, t('AtlasHelpButton.success')) return {'FINISHED'} @@ -386,7 +387,7 @@ def execute(self, context): @register_wrap class InstallShotariya(bpy.types.Operator): bl_idname = "cats_atlas.install_shotariya_popup" - bl_label = 'Error while loading Material Combiner:' + bl_label = t('InstallShotariya.label') bl_options = {'INTERNAL'} action = bpy.props.EnumProperty( @@ -411,14 +412,14 @@ def draw(self, context): if self.action == 'INSTALL': row = col.row(align=True) - row.label(text="Material Combiner is not installed!") + row.label(text=t('InstallShotariya.error.install1')) row.scale_y = 0.75 row = col.row(align=True) row.scale_y = 0.75 - row.label(text="The plugin 'Material Combiner' by shotariya is required for this function.") + row.label(text=t('InstallShotariya.error.install2')) col.separator() row = col.row(align=True) - row.label(text="Please download and install it manually:") + row.label(text=t('InstallShotariya.error.install3')) row.scale_y = 0.75 col.separator() row = col.row(align=True) @@ -427,27 +428,27 @@ def draw(self, context): elif self.action == 'ENABLE': row = col.row(align=True) - row.label(text="Material Combiner is not enabled!") + row.label(text=t('InstallShotariya.error.enable1')) row.scale_y = 0.75 row = col.row(align=True) row.scale_y = 0.75 - row.label(text="The plugin 'Material Combiner' by shotariya is required for this function.") + row.label(text=t('InstallShotariya.error.enable2')) col.separator() row = col.row(align=True) - row.label(text="Please enable it in your User Preferences.") + row.label(text=t('InstallShotariya.error.enable3')) row.scale_y = 0.75 col.separator() elif self.action == 'VERSION': row = col.row(align=True) - row.label(text="Material Combiner is outdated!") + row.label(text=t('InstallShotariya.error.version1')) row.scale_y = 0.75 row = col.row(align=True) row.scale_y = 0.75 - row.label(text="The latest version is required for this function.") + row.label(text=t('InstallShotariya.error.version2')) col.separator() row = col.row(align=True) - row.label(text="Please download and install it manually:") + row.label(text=t('InstallShotariya.error.version3')) row.scale_y = 0.75 col.separator() row = col.row(align=True) @@ -458,13 +459,13 @@ def draw(self, context): @register_wrap class ShotariyaButton(bpy.types.Operator): bl_idname = 'cats_atlas.download_shotariya' - bl_label = 'Download Material Combiner' + bl_label = t('ShotariyaButton.label') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://vrcat.club/threads/material-combiner-blender-addon-1-1-3.2255/') + webbrowser.open(t('ShotariyaButton.URL')) - self.report({'INFO'}, 'Material Combiner link opened') + self.report({'INFO'}, 'ShotariyaButton.success') return {'FINISHED'} diff --git a/tools/bonemerge.py b/tools/bonemerge.py index 69c6f2fe..a4229148 100644 --- a/tools/bonemerge.py +++ b/tools/bonemerge.py @@ -29,6 +29,7 @@ from . import common as Common from .register import register_wrap from .. import globs +from ..translations import t # wm = bpy.context.window_manager # wm.progress_begin(0, len(bone_merge)) @@ -39,9 +40,8 @@ @register_wrap class BoneMergeButton(bpy.types.Operator): bl_idname = 'cats_bonemerge.merge_bones' - bl_label = 'Merge Bones' - bl_description = 'Merges the given percentage of bones together.\n' \ - 'This is useful to reduce the amount of bones used by Dynamic Bones.' + bl_label = t('BoneMergeButton.label') + bl_description = t('BoneMergeButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -94,7 +94,7 @@ def execute(self, context): saved_data.load() wm.progress_end() - self.report({'INFO'}, 'Merged bones.') + self.report({'INFO'}, t('BoneMergeButton.success')) return {'FINISHED'} # Go through this until the last child is reached diff --git a/tools/common.py b/tools/common.py index 287fe109..55daeb50 100644 --- a/tools/common.py +++ b/tools/common.py @@ -41,6 +41,7 @@ from . import armature_bones as Bones from .register import register_wrap from mmd_tools_local import utils +from ..translations import t # TODO: # - Add check if hips bone really needs to be rotated @@ -1605,7 +1606,7 @@ def show_error(scale, error_list, override_header=False): dpi_scale = scale error = error_list - header = 'Report: Error' + header = t('ShowError.label') if override: header = error_list[0] @@ -1627,7 +1628,7 @@ def show_error(scale, error_list, override_header=False): @register_wrap class ShowError(bpy.types.Operator): bl_idname = 'cats_common.show_error' - bl_label = 'Report: Error' + bl_label = t('ShowError.label') def execute(self, context): return {'FINISHED'} diff --git a/tools/copy_protection.py b/tools/copy_protection.py index 620d1b14..36f71873 100644 --- a/tools/copy_protection.py +++ b/tools/copy_protection.py @@ -30,14 +30,14 @@ from . import common as Common from .register import register_wrap +from ..translations import t @register_wrap class CopyProtectionEnable(bpy.types.Operator): bl_idname = 'cats_copyprotection.enable' - bl_label = 'Enable Protection' - bl_description = 'Protects your model from piracy. NOT a 100% protection!' \ - '\nRead the documentation before use' + bl_label = t('CopyProtectionEnable.label') + bl_description = t('CopyProtectionEnable.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -122,15 +122,15 @@ def execute(self, context): # Make obfuscated basis the new basis and repair shape key order Common.sort_shape_keys(mesh.name) - self.report({'INFO'}, 'Model secured!') + self.report({'INFO'}, t('CopyProtectionEnable.success')) return {'FINISHED'} @register_wrap class CopyProtectionDisable(bpy.types.Operator): bl_idname = 'cats_copyprotection.disable' - bl_label = 'Disable Protection' - bl_description = 'Removes the copy protections from this model' + bl_label = t('CopyProtectionDisable.label') + bl_description = t('CopyProtectionDisable.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -152,18 +152,18 @@ def execute(self, context): Common.sort_shape_keys(mesh.name) - self.report({'INFO'}, 'Model un-secured!') + self.report({'INFO'}, t('CopyProtectionDisable.success')) return {'FINISHED'} @register_wrap class ProtectionTutorialButton(bpy.types.Operator): bl_idname = 'cats_copyprotection.tutorial' - bl_label = 'Go to Documentation' + bl_label = t('ProtectionTutorialButton.label') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://github.com/michaeldegroot/cats-blender-plugin#copy-protection') + webbrowser.open(t('ProtectionTutorialButton.URL')) # mesh = Common.get_meshes_objects()[0] # Common.select(mesh) @@ -173,5 +173,5 @@ def execute(self, context): # if i == 1: # shapekey.value = 1.5 - self.report({'INFO'}, 'Documentation') + self.report({'INFO'}, t('ProtectionTutorialButton.success')) return {'FINISHED'} diff --git a/tools/credits.py b/tools/credits.py index 8aec4782..c97a9c23 100644 --- a/tools/credits.py +++ b/tools/credits.py @@ -26,41 +26,42 @@ import bpy import webbrowser from .register import register_wrap +from ..translations import t @register_wrap class ForumButton(bpy.types.Operator): bl_idname = 'cats_credits.forum' - bl_label = 'Go to the Forums' + bl_label = t('ForumButton.label') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - webbrowser.open('https://vrcat.club/threads/cats-blender-plugin.6/') + webbrowser.open(t('ForumButton.URL')) - self.report({'INFO'}, 'Forum opened') + self.report({'INFO'}, t('ForumButton.success')) return {'FINISHED'} @register_wrap class DiscordButton(bpy.types.Operator): bl_idname = 'cats_credits.discord' - bl_label = 'Join our Discord' + bl_label = t('DiscordButton.label') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - webbrowser.open('https://discord.gg/f8yZGnv') + webbrowser.open(t('DiscordButton.URL')) - self.report({'INFO'}, 'Discord opened') + self.report({'INFO'}, t('DiscordButton.success')) return {'FINISHED'} @register_wrap class PatchnotesButton(bpy.types.Operator): bl_idname = 'cats_credits.patchnotes' - bl_label = 'Latest Patchnotes' + bl_label = t('PatchnotesButton.label') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - webbrowser.open('https://github.com/michaeldegroot/cats-blender-plugin/releases') + webbrowser.open(t('PatchnotesButton.URL')) - self.report({'INFO'}, 'patchnotes opened') + self.report({'INFO'}, t('PatchnotesButton.success')) return {'FINISHED'} diff --git a/tools/decimation.py b/tools/decimation.py index 7f8f5e24..f75f296c 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -29,6 +29,7 @@ from . import common as Common from . import armature_bones as Bones from .register import register_wrap +from ..translations import t ignore_shapes = [] @@ -38,8 +39,8 @@ @register_wrap class ScanButton(bpy.types.Operator): bl_idname = 'cats_decimation.auto_scan' - bl_label = 'Scan for decimation models' - bl_description = 'Separates the mesh.' + bl_label = t('ScanButton.label') + bl_description = t('ScanButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -65,9 +66,8 @@ def execute(self, context): @register_wrap class AddShapeButton(bpy.types.Operator): bl_idname = 'cats_decimation.add_shape' - bl_label = 'Add' - bl_description = 'Adds the selected shape key to the whitelist.\n' \ - 'This means that every mesh containing that shape key will be not decimated.' + bl_label = t('AddShapeButton.label') + bl_description = t('AddShapeButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -92,9 +92,8 @@ def execute(self, context): @register_wrap class AddMeshButton(bpy.types.Operator): bl_idname = 'cats_decimation.add_mesh' - bl_label = 'Add' - bl_description = 'Adds the selected mesh to the whitelist.\n' \ - 'This means that this mesh will be not decimated.' + bl_label = t('AddMeshButton.label') + bl_description = t('AddMeshButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -120,9 +119,8 @@ def execute(self, context): @register_wrap class RemoveShapeButton(bpy.types.Operator): bl_idname = 'cats_decimation.remove_shape' - bl_label = '' - bl_description = 'Removes the selected shape key from the whitelist.\n' \ - 'This means that this shape key is no longer decimation safe!' + bl_label = t('RemoveShapeButton.label') + bl_description = t('RemoveShapeButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} shape_name = bpy.props.StringProperty() @@ -135,9 +133,8 @@ def execute(self, context): @register_wrap class RemoveMeshButton(bpy.types.Operator): bl_idname = 'cats_decimation.remove_mesh' - bl_label = '' - bl_description = 'Removes the selected mesh from the whitelist.\n' \ - 'This means that this mesh will be decimated.' + bl_label = t('RemoveMeshButton.label') + bl_description = t('RemoveMeshButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} mesh_name = bpy.props.StringProperty() @@ -150,15 +147,14 @@ def execute(self, context): @register_wrap class AutoDecimateButton(bpy.types.Operator): bl_idname = 'cats_decimation.auto_decimate' - bl_label = 'Quick Decimation' - bl_description = 'This will automatically decimate your model while preserving the shape keys.\n' \ - 'You should manually remove unimportant meshes first.' + bl_label = t('AutoDecimateButton.label') + bl_description = t('AutoDecimateButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): meshes = Common.get_meshes_objects() if not meshes or len(meshes) == 0: - self.report({'ERROR'}, 'No meshes found!') + self.report({'ERROR'}, t('AutoDecimateButton.error.noMesh')) return {'FINISHED'} saved_data = Common.SavedData() @@ -269,19 +265,19 @@ def decimate(self, context): print((current_tris_count - tris_count), '>', max_tris) if (current_tris_count - tris_count) > max_tris: - message = ['This model can not be decimated to ' + str(max_tris) + ' tris with the specified settings.'] + message = [t('decimate.cantDecimateWithSettings', number=str(max_tris))] if safe_decimation: - message.append('Try to use Custom, Half or Full Decimation.') + message.append(t('decimate.safeTryOptions')) elif half_decimation: - message.append('Try to use Custom or Full Decimation.') + message.append(t('decimate.halfTryOptions')) elif custom_decimation: - message.append('Select fewer shape keys and/or meshes or use Full Decimation.') + message.append(t('decimate.customTryOptions')) if save_fingers: if full_decimation: - message.append("Disable 'Save Fingers' or increase the Tris Count.") + message.append(t('decimate.disableFingersOrIncrease')) else: message[1] = message[1][:-1] - message.append("or disable 'Save Fingers'.") + message.append(t('decimate.disableFingers')) Common.show_error(6, message) return @@ -290,11 +286,11 @@ def decimate(self, context): except ZeroDivisionError: decimation = 1 if decimation >= 1: - Common.show_error(6, ['The model already has less than ' + str(max_tris) + ' tris. Nothing had to be decimated.']) + Common.show_error(6, [t('decimate.noDecimationNeeded', number=str(max_tris))]) return elif decimation <= 0: - Common.show_error(4.5, ['The model could not be decimated to ' + str(max_tris) + ' tris.', - 'It got decimated as much as possible within the limits.']) + Common.show_error(4.5, [t('decimate.cantDecimate1', number=str(max_tris)), + t('decimate.cantDecimate2')]) meshes.sort(key=lambda x: x[1]) diff --git a/tools/eyetracking.py b/tools/eyetracking.py index 459b8c79..0556b195 100644 --- a/tools/eyetracking.py +++ b/tools/eyetracking.py @@ -34,6 +34,7 @@ from . import common as Common from . import armature as Armature from .register import register_wrap +from ..translations import t iris_heights = None @@ -42,10 +43,8 @@ @register_wrap class CreateEyesButton(bpy.types.Operator): bl_idname = 'cats_eyes.create_eye_tracking' - bl_label = 'Create Eye Tracking' - bl_description = 'This will let you track someone when they come close to you and it enables blinking.\n' \ - "You should do decimation before this operation.\n" \ - "Test the resulting eye movement in the 'Testing' tab" + bl_label = t('CreateEyesButton.label') + bl_description = t('CreateEyesButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} mesh = None @@ -92,23 +91,22 @@ def execute(self, context): or context.scene.lowerlid_left == "" or context.scene.lowerlid_right == ""): saved_data.load() - self.report({'ERROR'}, 'You have no shape keys selected.' - '\nPlease choose a mesh containing shape keys or check "Disable Eye Blinking".') + self.report({'ERROR'}, t('CreateEyesButton.error.noShapeSelected')) return {'CANCELLED'} if head is None: saved_data.load() - self.report({'ERROR'}, 'The bone "' + context.scene.head + '" does not exist.') + self.report({'ERROR'}, t('CreateEyesButton.error.missingBone', bone=context.scene.head)) return {'CANCELLED'} if not old_eye_left: saved_data.load() - self.report({'ERROR'}, 'The bone "' + context.scene.eye_left + '" does not exist.') + self.report({'ERROR'}, t('CreateEyesButton.error.missingBone', bone=context.scene.eye_left)) return {'CANCELLED'} if not old_eye_right: saved_data.load() - self.report({'ERROR'}, 'The bone "' + context.scene.eye_right + '" does not exist.') + self.report({'ERROR'}, t('CreateEyesButton.error.missingBone', bone=context.scene.eye_right)) return {'CANCELLED'} if not context.scene.disable_eye_movement: @@ -121,17 +119,14 @@ def execute(self, context): if eye_name: saved_data.load() - self.report({'ERROR'}, 'The bone "' + eye_name + '" has no existing vertex group or no vertices assigned to it.' - '\nThis might be because you selected the wrong mesh or the wrong bone.' - '\nAlso make sure that the selected eye bones actually move the eyes in pose mode.') + self.report({'ERROR'}, t('CreateEyesButton.error.noVertex', bone=eye_name)) return {'CANCELLED'} # Find existing LeftEye/RightEye and rename or delete if 'LeftEye' in armature.data.edit_bones: if old_eye_left.name == 'LeftEye': saved_data.load() - self.report({'ERROR'}, 'Please do not use "LeftEye" as the input bone.' - '\nIf you are sure that you want to use that bone please rename it to "Eye_L".') + self.report({'ERROR'}, t('CreateEyesButton.error.dontUse', eyeName='LeftEye', eyeNameShort='Eye_L')) return {'CANCELLED'} else: armature.data.edit_bones.remove(armature.data.edit_bones.get('LeftEye')) @@ -139,8 +134,7 @@ def execute(self, context): if 'RightEye' in armature.data.edit_bones: if old_eye_right.name == 'RightEye': saved_data.load() - self.report({'ERROR'}, 'Please do not use "RightEye" as the input bone.' - '\nIf you are sure that you want to use that bone please rename it to "Eye_R".') + self.report({'ERROR'}, t('CreateEyesButton.error.dontUse', eyeName='RightEye', eyeNameShort='Eye_R')) return {'CANCELLED'} else: armature.data.edit_bones.remove(armature.data.edit_bones.get('RightEye')) @@ -257,11 +251,10 @@ def execute(self, context): if not is_correct['result']: self.report({'ERROR'}, is_correct['message']) - self.report({'ERROR'}, 'Eye tracking will not work unless the bone hierarchy is exactly as following: Hips > Spine > Chest > Neck > Head' - '\nFurthermore the mesh containing the eyes has to be called "Body" and the armature "Armature".') + self.report({'ERROR'}, t('CreateEyesButton.error.hierarchy')) else: context.scene.eye_mode = 'TESTING' - self.report({'INFO'}, 'Created eye tracking!') + self.report({'INFO'}, t('CreateEyesButton.success')) return {'FINISHED'} @@ -487,10 +480,8 @@ def repair_shapekeys_mouth(mesh_name): # TODO Add vertex repairing! @register_wrap class StartTestingButton(bpy.types.Operator): bl_idname = 'cats_eyes.start_testing' - bl_label = 'Start Eye Testing' - bl_description = 'This will let you test how the eye movement will look ingame.\n' \ - "Don't forget to stop the Testing process afterwards.\n" \ - 'Bones "LeftEye" and "RightEye" are required' + bl_label = t('StartTestingButton.label') + bl_description = t('StartTestingButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -542,8 +533,8 @@ def execute(self, context): @register_wrap class StopTestingButton(bpy.types.Operator): bl_idname = 'cats_eyes.stop_testing' - bl_label = 'Stop Eye Testing' - bl_description = 'Stops the testing process' + bl_label = t('StopTestingButton.label') + bl_description = t('StopTestingButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -585,7 +576,7 @@ def set_rotation(self, context): if not eye_left: StopTestingButton.execute(self, context) - self.report({'ERROR'}, "Something went wrong. Please try eye testing again.") + self.report({'ERROR'}, t('StopTestingButton.error.tryAgain')) return None eye_left_data.select = True @@ -643,8 +634,8 @@ def stop_testing(self, context): @register_wrap class ResetRotationButton(bpy.types.Operator): bl_idname = 'cats_eyes.reset_rotation' - bl_label = 'Reset Rotation' - bl_description = "This resets the eye positions" + bl_label = t('ResetRotationButton.label') + bl_description = t('ResetRotationButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -683,9 +674,8 @@ def execute(self, context): @register_wrap class AdjustEyesButton(bpy.types.Operator): bl_idname = 'cats_eyes.adjust_eyes' - bl_label = 'Set Range' - bl_description = "Lets you readjust the movement range of the eyes.\n" \ - "This gets saved" + bl_label = t('AdjustEyesButton.label') + bl_description = t('AdjustEyesButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -703,16 +693,12 @@ def execute(self, context): mesh_name = context.scene.mesh_name_eye if not Common.vertex_group_exists(mesh_name, 'LeftEye'): - self.report({'ERROR'}, 'The bone "' + 'LeftEye' + '" has no existing vertex group or no vertices assigned to it.' - '\nThis might be because you selected the wrong mesh or the wrong bone.' - '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.') + self.report({'ERROR'}, t('AdjustEyesButton.error.noVertex', bone='LeftEye')) return {'CANCELLED'} # Find the existing vertex group of the right eye bone if not Common.vertex_group_exists(mesh_name, 'RightEye'): - self.report({'ERROR'}, 'The bone "' + 'RightEye' + '" has no existing vertex group or no vertices assigned to it.' - '\nThis might be because you selected the wrong mesh or the wrong bone.' - '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.') + self.report({'ERROR'}, t('AdjustEyesButton.error.noVertex', bone='RightEye')) return {'CANCELLED'} armature = Common.set_default_stage() @@ -742,10 +728,8 @@ def execute(self, context): @register_wrap class StartIrisHeightButton(bpy.types.Operator): bl_idname = 'cats_eyes.adjust_iris_height_start' - bl_label = 'Start Iris Height Adjustment' - bl_description = "Lets you readjust the distance of the iris from the eye ball.\n" \ - "Use this to fix clipping of the iris into the eye ball.\n" \ - "This gets saved" + bl_label = t('StartIrisHeightButton.label') + bl_description = t('StartIrisHeightButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -793,8 +777,8 @@ def execute(self, context): @register_wrap class TestBlinking(bpy.types.Operator): bl_idname = 'cats_eyes.test_blinking' - bl_label = 'Test' - bl_description = "This lets you see how eye blinking will look ingame" + bl_label = t('TestBlinking.label') + bl_description = t('TestBlinking.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -822,8 +806,8 @@ def execute(self, context): @register_wrap class TestLowerlid(bpy.types.Operator): bl_idname = 'cats_eyes.test_lowerlid' - bl_label = 'Test' - bl_description = "This lets you see how lowerlids will look ingame" + bl_label = t('TestLowerlid.label') + bl_description = t('TestLowerlid.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -853,8 +837,8 @@ def execute(self, context): @register_wrap class ResetBlinkTest(bpy.types.Operator): bl_idname = 'cats_eyes.reset_blink_test' - bl_label = 'Reset Shapes' - bl_description = "This resets the blink testing" + bl_label = t('ResetBlinkTest.label') + bl_description = t('ResetBlinkTest.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): diff --git a/tools/importer.py b/tools/importer.py index 31787d1b..e330ebde 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -38,6 +38,7 @@ from . import fbx_patch as Fbx_patch from .common import version_2_79_or_older from .register import register_wrap +from ..translations import t mmd_tools_installed = False try: @@ -60,29 +61,11 @@ @register_wrap class ImportAnyModel(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): bl_idname = 'cats_importer.import_any_model' - bl_label = 'Import Any Model' + bl_label = t('ImportAnyModel.label') if version_2_79_or_older(): - bl_description = 'Import a model of any supported type.' \ - '\n' \ - '\nSupported types:' \ - '\n- MMD: .pmx/.pmd' \ - '\n- XNALara: .xps/.mesh/.ascii' \ - '\n- Source: .smd/.qc' \ - '\n- VRM: .vrm' \ - '\n- FBX .fbx ' \ - '\n- DAE: .dae ' \ - '\n- ZIP: .zip' + bl_description = t('ImportAnyModel.desc2.79') else: - bl_description = 'Import a model of any supported type.' \ - '\n' \ - '\nSupported types:' \ - '\n- MMD: .pmx/.pmd' \ - '\n- XNALara: .xps/.mesh/.ascii' \ - '\n- Source: .smd/.qc/.vta/.dmx' \ - '\n- VRM: .vrm' \ - '\n- FBX: .fbx' \ - '\n- DAE: .dae ' \ - '\n- ZIP: .zip' + bl_description = t('ImportAnyModel.desc2.8') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} files = bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) @@ -90,8 +73,8 @@ class ImportAnyModel(bpy.types.Operator, bpy_extras.io_utils.ImportHelper): filter_glob = bpy.props.StringProperty(default=formats_279 if version_2_79_or_older() else formats, options={'HIDDEN'}) text1 = bpy.props.BoolProperty( - name='IMPORTANT INFO (hover here)', - description='If you want to modify the import settings, use the button next to the Import button.\n\n', + name=t('ImportAnyModel.importantInfo.label'), + description=t('ImportAnyModel.importantInfo.desc'), default=False ) @@ -123,7 +106,7 @@ def execute(self, context): if has_zip_file: if not zip_files: - Common.show_error(4, ['The selected zip file contains no importable models.']) + Common.show_error(4, [t('ImportAnyModel.error.emptyZip')]) # Import all models from zip files that contain only one importable model remove_keys = [] @@ -205,8 +188,7 @@ def import_file(directory, file_name): bpy.ops.import_scene.fbx('INVOKE_DEFAULT') except RuntimeError as e: if 'unsupported, must be 7100 or later' in str(e): - Common.show_error(6.2, ['The FBX file version is unsupported!', - 'Please use a tool such as the "Autodesk FBX Converter" to make it compatible.']) + Common.show_error(6.2, [t('ImportAnyModel.error.unsupportedFBX')]) print(str(e)) # VRM @@ -322,8 +304,8 @@ def fix_armatures_post_import(pre_import_objects): @register_wrap class ZipPopup(bpy.types.Operator): bl_idname = "cats_importer.zip_popup" - bl_label = "Zip Model Selection:" - bl_description = 'Shows the models contained in the zip files' + bl_label = t('ZipPopup.label') + bl_description = t('ZipPopup.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -352,10 +334,10 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.9 - row.label(text='Select which model you want to import') + row.label(text=t('ZipPopup.selectModel1')) row = col.row(align=True) row.scale_y = 0.9 - row.label(text='Then confirm with OK') + row.label(text=t('ZipPopup.selectModel2')) col.separator() row = col.row(align=True) @@ -378,7 +360,7 @@ def get_zip_content(self, context): choices.append(( file_id, encode_str(file_name), - 'Import model "' + encode_str(file_name) + '" from the zip "' + encode_str(zip_name) + '"')) + t('get_zip_content.choose', model=encode_str(file_name), zipName=encode_str(zip_name)))) if len(choices) == 0: choices.append(('None', 'None', 'None')) @@ -398,8 +380,8 @@ def encode_str(s): @register_wrap class ModelsPopup(bpy.types.Operator): bl_idname = "cats_importer.model_popup" - bl_label = "Select which you want to import:" - bl_description = 'Show individual import options' + bl_label = t('ModelsPopup.label') + bl_description = t('ModelsPopup.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -433,8 +415,8 @@ def draw(self, context): @register_wrap class ImportMMD(bpy.types.Operator): bl_idname = 'cats_importer.import_mmd' - bl_label = 'MMD' - bl_description = 'Import a MMD model (.pmx/.pmd)' + bl_label = t('ImportMMD.label') + bl_description = t('ImportMMD.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -464,8 +446,8 @@ def execute(self, context): @register_wrap class ImportXPS(bpy.types.Operator): bl_idname = 'cats_importer.import_xps' - bl_label = 'XNALara' - bl_description = 'Import a XNALara model (.xps/.mesh/.ascii)' + bl_label = t('ImportXPS.label') + bl_description = t('ImportXPS.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -489,8 +471,8 @@ def execute(self, context): @register_wrap class ImportSource(bpy.types.Operator): bl_idname = 'cats_importer.import_source' - bl_label = 'Source' - bl_description = 'Import a Source model (.smd/.qc/.vta/.dmx)' + bl_label = t('ImportSource.label') + bl_description = t('ImportSource.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -511,8 +493,8 @@ def execute(self, context): @register_wrap class ImportFBX(bpy.types.Operator): bl_idname = 'cats_importer.import_fbx' - bl_label = 'FBX' - bl_description = 'Import a FBX model (.fbx)' + bl_label = t('ImportFBX.label') + bl_description = t('ImportFBX.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -541,8 +523,8 @@ def execute(self, context): @register_wrap class ImportVRM(bpy.types.Operator): bl_idname = 'cats_importer.import_vrm' - bl_label = 'VRM' - bl_description = 'Import a VRM model (.vrm)' + bl_label = t('ImportVRM.label') + bl_description = t('ImportVRM.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -563,7 +545,7 @@ def execute(self, context): @register_wrap class InstallXPS(bpy.types.Operator): bl_idname = "cats_importer.install_xps" - bl_label = "XPS Tools is not installed or enabled!" + bl_label = t('InstallXPS.label') bl_options = {'INTERNAL'} def execute(self, context): @@ -585,13 +567,13 @@ def draw(self, context): # row.label(text="The plugin 'XPS Tools' is required for this function.") col.separator() row = col.row(align=True) - row.label(text="If it is not enabled please enable it in your User Preferences.") + row.label(text=t('InstallX.pleaseInstall1')) row = col.row(align=True) - row.label(text="If it is not installed please download and install it manually.") + row.label(text=t('InstallX.pleaseInstall2')) col.separator() col.separator() row = col.row(align=True) - row.label(text="Make sure to install the version for Blender " + current_blender_version, icon="INFO") + row.label(text=t('InstallX.pleaseInstall3', blenderVersion=current_blender_version), icon="INFO") col.separator() row = col.row(align=True) row.operator(XpsToolsButton.bl_idname, icon=globs.ICON_URL) @@ -600,7 +582,7 @@ def draw(self, context): @register_wrap class InstallSource(bpy.types.Operator): bl_idname = "cats_importer.install_source" - bl_label = "Source Tools is not installed or enabled!" + bl_label = t('InstallSource.label') bl_options = {'INTERNAL'} def execute(self, context): @@ -622,13 +604,13 @@ def draw(self, context): # row.label(text="The plugin 'Source Tools' is required for this function.") col.separator() row = col.row(align=True) - row.label(text="If it is not enabled please enable it in your User Preferences.") + row.label(text=t('InstallX.pleaseInstall1')) row = col.row(align=True) - row.label(text="If it is not installed please download and install it manually.") + row.label(text=t('InstallX.pleaseInstall2')) col.separator() col.separator() row = col.row(align=True) - row.label(text="Make sure to install the version for Blender " + current_blender_version, icon="INFO") + row.label(text=t('InstallX.pleaseInstall3', blenderVersion=current_blender_version), icon="INFO") col.separator() row = col.row(align=True) row.operator(SourceToolsButton.bl_idname, icon=globs.ICON_URL) @@ -637,7 +619,7 @@ def draw(self, context): @register_wrap class InstallVRM(bpy.types.Operator): bl_idname = "cats_importer.install_vrm" - bl_label = "VRM Importer is not installed or enabled!" + bl_label = t('InstallVRM.label') bl_options = {'INTERNAL'} def execute(self, context): @@ -659,12 +641,12 @@ def draw(self, context): # row.label(text="The plugin 'VRM Importer' is required for this function.") col.separator() row = col.row(align=True) - row.label(text="If it is not enabled please enable it in your User Preferences.") + row.label(text=t('InstallX.pleaseInstall1')) row = col.row(align=True) - row.label(text="Currently you have to select 'Testing' in the addons settings.") + row.label(text=t('InstallX.pleaseInstallTesting')) col.separator() row = col.row(align=True) - row.label(text="If it is not installed please download and install it manually.") + row.label(text=t('InstallX.pleaseInstall2')) col.separator() row = col.row(align=True) row.operator(VrmToolsButton.bl_idname, icon=globs.ICON_URL) @@ -673,7 +655,7 @@ def draw(self, context): @register_wrap class EnableMMD(bpy.types.Operator): bl_idname = "cats_importer.enable_mmd" - bl_label = "Mmd_tools is not enabled!" + bl_label = t('EnableMMD.label') bl_options = {'INTERNAL'} def execute(self, context): @@ -692,9 +674,9 @@ def draw(self, context): col = layout.column(align=True) row = col.row(align=True) - row.label(text="The plugin 'mmd_tools' is required for this function.") + row.label(text=t('EnableMMD.required1')) row = col.row(align=True) - row.label(text="Please restart Blender.") + row.label(text=t('EnableMMD.required2')) # def popup_install_xps(self, context): @@ -750,42 +732,42 @@ def draw(self, context): @register_wrap class XpsToolsButton(bpy.types.Operator): bl_idname = 'cats_importer.download_xps_tools' - bl_label = 'Download XPS Tools' + bl_label = t('XpsToolsButton.label') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://github.com/johnzero7/XNALaraMesh') + webbrowser.open(t('XpsToolsButton.URL')) - self.report({'INFO'}, 'XPS Tools link opened') + self.report({'INFO'}, t('XpsToolsButton.success')) return {'FINISHED'} @register_wrap class SourceToolsButton(bpy.types.Operator): bl_idname = 'cats_importer.download_source_tools' - bl_label = 'Download Source Tools' + bl_label = t('SourceToolsButton.label') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://github.com/Artfunkel/BlenderSourceTools') + webbrowser.open(t('SourceToolsButton.URL')) - self.report({'INFO'}, 'Source Tools link opened') + self.report({'INFO'}, t('SourceToolsButton.success')) return {'FINISHED'} @register_wrap class VrmToolsButton(bpy.types.Operator): bl_idname = 'cats_importer.download_vrm' - bl_label = 'Download VRM Importer' + bl_label = t('VrmToolsButton.label') bl_options = {'INTERNAL'} def execute(self, context): if Common.version_2_79_or_older(): - webbrowser.open('https://github.com/iCyP/VRM_IMPORTER_for_Blender2_79') + webbrowser.open(t('VrmToolsButton.URL_2.79')) else: - webbrowser.open('https://github.com/saturday06/VRM_IMPORTER_for_Blender2_8') + webbrowser.open(t('VrmToolsButton.URL_2.8')) - self.report({'INFO'}, 'VRM Importer link opened') + self.report({'INFO'}, t('VrmToolsButton.success')) return {'FINISHED'} @@ -806,10 +788,8 @@ def execute(self, context): @register_wrap class ExportModel(bpy.types.Operator): bl_idname = 'cats_importer.export_model' - bl_label = 'Export Model' - bl_description = 'Export this model as .fbx for Unity.\n' \ - '\n' \ - 'Automatically sets the optimal export settings' + bl_label = t('ExportModel.label') + bl_description = t('ExportModel.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} action = bpy.props.EnumProperty( @@ -934,7 +914,7 @@ def execute(self, context): except (TypeError, ValueError): bpy.ops.export_scene.fbx('INVOKE_DEFAULT') except AttributeError: - self.report({'ERROR'}, 'FBX Exporter not enabled! Please enable it in your User Preferences.') + self.report({'ERROR'}, t('ExportModel.error.notEnabled')) return {'FINISHED'} @@ -942,7 +922,7 @@ def execute(self, context): @register_wrap class ErrorDisplay(bpy.types.Operator): bl_idname = "cats_importer.display_error" - bl_label = "Warning:" + bl_label = t('ErrorDisplay.label') bl_options = {'INTERNAL'} tris_count = 0 @@ -980,15 +960,15 @@ def draw(self, context): if self.tris_count > max_tris: row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Too many polygons!", icon='ERROR') + row.label(text=t('ErrorDisplay.polygons1'), icon='ERROR') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="You have " + str(self.tris_count) + " tris in this model, but you shouldn't have more than 70,000!") + row.label(text=t('ErrorDisplay.polygons2', number=str(self.tris_count))) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="You should decimate before you export this model.") + row.label(text=t('ErrorDisplay.polygons3')) col.separator() col.separator() col.separator() @@ -1016,19 +996,19 @@ def draw(self, context): if self.mat_count > max_mats: row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Model not optimized!", icon='INFO') + row.label(text=t('ErrorDisplay.materials1'), icon='INFO') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="This model has " + str(self.mat_count) + " materials!") + row.label(text=t('ErrorDisplay.materials2', number=str(self.mat_count))) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="You should try to have a maximum of 4 materials on your model.") + row.label(text=t('ErrorDisplay.materials3')) col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Creating a texture atlas in CATS is very easy, so please make use of it.") + row.label(text=t('ErrorDisplay.materials4')) col.separator() col.separator() col.separator() @@ -1036,26 +1016,26 @@ def draw(self, context): if self.meshes_count > max_meshes_light: row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Meshes not joined!", icon='ERROR') + row.label(text=t('ErrorDisplay.meshes1'), icon='ERROR') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="This model has " + str(self.meshes_count) + " meshes!") + row.label(text=t('ErrorDisplay.meshes2', number=str(self.meshes_count))) col.separator() row = col.row(align=True) row.scale_y = 0.75 if self.meshes_count <= max_meshes_hard: - row.label(text="It is not very optimized and might cause lag for you and others.") + row.label(text=t('ErrorDisplay.meshes3')) else: - row.label(text="It is extremely unoptimized and will cause lag for you and others.") + row.label(text=t('ErrorDisplay.meshes3_alt')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="You should always join your meshes, it's very easy:") + row.label(text=t('ErrorDisplay.meshes4')) col.separator() row = col.row(align=True) row.scale_y = 1 - row.operator(armature_manual.JoinMeshes.bl_idname, text='Join Meshes', icon='AUTOMERGE_ON') + row.operator(armature_manual.JoinMeshes.bl_idname, text=t('ErrorDisplay.JoinMeshes.label'), icon='AUTOMERGE_ON') col.separator() col.separator() col.separator() @@ -1063,12 +1043,12 @@ def draw(self, context): if self.broken_shapes: row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Broken shapekeys!", icon='ERROR') + row.label(text=t('ErrorDisplay.brokenShapekeys1'), icon='ERROR') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="This model has " + str(len(self.broken_shapes)) + " broken shapekey(s):") + row.label(text=t('ErrorDisplay.brokenShapekeys2', number=str(len(self.broken_shapes)))) col.separator() for shapekey in self.broken_shapes: @@ -1079,10 +1059,10 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="You will not be able to upload this model until you fix these shapekeys.") + row.label(text=t('ErrorDisplay.brokenShapekeys3')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Either delete or repair them before export.") + row.label(text=t('ErrorDisplay.brokenShapekeys4')) col.separator() col.separator() col.separator() @@ -1090,18 +1070,18 @@ def draw(self, context): if not self.textures_found and Settings.get_embed_textures(): row = col.row(align=True) row.scale_y = 0.75 - row.label(text="No textures found!", icon='ERROR') + row.label(text=t('ErrorDisplay.textures1'), icon='ERROR') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="This model has no textures assigned but you have 'Embed Textures' enabled.") + row.label(text=t('ErrorDisplay.textures2')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Therefore, no textures will embedded into the FBX.") + row.label(text=t('ErrorDisplay.textures3')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="This is not an issue, but you will have to import the textures manually into Unity.") + row.label(text=t('ErrorDisplay.textures4')) col.separator() col.separator() col.separator() @@ -1109,15 +1089,15 @@ def draw(self, context): if len(self.eye_meshes_not_named_body) == 1: row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Eyes not named 'Body'!", icon='ERROR') + row.label(text=t('ErrorDisplay.eyes1'), icon='ERROR') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="The mesh '" + self.eye_meshes_not_named_body[0] + "' has Eye Tracking shapekeys but is not named 'Body'.") + row.label(text=t('ErrorDisplay.eyes2', naéme=self.eye_meshes_not_named_body[0])) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="If you want Eye Tracking to work, rename this mesh to 'Body'.") + row.label(text=t( 'ErrorDisplay.eyes3')) col.separator() col.separator() col.separator() @@ -1125,21 +1105,21 @@ def draw(self, context): elif len(self.eye_meshes_not_named_body) > 1: row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Eyes not named 'Body'!", icon='ERROR') + row.label(text=t('ErrorDisplay.eyes1'), icon='ERROR') col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Multiple meshes have Eye Tracking shapekeys but are not named 'Body'.") + row.label(text=t('ErrorDisplay.eyes2_alt')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Make sure that the mesh containing the eyes is named 'Body' in order") + row.label(text=t('ErrorDisplay.eyes3_alt')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="to get Eye Tracking to work.") + row.label(text=t('ErrorDisplay.eyes4_alt')) col.separator() col.separator() col.separator() row = col.row(align=True) - row.operator(ExportModel.bl_idname, text='Continue to Export', icon=globs.ICON_EXPORT).action = 'NO_CHECK' + row.operator(ExportModel.bl_idname, text=t('ErrorDisplay.continue'), icon=globs.ICON_EXPORT).action = 'NO_CHECK' diff --git a/tools/material.py b/tools/material.py index 03884e88..08ce0466 100644 --- a/tools/material.py +++ b/tools/material.py @@ -32,13 +32,14 @@ from . import common as Common from .register import register_wrap +from ..translations import t @register_wrap class OneTexPerMatButton(bpy.types.Operator): bl_idname = 'cats_material.one_tex' - bl_label = 'One Material Texture' - bl_description = 'Have all material slots ignore extra texture slots as these are not used by VRChat' + bl_label = t('OneTexPerMatButton.label') + bl_description = t('OneTexPerMatButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -52,7 +53,7 @@ def execute(self, context): # Common.add_principled_shader() # return {'FINISHED'} if not Common.version_2_79_or_older(): - self.report({'ERROR'}, 'This function is not yet compatible with Blender 2.8!') + self.report({'ERROR'}, t('ToolsMaterial.error.notCompatible')) return {'CANCELLED'} # TODO @@ -68,17 +69,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'All materials have one texture now.') + self.report({'INFO'}, t('OneTexPerMatButton.success')) return{'FINISHED'} @register_wrap class OneTexPerMatOnlyButton(bpy.types.Operator): bl_idname = 'cats_material.one_tex_only' - bl_label = 'One Material Texture' - bl_description = 'Have all material slots ignore extra texture slots as these are not used by VRChat.' \ - '\nAlso removes the textures from the material instead of disabling it.' \ - '\nThis makes no difference, but cleans the list for the perfectionists' + bl_label = t('OneTexPerMatOnlyButton.label') + bl_description = t('OneTexPerMatOnlyButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -89,7 +88,7 @@ def poll(cls, context): def execute(self, context): if not Common.version_2_79_or_older(): - self.report({'ERROR'}, 'This function is not yet compatible with Blender 2.8!') + self.report({'ERROR'}, t('ToolsMaterial.error.notCompatible')) return {'CANCELLED'} # TODO @@ -105,16 +104,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'All materials have one texture now.') + self.report({'INFO'}, t('OneTexPerXButton.success')) return{'FINISHED'} @register_wrap class StandardizeTextures(bpy.types.Operator): bl_idname = 'cats_material.standardize_textures' - bl_label = 'Standardize Textures' - bl_description = 'Enables Color and Alpha on every texture, sets the blend method to Multiply' \ - '\nand changes the materials transparency to Z-Transparency' + bl_label = t('StandardizeTextures.label') + bl_description = t('StandardizeTextures.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -125,7 +123,7 @@ def poll(cls, context): def execute(self, context): if not Common.version_2_79_or_older(): - self.report({'ERROR'}, 'This function is not yet compatible with Blender 2.8!') + self.report({'ERROR'}, t('ToolsMaterial.error.notCompatible')) return {'CANCELLED'} # TODO @@ -147,18 +145,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'All textures are now standardized.') + self.report({'INFO'}, t('StandardizeTextures.success')) return{'FINISHED'} @register_wrap class CombineMaterialsButton(bpy.types.Operator): bl_idname = 'cats_material.combine_mats' - bl_label = 'Combine Same Materials' - bl_description = 'Combines similar materials into one, reducing draw calls.\n' \ - 'Your avatar should visibly look the same after this operation.\n' \ - 'This is a very important step for optimizing your avatar.\n' \ - 'If you have problems with this, please tell us!\n' + bl_label = t('CombineMaterialsButton.label') + bl_description = t('CombineMaterialsButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} combined_tex = {} @@ -376,9 +371,9 @@ def execute(self, context): saved_data.load() if i == 0: - self.report({'INFO'}, 'No materials combined.') + self.report({'INFO'}, t('CombineMaterialsButton.error.noChanges')) else: - self.report({'INFO'}, 'Combined ' + str(i) + ' materials!') + self.report({'INFO'}, t('CombineMaterialsButton.success', number=str(i))) return{'FINISHED'} @@ -386,10 +381,8 @@ def execute(self, context): @register_wrap class ConvertAllToPngButton(bpy.types.Operator): bl_idname = 'cats_material.convert_all_to_png' - bl_label = 'Convert Textures to PNG' - bl_description = 'Converts all texture files into PNG files.' \ - '\nThis helps with transparency and compatibility issues.' \ - '\n\nThe converted image files will be saved next to the old ones' + bl_label = t('ConvertAllToPngButton.label') + bl_description = t('ConvertAllToPngButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} # Inspired by: @@ -414,7 +407,7 @@ def execute(self, context): wm.progress_end() - self.report({'INFO'}, 'Converted ' + str(len(images_to_convert)) + ' to PNG files.') + self.report({'INFO'}, t('ConvertAllToPngButton.success', number=str(len(images_to_convert)))) return {'FINISHED'} def get_convert_list(self): diff --git a/tools/rootbone.py b/tools/rootbone.py index 35f25737..211ebd97 100644 --- a/tools/rootbone.py +++ b/tools/rootbone.py @@ -30,14 +30,14 @@ from . import common as Common from .register import register_wrap from .. import globs +from ..translations import t @register_wrap class RootButton(bpy.types.Operator): bl_idname = 'cats_root.create_root' - bl_label = 'Parent Bones' - bl_description = 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' \ - 'Very useful for Dynamic Bones.' + bl_label = t('RootButton.label') + bl_description = t('RootButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -74,7 +74,7 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Bones parented!') + self.report({'INFO'}, t('RootButton.success')) return{'FINISHED'} @@ -168,8 +168,8 @@ def get_parent_root_bones(self, context): @register_wrap class RefreshRootButton(bpy.types.Operator): bl_idname = 'cats_root.refresh_root_list' - bl_label = 'Refresh List' - bl_description = 'This will clear the group bones list cache and rebuild it, useful if bones have changed or your model.' + bl_label = t('RefreshRootButton.label') + bl_description = t('RefreshRootButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -178,6 +178,6 @@ def execute(self, context): print(globs.root_bones) globs.root_bones_choices = {} - self.report({'INFO'}, 'Root bones refreshed, check the root bones list again.') + self.report({'INFO'}, t('RefreshRootButton.success')) return{'FINISHED'} diff --git a/tools/settings.py b/tools/settings.py index 765d0105..6744d895 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -38,6 +38,7 @@ from ..tools.register import register_wrap from ..googletrans import Translator from . import translate as Translate +from ..translations import t main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve() resources_dir = os.path.join(str(main_dir), "resources") @@ -57,38 +58,37 @@ @register_wrap class RevertChangesButton(bpy.types.Operator): bl_idname = 'cats_settings.revert' - bl_label = 'Revert Settings' - bl_description = 'Revert the changes back to how they were on Blender start-up' + bl_label = t('RevertChangesButton.label') + bl_description = t('RevertChangesButton.desc') bl_options = {'INTERNAL'} def execute(self, context): for setting in settings_default.keys(): setattr(bpy.context.scene, setting, settings_data_unchanged.get(setting)) save_settings() - self.report({'INFO'}, 'Settings reverted.') + self.report({'INFO'}, t('RevertChangesButton.success')) return {'FINISHED'} @register_wrap class ResetGoogleDictButton(bpy.types.Operator): bl_idname = 'cats_settings.reset_google_dict' - bl_label = 'Clear Local Google Translations' - bl_description = "Deletes all currently saved Google Translations. You can't undo this" + bl_label = t('ResetGoogleDictButton.label') + bl_description = t('ResetGoogleDictButton.desc') bl_options = {'INTERNAL'} def execute(self, context): Translate.reset_google_dict() Translate.load_translations() - self.report({'INFO'}, 'Local Google Dictionary cleared!') + self.report({'INFO'}, t('ResetGoogleDictButton.resetInfo')) return {'FINISHED'} @register_wrap class DebugTranslations(bpy.types.Operator): bl_idname = 'cats_settings.debug_translations' - bl_label = 'Debug Google Translations' - bl_description = "Tests Google transaltions and prints the response into a file called 'google-response.txt' located in the cats addon folder > resources" \ - "\nThis button is only visible in the cats development version" + bl_label = t('DebugTranslations.label') + bl_description = t('DebugTranslations.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -97,10 +97,10 @@ def execute(self, context): try: translator.translate('猫') except: - self.report({'INFO'}, 'Errors found, response printed!!') + self.report({'INFO'}, t('DebugTranslations.error')) bpy.context.scene.debug_translations = False - self.report({'INFO'}, 'No issues with Google Translations found, response printed!') + self.report({'INFO'}, t('DebugTranslations.success')) return {'FINISHED'} diff --git a/tools/shapekey.py b/tools/shapekey.py index 22f4e63f..688b0142 100644 --- a/tools/shapekey.py +++ b/tools/shapekey.py @@ -27,14 +27,15 @@ import bpy from . import common as Common from .register import register_wrap +from ..translations import t @register_wrap class ShapeKeyApplier(bpy.types.Operator): # Replace the 'Basis' shape key with the currently selected shape key bl_idname = "cats_shapekey.shape_key_to_basis" - bl_label = "Apply Selected Shapekey to Basis" - bl_description = 'Applies the selected shape key to the new Basis at it\'s current strength and creates a reverted shape key from the selected one' + bl_label = t('ShapeKeyApplier.label') + bl_description = t('ShapeKeyApplier.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -53,16 +54,10 @@ def execute(self, context): if ' - Reverted' in new_basis_shapekey_name and new_basis_shapekey.relative_key.name != 'Basis': for shapekey in mesh.data.shape_keys.key_blocks: if ' - Reverted' in shapekey.name and shapekey.relative_key.name == 'Basis': - Common.show_error(7.3, ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', - 'Start with the shape key called "' + shapekey.name + '".', - '', - "If you didn't change the shape key order, you can revert the shape keys from top to bottom."]) + Common.show_error(t('ShapeKeyApplier.error.revert.scale'), t('ShapeKeyApplier.error.revert', name=shapekey.name)) return {'FINISHED'} - Common.show_error(7.3, ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', - 'Start with the reverted shape key that uses the relative key called "Basis".', - '', - "If you didn't change the shape key order, you can revert the shape keys from top to bottom."]) + Common.show_error(t('ShapeKeyApplier.error.revert.scale'), t('ShapeKeyApplier.error.revert')) return {'FINISHED'} # Set up shape keys @@ -129,12 +124,12 @@ def execute(self, context): # If a reversed shapekey was applied as basis, fix the name if ' - Reverted - Reverted' in old_basis_shapekey.name: old_basis_shapekey.name = old_basis_shapekey.name.replace(' - Reverted - Reverted', '') - self.report({'INFO'}, 'Successfully removed shapekey "' + old_basis_shapekey.name + '" from the Basis.') + self.report({'INFO'}, t('ShapeKeyApplier.successRemoved', name=old_basis_shapekey.name)) else: - self.report({'INFO'}, 'Successfully set shapekey "' + new_basis_shapekey_name + '" as the new Basis.') + self.report({'INFO'}, t('ShapeKeyApplier.successSet', name=new_basis_shapekey_name)) return {'FINISHED'} def addToShapekeyMenu(self, context): self.layout.separator() - self.layout.operator(ShapeKeyApplier.bl_idname, text="Apply Selected Shapekey to Basis", icon="KEY_HLT") + self.layout.operator(ShapeKeyApplier.bl_idname, text=t('addToShapekeyMenu.ShapeKeyApplier.label'), icon="KEY_HLT") diff --git a/tools/supporter.py b/tools/supporter.py index c69d54b6..818401e8 100644 --- a/tools/supporter.py +++ b/tools/supporter.py @@ -44,6 +44,7 @@ from . import settings as Settings from .. import globs from ..tools.register import register_wrap +from ..translations import t # global variables preview_collections = {} @@ -60,21 +61,21 @@ @register_wrap class PatreonButton(bpy.types.Operator): bl_idname = 'cats_supporter.patreon' - bl_label = 'Become a Patron' + bl_label = t('PatreonButton.label') bl_options = {'INTERNAL'} def execute(self, context): - webbrowser.open('https://www.patreon.com/catsblenderplugin') + webbrowser.open(t('PatreonButton.URL')) - self.report({'INFO'}, 'Patreon page opened') + self.report({'INFO'}, t('PatreonButton.success')) return {'FINISHED'} @register_wrap class ReloadButton(bpy.types.Operator): bl_idname = 'cats_supporter.reload' - bl_label = 'Reload List' - bl_description = 'Reloads the supporter list' + bl_label = t('ReloadButton.label') + bl_description = t('ReloadButton.desc') bl_options = {'INTERNAL'} @classmethod @@ -89,8 +90,8 @@ def execute(self, context): @register_wrap class DynamicPatronButton(bpy.types.Operator): bl_idname = 'cats_supporter.dynamic_patron_button' - bl_label = 'Supporter Name' - bl_description = 'This is an awesome supporter' + bl_label = t('DynamicPatronButton.label') + bl_description = t('DynamicPatronButton.desc') bl_options = {'INTERNAL'} website = None @@ -113,7 +114,7 @@ def register_dynamic_buttons(): name = supporter.get('displayname') idname = 'support.' + ''.join(filter(str.isalpha, name.lower())) - description = name + ' is an awesome supporter' + description = t('register_dynamic_buttons.desc', name=name) if supporter.get('description'): # description = name + ' says:\n\n' + supporter.get('description') + '\n' description = supporter.get('description') diff --git a/tools/translate.py b/tools/translate.py index 8ec3cc2e..1199a1eb 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -40,6 +40,7 @@ from .. import globs from ..googletrans import Translator from mmd_tools_local import translations +from ..translations import t dictionary = None dictionary_google = None @@ -53,13 +54,13 @@ @register_wrap class TranslateShapekeyButton(bpy.types.Operator): bl_idname = 'cats_translate.shapekeys' - bl_label = 'Translate Shape Keys' - bl_description = "Translates all shape keys using the internal dictionary and Google Translate" + bl_label = t('TranslateShapekeyButton.label') + bl_description = t('TranslateShapekeyButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): if bpy.app.version < (2, 79, 0): - self.report({'ERROR'}, 'You need Blender 2.79 or higher for this function.') + self.report({'ERROR'}, t('TranslateX.error.wrongVersion')) return {'FINISHED'} saved_data = Common.SavedData() @@ -89,15 +90,15 @@ def execute(self, context): saved_data.load() - self.report({'INFO'}, 'Translated ' + str(i) + ' shape keys.') + self.report({'INFO'}, t('TranslateShapekeyButton.success', number=str(i))) return {'FINISHED'} @register_wrap class TranslateBonesButton(bpy.types.Operator): bl_idname = 'cats_translate.bones' - bl_label = 'Translate Bones' - bl_description = "Translates all bones using the internal dictionary and Google Translate" + bl_label = t('TranslateBonesButton.label') + bl_description = t('TranslateBonesButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -121,20 +122,20 @@ def execute(self, context): if translated: count += 1 - self.report({'INFO'}, 'Translated ' + str(count) + ' bones.') + self.report({'INFO'}, t('TranslateBonesButton.success', number=str(count))) return {'FINISHED'} @register_wrap class TranslateObjectsButton(bpy.types.Operator): bl_idname = 'cats_translate.objects' - bl_label = 'Translate Meshes & Objects' - bl_description = "Translates all meshes and objects using the internal dictionary and Google Translate" + bl_label = t('TranslateObjectsButton.label') + bl_description = t('TranslateObjectsButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): if bpy.app.version < (2, 79, 0): - self.report({'ERROR'}, 'You need Blender 2.79 or higher for this function.') + self.report({'ERROR'}, t('TranslateX.error.wrongVersion')) return {'FINISHED'} to_translate = [] for obj in Common.get_objects(): @@ -165,20 +166,20 @@ def execute(self, context): if translated: i += 1 - self.report({'INFO'}, 'Translated ' + str(i) + ' meshes and objects.') + self.report({'INFO'}, t('TranslateObjectsButton.success', number=str(i))) return {'FINISHED'} @register_wrap class TranslateMaterialsButton(bpy.types.Operator): bl_idname = 'cats_translate.materials' - bl_label = 'Translate Materials' - bl_description = "Translates all materials using the internal dictionary and Google Translate" + bl_label = t('TranslateMaterialsButton.label') + bl_description = t('TranslateMaterialsButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): if bpy.app.version < (2, 79, 0): - self.report({'ERROR'}, 'You need Blender 2.79 or higher for this function.') + self.report({'ERROR'}, t('TranslateX.error.wrongVersion')) return {'FINISHED'} saved_data = Common.SavedData() @@ -202,20 +203,20 @@ def execute(self, context): i += 1 saved_data.load() - self.report({'INFO'}, 'Translated ' + str(i) + ' materials.') + self.report({'INFO'}, t('TranslateMaterialsButton.success', number=str(i))) return {'FINISHED'} @register_wrap class TranslateTexturesButton(bpy.types.Operator): bl_idname = 'cats_translate.textures' - bl_label = 'Translate Textures' - bl_description = "Translates all textures using the internal dictionary and Google Translate" + bl_label = t('TranslateTexturesButton.label') + bl_description = t('TranslateTexturesButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): # It currently seems to do nothing. This should probably only added when the folder textures really get translated. Currently only the materials are important - self.report({'INFO'}, 'Translated all textures') + self.report({'INFO'}, t('TranslateTexturesButton.success_alt')) return {'FINISHED'} translator = Translator() @@ -233,7 +234,7 @@ def execute(self, context): try: translations = translator.translate(to_translate) except SSLError: - self.report({'ERROR'}, 'Could not connect to Google. Please check your internet connection.') + self.report({'ERROR'}, t('TranslateTexturesButton.error.noInternet')) return {'FINISHED'} for translation in translations: @@ -250,20 +251,20 @@ def execute(self, context): Common.unselect_all() - self.report({'INFO'}, 'Translated ' + str(i) + 'textures.') + self.report({'INFO'}, t('TranslateTexturesButton.success', number=str(i))) return {'FINISHED'} @register_wrap class TranslateAllButton(bpy.types.Operator): bl_idname = 'cats_translate.all' - bl_label = 'Translate Everything' - bl_description = "Translates everything using the internal dictionary and Google Translate" + bl_label = t('TranslateAllButton.label') + bl_description = t('TranslateAllButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): if bpy.app.version < (2, 79, 0): - self.report({'ERROR'}, 'You need Blender 2.79 or higher for this function.') + self.report({'ERROR'}, t('TranslateX.error.wrongVersion')) return {'FINISHED'} error_shown = False @@ -298,7 +299,7 @@ def execute(self, context): if error_shown: return {'CANCELLED'} - self.report({'INFO'}, 'Translated everything.') + self.report({'INFO'}, t('TranslateAllButton.success')) return {'FINISHED'} @@ -432,42 +433,32 @@ def update_dictionary(to_translate_list, translating_shapes=False, self=None): except requests.exceptions.ConnectionError: print('CONNECTION TO GOOGLE FAILED!') if self: - self.report({'ERROR'}, 'Could not connect to Google. Some parts could not be translated.') + self.report({'ERROR'}, t('update_dictionary.error.cantConnect')) return except json.JSONDecodeError: if self: - self.report({'ERROR'}, 'It looks like you got banned from Google Translate temporarily!' - '\nCats translated what it could with the local dictionary,' - '\nbut you will have to try again later for the Google translations.') + self.report({'ERROR'}, t('update_dictionary.error.temporaryBan') + t('update_dictionary.error.catsTranslated')) print('YOU GOT BANNED BY GOOGLE!') return except RuntimeError as e: error = Common.html_to_text(str(e)) if self: if 'Please try your request again later' in error: - self.report({'ERROR'}, 'It looks like you got banned from Google Translate temporarily!' - '\nCats translated what it could with the local dictionary, but you will have to try again later for the Google translations.') + self.report({'ERROR'}, t('update_dictionary.error.temporaryBan') + t('update_dictionary.error.catsTranslated')) print('YOU GOT BANNED BY GOOGLE!') return if 'Error 403' in error: - self.report({'ERROR'}, 'Cats was not able to access Google Translate!' - '\nCats translated what it could with the local dictionary, but you will have to try again later for the Google translations.') + self.report({'ERROR'}, t('update_dictionary.error.cantAccess') + t('update_dictionary.error.catsTranslated')) print('NO PERMISSION TO USE GOOGLE TRANSLATE!') return - self.report({'ERROR'}, 'You got an error message from Google Translate!' - '\nCats translated what it could with the local dictionary, but you will have to try again later for the Google translations.' - '\n' - '\nGoogle: ' + error) + self.report({'ERROR'}, t('update_dictionary.error.errorMsg') + t('update_dictionary.error.catsTranslated') + '\n' + '\nGoogle: ' + error) print('', 'You got an error message from Google:', error, '') return except AttributeError: if self: - self.report({'ERROR'}, 'Could not get translations from Google Translate!' - '\nThis means that Google changed their API and translations will no longer work until this is fixed.' - '\nPlease translate manually or wait for an CATS update.' - '\nFor updates and dicussions please join our Discord. The link can be found in the Credits panel down below.') + self.report({'ERROR'}, t('update_dictionary.error.apiChanged')) print('GOOGLE API CHANGED') return diff --git a/tools/viseme.py b/tools/viseme.py index cbf96a55..98454ebd 100644 --- a/tools/viseme.py +++ b/tools/viseme.py @@ -29,14 +29,14 @@ from .register import register_wrap from collections import OrderedDict +from ..translations import t @register_wrap class AutoVisemeButton(bpy.types.Operator): bl_idname = 'cats_viseme.create' - bl_label = 'Create Visemes' - bl_description = 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' \ - 'It will generate 15 shape keys from the 3 shape keys you specify' + bl_label = t('AutoVisemeButton.label') + bl_description = t('AutoVisemeButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} @classmethod @@ -49,13 +49,13 @@ def execute(self, context): mesh = Common.get_objects()[context.scene.mesh_name_viseme] if not Common.has_shapekeys(mesh): - self.report({'ERROR'}, 'This mesh has no shapekeys!') + self.report({'ERROR'}, t('AutoVisemeButton.error.noShapekeys')) return {'CANCELLED'} if context.scene.mouth_a == "Basis" \ or context.scene.mouth_o == "Basis" \ or context.scene.mouth_ch == "Basis": - self.report({'ERROR'}, 'Please select the correct mouth shapekeys instead of "Basis"!') + self.report({'ERROR'}, t('AutoVisemeButton.error.selectShapekeys')) return {'CANCELLED'} saved_data = Common.SavedData() @@ -251,7 +251,7 @@ def execute(self, context): wm.progress_end() - self.report({'INFO'}, 'Created mouth visemes!') + self.report({'INFO'}, t('AutoVisemeButton.success')) return {'FINISHED'} diff --git a/translations/__init__.py b/translations/__init__.py new file mode 100644 index 00000000..0ec2aacd --- /dev/null +++ b/translations/__init__.py @@ -0,0 +1,25 @@ +# Thanks to https://www.thegrove3d.com/learn/how-to-translate-a-blender-addon/ for the idea + +from bpy.app.translations import locale + +if locale in ['ja_JP']: + from .ja_JP import dictionary +else: + from .en_US import dictionary + +def t(phrase: str, *args, **kwargs): + # Translate the given phrase into Blender's current language. + try: + output = dictionary[phrase] + if isinstance(output, list): + newList = [] + for string in output: + newList.append(string.format(*args, **kwargs)) + return newList + elif not isinstance(output, str): + return output + else: + return output.format(*args, **kwargs) + except KeyError: + print('Warning - Text missing for: ' + phrase) + return phrase \ No newline at end of file diff --git a/translations/en_US.py b/translations/en_US.py new file mode 100644 index 00000000..57200c2c --- /dev/null +++ b/translations/en_US.py @@ -0,0 +1,1170 @@ +dictionary = { + # Class.label / Class.desc (tooltip) + # Class.property + + # Main file + 'Main.error.restartAdmin': '\n\nFaulty CATS installation found!' + '\nTo fix this restart Blender as admin! ' + '\n', + 'Main.error.deleteFollowing': ' ' \ + ' '\ + '\n\nFaulty CATS installation found!' \ + '\nTo fix this delete the following files and folders inside your addons folder:' \ + '\n', + 'Main.error.installViaPreferences': '\n\nFaulty CATS installation found!' + '\nPlease install CATS via User Preferences and restart Blender!' + '\n', + 'Main.error.restartAndEnable': '\n\nFaulty CATS installation was found and fixed!' + '\nPlease restart Blender and enable CATS again!' + '\n', + 'Main.error.unsupportedVersion': '\n\nBlender versions older than 2.79 are not supported by Cats. ' + '\nPlease use Blender 2.79 or later.' + '\n', + 'Main.error.beta2.80': '\n\nYou are still on the beta version of Blender 2.80!' + '\nPlease update to the release version of Blender 2.80.' + '\n', + 'Main.error.restartAndEnable_alt': '\n\nPlease restart Blender and enable CATS again!' + '\n', + + # UI Main + 'ToolPanel.label': 'Cats Blender Plugin', + 'ToolPanel.category': 'CATS', + + # UI Armature + 'ArmaturePanel.label': 'Model', + 'ArmaturePanel.warn.oldBlender1': 'Old Blender version detected!', + 'ArmaturePanel.warn.oldBlender2': 'Some features might not work!', + 'ArmaturePanel.warn.oldBlender3': 'Please update to Blender 2.79!', + 'ArmaturePanel.warn.noDict1': 'Dictionary not found!', + 'ArmaturePanel.warn.noDict2': 'Translations will work, but are not optimized.', + 'ArmaturePanel.warn.noDict3': 'Reinstall Cats to fix this.', + 'ArmaturePanel.ImportAnyModel.label': 'Import Model', + 'ModelSettings.label': 'Fix Model Settings', + 'ModelSettings.warn.fbtFix1': 'The Full Body Tracking Fix', + 'ModelSettings.warn.fbtFix2': 'is no longer needed for VRChat.', + 'ModelSettings.warn.fbtFix3': 'It\'s still available in Model Options.', + + # UI Manual + 'ManualPanel.label': 'Model Options', + 'ManualPanel.separateBy': 'Separate by:', + 'ManualPanel.SeparateByMaterials.label': 'Materials', + 'ManualPanel.SeparateByLooseParts.label': 'Loose Parts', + 'ManualPanel.SeparateByShapekeys.label': 'Shapes', + 'ManualPanel.joinMeshes': 'Join Meshes:', + 'ManualPanel.JoinMeshes.label': 'All', + 'ManualPanel.JoinMeshesSelected.label': 'Selected', + 'ManualPanel.mergeWeights': 'Merge Weights:', + 'ManualPanel.MergeWeights.label': 'To Parents', + 'ManualPanel.MergeWeightsToActive.label': 'To Active', + 'ManualPanel.translate': 'Translate:', + 'ManualPanel.TranslateAllButton.label': 'All', + 'ManualPanel.TranslateShapekeyButton.label': 'Shape Keys', + 'ManualPanel.TranslateObjectsButton.label': 'Objects', + 'ManualPanel.TranslateBonesButton.label': 'Bones', + 'ManualPanel.TranslateMaterialsButton.label': 'Materials', + 'ManualPanel.delete': 'Delete:', + 'ManualPanel.RemoveZeroWeightBones.label': 'Zero Weight Bones', + 'ManualPanel.RemoveConstraints': 'Constraints', + 'ManualPanel.RemoveZeroWeightGroups': 'Zero Weight Vertex Groups', + 'ManualPanel.normals': 'Normals:', + 'ManualPanel.RecalculateNormals.label': 'Recalculate', + 'ManualPanel.FlipNormals.label': 'Flip', + 'ManualPanel.fbtFix': 'Full Body Tracking Fix:', + 'ManualPanel.FixFBTButton.label': 'Add', + 'ManualPanel.RemoveFBTButton.label': 'Remove', + + # UI Custom + 'CustomPanel.label': 'Custom Model Creation', + 'CustomPanel.CustomModelTutorialButton': 'How to use', + 'CustomPanel.mergeArmatures': 'Merge Armatures:', + 'CustomPanel.warn.twoArmatures': 'Two armatures are required!', + 'CustomPanel.mergeInto': 'Base', + 'CustomPanel.toMerge': 'To Merge', + 'CustomPanel.attachToBone': 'Attach to', + 'CustomPanel.armaturesCanMerge': 'Armatures can be merged automatically!', + 'CustomPanel.attachMesh1': 'Attach Mesh to Armature:', + 'CustomPanel.attachMesh2': 'Mesh', + 'CustomPanel.warn.noArmOrMesh1': 'An armature and a mesh are required!', + 'CustomPanel.warn.noArmOrMesh2': 'Make sure that the mesh has no parent.', + + # UI Decimation + 'DecimationPanel.label': 'Decimation', + 'DecimationPanel.decimationMode': 'Decimation Mode:', + 'DecimationPanel.safeModeDesc': ' Decent results - No shape key loss', + 'DecimationPanel.halfModeDesc': ' Good results - Minimal shape key loss', + 'DecimationPanel.fullModeDesc': ' Best results - Full shape key loss', + 'DecimationPanel.customSeparateMaterials': 'Start by Separating by Materials:', + 'DecimationPanel.SeparateByMaterials.label': 'Separate by Materials', + 'DecimationPanel.customJoinMeshes': 'Stop by Joining Meshes:', + 'DecimationPanel.customWhitelist': 'Whitelisted:', + 'DecimationPanel.warn.noShapekeySelected': 'No shape key selected', + 'DecimationPanel.warn.noDecimation': 'Every mesh is selected. This equals no Decimation.', + 'DecimationPanel.warn.noMeshSelected': 'No mesh selected', + 'DecimationPanel.warn.emptyList': 'Both lists are empty, this equals Full Decimation!', + 'DecimationPanel.warn.correctWhitelist': 'Both whitelists are considered during decimation', + + # UI Eye tracking + 'EyeTrackingPanel.label': 'Eye Tracking', + 'EyeTrackingPanel.error.noMesh': 'No meshes found!', + 'EyeTrackingPanel.error.noArm': 'No model found!', + 'EyeTrackingPanel.error.wrongNameArm1': 'Your armature has to be named \'Armature\'', + 'EyeTrackingPanel.error.wrongNameArm2': ' for Eye Tracking to work!', + 'EyeTrackingPanel.error.wrongNameArm3': ' (currently \'', + 'EyeTrackingPanel.error.wrongNameBody1': 'The mesh containing the eyes has to be', + 'EyeTrackingPanel.error.wrongNameBody2': ' named \'Body\' for Eye Tracking to work!', + 'EyeTrackingPanel.error.wrongNameBody3': ' (currently \'', + 'EyeTrackingPanel.warn.assignEyes1': 'Don\'t forget to assign \'LeftEye\' and \'RightEye\'', + 'EyeTrackingPanel.warn.assignEyes2': ' to the eyes in Unity!', + + # UI Visemes + 'VisemePanel.label': 'Visemes', + 'VisemePanel.error.noMesh': 'No meshes found!', + + # UI Bone_root + 'BoneRootPanel.label': 'Bone Parenting', + + # UI Optimization + 'OptimizePanel.label': 'Optimization', + 'OptimizePanel.atlasDesc': 'A greatly improved Atlas Generator.', + 'OptimizePanel.atlasAuthor': 'Made by Shotariya', + 'OptimizePanel.matCombDisabled1': 'Material Combiner is not enabled!', + 'OptimizePanel.matCombDisabled2': 'Enable it in your user preferences:', + 'OptimizePanel.matCombOutdated1': 'Your Material Combiner is outdated!', + 'OptimizePanel.matCombOutdated2': 'Please update to the latest version.', + 'OptimizePanel.matCombOutdated3': 'Update via the \'Updates\' panel', + 'OptimizePanel.matCombOutdated4': 'in the \'MatCombiner\' tab to the {location}', + 'OptimizePanel.matCombOutdated5_2.79': 'left.', + '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!', + + # UI Copy protection + 'CopyProtectionPanel.label': 'Copy Protection', + 'CopyProtectionPanel.desc1': 'Tries to protect your avatar from Unity cache ripping.', + 'CopyProtectionPanel.desc2': 'This protection is not 100% safe!', + 'CopyProtectionPanel.desc3': 'Before use: Read the documentation!', + + # UI Settings & Updates + 'UpdaterPanel.label': 'Settings & Updates', + 'UpdaterPanel.name': 'Settings:', + 'UpdaterPanel.requireRestart1': 'Restart required.', + 'UpdaterPanel.requireRestart2': 'Some changes require a Blender restart.', + + # UI Supporter + 'SupporterPanel.label': 'Supporters', + 'SupporterPanel.desc': 'Do you like this plugin and want to support us?', + 'SupporterPanel.thanks': 'Thanks to our awesome supporters! <3', + 'SupporterPanel.missingName1': 'Is your name missing?', + 'SupporterPanel.missingName2': ' Please contact us in our discord!', + + # UI Credits + 'CreditsPanel.label': 'Credits', + 'CreditsPanel.desc1': 'Cats Blender Plugin (', + 'CreditsPanel.desc2': 'Created by Hotox and GiveMeAllYourCats', + 'CreditsPanel.desc3': 'For the awesome VRChat community <3', + 'CreditsPanel.desc4': 'Special thanks to: Shotariya and Neitri', + 'CreditsPanel.desc5': 'Do you need help or found a bug?', + + # Tools Armature + 'FixArmature.label': 'Fix Model', + 'FixArmature.desc': 'Automatically:\n' \ + '- Reparents bones\n' \ + '- Removes unnecessary bones, objects, groups & constraints\n' \ + '- Translates and renames bones & objects\n' \ + '- Merges weight paints\n' \ + '- Corrects the hips\n' \ + '- Joins meshes\n' \ + '- Converts morphs into shapes\n' \ + '- Corrects shading', + 'FixArmature.error.noMesh': ['No mesh inside the armature found!', + 'If there are meshes outside of the armature,', + 'set the armature as the parent of the meshes.'], + # Format strings? vvvv t(str, fixed_uv_coords) -> The model was successfully fixed, but there were {} faulty UV + 'FixArmature.error.faultyUV1': 'The model was successfully fixed, but there were {uvcoord} faulty UV coordinates.', # 'The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', + 'FixArmature.error.faultyUV2': 'This could result in broken textures and you might have to fix them manually.', + 'FixArmature.error.faultyUV3': 'This issue is often caused by edits in PMX editor.', + 'FixArmature.fixedSuccess': 'Model successfully fixed.', + 'FixArmature.bonesNotFound': 'The following bones were not found:', + 'FixArmature.cantFix1': 'Looks like you found a model which Cats could not fix!', + 'FixArmature.cantFix2': 'If this is a non modified model we would love to make it compatible.', + 'FixArmature.cantFix3': 'Report it to us in the forum or in our discord, links can be found in the Credits panel.', + 'FixArmature.notParent': ' is not parented at all, this will cause problems!', + 'FixArmature.notParentTo1': ' is not parented to ', + 'FixArmature.notParentTo2': ', this will cause problems!', + + # Tools Armature Manual + 'StartPoseMode.label': 'Start Pose Mode', + 'StartPoseMode.desc': 'Starts the pose mode.\n' \ + 'This lets you test how your model will move', + + 'StopPoseMode.label': 'Stop Pose Mode', + 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', + + 'PoseToShape.label': 'Pose to Shape Key', + 'PoseToShape.desc': 'This saves your current pose as a new shape key.' \ + '\nThe new shape key will be at the bottom of your shape key list of the mesh', + + 'PoseNamePopup.label': 'Give this shapekey a name:', + 'PoseNamePopup.desc': 'Sets the shapekey name. Press anywhere outside to skip', + 'PoseNamePopup.success': 'Pose successfully saved as shape key.', + + 'PoseToRest.label': 'Apply as Rest Pose', + 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' \ + '\n' \ + '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' \ + '\nWARNING: This can have unwanted effects on shape keys, so be careful when modifying the head with this', + 'PoseToRest.success': 'Pose successfully applied as rest pose.', + + 'JoinMeshes.label': 'Join Meshes', + 'JoinMeshes.desc': 'Joins all meshes of this model together.' \ + '\nIt also:' \ + '\n - Reorders all shape keys correctly' \ + '\n - Applies all transforms' \ + '\n - Repairs broken armature modifiers' \ + '\n - Applies all decimation and mirror modifiers' \ + '\n - Merges UV maps correctly', + 'JoinMeshes.failure': 'Meshes could not be joined!', + 'JoinMeshes.success': 'Meshes joined.', + + 'JoinMeshesSelected.label': 'Join Selected Meshes', + 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' \ + '\nIt also:' \ + '\n - Reorders all shape keys correctly' \ + '\n - Applies all transforms' \ + '\n - Repairs broken armature modifiers' \ + '\n - Applies all decimation and mirror modifiers' \ + '\n - Merges UV maps correctly', + 'JoinMeshesSelected.error.noSelect': 'No meshes selected! Please select the meshes you want to join in the hierarchy!', + 'JoinMeshesSelected.error.cantJoin': 'Selected meshes could not be joined!', + 'JoinMeshesSelected.success': 'Selected meshes joined.', + + 'SeparateByMaterials.label': 'Separate by Materials', + 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' \ + '\n' \ + 'Warning: Never decimate something where you might need the shape keys later (face, mouth, eyes..)', + 'SeparateByMaterials.success': 'Successfully separated by materials.', + + 'SeparateByLooseParts.label': 'Separate by Loose Parts', + 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' \ + 'This acts like separating by materials but creates more meshes for more precision', + 'SeparateByLooseParts.success': 'Successfully separated by loose parts.', + + 'SeparateByShapekeys.label': 'Separate by Shape Keys', + 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' \ + '\ndepending on whether it is effected by a shape key or not.' \ + '\n' \ + '\nVery 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,' \ + '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ + '\n' \ + '\nUseful if you have the Copy Protection enabled on multiple selected parts of your model', + 'SeparateByCopyProtection.success': 'Successfully separated by shape keys.', + + 'SeparateByX.error.noMesh': 'No meshes found!', + 'SeparateByX.error.multipleMesh': 'Multiple meshes found!' \ + '\nPlease select the mesh you want to separate!', + 'SeparateByX.warn.noSeparation': 'No meshes had to be separated!', + + 'MergeWeights.label': 'Merge Weights to Parent', + 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' \ + '\n' \ + '\nOnly available in Edit or Pose Mode with bones selected', + 'MergeWeights.success': 'Deleted {number} bones and added their weights to their parents.', + + 'MergeWeightsToActive.label': 'Merge Weights to Active', + 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' \ + '\nThe active bone is the one you selected last.' \ + '\n' \ + '\nOnly available in Edit or Pose Mode with bones selected', + 'MergeWeightsToActive.success': 'Deleted {number} bones and added their weights to the active bone.', + + 'ApplyTransformations.label': 'Apply Transformations', + 'ApplyTransformations.desc': 'Applies the position, rotation and scale to the armature and it\'s meshes', + 'ApplyTransformations.success': 'Transformations applied.', + + 'ApplyAllTransformations.label': 'Apply All Transformations', + 'ApplyAllTransformations.desc': 'Applies the position, rotation and scale of all objects', + 'ApplyAllTransformations.success': 'Transformations applied.', + + 'RemoveZeroWeightBones.label': 'Remove Zero Weight Bones', + 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' \ + 'Don\'t use this if you plan to use \'Fix Model\'', + 'RemoveZeroWeightBones.success': 'Deleted {number} zero weight bones.', + + 'RemoveZeroWeightGroups.label': 'Remove Zero Weight Vertex Groups', + 'RemoveZeroWeightGroups.desc': 'Cleans up the vertex groups of all meshes, deleting all groups that don\'t directly affect any vertices', + 'RemoveZeroWeightGroups.success': 'Removed {number} zero weight vertex groups.', + + 'RemoveConstraints.label': 'Remove Bone Constraints', + 'RemoveConstraints.desc': 'Removes constrains between bones causing specific bone movement as these are not used by VRChat', + 'RemoveConstraints.success': 'Removed all bone constraints.', + + 'RecalculateNormals.label': 'Recalculate Normals', + 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' \ + 'Don\'t use this on good looking meshes as this can screw them up.\n' \ + 'Use this if there are random inverted or darker faces on the mesh', + 'RecalculateNormals.success': 'Recalculated all normals.', + + 'FlipNormals.label': 'Flip Normals', + 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' \ + 'Use this if all normals are inverted', + 'FlipNormals.success': 'Flipped all normals.', + + 'RemoveDoubles.label': 'Remove Doubles', + 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ + '\nThis is more safe than doing it manually:' \ + '\n - leaves shape keys completely untouched' \ + '\n - but removes less doubles overall', + 'RemoveDoubles.success': 'Removed {number} vertices.', + + 'RemoveDoublesNormal.label': 'Remove Doubles Normally', + 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ + '\nThis is exactly like doing it manually', + 'RemoveDoublesNormal.success': 'Removed {number} vertices.', + + 'FixVRMShapesButton.label': 'Fix Koikatsu Shapekeys', + 'FixVRMShapesButton.desc': 'Fixes the shapekeys of Koikatsu models', + 'FixVRMShapesButton.warn.notDetected': 'No shapekeys detected!', + 'FixVRMShapesButton.success': 'Fixed VRM shapekeys.', + + 'FixFBTButton.label': 'Fix Full Body Tracking', + 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' \ + '\n' \ + '\nApplies a general fix for Full Body Tracking.' \ + '\nIgnore the \"Spine length zero\" warning in Unity', + 'FixFBTButton.error.bonesNotFound': 'Required bones could not be found!' + '\nPlease make sure that your armature contains the following bones:' + '\n - Hips, Spine, Left leg, Right leg' + '\nExact names are required!', + 'FixFBTButton.error.alreadyApplied': 'Full Body Tracking Fix already applied!', + 'FixFBTButton.success': 'Successfully applied the Full Body Tracking fix.', + + 'RemoveFBTButton.label': 'Remove Full Body Tracking Fix', + 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' \ + '\n' \ + '\nRequires bones:' \ + '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2', + 'RemoveFBTButton.error.bonesNotFound': 'Required bones could not be found!' + '\nPlease make sure that your armature contains the following bones:' + '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2' + '\nExact names are required!', + 'RemoveFBTButton.error.notApplied': 'The Full Body Tracking Fix is not applied!', + 'RemoveFBTButton.success': 'Successfully removed the Full Body Tracking fix.', + + 'DuplicateBonesButton.label': 'Duplicate Bones', + 'DuplicateBonesButton.desc': 'Duplicates the selected bones including their weight and renames them to _L and _R', + 'DuplicateBonesButton.success': 'Successfully duplicated {number} bones.', + + # Tools Armature Custom + 'MergeArmature.label': 'Merge Armatures', + 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' \ + '\nYou should fix both armatures with Cats first.' \ + '\nOnly move the mesh of the merge armature to the desired position, the bones will be moved automatically', + 'MergeArmature.error.notFound': 'The armature "{name}" could not be found.', + 'MergeArmature.error.checkTransforms': [ 'Please make sure that the parent of the merge armature has the following transforms:', + ' - Location at 0', + ' - Rotation at 0', + ' - Scale at 1'], + 'MergeArmature.error.pleaseFix': [ 'Please use the "Fix Model" feature on the selected armatures first!', + 'Make sure to select the armature you want to fix above the "Fix Model" button!', + 'After that please only move the mesh (not the armature!) to the desired position.'], + 'MergeArmature.success': 'Armatures successfully joined.', + + 'AttachMesh.label': 'Attach Mesh', + 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' \ + '\n' \ + '\nINFO: The mesh will only be assigned to the selected bone.' \ + '\nE.g.: A jacket won\'t work, because it requires multiple bones', + 'AttachMesh.success': 'Mesh successfully attached to armature.', + + 'CustomModelTutorialButton.label': 'Go to Documentation', + 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) + 'CustomModelTutorialButton.success': 'Documentation', + + 'merge_armatures.error.transformReset': ['If you want to rotate the new part, only modify the mesh instead of the armature,', + 'or select "Apply Transforms"!', + '', + 'The transforms of the merge armature got reset and the mesh you have to modify got selected.', + 'Now place this selected mesh where and how you want it to be and then merge the armatures again.', + 'If you don\'t want that, undo this operation.'], + 'merge_armatures.error.pleaseUndo': ['Something went wrong! Please undo, check your selections and try again.'], + + # Tools Atlas + 'EnableSMC.label': 'Enable Material Combiner', + 'EnableSMC.desc': 'Enables Material Combiner', + 'EnableSMC.success': 'Enabled Material Combiner!', + + 'AtlasHelpButton.label': 'Generate Material List', + 'AtlasHelpButton.desc': 'Open useful Atlas Tips', + 'AtlasHelpButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin/#texture-atlas', + 'AtlasHelpButton.success': 'Atlas Help opened.', + + 'InstallShotariya.label': 'Error while loading Material Combiner:', + 'InstallShotariya.error.install1': 'Material Combiner is not installed!', + 'InstallShotariya.error.install2': 'The plugin \'Material Combiner\' by Shotariya is required for this function.', + 'InstallShotariya.error.install3': 'Please download and install it manually:', + 'InstallShotariya.error.enable1': 'Material Combiner is not enabled!', + 'InstallShotariya.error.enable2': 'The plugin \'Material Combiner\' by Shotariya is required for this function.', + 'InstallShotariya.error.enable3': 'Please enable it in your User Preferences.', + 'InstallShotariya.error.version1': 'Material Combiner is outdated!', + 'InstallShotariya.error.version2': 'The latest version is required for this function.', + 'InstallShotariya.error.version3': 'Please download and install it manually:', + + 'ShotariyaButton.label': 'Download Material Combiner', + 'ShotariyaButton.URL': 'https://vrcat.club/threads/material-combiner-blender-addon-1-1-3.2255/', + 'ShotariyaButton.success': 'Material Combiner link opened', + + # Tools Bonemerge + 'BoneMergeButton.label': 'Merge Bones', + 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' \ + 'This is useful to reduce the amount of bones used by Dynamic Bones.', + 'BoneMergeButton.success': 'Merged bones.', + + # Tools Common + 'ShowError.label': 'Report: Error', + + # Tools Copy protection + 'CopyProtectionEnable.label': 'Enable Protection', + 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' \ + '\nRead 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', + + # Tools Credits + 'ForumButton.label': 'Go to the Forums', + 'ForumButton.URL': 'https://vrcat.club/threads/cats-blender-plugin.6/', + 'ForumButton.success': 'Forum opened.', + + 'DiscordButton.label': 'Join our Discord', + 'DiscordButton.URL': 'https://discord.gg/f8yZGnv', + 'DiscordButton.success': 'Discord opened.', + + 'PatchnotesButton.label': 'Latest Patchnotes', + 'PatchnotesButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin/releases', + 'PatchnotesButton.success': 'Patchnotes opened.', + + # Tools Decimation + 'ScanButton.label': 'Scan for decimation models', + 'ScanButton.desc': 'Separates the mesh.', + + 'AddShapeButton.label': 'Add', + 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' \ + 'This means that every mesh containing that shape key will be not decimated.', + + 'AddMeshButton.label': 'Add', + 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' \ + 'This means that this mesh will be not decimated.', + + 'RemoveShapeButton.label': '', + 'RemoveShapeButton.desc': 'Removes the selected shape key from the whitelist.\n' \ + 'This means that this shape key is no longer decimation safe!', + + 'RemoveMeshButton.label': '', + 'RemoveMeshButton.desc': 'Removes the selected mesh from the whitelist.\n' \ + 'This means that this mesh will be decimated.', + + 'AutoDecimateButton.label': 'Quick Decimation', + 'AutoDecimateButton.desc': 'This will automatically decimate your model while preserving the shape keys.\n' \ + 'You should manually remove unimportant meshes first.', + 'AutoDecimateButton.error.noMesh': 'No meshes found!', + + 'decimate.cantDecimateWithSettings': 'This model can not be decimated to {number} tris with the specified settings.', + 'decimate.safeTryOptions': 'Try to use Custom, Half or Full Decimation.', + 'decimate.halfTryOptions': 'Try to use Custom or Full Decimation.', + 'decimate.customTryOptions': 'Select fewer shape keys and/or meshes or use Full Decimation.', + 'decimate.disableFingersOrIncrease': 'Disable \'Save Fingers\' or increase the Tris Count.', + 'decimate.disableFingers': 'or disable \'Save Fingers\'.', # This comes after one of the previous xTryOptions + 'decimate.noDecimationNeeded': 'The model already has less than {number} tris. Nothing had to be decimated.', + 'decimate.cantDecimate1': 'The model could not be decimated to {number} tris.', + 'decimate.cantDecimate2': 'It got decimated as much as possible within the limits.', + + # Tools Eyetracking + 'CreateEyesButton.label': 'Create Eye Tracking', + 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' \ + 'You should do decimation before this operation.\n' \ + 'Test the resulting eye movement in the \'Testing\' tab.', + 'CreateEyesButton.error.noShapeSelected': 'You have no shape keys selected.' + '\nPlease choose a mesh containing shape keys or check "Disable Eye Blinking".', + 'CreateEyesButton.error.missingBone': 'The bone "{bone}" does not exist.', + 'CreateEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' + '\nThis might be because you selected the wrong mesh or the wrong bone.' + '\nAlso make sure that the selected eye bones actually move the eyes in pose mode.', + 'CreateEyesButton.error.dontUse': 'Please do not use "{eyeName}" as the input bone.' + '\nIf you are sure that you want to use that bone please rename it to "{eyeNameShort}".', + 'CreateEyesButton.error.hierarchy': 'Eye tracking will not work unless the bone hierarchy is exactly as following: Hips > Spine > Chest > Neck > Head' + '\nFurthermore the mesh containing the eyes has to be called "Body" and the armature "Armature".', + 'CreateEyesButton.success': 'Created eye tracking!', + + 'StartTestingButton.label': 'Start Eye Testing', + 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' \ + 'Don\'t forget to stop the Testing process afterwards.\n' \ + 'Bones "LeftEye" and "RightEye" are required.', + + 'StopTestingButton.label': 'Stop Eye Testing', + 'StopTestingButton.desc': 'Stops the testing process.', + 'StopTestingButton.error.tryAgain': 'Something went wrong. Please try eye testing again.', + + 'ResetRotationButton.label': 'Reset Rotation', + 'ResetRotationButton.desc': 'This resets the eye positions.', + + 'AdjustEyesButton.label': 'Set Range', + 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' \ + 'This gets saved', + 'AdjustEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' + '\nThis might be because you selected the wrong mesh or the wrong bone.' + '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.', + + 'StartIrisHeightButton.label': 'Start Iris Height Adjustment', + 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' \ + 'Use this to fix clipping of the iris into the eye ball.\n' \ + 'This gets saved.', + + 'TestBlinking.label': 'Test', + 'TestBlinking.desc': 'This lets you see how eye blinking will look in-game.', + + 'TestLowerlid.label': 'Test', + 'TestLowerlid.desc': 'This lets you see how lowerlids will look in-game.', + + 'ResetBlinkTest.label': 'Reset Shapes', + 'ResetBlinkTest.desc': 'This resets the blink testing.', + + # Tools Importer + 'ImportAnyModel.label': 'Import Any Model', + 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' \ + '\n' \ + '\nSupported types:' \ + '\n- MMD: .pmx/.pmd' \ + '\n- XNALara: .xps/.mesh/.ascii' \ + '\n- Source: .smd/.qc' \ + '\n- VRM: .vrm' \ + '\n- FBX .fbx ' \ + '\n- DAE: .dae ' \ + '\n- ZIP: .zip', + 'ImportAnyModel.desc2.8': 'Import a model of any supported type.' \ + '\n' \ + '\nSupported types:' \ + '\n- MMD: .pmx/.pmd' \ + '\n- XNALara: .xps/.mesh/.ascii' \ + '\n- Source: .smd/.qc/.vta/.dmx' \ + '\n- VRM: .vrm' \ + '\n- FBX: .fbx' \ + '\n- DAE: .dae ' \ + '\n- ZIP: .zip', + 'ImportAnyModel.importantInfo.label': 'IMPORTANT INFO (hover here)', + 'ImportAnyModel.importantInfo.desc': 'If you want to modify the import settings, use the button next to the Import button.\n\n', + 'ImportAnyModel.error.emptyZip': 'The selected zip file contains no importable models.', + 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' \ + '\nPlease use a tool such as the "Autodesk FBX Converter" to make it compatible.', + + 'ZipPopup.label': 'Zip Model Selection:', + 'ZipPopup.desc': 'Shows the models contained in the zip files', + 'ZipPopup.selectModel1': 'Select which model you want to import', + 'ZipPopup.selectModel2': 'Then confirm with OK', + + 'get_zip_content.choose': 'Import model "{model}" from the zip "{zipName}?"', + + 'ModelsPopup.label': 'Select which you want to import:', + 'ModelsPopup.desc': 'Show individual import options', + + 'ImportMMD.label': 'MMD', + 'ImportMMD.desc': 'Import a MMD model (.pmx/pmd)', + + 'ImportXPS.label': 'XNALara', + 'ImportXPS.desc': 'Import a XNALara model (.xps/.mesh/.ascii)', + + 'ImportSource.label': 'Source', + 'ImportSource.desc': 'Import a Source model (.smd/.qc/.vta/.dmx)', + + 'ImportFBX.label': 'FBX', + 'ImportFBX.desc': 'Import a FBX model (.fbx)', + + 'ImportVRM.label': 'VRM', + 'ImportVRM.desc': 'Import a VRM model (.vrm)', + + 'InstallXPS.label': 'XPS Tools is not installed or enabled!', + + 'InstallSource.label': 'Source Tools is not installed or enabled!', + + 'InstallVRM.label': 'VRM Importer is not installed or enabled!', + + 'InstallX.pleaseInstall1': 'If it is not enabled please enable it in your User Preferences.', + 'InstallX.pleaseInstall2': 'If it is not installed please download and install it manually.', + 'InstallX.pleaseInstall3': 'Make sure to install the version for Blender {blenderVersion}', + 'InstallX.pleaseInstallTesting': 'Currently you have to select \'Testing\' in the addons settings.', + + 'EnableMMD.label': 'Mmd_tools is not enabled!', + 'EnableMMD.required1': 'The plugin "mmd_tools" is required for this function.', + 'EnableMMD.required2': 'Please restart Blender.', + + 'XpsToolsButton.label': 'Download XPS Tools', + 'XpsToolsButton.URL': 'https://github.com/johnzero7/XNALaraMesh', + 'XpsToolsButton.success': 'XPS Tools link opened', + + 'SourceToolsButton.label': 'Download Source Tools', + 'SourceToolsButton.URL': 'https://github.com/Artfunkel/BlenderSourceTools', + 'SourceToolsButton.success': 'Source Tools link opened', + + 'VrmToolsButton.label': 'Download VRM Importer', + 'VrmToolsButton.URL_2.79': 'https://github.com/iCyP/VRM_IMPORTER_for_Blender2_79', + 'VrmToolsButton.URL_2.8': 'https://github.com/saturday06/VRM_IMPORTER_for_Blender2_8', + 'VrmToolsButton.success': 'VRM Importer link opened', + + 'ExportModel.label': 'Export Model', + 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' \ + '\n' \ + 'Automatically sets the optimal export settings', + 'ExportModel.error.notEnabled': 'FBX Exporter not enabled! Please enable it in your User Preferences.', + + 'ErrorDisplay.label': 'Warning:', + 'ErrorDisplay.polygons1': 'Too many polygons!', + 'ErrorDisplay.polygons2': 'You have {number} tris in this model, but you shouldn\'t have more than 70,000!', + 'ErrorDisplay.polygons3': 'You should decimate before you export this model.', + 'ErrorDisplay.materials1': 'Model not optimized!', + 'ErrorDisplay.materials2': 'This model has {number} materials!', + 'ErrorDisplay.materials3': 'You should try to have a maximum of 4 materials on your model.', + 'ErrorDisplay.materials4': 'Creating a texture atlas in CATS is very easy, so please make use of it.', + 'ErrorDisplay.meshes1': 'Meshes not joined!', + 'ErrorDisplay.meshes2': 'This model has {number} meshes!', + 'ErrorDisplay.meshes3': 'It is not very optimized and might cause lag for you and others!', + 'ErrorDisplay.meshes3_alt': "It is extremely unoptimized and will cause laugh for you and others!", + 'ErrorDisplay.meshes4': 'You should always join your meshes, it\'s very easy:', + 'ErrorDisplay.JoinMeshes.label': 'Join Meshes', + 'ErrorDisplay.brokenShapekeys1': 'Broken shapekeys!', + 'ErrorDisplay.brokenShapekeys2': 'This model has {number} broken shapekey(s):', + 'ErrorDisplay.brokenShapekeys3': 'You will not be able to upload this model until you fix these shapekeys.', + 'ErrorDisplay.brokenShapekeys4': 'Either delete or repair them before export.', + 'ErrorDisplay.textures1': 'No textures found!', + 'ErrorDisplay.textures2': 'This model has no textures assigned but you have \'Embed Textures\' enabled.', + 'ErrorDisplay.textures3': 'Therefore, no textures will be embedded into the FBX.', + 'ErrorDisplay.textures4': 'This is not an issue, but you will have to import the textures manually into Unity.', + 'ErrorDisplay.eyes1': 'Eyes not named \'Body\'!', + 'ErrorDisplay.eyes2': 'The mesh \'{name}\' has Eye Tracking shapekeys but is not named \'Body\'.', + 'ErrorDisplay.eyes2_alt': 'Multiple meshes have Eye Tracking shapekeys but are not named \'Body\'.', + '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', + + # Tools Material + 'OneTexPerMatButton.label': 'One Material Texture', + 'OneTexPerMatButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.', + + 'OneTexPerMatOnlyButton.label': 'One Material Texture', + 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' \ + '\nAlso removes the textures from the material instead of disabling it.' \ + '\nThis 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' \ + '\nand 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.\n' \ + 'Your avatar should visibly look the same after this operation.\n' \ + 'This is a very important step for optimizing your avatar.\n' \ + 'If you have problems with this, please tell us!\n', + 'CombineMaterialsButton.error.noChanges': 'No materials combined.', + 'CombineMaterialsButton.success': 'Combined {number} materials!', + + 'ConvertAllToPngButton.label': 'Convert Textures to PNG', + 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' \ + '\nThis helps with transparency and compatibility issues.' \ + '\n\nThe converted image files will be saved next to the old ones', + 'ConvertAllToPngButton.success': 'Converted {number} to PNG files.', + + # Tools Root bone + 'RootButton.label': 'Parent Bones', + 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' \ + 'Very useful for Dynamic Bones.', + 'RootButton.success': 'Bones parented!', + + 'RefreshRootButton.label': 'Refresh List', + 'RefreshRootButton.desc': 'This will clear the group bones list cache and rebuild it, useful if bones have changed or your model.', + 'RefreshRootButton.success': 'Root bones refreshed, check the root bones list again.', + + # Tools Settings + 'RevertChangesButton.label': 'Revert Settings', + 'RevertChangesButton.desc': 'Revert the changes back to how they were on Blender start-up.', + 'RevertChangesButton.success': 'Settings reverted.', + + 'ResetGoogleDictButton.label': 'Clear Local Google Translations', + 'ResetGoogleDictButton.desc': 'Deletes all currently saved Google Translations. You can\'t undo this', + 'ResetGoogleDictButton.resetInfo': 'Local Google Dictionary cleared!', + + 'DebugTranslations.label': 'Debug Google Translations', # DEV ONLY + 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' \ + '\nThis button is only visible in the cats development version', # DEV ONLY + 'DebugTranslations.error': 'Errors found, response printed!!', # DEV ONLY + 'DebugTranslations.success': 'No issues with Google Translations found, response printed!', # DEV ONLY + + # Tools Shapekey + 'ShapeKeyApplier.label': 'Apply Selected Shapekey to Basis', + 'ShapeKeyApplier.desc': 'Applies the selected shape key to the new Basis at it\'s current strength and creates a reverted shape key from the selected one', + 'ShapeKeyApplier.error.revertCustomBasis': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', + 'Start with the shape key called "{name}".', + '', + 'If you didn\'t change the shape key order, you can revert the shape keys from top to bottom.'], + 'ShapeKeyApplier.error.revertCustomBasis.scale': 7.3, + 'ShapeKeyApplier.error.revert': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', + 'Start with the reverted shape key that uses the relative key called "Basis".', + '', + "If you didn't change the shape key order, you can revert the shape keys from top to bottom."], + 'ShapeKeyApplier.error.revert.scale': 7.3, + 'ShapeKeyApplier.successRemoved': 'Successfully removed shapekey "{name}" from the Basis.', + 'ShapeKeyApplier.successSet': 'Successfully set shapekey "{name}" as the new Basis.', + + 'addToShapekeyMenu.ShapeKeyApplier.label': 'Apply Selected Shapekey to Basis', + + # Tools Supporter + 'PatreonButton.label': 'Become a Patron', + 'PatreonButton.URL': 'https://www.patreon.com/catsblenderplugin', + 'PatreonButton.success': 'Patreon page opened.', + + 'ReloadButton.label': 'Reload List', + 'ReloadButton.desc': 'Reloads the supporter list', + + 'DynamicPatronButton.label': 'Supporter Name', + 'DynamicPatronButton.desc': 'This is an awesome supporter', + + 'register_dynamic_buttons.desc': '{name} is an awesome supporter', + + # Tools Translate + 'TranslateShapekeyButton.label': 'Translate Shape Keys', + 'TranslateShapekeyButton.desc': 'Translates all shape keys using the internal dictionary and Google Translate', + 'TranslateShapekeyButton.success': 'Translated {number} shape keys.', + + 'TranslateBonesButton.label': 'Translate Bones', + 'TranslateBonesButton.desc': 'Translates all bones using the internal dictionary and Google Translate', + 'TranslateBonesButton.success': 'Translated {number} bones.', + + 'TranslateObjectsButton.label': 'Translate Meshes & Objects', + 'TranslateObjectsButton.desc': 'Translates all meshes and objects using the internal dictionary and Google Translate', + 'TranslateObjectsButton.success': 'Translated {number} meshes and objects.', + + 'TranslateMaterialsButton.label': 'Translate Materials', + 'TranslateMaterialsButton.desc': 'Translates all materials using the internal dictionary and Google Translate', + 'TranslateMaterialsButton.success': 'Translated {number} materials.', + + 'TranslateTexturesButton.label': 'Translate Textures', + 'TranslateTexturesButton.desc': 'Translates all textures using the internal dictionary and Google Translate', + 'TranslateTexturesButton.success_alt': 'Translated all textures', + 'TranslateTexturesButton.error.noInternet': 'Could not connect to Google. Please check your internet connection.', + 'TranslateTexturesButton.success': 'Translated {number} textures', + + 'TranslateAllButton.label': 'Translate Everything', + 'TranslateAllButton.desc': 'Translates everything using the internal dictionary and Google Translate', + 'TranslateAllButton.success': 'Translated everything.', + + 'TranslateX.error.wrongVersion': 'You need Blender 2.79 or higher for this function.', + + 'update_dictionary.error.cantConnect': 'Could not connect to Google. Some parts could not be translated.', + 'update_dictionary.error.temporaryBan': 'It looks like you got banned from Google Translate temporarily!', + 'update_dictionary.error.catsTranslated': '\nCats translated what it could with the local dictionary, but you will have to try again later for the Google translations.', + 'update_dictionary.error.cantAccess': 'Cats was not able to access Google Translate!', + 'update_dictionary.error.errorMsg': 'You got an error message from Google Translate!', + 'update_dictionary.error.apiChanged': 'Could not get translations from Google Translate!' + '\nThis means that Google changed their API and translations will no longer work until this is fixed.' + '\nPlease translate manually or wait for an CATS update.' + '\nFor updates and dicussions please join our Discord. The link can be found in the Credits panel down below.', + + # Tools Viseme + 'AutoVisemeButton.label': 'Create Visemes', + 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' \ + 'It will generate 15 shape keys from the 3 shape keys you specify', + 'AutoVisemeButton.error.noShapekeys': 'This mesh has no shapekeys!', + 'AutoVisemeButton.error.selectShapekeys': 'Please select the correct mouth shapekeys instead of "Basis"!', + 'AutoVisemeButton.success': 'Created mouth visemes!', + + # Extentions + 'Scene.armature.label': 'Armature', + 'Scene.armature.desc': 'Select the armature which will be used by Cats', + + 'Scene.zip_content.label': 'Zip Content', + 'Scene.zip_content.desc': 'Select the model you want to import', + + 'Scene.keep_upper_chest.label': 'Keep Upper Chest', + 'Scene.keep_upper_chest.desc': 'VRChat now partially supports the Upper Chest bone, so deleting it is no longer necessary.' + '\n\nWARNING: Currently this breaks Eye Tracking, so don\'t check this if you want Eye Tracking', + + 'Scene.combine_mats.label': 'Combine Same Materials', + 'Scene.combine_mats.desc': 'Combines similar materials into one, reducing draw calls.\n\n' + 'Your avatar should visibly look the same after this operation.\n' + 'This is a very important step for optimizing your avatar.\n' + 'If you have problems with this, uncheck this option and tell us!\n', + + 'Scene.remove_zero_weight.label': 'Remove Zero Weight Bones', + 'Scene.remove_zero_weight.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' + '\nUncheck this if bones or vertex groups that you want to keep got deleted', + + 'Scene.keep_end_bones.label': 'Keep End Bones', + 'Scene.keep_end_bones.desc': 'Saves end bones from deletion.' + '\n\nThis can improve skirt movement for dynamic bones, but increases the bone count.' + '\nThis can also fix issues with crumbled finger bones in Unity.' + '\nMake sure to always uncheck "Add Leaf Bones" when exporting or use the CATS export button', + + 'Scene.keep_twist_bones.label': 'Keep Twist Bones', + 'Scene.keep_twist_bones.desc': 'This will keep any bone with "Twist" in the name.' + '\nSo if there are certain bones that you want to keep, you can add "Twist" to them and they won\'t get deleted.' + '\n\nVRChat can now make use of twist bones, so you can use this option to keep them', + + 'Scene.fix_twist_bones.label': 'Fix MMD Twist Bones', + 'Scene.fix_twist_bones.desc': 'This will make MMD arm twist bones usable in VRChat.' + '\nWIll only work if the twist bones are properly named.' + '\nRequired names:' + '\n - ArmTwist[1-3]_[L/R]' + '\n - HandTwist[1-3]_[L/R]' + '\n\nYou don\'t need to enable "Keep Twist Bones" for this to work', + + 'Scene.join_meshes.label': 'Join Meshes', + 'Scene.join_meshes.desc': 'Joins all meshes of this model together.' + '\nIt also:' + '\n - Applies all transformations' + '\n - Repairs broken armature modifiers' + '\n - Applies all decimation and mirror modifiers' + '\n - Merges UV maps correctly' + '\n' + '\nINFO: You should always join your meshes', + + 'Scene.connect_bones.label': 'Connect Bones', + 'Scene.connect_bones.desc': 'This connects all bones to their child bone if they have exactly one child bone.\n' + 'This will not change how the bones function in any way, it just improves the aesthetic of the armature', + + 'Scene.fix_materials.label': 'Fix Materials', + 'Scene.fix_materials.desc': 'This will apply some VRChat related fixes to materials', + + 'Scene.remove_rigidbodies_joints.label': 'Remove Rigidbodies and Joints', + 'Scene.remove_rigidbodies_joints.desc': 'Rigidbodies and joints are used by MMD software to simulate physics.' + '\nThey are completely useless for VRChat, so removing them is recommended for VRChat users!', + + 'Scene.use_google_only.label': 'Use Old Translations (not recommended)', + 'Scene.use_google_only.desc': 'Ignores the internal dictionary and only uses the Google Translator for shape key translations.' + '\n' + '\nThis will result in slower translation speed and worse translations, but the translations will be like in CATS version 0.9.0 and older.' + '\nOnly use this if you have animations which rely on the old translations and you don\'t want to convert them to the new ones', + + 'Scene.show_more_options.label': 'Show More Options', + 'Scene.show_more_options.desc': 'Shows more model options', + + 'Scene.merge_mode.label': 'Merge Mode', + 'Scene.merge_mode.desc': 'Mode', + 'Scene.merge_mode.armature.label': 'Merge Armatures', + 'Scene.merge_mode.armature.desc': 'Here you can merge two armatures together.', + 'Scene.merge_mode.mesh.label': 'Attach Mesh', + 'Scene.merge_mode.mesh.desc': 'Here you can attach a mesh to an armature.', + + 'Scene.merge_armature_into.label': 'Base Armature', + 'Scene.merge_armature_into.desc': 'Select the armature into which the other armature will be merged\n', + + 'Scene.merge_armature.label': 'Merge Armature', + 'Scene.merge_armature.desc': 'Select the armature which will be merged into the selected armature above\n', + + 'Scene.attach_to_bone.label': 'Attach to Bone', + 'Scene.attach_to_bone.desc': 'Select the bone to which the armature will be attached to\n', + + 'Scene.attach_mesh.label': 'Attach Mesh', + 'Scene.attach_mesh.desc': 'Select the mesh which will be attached to the selected bone in the selected armature\n', + + 'Scene.merge_same_bones.label': 'Merge All Bones', + 'Scene.merge_same_bones.desc': 'Merges all bones together that have the same name instead of only the base bones (Hips, Spine, etc).' + '\nYou will have to make sure that all the bones you want to merge have the same name.' + '\n' + '\nIf this is checked, you won\'t need to fix the model with CATS beforehand but it is still advised to do so.' + '\nIf this is unchecked, CATS will only merge the base bones (Hips, Spine, etc).' + '\n' + '\nThis can have unintended side effects, so check your model afterwards!' + '\n', + + 'Scene.apply_transforms.label': 'Apply Transforms', + 'Scene.apply_transforms.desc': 'Check this if both armatures and meshes are already at their correct positions.' + '\nThis will cause them to stay exactly where they are when merging', + + 'Scene.merge_armatures_join_meshes.label': 'Join Meshes', + 'Scene.merge_armatures_join_meshes.desc': 'This will join all meshes.' + '\nNot checking this will always apply transforms', + + 'Scene.merge_armatures_remove_zero_weight_bones.label': 'Remove Zero Weight Bones', + 'Scene.merge_armatures_remove_zero_weight_bones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' + '\nUncheck this if bones or vertex groups that you want to keep got deleted', + # Decimation + 'Scene.decimation_mode.label': 'Decimation Mode', + 'Scene.decimation_mode.desc': 'Decimation Mode', + 'Scene.decimation_mode.safe.label': 'Safe', + 'Scene.decimation_mode.safe.desc': 'Decent results - no shape key loss\n' + '\n' + 'This will only decimate meshes with no shape keys.\n' + 'The results are decent and you won\'t lose any shape keys.\n' + 'Eye Tracking and Lip Syncing will be fully preserved.', + 'Scene.decimation_mode.half.label': 'Half', + 'Scene.decimation_mode.half.desc': 'Good results - minimal shape key loss\n' + '\n' + 'This will only decimate meshes with less than 4 shape keys as those are often not used.\n' + 'The results are better but you will lose the shape keys in some meshes.\n' + 'Eye Tracking and Lip Syncing should still work.', + 'Scene.decimation_mode.full.label': 'Full', + 'Scene.decimation_mode.full.desc': 'Best results - full shape key loss\n' + '\n' + 'This will decimate your whole model deleting all shape keys in the process.\n' + 'This will give the best results but you will lose the ability to add blinking and Lip Syncing.\n' + 'Eye Tracking will still work if you disable Eye Blinking.', + 'Scene.decimation_mode.custom.label': 'Custom', + 'Scene.decimation_mode.custom.desc': 'Custom results - custom shape key loss\n' + '\n' + 'This will let you choose which meshes and shape keys should not be decimated.\n', + + 'Scene.selection_mode.label': 'Selection Mode', + 'Scene.selection_mode.desc': 'Selection Mode', + 'Scene.selection_mode.shapekeys.label': 'Shape Keys', + 'Scene.selection_mode.shapekeys.desc': 'Select all the shape keys you want to preserve here.', + 'Scene.selection_mode.meshes.label': 'Meshes', + 'Scene.selection_mode.meshes.desc': 'Select all the meshes you don\'t want to decimate here.', + + 'Scene.add_shape_key.label': 'Shape', + 'Scene.add_shape_key.desc': 'The shape key you want to keep', + + 'Scene.add_mesh.label': 'Mesh', + 'Scene.add_mesh.desc': 'The mesh you want to leave untouched by the decimation', + + 'Scene.decimate_fingers.label': 'Save Fingers', + 'Scene.decimate_fingers.desc': 'Check this if you don\'t want to decimate your fingers!\n' + 'Results will be worse but there will be no issues with finger movement.\n' + 'This is probably only useful if you have a VR headset.\n' + '\n' + 'This operation requires the finger bones to be named specifically:\n' + 'Thumb(0-2)_(L/R)\n' + 'IndexFinger(1-3)_(L/R)\n' + 'MiddleFinger(1-3)_(L/R)\n' + 'RingFinger(1-3)_(L/R)\n' + 'LittleFinger(1-3)_(L/R)', + + 'Scene.decimate_hands.label': 'Save Hands', + 'Scene.decimate_hands.desc': 'Check this if you don\'t want to decimate your full hands!\n' + 'Results will be worse but there will be no issues with hand movement.\n' + 'This is probably only useful if you have a VR headset.\n' + '\n' + 'This operation requires the finger and hand bones to be named specifically:\n' + 'Left/Right wrist\n' + 'Thumb(0-2)_(L/R)\n' + 'IndexFinger(1-3)_(L/R)\n' + 'MiddleFinger(1-3)_(L/R)\n' + 'RingFinger(1-3)_(L/R)\n' + 'LittleFinger(1-3)_(L/R)', + + 'Scene.decimation_remove_doubles.label': 'Remove Doubles', + 'Scene.decimation_remove_doubles.desc': 'Uncheck this if you got issues with with this checked', + + 'Scene.max_tris.label': 'Tris', + 'Scene.max_tris.desc': 'The target amount of tris after decimation', + # Eye Tracking + 'Scene.eye_mode.label': 'Eye Mode', + 'Scene.eye_mode.desc': 'Mode', + 'Scene.eye_mode.creation.label': 'Creation', + 'Scene.eye_mode.creation.desc': 'Here you can create eye tracking.', + 'Scene.eye_mode.testing.label': 'Testing', + 'Scene.eye_mode.testing.desc': 'Here you can test how eye tracking will look in-game.', + + 'Scene.mesh_name_eye.label': 'Mesh', + 'Scene.mesh_name_eye.desc': 'The mesh with the eyes vertex groups', + + 'Scene.head.label': 'Head', + 'Scene.head.desc': 'The head bone containing the eye bones', + + 'Scene.eye_left.label': 'Left Eye', + 'Scene.eye_left.desc': 'The models left eye bone', + + 'Scene.eye_right.label': 'Right Eye', + 'Scene.eye_right.desc': 'The models right eye bone', + + 'Scene.wink_left.label': 'Blink Left', + 'Scene.wink_left.desc': 'The shape key containing a blink with the left eye', + + 'Scene.wink_right.label': 'Blink Right', + 'Scene.wink_right.desc': 'The shape key containing a blink with the right eye', + + 'Scene.lowerlid_left.label': 'Lowerlid Left', + 'Scene.lowerlid_left.desc': 'The shape key containing a slightly raised left lower lid.\n' + 'Can be set to "Basis" to disable lower lid movement', + + 'Scene.lowerlid_right.label': 'Lowerlid Right', + 'Scene.lowerlid_right.desc': 'The shape key containing a slightly raised right lower lid.\n' + 'Can be set to "Basis" to disable lower lid movement', + + 'Scene.disable_eye_movement.label': 'Disable Eye Movement', + 'Scene.disable_eye_movement.desc': 'IMPORTANT: Do your decimation first if you check this!\n' + '\n' + 'Disables eye movement. Useful if you only want blinking.\n' + 'This creates eye bones with no movement bound to them.\n' + 'You still have to assign "LeftEye" and "RightEye" to the eyes in Unity', + + 'Scene.disable_eye_blinking.label': 'Disable Eye Blinking', + 'Scene.disable_eye_blinking.desc': 'Disables eye blinking. Useful if you only want eye movement.\n' + 'This will create the necessary shape keys but leaves them empty', + + 'Scene.eye_distance.label': 'Eye Movement Range', + 'Scene.eye_distance.desc': 'Higher = more eye movement\n' + 'Lower = less eye movement\n' + 'Warning: Too little or too much range can glitch the eyes.\n' + 'Test your results in the "Eye Testing"-Tab!\n', + + 'Scene.eye_rotation_x.label': 'Up - Down', + 'Scene.eye_rotation_x.desc': 'Rotate the eye bones on the vertical axis', + + 'Scene.eye_rotation_y.label': 'Left - Right', + 'Scene.eye_rotation_y.desc': 'Rotate the eye bones on the horizontal axis.' + '\nThis is from your own point of view', + + 'Scene.iris_height.label': 'Iris Height', + 'Scene.iris_height.desc': 'Moves the iris away from the eye ball', + + 'Scene.eye_blink_shape.label': 'Blink Strength', + 'Scene.eye_blink_shape.desc': 'Test the blinking of the eye', + + 'Scene.eye_lowerlid_shape.label': 'Lowerlid Strength', + 'Scene.eye_lowerlid_shape.desc': 'Test the lowerlid blinking of the eye', + + 'Scene.mesh_name_viseme.label': 'Mesh', + 'Scene.mesh_name_viseme.desc': 'The mesh with the mouth shape keys', + # Visemes + 'Scene.mouth_a.label': 'Viseme AA', + 'Scene.mouth_a.desc': 'Shape key containing mouth movement that looks like someone is saying "aa".\nDo not put empty shape keys like "Basis" in here', + + 'Scene.mouth_o.label': 'Viseme OH', + 'Scene.mouth_o.desc': 'Shape key containing mouth movement that looks like someone is saying "oh".\nDo not put empty shape keys like "Basis" in here', + + 'Scene.mouth_ch.label': 'Viseme CH', + 'Scene.mouth_ch.desc': 'Shape key containing mouth movement that looks like someone is saying "ch". Opened lips and clenched teeth.\nDo not put empty shape keys like "Basis" in here', + + 'Scene.shape_intensity.label': 'Shape Key Mix Intensity', + 'Scene.shape_intensity.desc': 'Controls the strength in the creation of the shape keys. Lower for less mouth movement strength', + # Bone Parenting + 'Scene.root_bone.label': 'To Parent', + 'Scene.root_bone.desc': 'List of bones that look like they could be parented together to a root bone', + # Optimize + 'Scene.optimize_mode.label': 'Optimize Mode', + 'Scene.optimize_mode.desc': 'Mode', + 'Scene.optimize_mode.atlas.label': 'Atlas', + 'Scene.optimize_mode.atlas.desc': 'Allows you to make a texture atlas.', + 'Scene.optimize_mode.material.label': 'Material', + 'Scene.optimize_mode.material.desc': 'Some various options on material manipulation.', + 'Scene.optimize_mode.bonemerging.label': 'Bone Merging', + 'Scene.optimize_mode.bonemerging.desc': 'Allows child bones to be merged into their parents.', + # Bone Merging + 'Scene.merge_ratio.label': 'Merge Ratio', + 'Scene.merge_ratio.desc': 'Higher = more bones will be merged\n' + 'Lower = less bones will be merged\n', + + 'Scene.merge_mesh.label': 'Mesh', + 'Scene.merge_mesh.desc': 'The mesh with the bones vertex groups', + + 'Scene.merge_bone.label': 'To Merge', + 'Scene.merge_bone.desc': 'List of bones that look like they could be merged together to reduce overall bones', + + 'Scene.embed_textures.label': 'Embed Textures on Export', + 'Scene.embed_textures.desc': 'Enable this to embed the texture files into the FBX file upon export.' + '\nUnity will automatically extract these textures and put them into a separate folder.' + '\nThis might not work for everyone and it increases the file size of the exported FBX file', + + 'Scene.use_custom_mmd_tools.label': 'Use Custom mmd_tools', + 'Scene.use_custom_mmd_tools.desc': 'Enable this to use your own version of mmd_tools. This will disable the internal cats mmd_tools', + + 'Scene.debug_translations.label': 'Debug Google Translations', + 'Scene.debug_translations.desc': 'Tests the Google Translations and prints the Google response in case of error', + + # Updater + 'CheckForUpdateButton.label': 'Check now for Update', + 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', + + 'UpdateToLatestButton.label': 'Update Now', + 'UpdateToLatestButton.desc': 'Update CATS to the latest version', + + 'UpdateToSelectedButton.label': 'Update to Selected version', + 'UpdateToSelectedButton.desc': 'Update CATS to the selected version', + + 'UpdateToDevButton.label': 'Update to Development version', + 'UpdateToDevButton.desc': 'Update CATS to the Development version', + + 'RemindMeLaterButton.label': 'Remind me later', + 'RemindMeLaterButton.desc': 'This hides the update notification \'til the next Blender restart', + 'RemindMeLaterButton.success': 'You will be reminded later', + + 'IgnoreThisVersionButton.label': 'Ignore this version', + 'IgnoreThisVersionButton.desc': 'This ignores this version. You will be reminded again when the next version releases', + 'IgnoreThisVersionButton.success': 'Version {name} will be ignored.', + + 'ShowPatchnotesPanel.label': 'Patchnotes', + 'ShowPatchnotesPanel.desc': 'Shows the patchnotes of the selected version', + 'ShowPatchnotesPanel.releaseDate': 'Released: {date}', + + 'ConfirmUpdatePanel.label': 'Confirm Update', + 'ConfirmUpdatePanel.desc': 'This shows you a panel in which you have to confirm your update choice', + 'ConfirmUpdatePanel.warn.dev1': 'Warning:', + 'ConfirmUpdatePanel.warn.dev2': ' The development version of CATS if the place where', + 'ConfirmUpdatePanel.warn.dev3': ' we test new features and bug fixes.', + 'ConfirmUpdatePanel.warn.dev4': ' This version might be very unstable and some features', + 'ConfirmUpdatePanel.warn.dev5': ' might not work correctly.', + 'ConfirmUpdatePanel.ShowPatchnotesPanel.label': 'Show Patchnotes', + 'ConfirmUpdatePanel.updateNow': 'Update now:', + + 'UpdateCompletePanel.label': 'Installation Report', + 'UpdateCompletePanel.desc': 'The update is now complete', + 'UpdateCompletePanel.success1': 'CATS was successfully updated.', + 'UpdateCompletePanel.success2': 'Restart Blender to complete the update.', + 'UpdateCompletePanel.failure1': 'Update failed.', + 'UpdateCompletePanel.failure2': 'See Updater Panel for more info.', + + 'UpdateNotificationPopup.label': 'Update available', + 'UpdateNotificationPopup.desc': 'This shows you that an update is available', + 'UpdateNotificationPopup.newUpdate': 'CATS v{name} available!', + 'UpdateNotificationPopup.ShowPatchnotesPanel.label': 'Show Patchnotes', + + 'check_for_update.cantCheck': 'Could not check for updates, try again later', + + 'download_file.cantConnect': 'Could not connect to Github', + 'download_file.cantFindZip': 'Could not find the downloaded zip', + 'download_file.cantFindCATS': 'Could not find CATS in the downloaded zip', + + 'draw_update_notification_panel.success': 'Restart Blender to complete update!', + 'draw_update_notification_panel.newUpdate': 'CATS v{name} available!', + 'draw_update_notification_panel.UpdateToLatestButton.label': 'Update Now', + 'draw_update_notification_panel.RemindMeLaterButton.label': 'Remind me later', + 'draw_update_notification_panel.IgnoreThisVersionButton.label': 'Ignore this version', + + 'draw_updater_panel.updateLabel': 'Updates:', + 'draw_updater_panel.updateLabel_alt': 'CATS Updater:', + 'draw_updater_panel.success': 'Restart Blender to complete update!', + 'draw_updater_panel.CheckForUpdateButton.label': 'Checking..', + 'draw_updater_panel.UpdateToLatestButton.label': 'Update now to {name}', + 'draw_updater_panel.CheckForUpdateButton.label_alt': 'Check now for Update', + 'draw_updater_panel.UpdateToLatestButton.label_alt': 'Up to Date!', + 'draw_updater_panel.UpdateToSelectedButton.label': 'Install version:', + 'draw_updater_panel.UpdateToDevButton.label': 'Install Development Version', + 'draw_updater_panel.currentVersion': 'Current version: {name}', + + 'bpy.types.Scene.cats_updater_version_list.label': 'Version', + 'bpy.types.Scene.cats_updater_version_list.desc': 'Select the version you want to install\n', + + 'bpy.types.Scene.cats_update_action.label': 'Choose action', + 'bpy.types.Scene.cats_update_action.desc': 'Action', + 'bpy.types.Scene.cats_update_action.update.label': 'Update Now', + 'bpy.types.Scene.cats_update_action.update.desc': 'Updates now to the latest version', + 'bpy.types.Scene.cats_update_action.ignore.label': 'Ignore this version', + 'bpy.types.Scene.cats_update_action.ignore.desc': 'This ignores this version. You will be reminded again when the next version releases', + 'bpy.types.Scene.cats_update_action.defer.label': 'Remind me later', + 'bpy.types.Scene.cats_update_action.defer.desc': 'Hides the update notification til the next Blender restart', + + +} \ No newline at end of file diff --git a/translations/ja_JP.py b/translations/ja_JP.py new file mode 100644 index 00000000..d36e259b --- /dev/null +++ b/translations/ja_JP.py @@ -0,0 +1,1170 @@ +dictionary = { + # Class.label / Class.desc (tooltip) + # Class.property + + # Main file + 'Main.error.restartAdmin': '\n\nFaulty CATS installation found!' + '\nTo fix this restart Blender as admin! ' + '\n', + 'Main.error.deleteFollowing': ' ' \ + ' '\ + '\n\nFaulty CATS installation found!' \ + '\nTo fix this delete the following files and folders inside your addons folder:' \ + '\n', + 'Main.error.installViaPreferences': '\n\nFaulty CATS installation found!' + '\nPlease install CATS via User Preferences and restart Blender!' + '\n', + 'Main.error.restartAndEnable': '\n\nFaulty CATS installation was found and fixed!' + '\nPlease restart Blender and enable CATS again!' + '\n', + 'Main.error.unsupportedVersion': '\n\nBlender versions older than 2.79 are not supported by Cats. ' + '\nPlease use Blender 2.79 or later.' + '\n', + 'Main.error.beta2.80': '\n\nYou are still on the beta version of Blender 2.80!' + '\nPlease update to the release version of Blender 2.80.' + '\n', + 'Main.error.restartAndEnable_alt': '\n\nPlease restart Blender and enable CATS again!' + '\n', + + # UI Main + 'ToolPanel.label': 'Cats ブレンダープラグイン', + 'ToolPanel.category': 'CATS', + + # UI Armature + 'ArmaturePanel.label': 'モデル', + 'ArmaturePanel.warn.oldBlender1': '古いブレンダーバージョンが検出されました!', + 'ArmaturePanel.warn.oldBlender2': '一部の機能は動作しない可能性があります!', + 'ArmaturePanel.warn.oldBlender3': 'ブレンダー2.79以上にアップデートしてください!', + 'ArmaturePanel.warn.noDict1': '辞書が見つかりません!', + 'ArmaturePanel.warn.noDict2': '翻訳は機能しますが、最適化されません。', + 'ArmaturePanel.warn.noDict3': 'これを修正するためにCatsを再インストールします。', + 'ArmaturePanel.ImportAnyModel.label': 'モデルのインポート', + 'ModelSettings.label': 'モデル設定の修正', + 'ModelSettings.warn.fbtFix1': 'フル ボディトラッキングの修正', + 'ModelSettings.warn.fbtFix2': 'もはやVrChatのために必要とされていません。', + 'ModelSettings.warn.fbtFix3': 'モデル オプションで引き続き使用できます。.', + + # UI Manual + 'ManualPanel.label': 'モデル オプション', + 'ManualPanel.separateBy': '別に:', + 'ManualPanel.SeparateByMaterials.label': '材料', + 'ManualPanel.SeparateByLooseParts.label': '緩い部品', + 'ManualPanel.SeparateByShapekeys.label': '図形', + 'ManualPanel.joinMeshes': 'メッシュに参加する:', + 'ManualPanel.JoinMeshes.label': 'すべて', + 'ManualPanel.JoinMeshesSelected.label': '選択', + 'ManualPanel.mergeWeights': 'ウェイトをマージする:', + 'ManualPanel.MergeWeights.label': '親へ', + 'ManualPanel.MergeWeightsToActive.label': 'アクティブに', + 'ManualPanel.translate': '翻訳する:', + 'ManualPanel.TranslateAllButton.label': 'すべて', + 'ManualPanel.TranslateShapekeyButton.label': 'シェイプキー', + 'ManualPanel.TranslateObjectsButton.label': 'オブジェクト', + 'ManualPanel.TranslateBonesButton.label': '骨', + 'ManualPanel.TranslateMaterialsButton.label': '材料', + 'ManualPanel.delete': '削除:', + 'ManualPanel.RemoveZeroWeightBones.label': 'ゼロウェイト ボーン', + 'ManualPanel.RemoveConstraints': '制約', + 'ManualPanel.RemoveZeroWeightGroups': 'ゼロウェイト頂点グループ', + 'ManualPanel.normals': '法線:', + 'ManualPanel.RecalculateNormals.label': '再計算', + 'ManualPanel.FlipNormals.label': 'フリップ', + 'ManualPanel.fbtFix': 'フル ボディ トラッキングの修正:', + 'ManualPanel.FixFBTButton.label': '追加', + 'ManualPanel.RemoveFBTButton.label': '削除', + + # UI Custom + 'CustomPanel.label': 'カスタム モデルの作成', + 'CustomPanel.CustomModelTutorialButton': '使用方法', + 'CustomPanel.mergeArmatures': 'マージアーマチュア:', + 'CustomPanel.warn.twoArmatures': '2つのアーマチュアが必要です!', + 'CustomPanel.mergeInto': 'ベース', + 'CustomPanel.toMerge': 'マージするには', + 'CustomPanel.attachToBone': '添付する', + 'CustomPanel.armaturesCanMerge': 'アーマチュアは自動的にマージすることができます!', + 'CustomPanel.attachMesh1': 'アーマチュアにメッシュをアタッチ:', + 'CustomPanel.attachMesh2': 'メッシュ', + 'CustomPanel.warn.noArmOrMesh1': 'アーマチュアとメッシュが必要です!', + 'CustomPanel.warn.noArmOrMesh2': 'メッシュに親が存在しないか確認します。', + + # UI Decimation + 'DecimationPanel.label': 'デシメーション', + 'DecimationPanel.decimationMode': 'デシメーションモード:', + 'DecimationPanel.safeModeDesc': ' まともな結果 - シェイプキーの損失はありません', + 'DecimationPanel.halfModeDesc': ' 良好な結果 - 最小のシェープキー損失', + 'DecimationPanel_fullModeDesc': ' 最良の結果 - フルシェイプキー損失', + 'DecimationPanel.customSeparateMaterials': '材料別に分離して開始:', + 'DecimationPanel.SeparateByMaterials.label': '材料別に分離', + 'DecimationPanel.customJoinMeshes': 'メッシュに参加して停止:', + 'DecimationPanel.customWhitelist': 'ホワイトリストに登録:', + 'DecimationPanel.warn.noShapekeySelected': 'シェイプ キーが選択されていません', + 'DecimationPanel.warn.noDecimation': 'すべてのメッシュが選択されます。これはデシメーションなしに等しい.', + 'DecimationPanel.warn.noMeshSelected': 'メッシュが選択されていません', + 'DecimationPanel.warn.emptyList': '両方のリストが空で、これは完全なデシメーションに等しい!', + 'DecimationPanel.warn.correctWhitelist': '両方のホワイトリストはデシメーション中に考慮されます', + + # UI Eye tracking + 'EyeTrackingPanel.label': 'アイトラッキング', + 'EyeTrackingPanel.error.noMesh': 'メッシュが見つかりません!', + 'EyeTrackingPanel.error.noArm': 'モデルが見つかりません!', + 'EyeTrackingPanel.error.wrongNameArm1': 'アイトラッキングが機能するには', + 'EyeTrackingPanel.error.wrongNameArm2': ' アーマチュアに\'Armature\'という名前を付ける必要があります。', + 'EyeTrackingPanel.error.wrongNameArm3': ' (現在は \'', + 'EyeTrackingPanel.error.wrongNameBody1': 'アイトラッキングが機能するには', + 'EyeTrackingPanel.error.wrongNameBody2': ' 目を含むメッシュに\'Body\'という名前を付ける必要があります!', + 'EyeTrackingPanel.error.wrongNameBody3': ' (現在は \'', + 'EyeTrackingPanel.warn.assignEyes1': '\'左目\'と\'右目\' を割り当てることを忘れないでください。', + 'EyeTrackingPanel.warn.assignEyes2': ' ユニティの目に', + + # UI Visemes + 'VisemePanel.label': 'バイセム', + 'VisemePanel.error.noMesh': 'メッシュが見つかりません!', + + # UI Bone_root + 'BoneRootPanel.label': 'ボーンの子育て', + + # UI Optimization + 'OptimizePanel.label': '最適化', + 'OptimizePanel.atlasDesc': 'アトラスジェネレータを大幅に改良。', + 'OptimizePanel.atlasAuthor': 'shotaryiaによって作られた', + 'OptimizePanel.matCombDisabled1': 'マテリアル コンバイナーが有効になっていません!', + 'OptimizePanel.matCombDisabled2': 'ユーザー設定で有効にする:', + 'OptimizePanel.matCombOutdated1': 'マテリアル コンバイナーが古い!', + 'OptimizePanel.matCombOutdated2': '最新バージョンにアップデートしてください.', + 'OptimizePanel.matCombOutdated3': '\'更新\' パネルを使用して更新します。', + 'OptimizePanel.matCombOutdated4': '\'MatCombiner\' タブの {location}', + 'OptimizePanel.matCombOutdated5_2.79': '左側にあります。。', + 'OptimizePanel.matCombOutdated5_2.8': '右側にあります。。', + 'OptimizePanel.matCombOutdated6': 'または手動でダウンロードしてインストールする:', + 'OptimizePanel.matCombOutdated6_alt': '手動でダウンロードしてインストールする:', + 'OptimizePanel.matCombNotInstalled': 'マテリアル コンバイナーがインストールされていません!', + + # UI Copy protection + 'CopyProtectionPanel.label': 'コピープロテクション', + 'CopyProtectionPanel.desc1': 'Unity キャッシュリッピングからアバターを保護しようとします。', + 'CopyProtectionPanel.desc2': 'この保護は100%安全ではありません!', + 'CopyProtectionPanel.desc3': '使用前: ドキュメントを読んでください!', + + # UI Settings & Updates + 'UpdaterPanel.label': '設定と更新', + 'UpdaterPanel.name': '設定:', + 'UpdaterPanel.requireRestart1': '再起動が必要.', + 'UpdaterPanel.requireRestart2': '一部の変更では、ブレンダーの再起動が必要です.', + + # UI Supporter + 'SupporterPanel.label': '支持者', + 'SupporterPanel.desc': 'このプラグインが好きで、私たちをサポートしたいですか?', + 'SupporterPanel.thanks': '私たちの素晴らしいサポーターに感謝!<3', + 'SupporterPanel.missingName1': 'あなたの名前は見つかりませんか?', + 'SupporterPanel.missingName2': ' 私たちのDiscordで私達にお問い合わせください!', + + # UI Credits + 'CreditsPanel.label': 'クレジット', + 'CreditsPanel.desc1': 'Cats Blender Plugin (', + 'CreditsPanel.desc2': 'HotoxとGiveMeAllYourCatsによって作成', + 'CreditsPanel.desc3': '素晴らしいVRChatコミュニティのために <3', + 'CreditsPanel.desc4': '特別な感謝: ShotariyaとNeitri', + 'CreditsPanel.desc5': '助けが必要ですか、バグを見つけましたか?', + + # Tools Armature + 'FixArmature.label': 'Fix Model', + 'FixArmature.desc': 'Automatically:\n' \ + '- Reparents bones\n' \ + '- Removes unnecessary bones, objects, groups & constraints\n' \ + '- Translates and renames bones & objects\n' \ + '- Merges weight paints\n' \ + '- Corrects the hips\n' \ + '- Joins meshes\n' \ + '- Converts morphs into shapes\n' \ + '- Corrects shading', + 'FixArmature.error.noMesh': ['No mesh inside the armature found!', + 'If there are meshes outside of the armature,', + 'set the armature as the parent of the meshes.'], + # Format strings? vvvv t(str, fixed_uv_coords) -> The model was successfully fixed, but there were {} faulty UV + 'FixArmature.error.faultyUV1': 'The model was successfully fixed, but there were {uvcoord} faulty UV coordinates.', # 'The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', + 'FixArmature.error.faultyUV2': 'This could result in broken textures and you might have to fix them manually.', + 'FixArmature.error.faultyUV3': 'This issue is often caused by edits in PMX editor.', + 'FixArmature.fixedSuccess': 'Model successfully fixed.', + 'FixArmature.bonesNotFound': 'The following bones were not found:', + 'FixArmature.cantFix1': 'Looks like you found a model which Cats could not fix!', + 'FixArmature.cantFix2': 'If this is a non modified model we would love to make it compatible.', + 'FixArmature.cantFix3': 'Report it to us in the forum or in our discord, links can be found in the Credits panel.', + 'FixArmature.notParent': ' is not parented at all, this will cause problems!', + 'FixArmature.notParentTo1': ' is not parented to ', + 'FixArmature.notParentTo2': ', this will cause problems!', + + # Tools Armature Manual + 'StartPoseMode.label': 'Start Pose Mode', + 'StartPoseMode.desc': 'Starts the pose mode.\n' \ + 'This lets you test how your model will move', + + 'StopPoseMode.label': 'Stop Pose Mode', + 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', + + 'PoseToShape.label': 'Pose to Shape Key', + 'PoseToShape.desc': 'This saves your current pose as a new shape key.' \ + '\nThe new shape key will be at the bottom of your shape key list of the mesh', + + 'PoseNamePopup.label': 'Give this shapekey a name:', + 'PoseNamePopup.desc': 'Sets the shapekey name. Press anywhere outside to skip', + 'PoseNamePopup.success': 'Pose successfully saved as shape key.', + + 'PoseToRest.label': 'Apply as Rest Pose', + 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' \ + '\n' \ + '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' \ + '\nWARNING: This can have unwanted effects on shape keys, so be careful when modifying the head with this', + 'PoseToRest.success': 'Pose successfully applied as rest pose.', + + 'JoinMeshes.label': 'Join Meshes', + 'JoinMeshes.desc': 'Joins all meshes of this model together.' \ + '\nIt also:' \ + '\n - Reorders all shape keys correctly' \ + '\n - Applies all transforms' \ + '\n - Repairs broken armature modifiers' \ + '\n - Applies all decimation and mirror modifiers' \ + '\n - Merges UV maps correctly', + 'JoinMeshes.failure': 'Meshes could not be joined!', + 'JoinMeshes.success': 'Meshes joined.', + + 'JoinMeshesSelected.label': 'Join Selected Meshes', + 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' \ + '\nIt also:' \ + '\n - Reorders all shape keys correctly' \ + '\n - Applies all transforms' \ + '\n - Repairs broken armature modifiers' \ + '\n - Applies all decimation and mirror modifiers' \ + '\n - Merges UV maps correctly', + 'JoinMeshesSelected.error.noSelect': 'No meshes selected! Please select the meshes you want to join in the hierarchy!', + 'JoinMeshesSelected.error.cantJoin': 'Selected meshes could not be joined!', + 'JoinMeshesSelected.success': 'Selected meshes joined.', + + 'SeparateByMaterials.label': 'Separate by Materials', + 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' \ + '\n' \ + 'Warning: Never decimate something where you might need the shape keys later (face, mouth, eyes..)', + 'SeparateByMaterials.success': 'Successfully separated by materials.', + + 'SeparateByLooseParts.label': 'Separate by Loose Parts', + 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' \ + 'This acts like separating by materials but creates more meshes for more precision', + 'SeparateByLooseParts.success': 'Successfully separated by loose parts.', + + 'SeparateByShapekeys.label': 'Separate by Shape Keys', + 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' \ + '\ndepending on whether it is effected by a shape key or not.' \ + '\n' \ + '\nVery 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,' \ + '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ + '\n' \ + '\nUseful if you have the Copy Protection enabled on multiple selected parts of your model', + 'SeparateByCopyProtection.success': 'Successfully separated by shape keys.', + + 'SeparateByX.error.noMesh': 'No meshes found!', + 'SeparateByX.error.multipleMesh': 'Multiple meshes found!' \ + '\nPlease select the mesh you want to separate!', + 'SeparateByX.warn.noSeparation': 'No meshes had to be separated!', + + 'MergeWeights.label': 'Merge Weights to Parent', + 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' \ + '\n' \ + '\nOnly available in Edit or Pose Mode with bones selected', + 'MergeWeights.success': 'Deleted {number} bones and added their weights to their parents.', + + 'MergeWeightsToActive.label': 'Merge Weights to Active', + 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' \ + '\nThe active bone is the one you selected last.' \ + '\n' \ + '\nOnly available in Edit or Pose Mode with bones selected', + 'MergeWeightsToActive.success': 'Deleted {number} bones and added their weights to the active bone.', + + 'ApplyTransformations.label': 'Apply Transformations', + 'ApplyTransformations.desc': 'Applies the position, rotation and scale to the armature and it\'s meshes', + 'ApplyTransformations.success': 'Transformations applied.', + + 'ApplyAllTransformations.label': 'Apply All Transformations', + 'ApplyAllTransformations.desc': 'Applies the position, rotation and scale of all objects', + 'ApplyAllTransformations.success': 'Transformations applied.', + + 'RemoveZeroWeightBones.label': 'Remove Zero Weight Bones', + 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' \ + 'Don\'t use this if you plan to use \'Fix Model\'', + 'RemoveZeroWeightBones.success': 'Deleted {number} zero weight bones.', + + 'RemoveZeroWeightGroups.label': 'Remove Zero Weight Vertex Groups', + 'RemoveZeroWeightGroups.desc': 'Cleans up the vertex groups of all meshes, deleting all groups that don\'t directly affect any vertices', + 'RemoveZeroWeightGroups.success': 'Removed {number} zero weight vertex groups.', + + 'RemoveConstraints.label': 'Remove Bone Constraints', + 'RemoveConstraints.desc': 'Removes constrains between bones causing specific bone movement as these are not used by VRChat', + 'RemoveConstraints.success': 'Removed all bone constraints.', + + 'RecalculateNormals.label': 'Recalculate Normals', + 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' \ + 'Don\'t use this on good looking meshes as this can screw them up.\n' \ + 'Use this if there are random inverted or darker faces on the mesh', + 'RecalculateNormals.success': 'Recalculated all normals.', + + 'FlipNormals.label': 'Flip Normals', + 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' \ + 'Use this if all normals are inverted', + 'FlipNormals.success': 'Flipped all normals.', + + 'RemoveDoubles.label': 'Remove Doubles', + 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ + '\nThis is more safe than doing it manually:' \ + '\n - leaves shape keys completely untouched' \ + '\n - but removes less doubles overall', + 'RemoveDoubles.success': 'Removed {number} vertices.', + + 'RemoveDoublesNormal.label': 'Remove Doubles Normally', + 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ + '\nThis is exactly like doing it manually', + 'RemoveDoublesNormal.success': 'Removed {number} vertices.', + + 'FixVRMShapesButton.label': 'Fix Koikatsu Shapekeys', + 'FixVRMShapesButton.desc': 'Fixes the shapekeys of Koikatsu models', + 'FixVRMShapesButton.warn.notDetected': 'No shapekeys detected!', + 'FixVRMShapesButton.success': 'Fixed VRM shapekeys.', + + 'FixFBTButton.label': 'Fix Full Body Tracking', + 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' \ + '\n' \ + '\nApplies a general fix for Full Body Tracking.' \ + '\nIgnore the \"Spine length zero\" warning in Unity', + 'FixFBTButton.error.bonesNotFound': 'Required bones could not be found!' + '\nPlease make sure that your armature contains the following bones:' + '\n - Hips, Spine, Left leg, Right leg' + '\nExact names are required!', + 'FixFBTButton.error.alreadyApplied': 'Full Body Tracking Fix already applied!', + 'FixFBTButton.success': 'Successfully applied the Full Body Tracking fix.', + + 'RemoveFBTButton.label': 'Remove Full Body Tracking Fix', + 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' \ + '\n' \ + '\nRequires bones:' \ + '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2', + 'RemoveFBTButton.error.bonesNotFound': 'Required bones could not be found!' + '\nPlease make sure that your armature contains the following bones:' + '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2' + '\nExact names are required!', + 'RemoveFBTButton.error.notApplied': 'The Full Body Tracking Fix is not applied!', + 'RemoveFBTButton.success': 'Successfully removed the Full Body Tracking fix.', + + 'DuplicateBonesButton.label': 'Duplicate Bones', + 'DuplicateBonesButton.desc': 'Duplicates the selected bones including their weight and renames them to _L and _R', + 'DuplicateBonesButton.success': 'Successfully duplicated {number} bones.', + + # Tools Armature Custom + 'MergeArmature.label': 'Merge Armatures', + 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' \ + '\nYou should fix both armatures with Cats first.' \ + '\nOnly move the mesh of the merge armature to the desired position, the bones will be moved automatically', + 'MergeArmature.error.notFound': 'The armature "{name}" could not be found.', + 'MergeArmature.error.checkTransforms': [ 'Please make sure that the parent of the merge armature has the following transforms:', + ' - Location at 0', + ' - Rotation at 0', + ' - Scale at 1'], + 'MergeArmature.error.pleaseFix': [ 'Please use the "Fix Model" feature on the selected armatures first!', + 'Make sure to select the armature you want to fix above the "Fix Model" button!', + 'After that please only move the mesh (not the armature!) to the desired position.'], + 'MergeArmature.success': 'Armatures successfully joined.', + + 'AttachMesh.label': 'Attach Mesh', + 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' \ + '\n' \ + '\nINFO: The mesh will only be assigned to the selected bone.' \ + '\nE.g.: A jacket won\'t work, because it requires multiple bones', + 'AttachMesh.success': 'Mesh successfully attached to armature.', + + 'CustomModelTutorialButton.label': 'Go to Documentation', + 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) + 'CustomModelTutorialButton.success': 'Documentation', + + 'merge_armatures.error.transformReset': ['If you want to rotate the new part, only modify the mesh instead of the armature,', + 'or select "Apply Transforms"!', + '', + 'The transforms of the merge armature got reset and the mesh you have to modify got selected.', + 'Now place this selected mesh where and how you want it to be and then merge the armatures again.', + 'If you don\'t want that, undo this operation.'], + 'merge_armatures.error.pleaseUndo': ['Something went wrong! Please undo, check your selections and try again.'], + + # Tools Atlas + 'EnableSMC.label': 'Enable Material Combiner', + 'EnableSMC.desc': 'Enables Material Combiner', + 'EnableSMC.success': 'Enabled Material Combiner!', + + 'AtlasHelpButton.label': 'Generate Material List', + 'AtlasHelpButton.desc': 'Open useful Atlas Tips', + 'AtlasHelpButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin/#texture-atlas', + 'AtlasHelpButton.success': 'Atlas Help opened.', + + 'InstallShotariya.label': 'Error while loading Material Combiner:', + 'InstallShotariya.error.install1': 'Material Combiner is not installed!', + 'InstallShotariya.error.install2': 'The plugin \'Material Combiner\' by Shotariya is required for this function.', + 'InstallShotariya.error.install3': 'Please download and install it manually:', + 'InstallShotariya.error.enable1': 'Material Combiner is not enabled!', + 'InstallShotariya.error.enable2': 'The plugin \'Material Combiner\' by Shotariya is required for this function.', + 'InstallShotariya.error.enable3': 'Please enable it in your User Preferences.', + 'InstallShotariya.error.version1': 'Material Combiner is outdated!', + 'InstallShotariya.error.version2': 'The latest version is required for this function.', + 'InstallShotariya.error.version3': 'Please download and install it manually:', + + 'ShotariyaButton.label': 'Download Material Combiner', + 'ShotariyaButton.URL': 'https://vrcat.club/threads/material-combiner-blender-addon-1-1-3.2255/', + 'ShotariyaButton.success': 'Material Combiner link opened', + + # Tools Bonemerge + 'BoneMergeButton.label': 'Merge Bones', + 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' \ + 'This is useful to reduce the amount of bones used by Dynamic Bones.', + 'BoneMergeButton.success': 'Merged bones.', + + # Tools Common + 'ShowError.label': 'Report: Error', + + # Tools Copy protection + 'CopyProtectionEnable.label': 'Enable Protection', + 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' \ + '\nRead 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', + + # Tools Credits + 'ForumButton.label': 'Go to the Forums', + 'ForumButton.URL': 'https://vrcat.club/threads/cats-blender-plugin.6/', + 'ForumButton.success': 'Forum opened.', + + 'DiscordButton.label': 'Join our Discord', + 'DiscordButton.URL': 'https://discord.gg/f8yZGnv', + 'DiscordButton.success': 'Discord opened.', + + 'PatchnotesButton.label': 'Latest Patchnotes', + 'PatchnotesButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin/releases', + 'PatchnotesButton.success': 'Patchnotes opened.', + + # Tools Decimation + 'ScanButton.label': 'Scan for decimation models', + 'ScanButton.desc': 'Separates the mesh.', + + 'AddShapeButton.label': 'Add', + 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' \ + 'This means that every mesh containing that shape key will be not decimated.', + + 'AddMeshButton.label': 'Add', + 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' \ + 'This means that this mesh will be not decimated.', + + 'RemoveShapeButton.label': '', + 'RemoveShapeButton.desc': 'Removes the selected shape key from the whitelist.\n' \ + 'This means that this shape key is no longer decimation safe!', + + 'RemoveMeshButton.label': '', + 'RemoveMeshButton.desc': 'Removes the selected mesh from the whitelist.\n' \ + 'This means that this mesh will be decimated.', + + 'AutoDecimateButton.label': 'Quick Decimation', + 'AutoDecimateButton.desc': 'This will automatically decimate your model while preserving the shape keys.\n' \ + 'You should manually remove unimportant meshes first.', + 'AutoDecimateButton.error.noMesh': 'No meshes found!', + + 'decimate.cantDecimateWithSettings': 'This model can not be decimated to {number} tris with the specified settings.', + 'decimate.safeTryOptions': 'Try to use Custom, Half or Full Decimation.', + 'decimate.halfTryOptions': 'Try to use Custom or Full Decimation.', + 'decimate.customTryOptions': 'Select fewer shape keys and/or meshes or use Full Decimation.', + 'decimate.disableFingersOrIncrease': 'Disable \'Save Fingers\' or increase the Tris Count.', + 'decimate.disableFingers': 'or disable \'Save Fingers\'.', # This comes after one of the previous xTryOptions + 'decimate.noDecimationNeeded': 'The model already has less than {number} tris. Nothing had to be decimated.', + 'decimate.cantDecimate1': 'The model could not be decimated to {number} tris.', + 'decimate.cantDecimate2': 'It got decimated as much as possible within the limits.', + + # Tools Eyetracking + 'CreateEyesButton.label': 'Create Eye Tracking', + 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' \ + 'You should do decimation before this operation.\n' \ + 'Test the resulting eye movement in the \'Testing\' tab.', + 'CreateEyesButton.error.noShapeSelected': 'You have no shape keys selected.' + '\nPlease choose a mesh containing shape keys or check "Disable Eye Blinking".', + 'CreateEyesButton.error.missingBone': 'The bone "{bone}" does not exist.', + 'CreateEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' + '\nThis might be because you selected the wrong mesh or the wrong bone.' + '\nAlso make sure that the selected eye bones actually move the eyes in pose mode.', + 'CreateEyesButton.error.dontUse': 'Please do not use "{eyeName}" as the input bone.' + '\nIf you are sure that you want to use that bone please rename it to "{eyeNameShort}".', + 'CreateEyesButton.error.hierarchy': 'Eye tracking will not work unless the bone hierarchy is exactly as following: Hips > Spine > Chest > Neck > Head' + '\nFurthermore the mesh containing the eyes has to be called "Body" and the armature "Armature".', + 'CreateEyesButton.success': 'Created eye tracking!', + + 'StartTestingButton.label': 'Start Eye Testing', + 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' \ + 'Don\'t forget to stop the Testing process afterwards.\n' \ + 'Bones "LeftEye" and "RightEye" are required.', + + 'StopTestingButton.label': 'Stop Eye Testing', + 'StopTestingButton.desc': 'Stops the testing process.', + 'StopTestingButton.error.tryAgain': 'Something went wrong. Please try eye testing again.', + + 'ResetRotationButton.label': 'Reset Rotation', + 'ResetRotationButton.desc': 'This resets the eye positions.', + + 'AdjustEyesButton.label': 'Set Range', + 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' \ + 'This gets saved', + 'AdjustEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' + '\nThis might be because you selected the wrong mesh or the wrong bone.' + '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.', + + 'StartIrisHeightButton.label': 'Start Iris Height Adjustment', + 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' \ + 'Use this to fix clipping of the iris into the eye ball.\n' \ + 'This gets saved.', + + 'TestBlinking.label': 'Test', + 'TestBlinking.desc': 'This lets you see how eye blinking will look in-game.', + + 'TestLowerlid.label': 'Test', + 'TestLowerlid.desc': 'This lets you see how lowerlids will look in-game.', + + 'ResetBlinkTest.label': 'Reset Shapes', + 'ResetBlinkTest.desc': 'This resets the blink testing.', + + # Tools Importer + 'ImportAnyModel.label': 'Import Any Model', + 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' \ + '\n' \ + '\nSupported types:' \ + '\n- MMD: .pmx/.pmd' \ + '\n- XNALara: .xps/.mesh/.ascii' \ + '\n- Source: .smd/.qc' \ + '\n- VRM: .vrm' \ + '\n- FBX .fbx ' \ + '\n- DAE: .dae ' \ + '\n- ZIP: .zip', + 'ImportAnyModel.desc2.8': 'Import a model of any supported type.' \ + '\n' \ + '\nSupported types:' \ + '\n- MMD: .pmx/.pmd' \ + '\n- XNALara: .xps/.mesh/.ascii' \ + '\n- Source: .smd/.qc/.vta/.dmx' \ + '\n- VRM: .vrm' \ + '\n- FBX: .fbx' \ + '\n- DAE: .dae ' \ + '\n- ZIP: .zip', + 'ImportAnyModel.importantInfo.label': 'IMPORTANT INFO (hover here)', + 'ImportAnyModel.importantInfo.desc': 'If you want to modify the import settings, use the button next to the Import button.\n\n', + 'ImportAnyModel.error.emptyZip': 'The selected zip file contains no importable models.', + 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' \ + '\nPlease use a tool such as the "Autodesk FBX Converter" to make it compatible.', + + 'ZipPopup.label': 'Zip Model Selection:', + 'ZipPopup.desc': 'Shows the models contained in the zip files', + 'ZipPopup.selectModel1': 'Select which model you want to import', + 'ZipPopup.selectModel2': 'Then confirm with OK', + + 'get_zip_content.choose': 'Import model "{model}" from the zip "{zipName}?"', + + 'ModelsPopup.label': 'Select which you want to import:', + 'ModelsPopup.desc': 'Show individual import options', + + 'ImportMMD.label': 'MMD', + 'ImportMMD.desc': 'Import a MMD model (.pmx/pmd)', + + 'ImportXPS.label': 'XNALara', + 'ImportXPS.desc': 'Import a XNALara model (.xps/.mesh/.ascii)', + + 'ImportSource.label': 'Source', + 'ImportSource.desc': 'Import a Source model (.smd/.qc/.vta/.dmx)', + + 'ImportFBX.label': 'FBX', + 'ImportFBX.desc': 'Import a FBX model (.fbx)', + + 'ImportVRM.label': 'VRM', + 'ImportVRM.desc': 'Import a VRM model (.vrm)', + + 'InstallXPS.label': 'XPS Tools is not installed or enabled!', + + 'InstallSource.label': 'Source Tools is not installed or enabled!', + + 'InstallVRM.label': 'VRM Importer is not installed or enabled!', + + 'InstallX.pleaseInstall1': 'If it is not enabled please enable it in your User Preferences.', + 'InstallX.pleaseInstall2': 'If it is not installed please download and install it manually.', + 'InstallX.pleaseInstall3': 'Make sure to install the version for Blender {blenderVersion}', + 'InstallX.pleaseInstallTesting': 'Currently you have to select \'Testing\' in the addons settings.', + + 'EnableMMD.label': 'Mmd_tools is not enabled!', + 'EnableMMD.required1': 'The plugin "mmd_tools" is required for this function.', + 'EnableMMD.required2': 'Please restart Blender.', + + 'XpsToolsButton.label': 'Download XPS Tools', + 'XpsToolsButton.URL': 'https://github.com/johnzero7/XNALaraMesh', + 'XpsToolsButton.success': 'XPS Tools link opened', + + 'SourceToolsButton.label': 'Download Source Tools', + 'SourceToolsButton.URL': 'https://github.com/Artfunkel/BlenderSourceTools', + 'SourceToolsButton.success': 'Source Tools link opened', + + 'VrmToolsButton.label': 'Download VRM Importer', + 'VrmToolsButton.URL_2.79': 'https://github.com/iCyP/VRM_IMPORTER_for_Blender2_79', + 'VrmToolsButton.URL_2.8': 'https://github.com/saturday06/VRM_IMPORTER_for_Blender2_8', + 'VrmToolsButton.success': 'VRM Importer link opened', + + 'ExportModel.label': 'Export Model', + 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' \ + '\n' \ + 'Automatically sets the optimal export settings', + 'ExportModel.error.notEnabled': 'FBX Exporter not enabled! Please enable it in your User Preferences.', + + 'ErrorDisplay.label': 'Warning:', + 'ErrorDisplay.polygons1': 'Too many polygons!', + 'ErrorDisplay.polygons2': 'You have {number} tris in this model, but you shouldn\'t have more than 70,000!', + 'ErrorDisplay.polygons3': 'You should decimate before you export this model.', + 'ErrorDisplay.materials1': 'Model not optimized!', + 'ErrorDisplay.materials2': 'This model has {number} materials!', + 'ErrorDisplay.materials3': 'You should try to have a maximum of 4 materials on your model.', + 'ErrorDisplay.materials4': 'Creating a texture atlas in CATS is very easy, so please make use of it.', + 'ErrorDisplay.meshes1': 'Meshes not joined!', + 'ErrorDisplay.meshes2': 'This model has {number} meshes!', + 'ErrorDisplay.meshes3': 'It is not very optimized and might cause lag for you and others!', + 'ErrorDisplay.meshes3_alt': "It is extremely unoptimized and will cause laugh for you and others!", + 'ErrorDisplay.meshes4': 'You should always join your meshes, it\'s very easy:', + 'ErrorDisplay.JoinMeshes.label': 'Join Meshes', + 'ErrorDisplay.brokenShapekeys1': 'Broken shapekeys!', + 'ErrorDisplay.brokenShapekeys2': 'This model has {number} broken shapekey(s):', + 'ErrorDisplay.brokenShapekeys3': 'You will not be able to upload this model until you fix these shapekeys.', + 'ErrorDisplay.brokenShapekeys4': 'Either delete or repair them before export.', + 'ErrorDisplay.textures1': 'No textures found!', + 'ErrorDisplay.textures2': 'This model has no textures assigned but you have \'Embed Textures\' enabled.', + 'ErrorDisplay.textures3': 'Therefore, no textures will be embedded into the FBX.', + 'ErrorDisplay.textures4': 'This is not an issue, but you will have to import the textures manually into Unity.', + 'ErrorDisplay.eyes1': 'Eyes not named \'Body\'!', + 'ErrorDisplay.eyes2': 'The mesh \'{name}\' has Eye Tracking shapekeys but is not named \'Body\'.', + 'ErrorDisplay.eyes2_alt': 'Multiple meshes have Eye Tracking shapekeys but are not named \'Body\'.', + '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', + + # Tools Material + 'OneTexPerMatButton.label': 'One Material Texture', + 'OneTexPerMatButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.', + + 'OneTexPerMatOnlyButton.label': 'One Material Texture', + 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' \ + '\nAlso removes the textures from the material instead of disabling it.' \ + '\nThis 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' \ + '\nand 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.\n' \ + 'Your avatar should visibly look the same after this operation.\n' \ + 'This is a very important step for optimizing your avatar.\n' \ + 'If you have problems with this, please tell us!\n', + 'CombineMaterialsButton.error.noChanges': 'No materials combined.', + 'CombineMaterialsButton.success': 'Combined {number} materials!', + + 'ConvertAllToPngButton.label': 'Convert Textures to PNG', + 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' \ + '\nThis helps with transparency and compatibility issues.' \ + '\n\nThe converted image files will be saved next to the old ones', + 'ConvertAllToPngButton.success': 'Converted {number} to PNG files.', + + # Tools Root bone + 'RootButton.label': 'Parent Bones', + 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' \ + 'Very useful for Dynamic Bones.', + 'RootButton.success': 'Bones parented!', + + 'RefreshRootButton.label': 'Refresh List', + 'RefreshRootButton.desc': 'This will clear the group bones list cache and rebuild it, useful if bones have changed or your model.', + 'RefreshRootButton.success': 'Root bones refreshed, check the root bones list again.', + + # Tools Settings + 'RevertChangesButton.label': 'Revert Settings', + 'RevertChangesButton.desc': 'Revert the changes back to how they were on Blender start-up.', + 'RevertChangesButton.success': 'Settings reverted.', + + 'ResetGoogleDictButton.label': 'Clear Local Google Translations', + 'ResetGoogleDictButton.desc': 'Deletes all currently saved Google Translations. You can\'t undo this', + 'ResetGoogleDictButton.resetInfo': 'Local Google Dictionary cleared!', + + 'DebugTranslations.label': 'Debug Google Translations', # DEV ONLY + 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' \ + '\nThis button is only visible in the cats development version', # DEV ONLY + 'DebugTranslations.error': 'Errors found, response printed!!', # DEV ONLY + 'DebugTranslations.success': 'No issues with Google Translations found, response printed!', # DEV ONLY + + # Tools Shapekey + 'ShapeKeyApplier.label': 'Apply Selected Shapekey to Basis', + 'ShapeKeyApplier.desc': 'Applies the selected shape key to the new Basis at it\'s current strength and creates a reverted shape key from the selected one', + 'ShapeKeyApplier.error.revertCustomBasis': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', + 'Start with the shape key called "{name}".', + '', + 'If you didn\'t change the shape key order, you can revert the shape keys from top to bottom.'], + 'ShapeKeyApplier.error.revertCustomBasis.scale': 7.3, + 'ShapeKeyApplier.error.revert': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', + 'Start with the reverted shape key that uses the relative key called "Basis".', + '', + "If you didn't change the shape key order, you can revert the shape keys from top to bottom."], + 'ShapeKeyApplier.error.revert.scale': 7.3, + 'ShapeKeyApplier.successRemoved': 'Successfully removed shapekey "{name}" from the Basis.', + 'ShapeKeyApplier.successSet': 'Successfully set shapekey "{name}" as the new Basis.', + + 'addToShapekeyMenu.ShapeKeyApplier.label': 'Apply Selected Shapekey to Basis', + + # Tools Supporter + 'PatreonButton.label': 'Become a Patron', + 'PatreonButton.URL': 'https://www.patreon.com/catsblenderplugin', + 'PatreonButton.success': 'Patreon page opened.', + + 'ReloadButton.label': 'Reload List', + 'ReloadButton.desc': 'Reloads the supporter list', + + 'DynamicPatronButton.label': 'Supporter Name', + 'DynamicPatronButton.desc': 'This is an awesome supporter', + + 'register_dynamic_buttons.desc': '{name} is an awesome supporter', + + # Tools Translate + 'TranslateShapekeyButton.label': 'Translate Shape Keys', + 'TranslateShapekeyButton.desc': 'Translates all shape keys using the internal dictionary and Google Translate', + 'TranslateShapekeyButton.success': 'Translated {number} shape keys.', + + 'TranslateBonesButton.label': 'Translate Bones', + 'TranslateBonesButton.desc': 'Translates all bones using the internal dictionary and Google Translate', + 'TranslateBonesButton.success': 'Translated {number} bones.', + + 'TranslateObjectsButton.label': 'Translate Meshes & Objects', + 'TranslateObjectsButton.desc': 'Translates all meshes and objects using the internal dictionary and Google Translate', + 'TranslateObjectsButton.success': 'Translated {number} meshes and objects.', + + 'TranslateMaterialsButton.label': 'Translate Materials', + 'TranslateMaterialsButton.desc': 'Translates all materials using the internal dictionary and Google Translate', + 'TranslateMaterialsButton.success': 'Translated {number} materials.', + + 'TranslateTexturesButton.label': 'Translate Textures', + 'TranslateTexturesButton.desc': 'Translates all textures using the internal dictionary and Google Translate', + 'TranslateTexturesButton.success_alt': 'Translated all textures', + 'TranslateTexturesButton.error.noInternet': 'Could not connect to Google. Please check your internet connection.', + 'TranslateTexturesButton.success': 'Translated {number} textures', + + 'TranslateAllButton.label': 'Translate Everything', + 'TranslateAllButton.desc': 'Translates everything using the internal dictionary and Google Translate', + 'TranslateAllButton.success': 'Translated everything.', + + 'TranslateX.error.wrongVersion': 'You need Blender 2.79 or higher for this function.', + + 'update_dictionary.error.cantConnect': 'Could not connect to Google. Some parts could not be translated.', + 'update_dictionary.error.temporaryBan': 'It looks like you got banned from Google Translate temporarily!', + 'update_dictionary.error.catsTranslated': '\nCats translated what it could with the local dictionary, but you will have to try again later for the Google translations.', + 'update_dictionary.error.cantAccess': 'Cats was not able to access Google Translate!', + 'update_dictionary.error.errorMsg': 'You got an error message from Google Translate!', + 'update_dictionary.error.apiChanged': 'Could not get translations from Google Translate!' + '\nThis means that Google changed their API and translations will no longer work until this is fixed.' + '\nPlease translate manually or wait for an CATS update.' + '\nFor updates and dicussions please join our Discord. The link can be found in the Credits panel down below.', + + # Tools Viseme + 'AutoVisemeButton.label': 'Create Visemes', + 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' \ + 'It will generate 15 shape keys from the 3 shape keys you specify', + 'AutoVisemeButton.error.noShapekeys': 'This mesh has no shapekeys!', + 'AutoVisemeButton.error.selectShapekeys': 'Please select the correct mouth shapekeys instead of "Basis"!', + 'AutoVisemeButton.success': 'Created mouth visemes!', + + # Extentions + 'Scene.armature.label': 'Armature', + 'Scene.armature.desc': 'Select the armature which will be used by Cats', + + 'Scene.zip_content.label': 'Zip Content', + 'Scene.zip_content.desc': 'Select the model you want to import', + + 'Scene.keep_upper_chest.label': 'Keep Upper Chest', + 'Scene.keep_upper_chest.desc': 'VRChat now partially supports the Upper Chest bone, so deleting it is no longer necessary.' + '\n\nWARNING: Currently this breaks Eye Tracking, so don\'t check this if you want Eye Tracking', + + 'Scene.combine_mats.label': 'Combine Same Materials', + 'Scene.combine_mats.desc': 'Combines similar materials into one, reducing draw calls.\n\n' + 'Your avatar should visibly look the same after this operation.\n' + 'This is a very important step for optimizing your avatar.\n' + 'If you have problems with this, uncheck this option and tell us!\n', + + 'Scene.remove_zero_weight.label': 'Remove Zero Weight Bones', + 'Scene.remove_zero_weight.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' + '\nUncheck this if bones or vertex groups that you want to keep got deleted', + + 'Scene.keep_end_bones.label': 'Keep End Bones', + 'Scene.keep_end_bones.desc': 'Saves end bones from deletion.' + '\n\nThis can improve skirt movement for dynamic bones, but increases the bone count.' + '\nThis can also fix issues with crumbled finger bones in Unity.' + '\nMake sure to always uncheck "Add Leaf Bones" when exporting or use the CATS export button', + + 'Scene.keep_twist_bones.label': 'Keep Twist Bones', + 'Scene.keep_twist_bones.desc': 'This will keep any bone with "Twist" in the name.' + '\nSo if there are certain bones that you want to keep, you can add "Twist" to them and they won\'t get deleted.' + '\n\nVRChat can now make use of twist bones, so you can use this option to keep them', + + 'Scene.fix_twist_bones.label': 'Fix MMD Twist Bones', + 'Scene.fix_twist_bones.desc': 'This will make MMD arm twist bones usable in VRChat.' + '\nWIll only work if the twist bones are properly named.' + '\nRequired names:' + '\n - ArmTwist[1-3]_[L/R]' + '\n - HandTwist[1-3]_[L/R]' + '\n\nYou don\'t need to enable "Keep Twist Bones" for this to work', + + 'Scene.join_meshes.label': 'Join Meshes', + 'Scene.join_meshes.desc': 'Joins all meshes of this model together.' + '\nIt also:' + '\n - Applies all transformations' + '\n - Repairs broken armature modifiers' + '\n - Applies all decimation and mirror modifiers' + '\n - Merges UV maps correctly' + '\n' + '\nINFO: You should always join your meshes', + + 'Scene.connect_bones.label': 'Connect Bones', + 'Scene.connect_bones.desc': 'This connects all bones to their child bone if they have exactly one child bone.\n' + 'This will not change how the bones function in any way, it just improves the aesthetic of the armature', + + 'Scene.fix_materials.label': 'Fix Materials', + 'Scene.fix_materials.desc': 'This will apply some VRChat related fixes to materials', + + 'Scene.remove_rigidbodies_joints.label': 'Remove Rigidbodies and Joints', + 'Scene.remove_rigidbodies_joints.desc': 'Rigidbodies and joints are used by MMD software to simulate physics.' + '\nThey are completely useless for VRChat, so removing them is recommended for VRChat users!', + + 'Scene.use_google_only.label': 'Use Old Translations (not recommended)', + 'Scene.use_google_only.desc': 'Ignores the internal dictionary and only uses the Google Translator for shape key translations.' + '\n' + '\nThis will result in slower translation speed and worse translations, but the translations will be like in CATS version 0.9.0 and older.' + '\nOnly use this if you have animations which rely on the old translations and you don\'t want to convert them to the new ones', + + 'Scene.show_more_options.label': 'Show More Options', + 'Scene.show_more_options.desc': 'Shows more model options', + + 'Scene.merge_mode.label': 'Merge Mode', + 'Scene.merge_mode.desc': 'Mode', + 'Scene.merge_mode.armature.label': 'Merge Armatures', + 'Scene.merge_mode.armature.desc': 'Here you can merge two armatures together.', + 'Scene.merge_mode.mesh.label': 'Attach Mesh', + 'Scene.merge_mode.mesh.desc': 'Here you can attach a mesh to an armature.', + + 'Scene.merge_armature_into.label': 'Base Armature', + 'Scene.merge_armature_into.desc': 'Select the armature into which the other armature will be merged\n', + + 'Scene.merge_armature.label': 'Merge Armature', + 'Scene.merge_armature.desc': 'Select the armature which will be merged into the selected armature above\n', + + 'Scene.attach_to_bone.label': 'Attach to Bone', + 'Scene.attach_to_bone.desc': 'Select the bone to which the armature will be attached to\n', + + 'Scene.attach_mesh.label': 'Attach Mesh', + 'Scene.attach_mesh.desc': 'Select the mesh which will be attached to the selected bone in the selected armature\n', + + 'Scene.merge_same_bones.label': 'Merge All Bones', + 'Scene.merge_same_bones.desc': 'Merges all bones together that have the same name instead of only the base bones (Hips, Spine, etc).' + '\nYou will have to make sure that all the bones you want to merge have the same name.' + '\n' + '\nIf this is checked, you won\'t need to fix the model with CATS beforehand but it is still advised to do so.' + '\nIf this is unchecked, CATS will only merge the base bones (Hips, Spine, etc).' + '\n' + '\nThis can have unintended side effects, so check your model afterwards!' + '\n', + + 'Scene.apply_transforms.label': 'Apply Transforms', + 'Scene.apply_transforms.desc': 'Check this if both armatures and meshes are already at their correct positions.' + '\nThis will cause them to stay exactly where they are when merging', + + 'Scene.merge_armatures_join_meshes.label': 'Join Meshes', + 'Scene.merge_armatures_join_meshes.desc': 'This will join all meshes.' + '\nNot checking this will always apply transforms', + + 'Scene.merge_armatures_remove_zero_weight_bones.label': 'Remove Zero Weight Bones', + 'Scene.merge_armatures_remove_zero_weight_bones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' + '\nUncheck this if bones or vertex groups that you want to keep got deleted', + # Decimation + 'Scene.decimation_mode.label': 'Decimation Mode', + 'Scene.decimation_mode.desc': 'Decimation Mode', + 'Scene.decimation_mode.safe.label': 'Safe', + 'Scene.decimation_mode.safe.desc': 'Decent results - no shape key loss\n' + '\n' + 'This will only decimate meshes with no shape keys.\n' + 'The results are decent and you won\'t lose any shape keys.\n' + 'Eye Tracking and Lip Syncing will be fully preserved.', + 'Scene.decimation_mode.half.label': 'Half', + 'Scene.decimation_mode.half.desc': 'Good results - minimal shape key loss\n' + '\n' + 'This will only decimate meshes with less than 4 shape keys as those are often not used.\n' + 'The results are better but you will lose the shape keys in some meshes.\n' + 'Eye Tracking and Lip Syncing should still work.', + 'Scene.decimation_mode.full.label': 'Full', + 'Scene.decimation_mode.full.desc': 'Best results - full shape key loss\n' + '\n' + 'This will decimate your whole model deleting all shape keys in the process.\n' + 'This will give the best results but you will lose the ability to add blinking and Lip Syncing.\n' + 'Eye Tracking will still work if you disable Eye Blinking.', + 'Scene.decimation_mode.custom.label': 'Custom', + 'Scene.decimation_mode.custom.desc': 'Custom results - custom shape key loss\n' + '\n' + 'This will let you choose which meshes and shape keys should not be decimated.\n', + + 'Scene.selection_mode.label': 'Selection Mode', + 'Scene.selection_mode.desc': 'Selection Mode', + 'Scene.selection_mode.shapekeys.label': 'Shape Keys', + 'Scene.selection_mode.shapekeys.desc': 'Select all the shape keys you want to preserve here.', + 'Scene.selection_mode.meshes.label': 'Meshes', + 'Scene.selection_mode.meshes.desc': 'Select all the meshes you don\'t want to decimate here.', + + 'Scene.add_shape_key.label': 'Shape', + 'Scene.add_shape_key.desc': 'The shape key you want to keep', + + 'Scene.add_mesh.label': 'Mesh', + 'Scene.add_mesh.desc': 'The mesh you want to leave untouched by the decimation', + + 'Scene.decimate_fingers.label': 'Save Fingers', + 'Scene.decimate_fingers.desc': 'Check this if you don\'t want to decimate your fingers!\n' + 'Results will be worse but there will be no issues with finger movement.\n' + 'This is probably only useful if you have a VR headset.\n' + '\n' + 'This operation requires the finger bones to be named specifically:\n' + 'Thumb(0-2)_(L/R)\n' + 'IndexFinger(1-3)_(L/R)\n' + 'MiddleFinger(1-3)_(L/R)\n' + 'RingFinger(1-3)_(L/R)\n' + 'LittleFinger(1-3)_(L/R)', + + 'Scene.decimate_hands.label': 'Save Hands', + 'Scene.decimate_hands.desc': 'Check this if you don\'t want to decimate your full hands!\n' + 'Results will be worse but there will be no issues with hand movement.\n' + 'This is probably only useful if you have a VR headset.\n' + '\n' + 'This operation requires the finger and hand bones to be named specifically:\n' + 'Left/Right wrist\n' + 'Thumb(0-2)_(L/R)\n' + 'IndexFinger(1-3)_(L/R)\n' + 'MiddleFinger(1-3)_(L/R)\n' + 'RingFinger(1-3)_(L/R)\n' + 'LittleFinger(1-3)_(L/R)', + + 'Scene.decimation_remove_doubles.label': 'Remove Doubles', + 'Scene.decimation_remove_doubles.desc': 'Uncheck this if you got issues with with this checked', + + 'Scene.max_tris.label': 'Tris', + 'Scene.max_tris.desc': 'The target amount of tris after decimation', + # Eye Tracking + 'Scene.eye_mode.label': 'Eye Mode', + 'Scene.eye_mode.desc': 'Mode', + 'Scene.eye_mode.creation.label': 'Creation', + 'Scene.eye_mode.creation.desc': 'Here you can create eye tracking.', + 'Scene.eye_mode.testing.label': 'Testing', + 'Scene.eye_mode.testing.desc': 'Here you can test how eye tracking will look in-game.', + + 'Scene.mesh_name_eye.label': 'Mesh', + 'Scene.mesh_name_eye.desc': 'The mesh with the eyes vertex groups', + + 'Scene.head.label': 'Head', + 'Scene.head.desc': 'The head bone containing the eye bones', + + 'Scene.eye_left.label': 'Left Eye', + 'Scene.eye_left.desc': 'The models left eye bone', + + 'Scene.eye_right.label': 'Right Eye', + 'Scene.eye_right.desc': 'The models right eye bone', + + 'Scene.wink_left.label': 'Blink Left', + 'Scene.wink_left.desc': 'The shape key containing a blink with the left eye', + + 'Scene.wink_right.label': 'Blink Right', + 'Scene.wink_right.desc': 'The shape key containing a blink with the right eye', + + 'Scene.lowerlid_left.label': 'Lowerlid Left', + 'Scene.lowerlid_left.desc': 'The shape key containing a slightly raised left lower lid.\n' + 'Can be set to "Basis" to disable lower lid movement', + + 'Scene.lowerlid_right.label': 'Lowerlid Right', + 'Scene.lowerlid_right.desc': 'The shape key containing a slightly raised right lower lid.\n' + 'Can be set to "Basis" to disable lower lid movement', + + 'Scene.disable_eye_movement.label': 'Disable Eye Movement', + 'Scene.disable_eye_movement.desc': 'IMPORTANT: Do your decimation first if you check this!\n' + '\n' + 'Disables eye movement. Useful if you only want blinking.\n' + 'This creates eye bones with no movement bound to them.\n' + 'You still have to assign "LeftEye" and "RightEye" to the eyes in Unity', + + 'Scene.disable_eye_blinking.label': 'Disable Eye Blinking', + 'Scene.disable_eye_blinking.desc': 'Disables eye blinking. Useful if you only want eye movement.\n' + 'This will create the necessary shape keys but leaves them empty', + + 'Scene.eye_distance.label': 'Eye Movement Range', + 'Scene.eye_distance.desc': 'Higher = more eye movement\n' + 'Lower = less eye movement\n' + 'Warning: Too little or too much range can glitch the eyes.\n' + 'Test your results in the "Eye Testing"-Tab!\n', + + 'Scene.eye_rotation_x.label': 'Up - Down', + 'Scene.eye_rotation_x.desc': 'Rotate the eye bones on the vertical axis', + + 'Scene.eye_rotation_y.label': 'Left - Right', + 'Scene.eye_rotation_y.desc': 'Rotate the eye bones on the horizontal axis.' + '\nThis is from your own point of view', + + 'Scene.iris_height.label': 'Iris Height', + 'Scene.iris_height.desc': 'Moves the iris away from the eye ball', + + 'Scene.eye_blink_shape.label': 'Blink Strength', + 'Scene.eye_blink_shape.desc': 'Test the blinking of the eye', + + 'Scene.eye_lowerlid_shape.label': 'Lowerlid Strength', + 'Scene.eye_lowerlid_shape.desc': 'Test the lowerlid blinking of the eye', + + 'Scene.mesh_name_viseme.label': 'Mesh', + 'Scene.mesh_name_viseme.desc': 'The mesh with the mouth shape keys', + # Visemes + 'Scene.mouth_a.label': 'Viseme AA', + 'Scene.mouth_a.desc': 'Shape key containing mouth movement that looks like someone is saying "aa".\nDo not put empty shape keys like "Basis" in here', + + 'Scene.mouth_o.label': 'Viseme OH', + 'Scene.mouth_o.desc': 'Shape key containing mouth movement that looks like someone is saying "oh".\nDo not put empty shape keys like "Basis" in here', + + 'Scene.mouth_ch.label': 'Viseme CH', + 'Scene.mouth_ch.desc': 'Shape key containing mouth movement that looks like someone is saying "ch". Opened lips and clenched teeth.\nDo not put empty shape keys like "Basis" in here', + + 'Scene.shape_intensity.label': 'Shape Key Mix Intensity', + 'Scene.shape_intensity.desc': 'Controls the strength in the creation of the shape keys. Lower for less mouth movement strength', + # Bone Parenting + 'Scene.root_bone.label': 'To Parent', + 'Scene.root_bone.desc': 'List of bones that look like they could be parented together to a root bone', + # Optimize + 'Scene.optimize_mode.label': 'Optimize Mode', + 'Scene.optimize_mode.desc': 'Mode', + 'Scene.optimize_mode.atlas.label': 'Atlas', + 'Scene.optimize_mode.atlas.desc': 'Allows you to make a texture atlas.', + 'Scene.optimize_mode.material.label': 'Material', + 'Scene.optimize_mode.material.desc': 'Some various options on material manipulation.', + 'Scene.optimize_mode.bonemerging.label': 'Bone Merging', + 'Scene.optimize_mode.bonemerging.desc': 'Allows child bones to be merged into their parents.', + # Bone Merging + 'Scene.merge_ratio.label': 'Merge Ratio', + 'Scene.merge_ratio.desc': 'Higher = more bones will be merged\n' + 'Lower = less bones will be merged\n', + + 'Scene.merge_mesh.label': 'Mesh', + 'Scene.merge_mesh.desc': 'The mesh with the bones vertex groups', + + 'Scene.merge_bone.label': 'To Merge', + 'Scene.merge_bone.desc': 'List of bones that look like they could be merged together to reduce overall bones', + + 'Scene.embed_textures.label': 'Embed Textures on Export', + 'Scene.embed_textures.desc': 'Enable this to embed the texture files into the FBX file upon export.' + '\nUnity will automatically extract these textures and put them into a separate folder.' + '\nThis might not work for everyone and it increases the file size of the exported FBX file', + + 'Scene.use_custom_mmd_tools.label': 'Use Custom mmd_tools', + 'Scene.use_custom_mmd_tools.desc': 'Enable this to use your own version of mmd_tools. This will disable the internal cats mmd_tools', + + 'Scene.debug_translations.label': 'Debug Google Translations', + 'Scene.debug_translations.desc': 'Tests the Google Translations and prints the Google response in case of error', + + # Updater + 'CheckForUpdateButton.label': 'Check now for Update', + 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', + + 'UpdateToLatestButton.label': 'Update Now', + 'UpdateToLatestButton.desc': 'Update CATS to the latest version', + + 'UpdateToSelectedButton.label': 'Update to Selected version', + 'UpdateToSelectedButton.desc': 'Update CATS to the selected version', + + 'UpdateToDevButton.label': 'Update to Development version', + 'UpdateToDevButton.desc': 'Update CATS to the Development version', + + 'RemindMeLaterButton.label': 'Remind me later', + 'RemindMeLaterButton.desc': 'This hides the update notification \'til the next Blender restart', + 'RemindMeLaterButton.success': 'You will be reminded later', + + 'IgnoreThisVersionButton.label': 'Ignore this version', + 'IgnoreThisVersionButton.desc': 'This ignores this version. You will be reminded again when the next version releases', + 'IgnoreThisVersionButton.success': 'Version {name} will be ignored.', + + 'ShowPatchnotesPanel.label': 'Patchnotes', + 'ShowPatchnotesPanel.desc': 'Shows the patchnotes of the selected version', + 'ShowPatchnotesPanel.releaseDate': 'Released: {date}', + + 'ConfirmUpdatePanel.label': 'Confirm Update', + 'ConfirmUpdatePanel.desc': 'This shows you a panel in which you have to confirm your update choice', + 'ConfirmUpdatePanel.warn.dev1': 'Warning:', + 'ConfirmUpdatePanel.warn.dev2': ' The development version of CATS if the place where', + 'ConfirmUpdatePanel.warn.dev3': ' we test new features and bug fixes.', + 'ConfirmUpdatePanel.warn.dev4': ' This version might be very unstable and some features', + 'ConfirmUpdatePanel.warn.dev5': ' might not work correctly.', + 'ConfirmUpdatePanel.ShowPatchnotesPanel.label': 'Show Patchnotes', + 'ConfirmUpdatePanel.updateNow': 'Update now:', + + 'UpdateCompletePanel.label': 'Installation Report', + 'UpdateCompletePanel.desc': 'The update is now complete', + 'UpdateCompletePanel.success1': 'CATS was successfully updated.', + 'UpdateCompletePanel.success2': 'Restart Blender to complete the update.', + 'UpdateCompletePanel.failure1': 'Update failed.', + 'UpdateCompletePanel.failure2': 'See Updater Panel for more info.', + + 'UpdateNotificationPopup.label': 'Update available', + 'UpdateNotificationPopup.desc': 'This shows you that an update is available', + 'UpdateNotificationPopup.newUpdate': 'CATS v{name} available!', + 'UpdateNotificationPopup.ShowPatchnotesPanel.label': 'Show Patchnotes', + + 'check_for_update.cantCheck': 'Could not check for updates, try again later', + + 'download_file.cantConnect': 'Could not connect to Github', + 'download_file.cantFindZip': 'Could not find the downloaded zip', + 'download_file.cantFindCATS': 'Could not find CATS in the downloaded zip', + + 'draw_update_notification_panel.success': 'Restart Blender to complete update!', + 'draw_update_notification_panel.newUpdate': 'CATS v{name} available!', + 'draw_update_notification_panel.UpdateToLatestButton.label': 'Update Now', + 'draw_update_notification_panel.RemindMeLaterButton.label': 'Remind me later', + 'draw_update_notification_panel.IgnoreThisVersionButton.label': 'Ignore this version', + + 'draw_updater_panel.updateLabel': 'Updates:', + 'draw_updater_panel.updateLabel_alt': 'CATS Updater:', + 'draw_updater_panel.success': 'Restart Blender to complete update!', + 'draw_updater_panel.CheckForUpdateButton.label': 'Checking..', + 'draw_updater_panel.UpdateToLatestButton.label': 'Update now to {name}', + 'draw_updater_panel.CheckForUpdateButton.label_alt': 'Check now for Update', + 'draw_updater_panel.UpdateToLatestButton.label_alt': 'Up to Date!', + 'draw_updater_panel.UpdateToSelectedButton.label': 'Install version:', + 'draw_updater_panel.UpdateToDevButton.label': 'Install Development Version', + 'draw_updater_panel.currentVersion': 'Current version: {name}', + + 'bpy.types.Scene.cats_updater_version_list.label': 'Version', + 'bpy.types.Scene.cats_updater_version_list.desc': 'Select the version you want to install\n', + + 'bpy.types.Scene.cats_update_action.label': 'Choose action', + 'bpy.types.Scene.cats_update_action.desc': 'Action', + 'bpy.types.Scene.cats_update_action.update.label': 'Update Now', + 'bpy.types.Scene.cats_update_action.update.desc': 'Updates now to the latest version', + 'bpy.types.Scene.cats_update_action.ignore.label': 'Ignore this version', + 'bpy.types.Scene.cats_update_action.ignore.desc': 'This ignores this version. You will be reminded again when the next version releases', + 'bpy.types.Scene.cats_update_action.defer.label': 'Remind me later', + 'bpy.types.Scene.cats_update_action.defer.desc': 'Hides the update notification til the next Blender restart', + + +} \ No newline at end of file diff --git a/ui/armature.py b/ui/armature.py index e7d500bc..3ad77377 100644 --- a/ui/armature.py +++ b/ui/armature.py @@ -10,12 +10,13 @@ from ..tools import eyetracking as Eyetracking from ..tools import armature_manual as Armature_manual from ..tools.register import register_wrap +from ..translations import t @register_wrap class ArmaturePanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_armature_v3' - bl_label = 'Model' + bl_label = t('ArmaturePanel.label') def draw(self, context): layout = self.layout @@ -30,13 +31,13 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Old Blender version detected!', icon='ERROR') + row.label(text=t('ArmaturePanel.warn.oldBlender1'), icon='ERROR') row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Some features might not work!', icon='BLANK1') + row.label(text=t('ArmaturePanel.warn.oldBlender2'), icon='BLANK1') row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Please update to Blender 2.79!', icon='BLANK1') + row.label(text=t('ArmaturePanel.warn.oldBlender3'), icon='BLANK1') col.separator() col.separator() @@ -55,13 +56,13 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Dictionary not found!', icon='INFO') + row.label(text=t('ArmaturePanel.warn.noDict1'), icon='INFO') row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Translations will work, but are not optimized.', icon='BLANK1') + row.label(text=t('ArmaturePanel.warn.noDict2'), icon='BLANK1') row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Reinstall Cats to fix this.', icon='BLANK1') + row.label(text=t('ArmaturePanel.warn.noDict3'), icon='BLANK1') col.separator() col.separator() @@ -104,7 +105,7 @@ def draw(self, context): split = col.row(align=True) row = split.row(align=True) row.scale_y = 1.7 - row.operator(Importer.ImportAnyModel.bl_idname, text='Import Model', icon='ARMATURE_DATA') + row.operator(Importer.ImportAnyModel.bl_idname, text=t('ArmaturePanel.ImportAnyModel.label'), icon='ARMATURE_DATA') row = split.row(align=True) row.alignment = 'RIGHT' row.scale_y = 1.7 @@ -114,7 +115,7 @@ def draw(self, context): split = col.row(align=True) row = split.row(align=True) row.scale_y = 1.4 - row.operator(Importer.ImportAnyModel.bl_idname, text='Import Model', icon='ARMATURE_DATA') + row.operator(Importer.ImportAnyModel.bl_idname, text=t('ArmaturePanel.ImportAnyModel.label'), icon='ARMATURE_DATA') row.operator(Importer.ExportModel.bl_idname, icon='ARMATURE_DATA').action = 'CHECK' row = split.row(align=True) row.scale_y = 1.4 @@ -184,7 +185,7 @@ def draw(self, context): @register_wrap class ModelSettings(bpy.types.Operator): bl_idname = "cats_armature.settings" - bl_label = "Fix Model Settings" + bl_label = t('ModelSettings.label') def execute(self, context): return {'FINISHED'} @@ -227,11 +228,11 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.7 - row.label(text='The Full Body Tracking Fix', icon='INFO') + row.label(text=t('ModelSettings.warn.fbtFix1'), icon='INFO') row = col.row(align=True) row.scale_y = 0.7 - row.label(text='is no longer needed for VrChat.', icon_value=Supporter.preview_collections["custom_icons"]["empty"].icon_id) + row.label(text=t('ModelSettings.warn.fbtFix2'), icon_value=Supporter.preview_collections["custom_icons"]["empty"].icon_id) row = col.row(align=True) row.scale_y = 0.7 - row.label(text='It\'s still available in Model Options.', icon_value=Supporter.preview_collections["custom_icons"]["empty"].icon_id) + row.label(text=t('ModelSettings.warn.fbtFix3'), icon_value=Supporter.preview_collections["custom_icons"]["empty"].icon_id) col.separator() diff --git a/ui/bone_root.py b/ui/bone_root.py index 96c63f41..754d9f65 100644 --- a/ui/bone_root.py +++ b/ui/bone_root.py @@ -3,12 +3,13 @@ from .main import ToolPanel from ..tools import rootbone as Rootbone from ..tools.register import register_wrap +from ..translations import t @register_wrap class BoneRootPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_boneroot_v3' - bl_label = 'Bone Parenting' + bl_label = t('BoneRootPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): diff --git a/ui/copy_protection.py b/ui/copy_protection.py index 984409ff..beefa6d0 100644 --- a/ui/copy_protection.py +++ b/ui/copy_protection.py @@ -6,12 +6,13 @@ from ..tools import copy_protection as Copy_protection from ..tools import importer as Importer from ..tools.register import register_wrap +from ..translations import t # @register_wrap class CopyProtectionPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_copyprotection_v3' - bl_label = 'Copy Protection' + bl_label = t('CopyProtectionPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -22,13 +23,13 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Tries to protect your avatar from Unity cache ripping.') + row.label(text=t('CopyProtectionPanel.desc1')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text='This protection is not 100% safe!') + row.label(text=t('CopyProtectionPanel.desc2')) col.separator() row = col.row(align=True) - row.label(text='Before use: Read the documentation!') + row.label(text=t('CopyProtectionPanel.desc3')) row = col.row(align=True) row.operator(Copy_protection.ProtectionTutorialButton.bl_idname, icon='FORWARD') col.separator() diff --git a/ui/credits.py b/ui/credits.py index d40a9a4a..564b2a2d 100644 --- a/ui/credits.py +++ b/ui/credits.py @@ -5,12 +5,13 @@ from ..tools import supporter as Supporter from ..tools import credits as Credits from ..tools.register import register_wrap +from ..translations import t @register_wrap class CreditsPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_credits_v3' - bl_label = 'Credits' + bl_label = t('CreditsPanel.label') def draw(self, context): layout = self.layout @@ -18,19 +19,19 @@ def draw(self, context): col = box.column(align=True) row = col.row(align=True) - row.label(text='Cats Blender Plugin (' + globs.version_str + ')', icon_value=Supporter.preview_collections["custom_icons"]["cats1"].icon_id) + row.label(text=t('CreditsPanel.desc1') + globs.version_str + ')', icon_value=Supporter.preview_collections["custom_icons"]["cats1"].icon_id) col.separator() row = col.row(align=True) - row.label(text='Created by Hotox and GiveMeAllYourCats') + row.label(text=t('CreditsPanel.desc2')) row = col.row(align=True) - row.label(text='For the awesome VRChat community <3') + row.label(text=t('CreditsPanel.desc3')) row.scale_y = 0.5 col.separator() row = col.row(align=True) - row.label(text='Special thanks to: Shotariya and Neitri') + row.label(text=t('CreditsPanel.desc4')) col.separator() row = col.row(align=True) - row.label(text='Do you need help or found a bug?') + row.label(text=t('CreditsPanel.desc5')) row = col.row(align=True) row.scale_y = 1.4 diff --git a/ui/custom.py b/ui/custom.py index 5b46e0cd..ccc6c814 100644 --- a/ui/custom.py +++ b/ui/custom.py @@ -7,12 +7,13 @@ from ..tools import armature_bones as Armature_bones from ..tools import armature_custom as Armature_custom from ..tools.register import register_wrap +from ..translations import t @register_wrap class CustomPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_custom_v3' - bl_label = 'Custom Model Creation' + bl_label = t('CustomPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -21,7 +22,7 @@ def draw(self, context): col = box.column(align=True) row = col.row(align=True) - row.operator(Armature_custom.CustomModelTutorialButton.bl_idname, text='How to Use', icon='FORWARD') + row.operator(Armature_custom.CustomModelTutorialButton.bl_idname, text=t('CustomPanel.CustomModelTutorialButton'), icon='FORWARD') col.separator() row = col.row(align=True) @@ -32,12 +33,12 @@ def draw(self, context): if context.scene.merge_mode == 'ARMATURE': row = col.row(align=True) row.scale_y = 1.05 - row.label(text='Merge Armatures:') + row.label(text=t('CustomPanel.mergeArmatures')) if len(Common.get_armature_objects()) <= 1: row = col.row(align=True) row.scale_y = 1.05 - col.label(text='Two armatures are required!', icon='INFO') + col.label(text=t('CustomPanel.warn.twoArmatures'), icon='INFO') return row = col.row(align=True) @@ -59,10 +60,10 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 1.05 - row.prop(context.scene, 'merge_armature_into', text='Base', icon=globs.ICON_MOD_ARMATURE) + row.prop(context.scene, 'merge_armature_into', text=t('CustomPanel.mergeInto'), icon=globs.ICON_MOD_ARMATURE) row = col.row(align=True) row.scale_y = 1.05 - row.prop(context.scene, 'merge_armature', text='To Merge', icon_value=Supporter.preview_collections["custom_icons"]["UP_ARROW"].icon_id) + row.prop(context.scene, 'merge_armature', text=t('CustomPanel.toMerge'), icon_value=Supporter.preview_collections["custom_icons"]["UP_ARROW"].icon_id) if not context.scene.merge_same_bones: found = False @@ -76,11 +77,11 @@ def draw(self, context): if not found: row = col.row(align=True) row.scale_y = 1.05 - row.prop(context.scene, 'attach_to_bone', text='Attach to', icon='BONE_DATA') + row.prop(context.scene, 'attach_to_bone', text=t('CustomPanel.attachToBone'), icon='BONE_DATA') else: row = col.row(align=True) row.scale_y = 1.05 - row.label(text='Armatures can be merged automatically!') + row.label(text=t('CustomPanel.armaturesCanMerge')) row = col.row(align=True) row.scale_y = 1.2 @@ -90,15 +91,15 @@ def draw(self, context): else: row = col.row(align=True) row.scale_y = 1.05 - row.label(text='Attach Mesh to Armature:') + row.label(text=t('CustomPanel.attachMesh1')) if len(Common.get_armature_objects()) == 0 or len(Common.get_meshes_objects(mode=1, check=False)) == 0: row = col.row(align=True) row.scale_y = 1.05 - col.label(text='An armature and a mesh are required!', icon='INFO') + col.label(text=t('CustomPanel.warn.noArmOrMesh1'), icon='INFO') row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Make sure that the mesh has no parent.', icon='BLANK1') + row.label(text=t('CustomPanel.warn.noArmOrMesh2'), icon='BLANK1') return row = col.row(align=True) @@ -107,14 +108,14 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 1.05 - row.prop(context.scene, 'merge_armature_into', text='Base', icon=globs.ICON_MOD_ARMATURE) + row.prop(context.scene, 'merge_armature_into', text=t('CustomPanel.mergeInto'), icon=globs.ICON_MOD_ARMATURE) row = col.row(align=True) row.scale_y = 1.05 - row.prop(context.scene, 'attach_mesh', text='Mesh', icon_value=Supporter.preview_collections["custom_icons"]["UP_ARROW"].icon_id) + row.prop(context.scene, 'attach_mesh', text=t('CustomPanel.attachMesh2'), icon_value=Supporter.preview_collections["custom_icons"]["UP_ARROW"].icon_id) row = col.row(align=True) row.scale_y = 1.05 - row.prop(context.scene, 'attach_to_bone', text='Attach to', icon='BONE_DATA') + row.prop(context.scene, 'attach_to_bone', text=t('CustomPanel.attachToBone'), icon='BONE_DATA') row = col.row(align=True) row.scale_y = 1.2 diff --git a/ui/decimation.py b/ui/decimation.py index 882343f8..f5ec3991 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -7,12 +7,13 @@ from ..tools import decimation as Decimation from ..tools import armature_manual as Armature_manual from ..tools.register import register_wrap +from ..translations import t @register_wrap class DecimationPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_decimation_v3' - bl_label = 'Decimation' + bl_label = t('DecimationPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -27,31 +28,31 @@ def draw(self, context): # row.label(text='It works but it might not look good. Test for yourself.') # col.separator() row = col.row(align=True) - row.label(text='Decimation Mode:') + 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=' Decent results - No shape key loss') + row.label(text=t('DecimationPanel.safeModeDesc')) elif context.scene.decimation_mode == 'HALF': - row.label(text=' Good results - Minimal shape key loss') + row.label(text=t('DecimationPanel.halfModeDesc')) elif context.scene.decimation_mode == 'FULL': - row.label(text=' Best results - Full shape key loss') + 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='Start by Separating by Materials:') + row.label(text=t('DecimationPanel.customSeparateMaterials')) row = col.row(align=True) row.scale_y = 1.2 - row.operator(Armature_manual.SeparateByMaterials.bl_idname, text='Separate by Materials', icon='PLAY') + row.operator(Armature_manual.SeparateByMaterials.bl_idname, text=t('DecimationPanel.SeparateByMaterials.label'), icon='PLAY') return else: row = col.row(align=True) - row.label(text='Stop by Joining Meshes:') + 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') @@ -59,7 +60,7 @@ def draw(self, context): col.separator() col.separator() row = col.row(align=True) - row.label(text='Whitelisted:') + row.label(text=t('DecimationPanel.customWhitelist')) row = col.row(align=True) row.prop(context.scene, 'selection_mode', expand=True) col.separator() @@ -75,7 +76,7 @@ def draw(self, context): col = box2.column(align=True) if len(Decimation.ignore_shapes) == 0: - col.label(text='No shape key selected') + col.label(text=t('DecimationPanel.warn.noShapekeySelected')) for shape in Decimation.ignore_shapes: row = layout_split(col, factor=0.8, align=False) @@ -90,13 +91,13 @@ def draw(self, context): if context.scene.add_mesh == '': row = col.row(align=True) - col.label(text='Every mesh is selected. This equals no Decimation.', icon='ERROR') + 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='No mesh selected') + col.label(text=t('DecimationPanel.warn.noMeshSelected')) for mesh in Decimation.ignore_meshes: row = layout_split(col, factor=0.8, align=False) @@ -107,10 +108,10 @@ def draw(self, context): col = box.column(align=True) if len(Decimation.ignore_shapes) == 0 and len(Decimation.ignore_meshes) == 0: - col.label(text='Both lists are empty, this equals Full Decimation!', icon='ERROR') + col.label(text=t('DecimationPanel.warn.emptyList'), icon='ERROR') row = col.row(align=True) else: - col.label(text='Both whitelists are considered during decimation', icon='INFO') + col.label(text=t('DecimationPanel.warn.correctWhitelist'), icon='INFO') row = col.row(align=True) # # row = col.row(align=True) diff --git a/ui/eye_tracking.py b/ui/eye_tracking.py index 38eeeb31..6a57f7f1 100644 --- a/ui/eye_tracking.py +++ b/ui/eye_tracking.py @@ -5,12 +5,12 @@ from ..tools import common as Common from ..tools import eyetracking as Eyetracking from ..tools.register import register_wrap - +from ..translations import t @register_wrap class EyeTrackingPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_eye_v3' - bl_label = 'Eye Tracking' + bl_label = t('EyeTrackingPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -28,7 +28,7 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 1.1 - row.label(text='No meshes found!', icon='ERROR') + row.label(text=t('EyeTrackingPanel.error.noMesh'), icon='ERROR') elif mesh_count > 1: col.separator() row = col.row(align=True) @@ -95,7 +95,7 @@ def draw(self, context): else: armature = Common.get_armature() if not armature: - box.label(text='No model found!', icon='ERROR') + box.label(text=t('EyeTrackingPanel.error.noArm'), icon='ERROR') return if bpy.context.active_object is not None and bpy.context.active_object.mode != 'POSE': @@ -146,12 +146,12 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.3 - row.label(text="Your armature has to be named 'Armature'", icon='ERROR') + row.label(text=t('EyeTrackingPanel.error.wrongNameArm1'), icon='ERROR') row = col.row(align=True) - row.label(text=" for Eye Tracking to work!") + row.label(text=t('EyeTrackingPanel.error.wrongNameArm2')) row = col.row(align=True) row.scale_y = 0.3 - row.label(text=" (currently '" + armature.name + "')") + row.label(text=t('EyeTrackingPanel.error.wrongNameArm3') + armature.name + "')") if context.scene.mesh_name_eye != 'Body': col.separator() @@ -159,21 +159,21 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.3 - row.label(text="The mesh containing the eyes has to be", icon='ERROR') + row.label(text=t('EyeTrackingPanel.error.wrongNameBody1'), icon='ERROR') row = col.row(align=True) - row.label(text=" named 'Body' for Eye Tracking to work!") + row.label(text=t('EyeTrackingPanel.error.wrongNameBody2')) row = col.row(align=True) row.scale_y = 0.3 - row.label(text=" (currently '" + context.scene.mesh_name_eye + "')") + row.label(text=t('EyeTrackingPanel.error.wrongNameBody3') + context.scene.mesh_name_eye + "')") col.separator() col.separator() col.separator() row = col.row(align=True) row.scale_y = 0.3 - row.label(text="Don't forget to assign 'LeftEye' and 'RightEye'", icon='INFO') + row.label(text=t('EyeTrackingPanel.warn.assignEyes1'), icon='INFO') row = col.row(align=True) - row.label(text=" to the eyes in Unity!") + row.label(text=t('EyeTrackingPanel.warn.assignEyes2')) row = col.row(align=True) row.scale_y = 1.5 diff --git a/ui/main.py b/ui/main.py index fb53bedb..01ec939a 100644 --- a/ui/main.py +++ b/ui/main.py @@ -1,10 +1,10 @@ from ..tools.common import version_2_79_or_older - +from ..translations import t class ToolPanel(object): - bl_label = 'Cats Blender Plugin' + bl_label = t('ToolPanel.label') bl_idname = '3D_VIEW_TS_vrc' - bl_category = 'CATS' + bl_category = t('ToolPanel.category') bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' if version_2_79_or_older() else 'UI' diff --git a/ui/manual.py b/ui/manual.py index bb9f64d3..adb9ffc4 100644 --- a/ui/manual.py +++ b/ui/manual.py @@ -7,12 +7,13 @@ from ..tools import translate as Translate from ..tools import armature_manual as Armature_manual from ..tools.register import register_wrap +from ..translations import t @register_wrap class ManualPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_manual_v3' - bl_label = 'Model Options' + bl_label = t('ManualPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -21,30 +22,29 @@ def draw(self, context): button_height = 1 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.operator(Armature_manual.SeparateByMaterials.bl_idname, text='Materials') - row.operator(Armature_manual.SeparateByLooseParts.bl_idname, text='Loose Parts') - row.operator(Armature_manual.SeparateByShapekeys.bl_idname, text='Shapes') + row.operator(Armature_manual.SeparateByMaterials.bl_idname, text=t('ManualPanel.SeparateByMaterials.label')) + row.operator(Armature_manual.SeparateByLooseParts.bl_idname, text=t('ManualPanel.SeparateByLooseParts.label')) + row.operator(Armature_manual.SeparateByShapekeys.bl_idname, text=t('ManualPanel.SeparateByShapekeys.label')) row = layout_split(col, factor=0.4, align=True) row.scale_y = button_height - row.label(text="Join Meshes:", icon='AUTOMERGE_ON') - row.operator(Armature_manual.JoinMeshes.bl_idname, text='All') - row.operator(Armature_manual.JoinMeshesSelected.bl_idname, text='Selected') + row.label(text=t('ManualPanel.joinMeshes'), icon='AUTOMERGE_ON') + row.operator(Armature_manual.JoinMeshes.bl_idname, text=t('ManualPanel.JoinMeshes.label')) + row.operator(Armature_manual.JoinMeshesSelected.bl_idname, text=t('ManualPanel.JoinMeshesSelected.label')) row = layout_split(col, factor=0.4, align=True) row.scale_y = button_height - row.label(text="Merge Weights:", icon='BONE_DATA') - row.operator(Armature_manual.MergeWeights.bl_idname, text='To Parents') - row.operator(Armature_manual.MergeWeightsToActive.bl_idname, text='To Active') + row.label(text=t('ManualPanel.mergeWeights'), icon='BONE_DATA') + row.operator(Armature_manual.MergeWeights.bl_idname, text=t('ManualPanel.MergeWeights.label')) + row.operator(Armature_manual.MergeWeightsToActive.bl_idname, text=t('ManualPanel.MergeWeightsToActive.label')) # Translate col.separator() row = col.row(align=True) - row.label(text="Translate:", icon='FILE_REFRESH') + row.label(text=t('ManualPanel.translate'), icon='FILE_REFRESH') row = col.row(align=True) row.scale_y = button_height @@ -54,15 +54,15 @@ def draw(self, context): row = split.row(align=True) row.scale_y = 2 - row.operator(Translate.TranslateAllButton.bl_idname, text='All', icon=globs.ICON_ALL) + row.operator(Translate.TranslateAllButton.bl_idname, text=t('ManualPanel.TranslateAllButton.label'), icon=globs.ICON_ALL) row = split.column(align=True) - row.operator(Translate.TranslateShapekeyButton.bl_idname, text='Shape Keys', icon='SHAPEKEY_DATA') - row.operator(Translate.TranslateObjectsButton.bl_idname, text='Objects', icon='MESH_DATA') + row.operator(Translate.TranslateShapekeyButton.bl_idname, text=t('ManualPanel.TranslateShapekeyButton.label'), icon='SHAPEKEY_DATA') + row.operator(Translate.TranslateObjectsButton.bl_idname, text=t('ManualPanel.TranslateObjectsButton.label'), icon='MESH_DATA') row = split.column(align=True) - row.operator(Translate.TranslateBonesButton.bl_idname, text='Bones', icon='BONE_DATA') - row.operator(Translate.TranslateMaterialsButton.bl_idname, text='Materials', icon='MATERIAL') + row.operator(Translate.TranslateBonesButton.bl_idname, text=t('ManualPanel.TranslateBonesButton.label'), icon='BONE_DATA') + row.operator(Translate.TranslateMaterialsButton.bl_idname, text=t('ManualPanel.TranslateMaterialsButton.label'), icon='MATERIAL') col.separator() row = col.row(align=True) @@ -76,15 +76,15 @@ def draw(self, context): col.separator() row = layout_split(col, factor=0.24, align=True) row.scale_y = button_height - row.label(text="Delete:", icon='X') + row.label(text=t('ManualPanel.delete'), icon='X') row2 = layout_split(row, factor=0.61, align=True) - row2.operator(Armature_manual.RemoveZeroWeightBones.bl_idname, text='Zero Weight Bones') - row2.operator(Armature_manual.RemoveConstraints.bl_idname, text='Constraints') + row2.operator(Armature_manual.RemoveZeroWeightBones.bl_idname, text=t('ManualPanel.RemoveZeroWeightBones.label')) + row2.operator(Armature_manual.RemoveConstraints.bl_idname, text=t('ManualPanel.RemoveConstraints')) row = layout_split(col, factor=0.24, align=True) row.scale_y = button_height row.label(text="") - row.operator(Armature_manual.RemoveZeroWeightGroups.bl_idname, text='Zero Weight Vertex Groups') + row.operator(Armature_manual.RemoveZeroWeightGroups.bl_idname, text=t('ManualPanel.RemoveZeroWeightGroups')) col.separator() row = col.row(align=True) @@ -94,9 +94,9 @@ def draw(self, context): col.separator() row = layout_split(col, factor=0.27, align=True) row.scale_y = button_height - row.label(text="Normals:", icon='SNAP_NORMAL') - row.operator(Armature_manual.RecalculateNormals.bl_idname, text='Recalculate') - row.operator(Armature_manual.FlipNormals.bl_idname, text='Flip') + row.label(text=t('ManualPanel.normals'), icon='SNAP_NORMAL') + row.operator(Armature_manual.RecalculateNormals.bl_idname, text=t('ManualPanel.RecalculateNormals.label')) + row.operator(Armature_manual.FlipNormals.bl_idname, text=t('ManualPanel.FlipNormals.label')) row = col.row(align=True) row.scale_y = 1 @@ -114,10 +114,10 @@ def draw(self, context): col.separator() row = layout_split(col, factor=0.6, align=True) row.scale_y = button_height - row.label(text="Full Body Tracking Fix:", icon='ARMATURE_DATA') + row.label(text=t('ManualPanel.fbtFix'), icon='ARMATURE_DATA') row2 = layout_split(row, factor=0.35, align=True) - row2.operator(Armature_manual.FixFBTButton.bl_idname, text='Add') - row2.operator(Armature_manual.RemoveFBTButton.bl_idname, text="Remove") + row2.operator(Armature_manual.FixFBTButton.bl_idname, text=t('ManualPanel.FixFBTButton.label')) + row2.operator(Armature_manual.RemoveFBTButton.bl_idname, text=t('ManualPanel.RemoveFBTButton.label')) row = col.row(align=True) row.scale_y = button_height diff --git a/ui/optimization.py b/ui/optimization.py index f8936f22..fa94c6b9 100644 --- a/ui/optimization.py +++ b/ui/optimization.py @@ -13,6 +13,7 @@ from ..tools.common import version_2_79_or_older from ..tools.register import register_wrap +from ..translations import t draw_smc_ui = None old_smc_version = False @@ -65,7 +66,7 @@ def check_for_smc(): @register_wrap class OptimizePanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_optimize_v3' - bl_label = 'Optimization' + bl_label = t('OptimizePanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -90,12 +91,12 @@ def draw(self, context): col = box.column(align=True) row = col.row(align=True) row.scale_y = 0.75 - row.label(text='A greatly improved Atlas Generator.') + row.label(text=t('OptimizePanel.atlasDesc')) split = col.row(align=True) row = split.row(align=True) row.scale_y = 0.9 - row.label(text='Made by shotaryia', icon_value=Supporter.preview_collections["custom_icons"]["heart1"].icon_id) + row.label(text=t('OptimizePanel.atlasAuthor'), icon_value=Supporter.preview_collections["custom_icons"]["heart1"].icon_id) row = split.row(align=True) row.alignment = 'RIGHT' row.scale_y = 0.9 @@ -160,10 +161,10 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Material Combiner is not enabled!") + row.label(text=t('OptimizePanel.matCombDisabled1')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Enable it in your user preferences:") + row.label(text=t('OptimizePanel.matCombDisabled2')) col.separator() row = col.row(align=True) row.operator(Atlas.EnableSMC.bl_idname, icon='CHECKBOX_HLT') @@ -179,23 +180,23 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Your Material Combiner is outdated!") + row.label(text=t('OptimizePanel.matCombOutdated1')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Please update to the latest version.") + row.label(text=t('OptimizePanel.matCombOutdated2')) col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Update via the 'Updates'' panel") + row.label(text=t('OptimizePanel.matCombOutdated3')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="in the 'MatCombiner' tab to the " + ("left." if version_2_79_or_older() else "right.")) + row.label(text=t('OptimizePanel.matCombOutdated4', location=t('OptimizePanel.matCombOutdated5_2.8') if version_2_79_or_older() else t('OptimizePanel.matCombOutdated5_2.79'))) col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Or download and install it manually:") + row.label(text=t('OptimizePanel.matCombOutdated6')) col.separator() row = col.row(align=True) row.operator(Atlas.ShotariyaButton.bl_idname, icon=globs.ICON_URL) @@ -211,13 +212,13 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Your Material Combiner is outdated!") + row.label(text=t('OptimizePanel.matCombOutdated1')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Please update to the latest version.") + row.label(text=t('OptimizePanel.matCombOutdated2')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Download and install it manually:") + row.label(text=t('OptimizePanel.matCombOutdated6_alt')) col.separator() row = col.row(align=True) row.operator(Atlas.ShotariyaButton.bl_idname, icon=globs.ICON_URL) @@ -233,10 +234,10 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Material Combiner is not installed!") + row.label(text=t('OptimizePanel.matCombNotInstalled')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text="Download and install it manually:") + row.label(text=t('OptimizePanel.matCombOutdated6_alt')) col.separator() row = col.row(align=True) row.operator(Atlas.ShotariyaButton.bl_idname, icon=globs.ICON_URL) diff --git a/ui/settings_updates.py b/ui/settings_updates.py index cc532712..335da1db 100644 --- a/ui/settings_updates.py +++ b/ui/settings_updates.py @@ -5,12 +5,13 @@ from .main import ToolPanel from ..tools import settings as Settings from ..tools.register import register_wrap +from ..translations import t @register_wrap class UpdaterPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_updater_v3' - bl_label = 'Settings & Updates' + bl_label = t('UpdaterPanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -20,7 +21,7 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.8 - row.label(text='Settings:', icon=globs.ICON_SETTINGS) + row.label(text=t('UpdaterPanel.name'), icon=globs.ICON_SETTINGS) col.separator() row = col.row(align=True) @@ -41,10 +42,10 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.8 - row.label(text='Restart required.', icon='ERROR') + row.label(text=t('UpdaterPanel.requireRestart1'), icon='ERROR') row = col.row(align=True) row.scale_y = 0.8 - row.label(text='Some changes require a Blender restart.', icon='BLANK1') + row.label(text=t('UpdaterPanel.requireRestart2'), icon='BLANK1') row = col.row(align=True) row.operator(Settings.RevertChangesButton.bl_idname, icon='RECOVER_LAST') diff --git a/ui/supporter.py b/ui/supporter.py index 3e975c8e..3a315815 100644 --- a/ui/supporter.py +++ b/ui/supporter.py @@ -4,12 +4,13 @@ from ..tools import supporter as Supporter from ..tools.register import register_wrap from ..tools.supporter import check_for_update_background +from ..translations import t @register_wrap class SupporterPanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_supporter_v3' - bl_label = 'Supporters' + bl_label = t('SupporterPanel.label') def draw(self, context): layout = self.layout @@ -20,7 +21,7 @@ def draw(self, context): check_for_update_background() row = col.row(align=True) - row.label(text='Do you like this plugin and want to support us?') + row.label(text=t('SupporterPanel.desc')) row = col.row(align=True) row.scale_y = 1.2 row.operator(Supporter.PatreonButton.bl_idname, icon_value=Supporter.preview_collections["custom_icons"]["heart1"].icon_id) @@ -30,7 +31,7 @@ def draw(self, context): col.separator() row = col.row(align=True) - row.label(text='Thanks to our awesome supporters! <3') + row.label(text=t('SupporterPanel.thanks')) col.separator() supporter_count = draw_supporter_list(col, show_tier=1) @@ -43,10 +44,10 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 1.2 - row.label(text='Is your name missing?', icon="INFO") + row.label(text=t('SupporterPanel.missingName1'), icon="INFO") row = col.row(align=True) row.scale_y = 0.3 - row.label(text=' Please contact us in our discord!') + row.label(text=t('SupporterPanel.missingName2')) col.separator() row = col.row(align=True) row.scale_y = 0.8 diff --git a/ui/visemes.py b/ui/visemes.py index bf65daa1..64352923 100644 --- a/ui/visemes.py +++ b/ui/visemes.py @@ -5,12 +5,13 @@ from ..tools import viseme as Viseme from ..tools.register import register_wrap +from ..translations import t @register_wrap class VisemePanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_viseme_v3' - bl_label = 'Visemes' + bl_label = t('VisemePanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -22,7 +23,7 @@ def draw(self, context): if mesh_count == 0: row = col.row(align=True) row.scale_y = 1.1 - row.label(text='No meshes found!', icon='ERROR') + row.label(text=t('VisemePanel.error.noMesh'), icon='ERROR') col.separator() elif mesh_count > 1: row = col.row(align=True) diff --git a/updater.py b/updater.py index bb07852f..ecdb070f 100644 --- a/updater.py +++ b/updater.py @@ -10,6 +10,7 @@ from threading import Thread from collections import OrderedDict from bpy.app.handlers import persistent +from .translations import t no_ver_check = False fake_update = False @@ -51,8 +52,8 @@ class CheckForUpdateButton(bpy.types.Operator): bl_idname = 'cats_updater.check_for_update' - bl_label = 'Check now for Update' - bl_description = 'Checks if a new update is available for CATS' + bl_label = t('CheckForUpdateButton.label') + bl_description = t('CheckForUpdateButton.desc') bl_options = {'INTERNAL'} @classmethod @@ -68,8 +69,8 @@ def execute(self, context): class UpdateToLatestButton(bpy.types.Operator): bl_idname = 'cats_updater.update_latest' - bl_label = 'Update Now' - bl_description = 'Updates CATS to the latest version' + bl_label = t('UpdateToLatestButton.label') + bl_description = t('UpdateToLatestButton.desc') bl_options = {'INTERNAL'} @classmethod @@ -108,8 +109,8 @@ def execute(self, context): class UpdateToDevButton(bpy.types.Operator): bl_idname = 'cats_updater.update_dev' - bl_label = 'Update to Development version' - bl_description = 'Updates CATS to the Development version' + bl_label = t('UpdateToDevButton.label') + bl_description = t('UpdateToDevButton.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -123,33 +124,33 @@ def execute(self, context): class RemindMeLaterButton(bpy.types.Operator): bl_idname = 'cats_updater.remind_me_later' - bl_label = 'Remind me later' - bl_description = 'This hides the update notification til the next Blender restart' + bl_label = t('RemindMeLaterButton.label') + bl_description = t('RemindMeLaterButton.desc') bl_options = {'INTERNAL'} def execute(self, context): global remind_me_later remind_me_later = True - self.report({'INFO'}, 'You will be reminded later') + self.report({'INFO'}, t('RemindMeLaterButton.success')) return {'FINISHED'} class IgnoreThisVersionButton(bpy.types.Operator): bl_idname = 'cats_updater.ignore_this_version' - bl_label = 'Ignore this version' - bl_description = 'This ignores this version. You will be reminded again when the next version releases' + bl_label = t('IgnoreThisVersionButton.label') + bl_description = t('IgnoreThisVersionButton.desc') bl_options = {'INTERNAL'} def execute(self, context): set_ignored_version() - self.report({'INFO'}, 'Version ' + latest_version_str + ' will be ignored.') + self.report({'INFO'}, t('IgnoreThisVersionButton.success', name=latest_version_str)) return {'FINISHED'} class ShowPatchnotesPanel(bpy.types.Operator): bl_idname = 'cats_updater.show_patchnotes' - bl_label = 'Patchnotes' - bl_description = 'Shows the patchnotes of the selected version' + bl_label = t('ShowPatchnotesPanel.label') + bl_description = t('ShowPatchnotesPanel.desc') bl_options = {'INTERNAL'} @classmethod @@ -183,7 +184,7 @@ def draw(self, context): col.separator() row = col.row(align=True) - row.label(text='Released: ' + version[2]) + row.label(text=t('ShowPatchnotesPanel.releaseDate', date=version[2])) col.separator() for line in version[1].replace('**', '').split('\r\n'): @@ -196,8 +197,8 @@ def draw(self, context): class ConfirmUpdatePanel(bpy.types.Operator): bl_idname = 'cats_updater.confirm_update_panel' - bl_label = 'Confirm Update' - bl_description = 'This shows you a panel in which you have to confirm your update choice' + bl_label = t('ConfirmUpdatePanel.label') + bl_description = t('ConfirmUpdatePanel.desc') bl_options = {'INTERNAL'} show_patchnotes = False @@ -239,22 +240,22 @@ def draw(self, context): col.separator() row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Warning:') + row.label(text=t('ConfirmUpdatePanel.warn.dev1')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text=' The development version of CATS is the place where') + row.label(text=t('ConfirmUpdatePanel.warn.dev2')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text=' we test new features and bug fixes.') + row.label(text=t('ConfirmUpdatePanel.warn.dev3')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text=' This version might be very unstable and some features') + row.label(text=t('ConfirmUpdatePanel.warn.dev4')) row = col.row(align=True) row.scale_y = 0.75 - row.label(text=' might not work correctly.') + row.label(text='ConfirmUpdatePanel.warn.dev5') else: - row.operator(ShowPatchnotesPanel.bl_idname, text='Show Patchnotes') + row.operator(ShowPatchnotesPanel.bl_idname, text=t('ConfirmUpdatePanel.ShowPatchnotesPanel.label')) col.separator() col.separator() @@ -262,13 +263,13 @@ def draw(self, context): row = col.row(align=True) row.scale_y = 0.65 # row.label(text='Update now to ' + version_str + ':', icon=ICON_URL) - row.label(text='Update now:', icon=ICON_URL) + row.label(text=t('ConfirmUpdatePanel.updateNow'), icon=ICON_URL) class UpdateCompletePanel(bpy.types.Operator): bl_idname = 'cats_updater.update_complete_panel' - bl_label = 'Installation Report' - bl_description = 'The update if now complete' + bl_label = t('UpdateCompletePanel.label') + bl_description = t('UpdateCompletePanel.desc') bl_options = {'INTERNAL'} show_patchnotes = False @@ -291,25 +292,25 @@ def draw(self, context): if update_finished: row = col.row(align=True) row.scale_y = 0.9 - row.label(text='CATS was successfully updated.', icon='FILE_TICK') + row.label(text=t('UpdateCompletePanel.success1'), icon='FILE_TICK') row = col.row(align=True) row.scale_y = 0.9 - row.label(text='Restart Blender to complete the update.', icon='BLANK1') + row.label(text=t('UpdateCompletePanel.success2'), icon='BLANK1') else: row = col.row(align=True) row.scale_y = 0.9 - row.label(text='Update failed.', icon='CANCEL') + row.label(text=t('UpdateCompletePanel.failure1'), icon='CANCEL') row = col.row(align=True) row.scale_y = 0.9 - row.label(text='See Updater Panel for more info.', icon='BLANK1') + row.label(text=t('UpdateCompletePanel.failure2'), icon='BLANK1') class UpdateNotificationPopup(bpy.types.Operator): bl_idname = 'cats_updater.update_notification_popup' - bl_label = 'Update available' - bl_description = 'This shows you that an update is available' + bl_label = t('UpdateNotificationPopup.label') + bl_description = t('UpdateNotificationPopup.desc') bl_options = {'INTERNAL'} def execute(self, context): @@ -342,8 +343,8 @@ def draw(self, context): row = layout_split(col, factor=0.55, align=True) row.scale_y = 1.05 - row.label(text='Cats v' + latest_version_str + ' available!', icon='SOLO_ON') - row.operator(ShowPatchnotesPanel.bl_idname, text='Show Patchnotes') + row.label(text=t('UpdateNotificationPopup.newUpdate', name=latest_version_str), icon='SOLO_ON') + row.operator(ShowPatchnotesPanel.bl_idname, text=t('UpdateNotificationPopup.ShowPatchnotesPanel.label')) col.separator() col.separator() @@ -378,7 +379,7 @@ def check_for_update(): # Get all releases from Github if not get_github_releases('Darkblader24') and not get_github_releases('michaeldegroot'): - finish_update_checking(error='Could not check for updates, try again later') + finish_update_checking(error=t('check_for_update.cantCheck')) return # Check if an update is needed @@ -554,7 +555,7 @@ def download_file(update_url): except urllib.error.URLError: print("FILE COULD NOT BE DOWNLOADED") shutil.rmtree(downloads_dir) - finish_update(error='Could not connect to Github') + finish_update(error=t('download_file.cantConnect')) return print('DOWNLOAD FINISHED') @@ -562,7 +563,7 @@ def download_file(update_url): if not os.path.isfile(update_zip_file): print("ZIP NOT FOUND!") shutil.rmtree(downloads_dir) - finish_update(error='Could not find the downloaded zip') + finish_update(error=t('download_file.cantFindZip')) return # Extract the downloaded zip @@ -597,7 +598,7 @@ def searchInit(path): print("INIT NOT FOUND!") shutil.rmtree(downloads_dir) # finish_reloading() - finish_update(error='Could not find CATS in the downloaded zip') + finish_update(error=t('download_file.cantFindCATS')) return # Remove old addon files @@ -764,23 +765,23 @@ def draw_update_notification_panel(layout): if update_finished: col.separator() row = col.row(align=True) - row.label(text='Restart Blender to complete update!', icon='ERROR') + row.label(text=t('draw_update_notification_panel.success'), icon='ERROR') col.separator() return row = col.row(align=True) row.scale_y = 0.75 - row.label(text='Cats v' + latest_version_str + ' available!', icon='SOLO_ON') + row.label(text=t('draw_update_notification_panel.newUpdate', name=latest_version_str), icon='SOLO_ON') col.separator() row = col.row(align=True) row.scale_y = 1.3 - row.operator(UpdateToLatestButton.bl_idname, text='Update Now') + row.operator(UpdateToLatestButton.bl_idname, text=t('draw_update_notification_panel.UpdateToLatestButton.label')) row = col.row(align=True) row.scale_y = 1 - row.operator(RemindMeLaterButton.bl_idname, text='Remind me later') - row.operator(IgnoreThisVersionButton.bl_idname, text='Ignore this version') + row.operator(RemindMeLaterButton.bl_idname, text=t('draw_update_notification_panel.RemindMeLaterButton.label')) + row.operator(IgnoreThisVersionButton.bl_idname, text=t('draw_update_notification_panel.IgnoreThisVersionButton.label')) def draw_updater_panel(context, layout, user_preferences=False): @@ -791,13 +792,13 @@ def draw_updater_panel(context, layout, user_preferences=False): row = col.row(align=True) row.scale_y = 0.8 - row.label(text='Updates:' if not user_preferences else 'Cats Updater:', icon=ICON_URL) + row.label(text=t('draw_updater_panel.updateLabel') if not user_preferences else t('draw_updater_panel.updateLabel_alt'), icon=ICON_URL) col.separator() if update_finished: col.separator() row = col.row(align=True) - row.label(text='Restart Blender to complete update!', icon='ERROR') + row.label(text=t('draw_updater_panel.success'), icon='ERROR') col.separator() return @@ -810,12 +811,12 @@ def draw_updater_panel(context, layout, user_preferences=False): if not used_updater_panel: row = col.row(align=True) row.scale_y = scale_big - row.operator(CheckForUpdateButton.bl_idname, text='Checking..') + row.operator(CheckForUpdateButton.bl_idname, text=t('draw_updater_panel.CheckForUpdateButton.label')) else: split = col.row(align=True) row = split.row(align=True) row.scale_y = scale_big - row.operator(CheckForUpdateButton.bl_idname, text='Checking..') + row.operator(CheckForUpdateButton.bl_idname, text=t('draw_updater_panel.CheckForUpdateButton.label')) row = split.row(align=True) row.alignment = 'RIGHT' row.scale_y = scale_big @@ -825,7 +826,7 @@ def draw_updater_panel(context, layout, user_preferences=False): split = col.row(align=True) row = split.row(align=True) row.scale_y = scale_big - row.operator(UpdateToLatestButton.bl_idname, text='Update now to ' + latest_version_str) + row.operator(UpdateToLatestButton.bl_idname, text=t('draw_updater_panel.UpdateToLatestButton.label', name=latest_version_str)) row = split.row(align=True) row.alignment = 'RIGHT' row.scale_y = scale_big @@ -834,13 +835,13 @@ def draw_updater_panel(context, layout, user_preferences=False): elif not used_updater_panel or not version_list: row = col.row(align=True) row.scale_y = scale_big - row.operator(CheckForUpdateButton.bl_idname, text='Check now for Update') + row.operator(CheckForUpdateButton.bl_idname, text=t('draw_updater_panel.CheckForUpdateButton.label_alt')) else: split = col.row(align=True) row = split.row(align=True) row.scale_y = scale_big - row.operator(UpdateToLatestButton.bl_idname, text='Up to Date!') + row.operator(UpdateToLatestButton.bl_idname, text=t('draw_updater_panel.UpdateToLatestButton.label_alt')) row = split.row(align=True) row.alignment = 'RIGHT' row.scale_y = scale_big @@ -866,7 +867,7 @@ def draw_updater_panel(context, layout, user_preferences=False): row = layout_split(split, factor=0.55, align=True) row.scale_y = scale_small row.active = True if not is_checking_for_update and version_list else False - row.operator(UpdateToSelectedButton.bl_idname, text='Install Version:') + row.operator(UpdateToSelectedButton.bl_idname, text=t('draw_updater_panel.UpdateToSelectedButton.label')) row.prop(context.scene, 'cats_updater_version_list', text='') row = split.row(align=True) row.scale_y = scale_small @@ -891,12 +892,12 @@ def draw_updater_panel(context, layout, user_preferences=False): row = col.row(align=True) row.scale_y = scale_small - row.operator(UpdateToDevButton.bl_idname, text='Install Development Version') + row.operator(UpdateToDevButton.bl_idname, text=t('draw_updater_panel.UpdateToDevButton.label')) col.separator() row = col.row(align=True) row.scale_y = 0.65 - row.label(text='Current version: ' + current_version_str) + row.label(text=t('draw_updater_panel.currentVersion', name=current_version_str)) # demo bare-bones preferences @@ -938,17 +939,17 @@ def register(bl_info, dev_branch, version_str): current_version.append(i) bpy.types.Scene.cats_updater_version_list = bpy.props.EnumProperty( - name='Version', - description='Select the version you want to install\n', + name=t('bpy.types.Scene.cats_updater_version_list.label'), + description=t('bpy.types.Scene.cats_updater_version_list.desc'), items=get_version_list ) bpy.types.Scene.cats_update_action = bpy.props.EnumProperty( - name="Choose action", - description="Action", + name=t('bpy.types.Scene.cats_update_action.label'), + description=t('bpy.types.Scene.cats_update_action.desc'), items=[ - ("UPDATE", "Update Now", "Updates now to the latest version"), - ("IGNORE", "Ignore this version", "This ignores this version. You will be reminded again when the next version releases"), - ("DEFER", "Remind me later", "Hides the update notification til the next Blender restart") + ("UPDATE", t('bpy.types.Scene.cats_update_action.update.label'), t('bpy.types.Scene.cats_update_action.update.desc')), + ("IGNORE", t('bpy.types.Scene.cats_update_action.ignore.label'), t( 'bpy.types.Scene.cats_update_action.ignore.desc')), + ("DEFER", t('bpy.types.Scene.cats_update_action.defer.label'), t( 'bpy.types.Scene.cats_update_action.defer.desc')) ] ) From 30090fe452f23c694c5f09f90e574ca0679079bf Mon Sep 17 00:00:00 2001 From: Jordo Date: Tue, 14 Jul 2020 06:28:16 +0200 Subject: [PATCH 03/64] Updated the Japanese translation file with every visible labels --- translations/ja_JP.py | 378 +++++++++++++++++++++--------------------- 1 file changed, 189 insertions(+), 189 deletions(-) diff --git a/translations/ja_JP.py b/translations/ja_JP.py index d36e259b..5706338d 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -167,7 +167,7 @@ 'CreditsPanel.desc5': '助けが必要ですか、バグを見つけましたか?', # Tools Armature - 'FixArmature.label': 'Fix Model', + 'FixArmature.label': 'モデル修正', 'FixArmature.desc': 'Automatically:\n' \ '- Reparents bones\n' \ '- Removes unnecessary bones, objects, groups & constraints\n' \ @@ -194,29 +194,29 @@ 'FixArmature.notParentTo2': ', this will cause problems!', # Tools Armature Manual - 'StartPoseMode.label': 'Start Pose Mode', + 'StartPoseMode.label': 'ポーズモードを開始', 'StartPoseMode.desc': 'Starts the pose mode.\n' \ 'This lets you test how your model will move', - 'StopPoseMode.label': 'Stop Pose Mode', + 'StopPoseMode.label': 'ポーズモードを停止する', 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', - 'PoseToShape.label': 'Pose to Shape Key', + 'PoseToShape.label': 'シェイプキーへのポーズ', 'PoseToShape.desc': 'This saves your current pose as a new shape key.' \ '\nThe new shape key will be at the bottom of your shape key list of the mesh', - 'PoseNamePopup.label': 'Give this shapekey a name:', + 'PoseNamePopup.label': 'このシェイプキーに名前を付ける:', 'PoseNamePopup.desc': 'Sets the shapekey name. Press anywhere outside to skip', 'PoseNamePopup.success': 'Pose successfully saved as shape key.', - 'PoseToRest.label': 'Apply as Rest Pose', + 'PoseToRest.label': 'レストポーズとして適用する', 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' \ '\n' \ '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' \ '\nWARNING: This can have unwanted effects on shape keys, so be careful when modifying the head with this', 'PoseToRest.success': 'Pose successfully applied as rest pose.', - 'JoinMeshes.label': 'Join Meshes', + 'JoinMeshes.label': 'メッシュに参加する', 'JoinMeshes.desc': 'Joins all meshes of this model together.' \ '\nIt also:' \ '\n - Reorders all shape keys correctly' \ @@ -227,7 +227,7 @@ 'JoinMeshes.failure': 'Meshes could not be joined!', 'JoinMeshes.success': 'Meshes joined.', - 'JoinMeshesSelected.label': 'Join Selected Meshes', + 'JoinMeshesSelected.label': '選択したメッシュを結合する', 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' \ '\nIt also:' \ '\n - Reorders all shape keys correctly' \ @@ -239,25 +239,25 @@ 'JoinMeshesSelected.error.cantJoin': 'Selected meshes could not be joined!', 'JoinMeshesSelected.success': 'Selected meshes joined.', - 'SeparateByMaterials.label': 'Separate by Materials', + 'SeparateByMaterials.label': '材料別に分離する', 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' \ '\n' \ 'Warning: Never decimate something where you might need the shape keys later (face, mouth, eyes..)', 'SeparateByMaterials.success': 'Successfully separated by materials.', - 'SeparateByLooseParts.label': 'Separate by Loose Parts', + 'SeparateByLooseParts.label': 'ルーズパーツに分離する', 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' \ 'This acts like separating by materials but creates more meshes for more precision', 'SeparateByLooseParts.success': 'Successfully separated by loose parts.', - 'SeparateByShapekeys.label': 'Separate by Shape Keys', + 'SeparateByShapekeys.label': 'シェイプキーに区切る', 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' \ '\ndepending on whether it is effected by a shape key or not.' \ '\n' \ '\nVery useful for manual decimation', 'SeparateByShapekeys.success': 'Successfully separated by shape keys.', - 'SeparateByCopyProtection.label': 'Separate by Copy Protection', + 'SeparateByCopyProtection.label': 'コピープロテクションに分離する', 'SeparateByCopyProtection.desc': 'Separates selected mesh into two parts,' \ '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ '\n' \ @@ -269,69 +269,69 @@ '\nPlease select the mesh you want to separate!', 'SeparateByX.warn.noSeparation': 'No meshes had to be separated!', - 'MergeWeights.label': 'Merge Weights to Parent', + 'MergeWeights.label': '親に重みをマージする', 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' \ '\n' \ '\nOnly available in Edit or Pose Mode with bones selected', 'MergeWeights.success': 'Deleted {number} bones and added their weights to their parents.', - 'MergeWeightsToActive.label': 'Merge Weights to Active', + 'MergeWeightsToActive.label': 'ウェイトをアクティブ にマージする', 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' \ '\nThe active bone is the one you selected last.' \ '\n' \ '\nOnly available in Edit or Pose Mode with bones selected', 'MergeWeightsToActive.success': 'Deleted {number} bones and added their weights to the active bone.', - 'ApplyTransformations.label': 'Apply Transformations', + 'ApplyTransformations.label': '変換を適用する', 'ApplyTransformations.desc': 'Applies the position, rotation and scale to the armature and it\'s meshes', 'ApplyTransformations.success': 'Transformations applied.', - 'ApplyAllTransformations.label': 'Apply All Transformations', + 'ApplyAllTransformations.label': 'すべての変換を適用する', 'ApplyAllTransformations.desc': 'Applies the position, rotation and scale of all objects', 'ApplyAllTransformations.success': 'Transformations applied.', - 'RemoveZeroWeightBones.label': 'Remove Zero Weight Bones', + 'RemoveZeroWeightBones.label': 'ゼロ ウェイト ボーンを削除する', 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' \ 'Don\'t use this if you plan to use \'Fix Model\'', 'RemoveZeroWeightBones.success': 'Deleted {number} zero weight bones.', - 'RemoveZeroWeightGroups.label': 'Remove Zero Weight Vertex Groups', + 'RemoveZeroWeightGroups.label': 'ゼロ ウェイトの頂点グループを削除', 'RemoveZeroWeightGroups.desc': 'Cleans up the vertex groups of all meshes, deleting all groups that don\'t directly affect any vertices', 'RemoveZeroWeightGroups.success': 'Removed {number} zero weight vertex groups.', - 'RemoveConstraints.label': 'Remove Bone Constraints', + 'RemoveConstraints.label': 'ボーン拘束を削除', 'RemoveConstraints.desc': 'Removes constrains between bones causing specific bone movement as these are not used by VRChat', 'RemoveConstraints.success': 'Removed all bone constraints.', - 'RecalculateNormals.label': 'Recalculate Normals', + 'RecalculateNormals.label': '法線を再計算する', 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' \ 'Don\'t use this on good looking meshes as this can screw them up.\n' \ 'Use this if there are random inverted or darker faces on the mesh', 'RecalculateNormals.success': 'Recalculated all normals.', - 'FlipNormals.label': 'Flip Normals', + 'FlipNormals.label': '法線を反転', 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' \ 'Use this if all normals are inverted', 'FlipNormals.success': 'Flipped all normals.', - 'RemoveDoubles.label': 'Remove Doubles', + 'RemoveDoubles.label': 'ダブルスを削除する', 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ '\nThis is more safe than doing it manually:' \ '\n - leaves shape keys completely untouched' \ '\n - but removes less doubles overall', 'RemoveDoubles.success': 'Removed {number} vertices.', - 'RemoveDoublesNormal.label': 'Remove Doubles Normally', + 'RemoveDoublesNormal.label': 'ダブルを通常どおりに削除する', 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ '\nThis is exactly like doing it manually', 'RemoveDoublesNormal.success': 'Removed {number} vertices.', - 'FixVRMShapesButton.label': 'Fix Koikatsu Shapekeys', + 'FixVRMShapesButton.label': 'コイカツシェイプキーを修正', 'FixVRMShapesButton.desc': 'Fixes the shapekeys of Koikatsu models', 'FixVRMShapesButton.warn.notDetected': 'No shapekeys detected!', 'FixVRMShapesButton.success': 'Fixed VRM shapekeys.', - 'FixFBTButton.label': 'Fix Full Body Tracking', + 'FixFBTButton.label': '全身追跡を修正', 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' \ '\n' \ '\nApplies a general fix for Full Body Tracking.' \ @@ -343,7 +343,7 @@ 'FixFBTButton.error.alreadyApplied': 'Full Body Tracking Fix already applied!', 'FixFBTButton.success': 'Successfully applied the Full Body Tracking fix.', - 'RemoveFBTButton.label': 'Remove Full Body Tracking Fix', + 'RemoveFBTButton.label': '全身追跡の修正を削除', 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' \ '\n' \ '\nRequires bones:' \ @@ -355,12 +355,12 @@ 'RemoveFBTButton.error.notApplied': 'The Full Body Tracking Fix is not applied!', 'RemoveFBTButton.success': 'Successfully removed the Full Body Tracking fix.', - 'DuplicateBonesButton.label': 'Duplicate Bones', + 'DuplicateBonesButton.label': 'ボーンの複製', 'DuplicateBonesButton.desc': 'Duplicates the selected bones including their weight and renames them to _L and _R', 'DuplicateBonesButton.success': 'Successfully duplicated {number} bones.', # Tools Armature Custom - 'MergeArmature.label': 'Merge Armatures', + 'MergeArmature.label': 'マージアーマチュア', 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' \ '\nYou should fix both armatures with Cats first.' \ '\nOnly move the mesh of the merge armature to the desired position, the bones will be moved automatically', @@ -374,14 +374,14 @@ 'After that please only move the mesh (not the armature!) to the desired position.'], 'MergeArmature.success': 'Armatures successfully joined.', - 'AttachMesh.label': 'Attach Mesh', + 'AttachMesh.label': 'メッシュをアタッチ', 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' \ '\n' \ '\nINFO: The mesh will only be assigned to the selected bone.' \ '\nE.g.: A jacket won\'t work, because it requires multiple bones', 'AttachMesh.success': 'Mesh successfully attached to armature.', - 'CustomModelTutorialButton.label': 'Go to Documentation', + 'CustomModelTutorialButton.label': 'ドキュメントに移動', 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) 'CustomModelTutorialButton.success': 'Documentation', @@ -394,16 +394,16 @@ 'merge_armatures.error.pleaseUndo': ['Something went wrong! Please undo, check your selections and try again.'], # Tools Atlas - 'EnableSMC.label': 'Enable Material Combiner', + 'EnableSMC.label': 'マテリアルコンバイナを有効にする', 'EnableSMC.desc': 'Enables Material Combiner', 'EnableSMC.success': 'Enabled Material Combiner!', - 'AtlasHelpButton.label': 'Generate Material List', + 'AtlasHelpButton.label': '材料リストを生成する', 'AtlasHelpButton.desc': 'Open useful Atlas Tips', 'AtlasHelpButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin/#texture-atlas', 'AtlasHelpButton.success': 'Atlas Help opened.', - 'InstallShotariya.label': 'Error while loading Material Combiner:', + 'InstallShotariya.label': 'マテリアルコンバイナ読み込み中にエラーが発生しました:', 'InstallShotariya.error.install1': 'Material Combiner is not installed!', 'InstallShotariya.error.install2': 'The plugin \'Material Combiner\' by Shotariya is required for this function.', 'InstallShotariya.error.install3': 'Please download and install it manually:', @@ -414,55 +414,55 @@ 'InstallShotariya.error.version2': 'The latest version is required for this function.', 'InstallShotariya.error.version3': 'Please download and install it manually:', - 'ShotariyaButton.label': 'Download Material Combiner', + 'ShotariyaButton.label': 'マテリアルコンバイナーをダウンロード', 'ShotariyaButton.URL': 'https://vrcat.club/threads/material-combiner-blender-addon-1-1-3.2255/', 'ShotariyaButton.success': 'Material Combiner link opened', # Tools Bonemerge - 'BoneMergeButton.label': 'Merge Bones', + 'BoneMergeButton.label': 'ボーンをマージする', 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' \ 'This is useful to reduce the amount of bones used by Dynamic Bones.', 'BoneMergeButton.success': 'Merged bones.', # Tools Common - 'ShowError.label': 'Report: Error', + 'ShowError.label': 'レポート: エラー', # Tools Copy protection - 'CopyProtectionEnable.label': 'Enable Protection', + 'CopyProtectionEnable.label': '保護を有効にする', 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' \ '\nRead the documentation before use', 'CopyProtectionEnable.success': 'Model secured!', - 'CopyProtectionDisable.label': 'Disable Protection', + 'CopyProtectionDisable.label': '保護を無効にする', 'CopyProtectionDisable.desc': 'Removes the copy protections from this model.', 'CopyProtectionDisable.success': 'Model un-secured!', - 'ProtectionTutorialButton.label': 'Go to Documentation', + 'ProtectionTutorialButton.label': 'ドキュメントに移動', 'ProtectionTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#copy-protection', 'ProtectionTutorialButton.success': 'Documentation', # Tools Credits - 'ForumButton.label': 'Go to the Forums', + 'ForumButton.label': 'フォーラムに移動する', 'ForumButton.URL': 'https://vrcat.club/threads/cats-blender-plugin.6/', 'ForumButton.success': 'Forum opened.', - 'DiscordButton.label': 'Join our Discord', + 'DiscordButton.label': 'Discordサーバーに参加する', 'DiscordButton.URL': 'https://discord.gg/f8yZGnv', 'DiscordButton.success': 'Discord opened.', - 'PatchnotesButton.label': 'Latest Patchnotes', + 'PatchnotesButton.label': '最新のパッチノート', 'PatchnotesButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin/releases', 'PatchnotesButton.success': 'Patchnotes opened.', # Tools Decimation - 'ScanButton.label': 'Scan for decimation models', + 'ScanButton.label': 'デシメーション モデルのスキャン', 'ScanButton.desc': 'Separates the mesh.', - 'AddShapeButton.label': 'Add', + 'AddShapeButton.label': '追加', 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' \ 'This means that every mesh containing that shape key will be not decimated.', - 'AddMeshButton.label': 'Add', + 'AddMeshButton.label': '追加', 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' \ 'This means that this mesh will be not decimated.', @@ -490,7 +490,7 @@ 'decimate.cantDecimate2': 'It got decimated as much as possible within the limits.', # Tools Eyetracking - 'CreateEyesButton.label': 'Create Eye Tracking', + 'CreateEyesButton.label': 'アイトラッキングを作成する', 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' \ 'You should do decimation before this operation.\n' \ 'Test the resulting eye movement in the \'Testing\' tab.', @@ -506,41 +506,41 @@ '\nFurthermore the mesh containing the eyes has to be called "Body" and the armature "Armature".', 'CreateEyesButton.success': 'Created eye tracking!', - 'StartTestingButton.label': 'Start Eye Testing', + 'StartTestingButton.label': 'スタートアイテスト', 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' \ 'Don\'t forget to stop the Testing process afterwards.\n' \ 'Bones "LeftEye" and "RightEye" are required.', - 'StopTestingButton.label': 'Stop Eye Testing', + 'StopTestingButton.label': 'ストップアイテスト', 'StopTestingButton.desc': 'Stops the testing process.', 'StopTestingButton.error.tryAgain': 'Something went wrong. Please try eye testing again.', - 'ResetRotationButton.label': 'Reset Rotation', + 'ResetRotationButton.label': '回転をリセットする', 'ResetRotationButton.desc': 'This resets the eye positions.', - 'AdjustEyesButton.label': 'Set Range', + 'AdjustEyesButton.label': '目の範囲を設定', 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' \ 'This gets saved', 'AdjustEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' '\nThis might be because you selected the wrong mesh or the wrong bone.' '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.', - 'StartIrisHeightButton.label': 'Start Iris Height Adjustment', + 'StartIrisHeightButton.label': 'アイリスの高さの調整を開始', 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' \ 'Use this to fix clipping of the iris into the eye ball.\n' \ 'This gets saved.', - 'TestBlinking.label': 'Test', + 'TestBlinking.label': 'テスト', 'TestBlinking.desc': 'This lets you see how eye blinking will look in-game.', - 'TestLowerlid.label': 'Test', + 'TestLowerlid.label': 'テスト', 'TestLowerlid.desc': 'This lets you see how lowerlids will look in-game.', - 'ResetBlinkTest.label': 'Reset Shapes', + 'ResetBlinkTest.label': '図形をリセットする', 'ResetBlinkTest.desc': 'This resets the blink testing.', # Tools Importer - 'ImportAnyModel.label': 'Import Any Model', + 'ImportAnyModel.label': '任意のモデルをインポート', 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' \ '\n' \ '\nSupported types:' \ @@ -561,20 +561,20 @@ '\n- FBX: .fbx' \ '\n- DAE: .dae ' \ '\n- ZIP: .zip', - 'ImportAnyModel.importantInfo.label': 'IMPORTANT INFO (hover here)', + 'ImportAnyModel.importantInfo.label': '重要な情報(ここにホバー)', 'ImportAnyModel.importantInfo.desc': 'If you want to modify the import settings, use the button next to the Import button.\n\n', 'ImportAnyModel.error.emptyZip': 'The selected zip file contains no importable models.', 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' \ '\nPlease use a tool such as the "Autodesk FBX Converter" to make it compatible.', - 'ZipPopup.label': 'Zip Model Selection:', + 'ZipPopup.label': '圧縮モデルの選択:', 'ZipPopup.desc': 'Shows the models contained in the zip files', 'ZipPopup.selectModel1': 'Select which model you want to import', 'ZipPopup.selectModel2': 'Then confirm with OK', 'get_zip_content.choose': 'Import model "{model}" from the zip "{zipName}?"', - 'ModelsPopup.label': 'Select which you want to import:', + 'ModelsPopup.label': 'インポートする項目を選択します:', 'ModelsPopup.desc': 'Show individual import options', 'ImportMMD.label': 'MMD', @@ -592,41 +592,41 @@ 'ImportVRM.label': 'VRM', 'ImportVRM.desc': 'Import a VRM model (.vrm)', - 'InstallXPS.label': 'XPS Tools is not installed or enabled!', + 'InstallXPS.label': 'XPS ツールがインストールされていないか、有効になっていない!', - 'InstallSource.label': 'Source Tools is not installed or enabled!', + 'InstallSource.label': 'ソース ツールがインストールされていないか、有効になっていません!', - 'InstallVRM.label': 'VRM Importer is not installed or enabled!', + 'InstallVRM.label': 'VRMインポーターがインストールされていないか、有効になっていません!', 'InstallX.pleaseInstall1': 'If it is not enabled please enable it in your User Preferences.', 'InstallX.pleaseInstall2': 'If it is not installed please download and install it manually.', 'InstallX.pleaseInstall3': 'Make sure to install the version for Blender {blenderVersion}', 'InstallX.pleaseInstallTesting': 'Currently you have to select \'Testing\' in the addons settings.', - 'EnableMMD.label': 'Mmd_tools is not enabled!', + 'EnableMMD.label': 'Mmd_toolsが有効になっていません!', 'EnableMMD.required1': 'The plugin "mmd_tools" is required for this function.', 'EnableMMD.required2': 'Please restart Blender.', - 'XpsToolsButton.label': 'Download XPS Tools', + 'XpsToolsButton.label': 'XPSツールをダウンロード', 'XpsToolsButton.URL': 'https://github.com/johnzero7/XNALaraMesh', 'XpsToolsButton.success': 'XPS Tools link opened', - 'SourceToolsButton.label': 'Download Source Tools', + 'SourceToolsButton.label': 'ソースツールをダウンロード', 'SourceToolsButton.URL': 'https://github.com/Artfunkel/BlenderSourceTools', 'SourceToolsButton.success': 'Source Tools link opened', - 'VrmToolsButton.label': 'Download VRM Importer', + 'VrmToolsButton.label': 'VRMインポーターをダウンロード', 'VrmToolsButton.URL_2.79': 'https://github.com/iCyP/VRM_IMPORTER_for_Blender2_79', 'VrmToolsButton.URL_2.8': 'https://github.com/saturday06/VRM_IMPORTER_for_Blender2_8', 'VrmToolsButton.success': 'VRM Importer link opened', - 'ExportModel.label': 'Export Model', + 'ExportModel.label': 'モデルのエクスポート', 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' \ '\n' \ 'Automatically sets the optimal export settings', 'ExportModel.error.notEnabled': 'FBX Exporter not enabled! Please enable it in your User Preferences.', - 'ErrorDisplay.label': 'Warning:', + 'ErrorDisplay.label': '警告:', 'ErrorDisplay.polygons1': 'Too many polygons!', 'ErrorDisplay.polygons2': 'You have {number} tris in this model, but you shouldn\'t have more than 70,000!', 'ErrorDisplay.polygons3': 'You should decimate before you export this model.', @@ -639,7 +639,7 @@ 'ErrorDisplay.meshes3': 'It is not very optimized and might cause lag for you and others!', 'ErrorDisplay.meshes3_alt': "It is extremely unoptimized and will cause laugh for you and others!", 'ErrorDisplay.meshes4': 'You should always join your meshes, it\'s very easy:', - 'ErrorDisplay.JoinMeshes.label': 'Join Meshes', + 'ErrorDisplay.JoinMeshes.label': 'メッシュに参加する', 'ErrorDisplay.brokenShapekeys1': 'Broken shapekeys!', 'ErrorDisplay.brokenShapekeys2': 'This model has {number} broken shapekey(s):', 'ErrorDisplay.brokenShapekeys3': 'You will not be able to upload this model until you fix these shapekeys.', @@ -657,10 +657,10 @@ 'ErrorDisplay.continue': 'Continue to Export', # Tools Material - 'OneTexPerMatButton.label': 'One Material Texture', + 'OneTexPerMatButton.label': '1つのマテリアルテクスチャ', 'OneTexPerMatButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.', - 'OneTexPerMatOnlyButton.label': 'One Material Texture', + 'OneTexPerMatOnlyButton.label': '1つのマテリアルテクスチャ', 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' \ '\nAlso removes the textures from the material instead of disabling it.' \ '\nThis makes no difference, but cleans the list for the perfectionists', @@ -668,12 +668,12 @@ '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.label': 'テクスチャを標準化する', 'StandardizeTextures.desc': 'Enables Color and Alpha on every texture, sets the blend method to Multiply' \ '\nand changes the materials transparency to Z-Transparency', 'StandardizeTextures.success': 'All textures are now standardized.', - 'CombineMaterialsButton.label': 'Combine Same Materials', + 'CombineMaterialsButton.label': '同じマテリアルを組み合わせる', 'CombineMaterialsButton.desc': 'Combines similar materials into one, reducing draw calls.\n' \ 'Your avatar should visibly look the same after this operation.\n' \ 'This is a very important step for optimizing your avatar.\n' \ @@ -681,39 +681,39 @@ 'CombineMaterialsButton.error.noChanges': 'No materials combined.', 'CombineMaterialsButton.success': 'Combined {number} materials!', - 'ConvertAllToPngButton.label': 'Convert Textures to PNG', + 'ConvertAllToPngButton.label': 'テクスチャをPNGに変換', 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' \ '\nThis helps with transparency and compatibility issues.' \ '\n\nThe converted image files will be saved next to the old ones', 'ConvertAllToPngButton.success': 'Converted {number} to PNG files.', # Tools Root bone - 'RootButton.label': 'Parent Bones', + 'RootButton.label': '親のボーンに接続する', 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' \ 'Very useful for Dynamic Bones.', 'RootButton.success': 'Bones parented!', - 'RefreshRootButton.label': 'Refresh List', + 'RefreshRootButton.label': 'リストの更新', 'RefreshRootButton.desc': 'This will clear the group bones list cache and rebuild it, useful if bones have changed or your model.', 'RefreshRootButton.success': 'Root bones refreshed, check the root bones list again.', # Tools Settings - 'RevertChangesButton.label': 'Revert Settings', + 'RevertChangesButton.label': '設定を元に戻す', 'RevertChangesButton.desc': 'Revert the changes back to how they were on Blender start-up.', 'RevertChangesButton.success': 'Settings reverted.', - 'ResetGoogleDictButton.label': 'Clear Local Google Translations', + 'ResetGoogleDictButton.label': 'ローカルのGoogle翻訳をクリア', 'ResetGoogleDictButton.desc': 'Deletes all currently saved Google Translations. You can\'t undo this', 'ResetGoogleDictButton.resetInfo': 'Local Google Dictionary cleared!', - 'DebugTranslations.label': 'Debug Google Translations', # DEV ONLY + 'DebugTranslations.label': 'Google翻訳をデバッグ', # DEV ONLY 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' \ '\nThis button is only visible in the cats development version', # DEV ONLY 'DebugTranslations.error': 'Errors found, response printed!!', # DEV ONLY 'DebugTranslations.success': 'No issues with Google Translations found, response printed!', # DEV ONLY # Tools Shapekey - 'ShapeKeyApplier.label': 'Apply Selected Shapekey to Basis', + 'ShapeKeyApplier.label': '選択したシェイプキーをBasisに適用', 'ShapeKeyApplier.desc': 'Applies the selected shape key to the new Basis at it\'s current strength and creates a reverted shape key from the selected one', 'ShapeKeyApplier.error.revertCustomBasis': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', 'Start with the shape key called "{name}".', @@ -728,45 +728,45 @@ 'ShapeKeyApplier.successRemoved': 'Successfully removed shapekey "{name}" from the Basis.', 'ShapeKeyApplier.successSet': 'Successfully set shapekey "{name}" as the new Basis.', - 'addToShapekeyMenu.ShapeKeyApplier.label': 'Apply Selected Shapekey to Basis', + 'addToShapekeyMenu.ShapeKeyApplier.label': '選択したシェイプキーをBasisに適用', # Tools Supporter - 'PatreonButton.label': 'Become a Patron', + 'PatreonButton.label': 'パトロンになる', 'PatreonButton.URL': 'https://www.patreon.com/catsblenderplugin', 'PatreonButton.success': 'Patreon page opened.', - 'ReloadButton.label': 'Reload List', + 'ReloadButton.label': 'リストを再読み込み', 'ReloadButton.desc': 'Reloads the supporter list', - 'DynamicPatronButton.label': 'Supporter Name', + 'DynamicPatronButton.label': 'サポーター名', 'DynamicPatronButton.desc': 'This is an awesome supporter', 'register_dynamic_buttons.desc': '{name} is an awesome supporter', # Tools Translate - 'TranslateShapekeyButton.label': 'Translate Shape Keys', + 'TranslateShapekeyButton.label': 'シェイプキーを翻訳する', 'TranslateShapekeyButton.desc': 'Translates all shape keys using the internal dictionary and Google Translate', 'TranslateShapekeyButton.success': 'Translated {number} shape keys.', - 'TranslateBonesButton.label': 'Translate Bones', + 'TranslateBonesButton.label': 'ボーンを翻訳する', 'TranslateBonesButton.desc': 'Translates all bones using the internal dictionary and Google Translate', 'TranslateBonesButton.success': 'Translated {number} bones.', - 'TranslateObjectsButton.label': 'Translate Meshes & Objects', + 'TranslateObjectsButton.label': 'メッシュとオブジェクトを翻訳する', 'TranslateObjectsButton.desc': 'Translates all meshes and objects using the internal dictionary and Google Translate', 'TranslateObjectsButton.success': 'Translated {number} meshes and objects.', - 'TranslateMaterialsButton.label': 'Translate Materials', + 'TranslateMaterialsButton.label': 'マテリアルを翻訳する', 'TranslateMaterialsButton.desc': 'Translates all materials using the internal dictionary and Google Translate', 'TranslateMaterialsButton.success': 'Translated {number} materials.', - 'TranslateTexturesButton.label': 'Translate Textures', + 'TranslateTexturesButton.label': 'テクスチャを翻訳する', 'TranslateTexturesButton.desc': 'Translates all textures using the internal dictionary and Google Translate', 'TranslateTexturesButton.success_alt': 'Translated all textures', 'TranslateTexturesButton.error.noInternet': 'Could not connect to Google. Please check your internet connection.', 'TranslateTexturesButton.success': 'Translated {number} textures', - 'TranslateAllButton.label': 'Translate Everything', + 'TranslateAllButton.label': 'すべてを翻訳する', 'TranslateAllButton.desc': 'Translates everything using the internal dictionary and Google Translate', 'TranslateAllButton.success': 'Translated everything.', @@ -783,7 +783,7 @@ '\nFor updates and dicussions please join our Discord. The link can be found in the Credits panel down below.', # Tools Viseme - 'AutoVisemeButton.label': 'Create Visemes', + 'AutoVisemeButton.label': 'バイセムを作成する', 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' \ 'It will generate 15 shape keys from the 3 shape keys you specify', 'AutoVisemeButton.error.noShapekeys': 'This mesh has no shapekeys!', @@ -791,38 +791,38 @@ 'AutoVisemeButton.success': 'Created mouth visemes!', # Extentions - 'Scene.armature.label': 'Armature', + 'Scene.armature.label': 'アーマチュア', 'Scene.armature.desc': 'Select the armature which will be used by Cats', - 'Scene.zip_content.label': 'Zip Content', + 'Scene.zip_content.label': 'コンテンツを圧縮する', 'Scene.zip_content.desc': 'Select the model you want to import', - 'Scene.keep_upper_chest.label': 'Keep Upper Chest', + 'Scene.keep_upper_chest.label': '上部の胸の骨を保つ', 'Scene.keep_upper_chest.desc': 'VRChat now partially supports the Upper Chest bone, so deleting it is no longer necessary.' '\n\nWARNING: Currently this breaks Eye Tracking, so don\'t check this if you want Eye Tracking', - 'Scene.combine_mats.label': 'Combine Same Materials', + 'Scene.combine_mats.label': '同じマテリアルを組み合わせる', 'Scene.combine_mats.desc': 'Combines similar materials into one, reducing draw calls.\n\n' 'Your avatar should visibly look the same after this operation.\n' 'This is a very important step for optimizing your avatar.\n' 'If you have problems with this, uncheck this option and tell us!\n', - 'Scene.remove_zero_weight.label': 'Remove Zero Weight Bones', + 'Scene.remove_zero_weight.label': 'ゼロウェイトボーンを削除する', 'Scene.remove_zero_weight.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' '\nUncheck this if bones or vertex groups that you want to keep got deleted', - 'Scene.keep_end_bones.label': 'Keep End Bones', + 'Scene.keep_end_bones.label': 'エンドボーンを保持', 'Scene.keep_end_bones.desc': 'Saves end bones from deletion.' '\n\nThis can improve skirt movement for dynamic bones, but increases the bone count.' '\nThis can also fix issues with crumbled finger bones in Unity.' '\nMake sure to always uncheck "Add Leaf Bones" when exporting or use the CATS export button', - 'Scene.keep_twist_bones.label': 'Keep Twist Bones', + 'Scene.keep_twist_bones.label': 'ツイストボーンを保持', 'Scene.keep_twist_bones.desc': 'This will keep any bone with "Twist" in the name.' '\nSo if there are certain bones that you want to keep, you can add "Twist" to them and they won\'t get deleted.' '\n\nVRChat can now make use of twist bones, so you can use this option to keep them', - 'Scene.fix_twist_bones.label': 'Fix MMD Twist Bones', + 'Scene.fix_twist_bones.label': 'MMDツイスト ボーンを修正する', 'Scene.fix_twist_bones.desc': 'This will make MMD arm twist bones usable in VRChat.' '\nWIll only work if the twist bones are properly named.' '\nRequired names:' @@ -830,7 +830,7 @@ '\n - HandTwist[1-3]_[L/R]' '\n\nYou don\'t need to enable "Keep Twist Bones" for this to work', - 'Scene.join_meshes.label': 'Join Meshes', + 'Scene.join_meshes.label': 'メッシュを結合する', 'Scene.join_meshes.desc': 'Joins all meshes of this model together.' '\nIt also:' '\n - Applies all transformations' @@ -840,46 +840,46 @@ '\n' '\nINFO: You should always join your meshes', - 'Scene.connect_bones.label': 'Connect Bones', + 'Scene.connect_bones.label': 'ボーンを接続する', 'Scene.connect_bones.desc': 'This connects all bones to their child bone if they have exactly one child bone.\n' 'This will not change how the bones function in any way, it just improves the aesthetic of the armature', - 'Scene.fix_materials.label': 'Fix Materials', + 'Scene.fix_materials.label': 'マテリアルを修正する', 'Scene.fix_materials.desc': 'This will apply some VRChat related fixes to materials', - 'Scene.remove_rigidbodies_joints.label': 'Remove Rigidbodies and Joints', + 'Scene.remove_rigidbodies_joints.label': 'リジッドボディとジョイントを除去', 'Scene.remove_rigidbodies_joints.desc': 'Rigidbodies and joints are used by MMD software to simulate physics.' '\nThey are completely useless for VRChat, so removing them is recommended for VRChat users!', - 'Scene.use_google_only.label': 'Use Old Translations (not recommended)', + 'Scene.use_google_only.label': '古い翻訳を使用する(推奨しません)', 'Scene.use_google_only.desc': 'Ignores the internal dictionary and only uses the Google Translator for shape key translations.' '\n' '\nThis will result in slower translation speed and worse translations, but the translations will be like in CATS version 0.9.0 and older.' '\nOnly use this if you have animations which rely on the old translations and you don\'t want to convert them to the new ones', - 'Scene.show_more_options.label': 'Show More Options', + 'Scene.show_more_options.label': 'その他のオプションを表示', 'Scene.show_more_options.desc': 'Shows more model options', - 'Scene.merge_mode.label': 'Merge Mode', + 'Scene.merge_mode.label': 'マージモード', 'Scene.merge_mode.desc': 'Mode', - 'Scene.merge_mode.armature.label': 'Merge Armatures', + 'Scene.merge_mode.armature.label': 'アーマチュアをマージ', 'Scene.merge_mode.armature.desc': 'Here you can merge two armatures together.', - 'Scene.merge_mode.mesh.label': 'Attach Mesh', + 'Scene.merge_mode.mesh.label': 'メッシュを取り付ける', 'Scene.merge_mode.mesh.desc': 'Here you can attach a mesh to an armature.', - 'Scene.merge_armature_into.label': 'Base Armature', + 'Scene.merge_armature_into.label': 'ベースアーマチュア', 'Scene.merge_armature_into.desc': 'Select the armature into which the other armature will be merged\n', - 'Scene.merge_armature.label': 'Merge Armature', + 'Scene.merge_armature.label': 'メッシュを取り付ける', 'Scene.merge_armature.desc': 'Select the armature which will be merged into the selected armature above\n', - 'Scene.attach_to_bone.label': 'Attach to Bone', + 'Scene.attach_to_bone.label': '骨に取り付ける', 'Scene.attach_to_bone.desc': 'Select the bone to which the armature will be attached to\n', - 'Scene.attach_mesh.label': 'Attach Mesh', + 'Scene.attach_mesh.label': 'メッシュを取り付ける', 'Scene.attach_mesh.desc': 'Select the mesh which will be attached to the selected bone in the selected armature\n', - 'Scene.merge_same_bones.label': 'Merge All Bones', + 'Scene.merge_same_bones.label': 'すべてのボーンをマージ', 'Scene.merge_same_bones.desc': 'Merges all bones together that have the same name instead of only the base bones (Hips, Spine, etc).' '\nYou will have to make sure that all the bones you want to merge have the same name.' '\n' @@ -889,57 +889,57 @@ '\nThis can have unintended side effects, so check your model afterwards!' '\n', - 'Scene.apply_transforms.label': 'Apply Transforms', + 'Scene.apply_transforms.label': '変換を適用する', 'Scene.apply_transforms.desc': 'Check this if both armatures and meshes are already at their correct positions.' '\nThis will cause them to stay exactly where they are when merging', - 'Scene.merge_armatures_join_meshes.label': 'Join Meshes', + 'Scene.merge_armatures_join_meshes.label': 'メッシュを結合する', 'Scene.merge_armatures_join_meshes.desc': 'This will join all meshes.' '\nNot checking this will always apply transforms', - 'Scene.merge_armatures_remove_zero_weight_bones.label': 'Remove Zero Weight Bones', + 'Scene.merge_armatures_remove_zero_weight_bones.label': 'ゼロウェイトボーンを削除する', 'Scene.merge_armatures_remove_zero_weight_bones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' '\nUncheck this if bones or vertex groups that you want to keep got deleted', # Decimation - 'Scene.decimation_mode.label': 'Decimation Mode', + 'Scene.decimation_mode.label': '単純化モード', 'Scene.decimation_mode.desc': 'Decimation Mode', - 'Scene.decimation_mode.safe.label': 'Safe', + 'Scene.decimation_mode.safe.label': '安全', 'Scene.decimation_mode.safe.desc': 'Decent results - no shape key loss\n' '\n' 'This will only decimate meshes with no shape keys.\n' 'The results are decent and you won\'t lose any shape keys.\n' 'Eye Tracking and Lip Syncing will be fully preserved.', - 'Scene.decimation_mode.half.label': 'Half', + 'Scene.decimation_mode.half.label': 'ハーフ', 'Scene.decimation_mode.half.desc': 'Good results - minimal shape key loss\n' '\n' 'This will only decimate meshes with less than 4 shape keys as those are often not used.\n' 'The results are better but you will lose the shape keys in some meshes.\n' 'Eye Tracking and Lip Syncing should still work.', - 'Scene.decimation_mode.full.label': 'Full', + 'Scene.decimation_mode.full.label': 'フル', 'Scene.decimation_mode.full.desc': 'Best results - full shape key loss\n' '\n' 'This will decimate your whole model deleting all shape keys in the process.\n' 'This will give the best results but you will lose the ability to add blinking and Lip Syncing.\n' 'Eye Tracking will still work if you disable Eye Blinking.', - 'Scene.decimation_mode.custom.label': 'Custom', + 'Scene.decimation_mode.custom.label': 'カスタム', 'Scene.decimation_mode.custom.desc': 'Custom results - custom shape key loss\n' '\n' 'This will let you choose which meshes and shape keys should not be decimated.\n', - 'Scene.selection_mode.label': 'Selection Mode', + 'Scene.selection_mode.label': '選択モード', 'Scene.selection_mode.desc': 'Selection Mode', - 'Scene.selection_mode.shapekeys.label': 'Shape Keys', + 'Scene.selection_mode.shapekeys.label': 'シェイプキー', 'Scene.selection_mode.shapekeys.desc': 'Select all the shape keys you want to preserve here.', - 'Scene.selection_mode.meshes.label': 'Meshes', + 'Scene.selection_mode.meshes.label': 'メッシュ', 'Scene.selection_mode.meshes.desc': 'Select all the meshes you don\'t want to decimate here.', - 'Scene.add_shape_key.label': 'Shape', + 'Scene.add_shape_key.label': '形状', 'Scene.add_shape_key.desc': 'The shape key you want to keep', - 'Scene.add_mesh.label': 'Mesh', + 'Scene.add_mesh.label': 'メッシュ', 'Scene.add_mesh.desc': 'The mesh you want to leave untouched by the decimation', - 'Scene.decimate_fingers.label': 'Save Fingers', + 'Scene.decimate_fingers.label': '指を救う', 'Scene.decimate_fingers.desc': 'Check this if you don\'t want to decimate your fingers!\n' 'Results will be worse but there will be no issues with finger movement.\n' 'This is probably only useful if you have a VR headset.\n' @@ -951,7 +951,7 @@ 'RingFinger(1-3)_(L/R)\n' 'LittleFinger(1-3)_(L/R)', - 'Scene.decimate_hands.label': 'Save Hands', + 'Scene.decimate_hands.label': '手を救う', 'Scene.decimate_hands.desc': 'Check this if you don\'t want to decimate your full hands!\n' 'Results will be worse but there will be no issues with hand movement.\n' 'This is probably only useful if you have a VR headset.\n' @@ -964,152 +964,152 @@ 'RingFinger(1-3)_(L/R)\n' 'LittleFinger(1-3)_(L/R)', - 'Scene.decimation_remove_doubles.label': 'Remove Doubles', + 'Scene.decimation_remove_doubles.label': '重複を削除', 'Scene.decimation_remove_doubles.desc': 'Uncheck this if you got issues with with this checked', - 'Scene.max_tris.label': 'Tris', + 'Scene.max_tris.label': 'トリス', 'Scene.max_tris.desc': 'The target amount of tris after decimation', # Eye Tracking - 'Scene.eye_mode.label': 'Eye Mode', + 'Scene.eye_mode.label': 'アイモード', 'Scene.eye_mode.desc': 'Mode', - 'Scene.eye_mode.creation.label': 'Creation', + 'Scene.eye_mode.creation.label': '創作', 'Scene.eye_mode.creation.desc': 'Here you can create eye tracking.', - 'Scene.eye_mode.testing.label': 'Testing', + 'Scene.eye_mode.testing.label': 'テスティング', 'Scene.eye_mode.testing.desc': 'Here you can test how eye tracking will look in-game.', - 'Scene.mesh_name_eye.label': 'Mesh', + 'Scene.mesh_name_eye.label': 'メッシュ', 'Scene.mesh_name_eye.desc': 'The mesh with the eyes vertex groups', - 'Scene.head.label': 'Head', + 'Scene.head.label': '頭', 'Scene.head.desc': 'The head bone containing the eye bones', - 'Scene.eye_left.label': 'Left Eye', + 'Scene.eye_left.label': '左目', 'Scene.eye_left.desc': 'The models left eye bone', - 'Scene.eye_right.label': 'Right Eye', + 'Scene.eye_right.label': '右目', 'Scene.eye_right.desc': 'The models right eye bone', - 'Scene.wink_left.label': 'Blink Left', + 'Scene.wink_left.label': '左点滅', 'Scene.wink_left.desc': 'The shape key containing a blink with the left eye', - 'Scene.wink_right.label': 'Blink Right', + 'Scene.wink_right.label': '右点滅', 'Scene.wink_right.desc': 'The shape key containing a blink with the right eye', - 'Scene.lowerlid_left.label': 'Lowerlid Left', + 'Scene.lowerlid_left.label': '下まぶた左', 'Scene.lowerlid_left.desc': 'The shape key containing a slightly raised left lower lid.\n' 'Can be set to "Basis" to disable lower lid movement', - 'Scene.lowerlid_right.label': 'Lowerlid Right', + 'Scene.lowerlid_right.label': '下まぶた右', 'Scene.lowerlid_right.desc': 'The shape key containing a slightly raised right lower lid.\n' 'Can be set to "Basis" to disable lower lid movement', - 'Scene.disable_eye_movement.label': 'Disable Eye Movement', + 'Scene.disable_eye_movement.label': '目の動きを無効にする', 'Scene.disable_eye_movement.desc': 'IMPORTANT: Do your decimation first if you check this!\n' '\n' 'Disables eye movement. Useful if you only want blinking.\n' 'This creates eye bones with no movement bound to them.\n' 'You still have to assign "LeftEye" and "RightEye" to the eyes in Unity', - 'Scene.disable_eye_blinking.label': 'Disable Eye Blinking', + 'Scene.disable_eye_blinking.label': '瞬きを無効にする', 'Scene.disable_eye_blinking.desc': 'Disables eye blinking. Useful if you only want eye movement.\n' 'This will create the necessary shape keys but leaves them empty', - 'Scene.eye_distance.label': 'Eye Movement Range', + 'Scene.eye_distance.label': '眼球運動範囲', 'Scene.eye_distance.desc': 'Higher = more eye movement\n' 'Lower = less eye movement\n' 'Warning: Too little or too much range can glitch the eyes.\n' 'Test your results in the "Eye Testing"-Tab!\n', - 'Scene.eye_rotation_x.label': 'Up - Down', + 'Scene.eye_rotation_x.label': '上 - 下', 'Scene.eye_rotation_x.desc': 'Rotate the eye bones on the vertical axis', - 'Scene.eye_rotation_y.label': 'Left - Right', + 'Scene.eye_rotation_y.label': '左 - 右', 'Scene.eye_rotation_y.desc': 'Rotate the eye bones on the horizontal axis.' '\nThis is from your own point of view', - 'Scene.iris_height.label': 'Iris Height', + 'Scene.iris_height.label': 'アイリスの高さ', 'Scene.iris_height.desc': 'Moves the iris away from the eye ball', - 'Scene.eye_blink_shape.label': 'Blink Strength', + 'Scene.eye_blink_shape.label': 'まばたきの強さ', 'Scene.eye_blink_shape.desc': 'Test the blinking of the eye', - 'Scene.eye_lowerlid_shape.label': 'Lowerlid Strength', + 'Scene.eye_lowerlid_shape.label': '下まぶたの強さ', 'Scene.eye_lowerlid_shape.desc': 'Test the lowerlid blinking of the eye', - 'Scene.mesh_name_viseme.label': 'Mesh', + 'Scene.mesh_name_viseme.label': 'メッシュ', 'Scene.mesh_name_viseme.desc': 'The mesh with the mouth shape keys', # Visemes - 'Scene.mouth_a.label': 'Viseme AA', + 'Scene.mouth_a.label': 'バイセム AA', 'Scene.mouth_a.desc': 'Shape key containing mouth movement that looks like someone is saying "aa".\nDo not put empty shape keys like "Basis" in here', - 'Scene.mouth_o.label': 'Viseme OH', + 'Scene.mouth_o.label': 'バイセム OH', 'Scene.mouth_o.desc': 'Shape key containing mouth movement that looks like someone is saying "oh".\nDo not put empty shape keys like "Basis" in here', - 'Scene.mouth_ch.label': 'Viseme CH', + 'Scene.mouth_ch.label': 'バイセム CH', 'Scene.mouth_ch.desc': 'Shape key containing mouth movement that looks like someone is saying "ch". Opened lips and clenched teeth.\nDo not put empty shape keys like "Basis" in here', - 'Scene.shape_intensity.label': 'Shape Key Mix Intensity', + 'Scene.shape_intensity.label': 'シェイプキーミックスの強度', 'Scene.shape_intensity.desc': 'Controls the strength in the creation of the shape keys. Lower for less mouth movement strength', # Bone Parenting - 'Scene.root_bone.label': 'To Parent', + 'Scene.root_bone.label': '親に', 'Scene.root_bone.desc': 'List of bones that look like they could be parented together to a root bone', # Optimize - 'Scene.optimize_mode.label': 'Optimize Mode', + 'Scene.optimize_mode.label': '最適化モード', 'Scene.optimize_mode.desc': 'Mode', - 'Scene.optimize_mode.atlas.label': 'Atlas', + 'Scene.optimize_mode.atlas.label': 'アトラス', 'Scene.optimize_mode.atlas.desc': 'Allows you to make a texture atlas.', - 'Scene.optimize_mode.material.label': 'Material', + 'Scene.optimize_mode.material.label': 'マテリアル', 'Scene.optimize_mode.material.desc': 'Some various options on material manipulation.', - 'Scene.optimize_mode.bonemerging.label': 'Bone Merging', + 'Scene.optimize_mode.bonemerging.label': '骨のマージ', 'Scene.optimize_mode.bonemerging.desc': 'Allows child bones to be merged into their parents.', # Bone Merging - 'Scene.merge_ratio.label': 'Merge Ratio', + 'Scene.merge_ratio.label': 'マージ率', 'Scene.merge_ratio.desc': 'Higher = more bones will be merged\n' 'Lower = less bones will be merged\n', - 'Scene.merge_mesh.label': 'Mesh', + 'Scene.merge_mesh.label': 'メッシュ', 'Scene.merge_mesh.desc': 'The mesh with the bones vertex groups', - 'Scene.merge_bone.label': 'To Merge', + 'Scene.merge_bone.label': 'マージするには', 'Scene.merge_bone.desc': 'List of bones that look like they could be merged together to reduce overall bones', - 'Scene.embed_textures.label': 'Embed Textures on Export', + 'Scene.embed_textures.label': 'エクスポート時にテクスチャを埋め込む', 'Scene.embed_textures.desc': 'Enable this to embed the texture files into the FBX file upon export.' '\nUnity will automatically extract these textures and put them into a separate folder.' '\nThis might not work for everyone and it increases the file size of the exported FBX file', - 'Scene.use_custom_mmd_tools.label': 'Use Custom mmd_tools', + 'Scene.use_custom_mmd_tools.label': 'カスタムmmd_toolsを使用する', 'Scene.use_custom_mmd_tools.desc': 'Enable this to use your own version of mmd_tools. This will disable the internal cats mmd_tools', - 'Scene.debug_translations.label': 'Debug Google Translations', + 'Scene.debug_translations.label': 'Google翻訳をデバッグ', 'Scene.debug_translations.desc': 'Tests the Google Translations and prints the Google response in case of error', # Updater - 'CheckForUpdateButton.label': 'Check now for Update', + 'CheckForUpdateButton.label': '今すぐアップデートを確認', 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', - 'UpdateToLatestButton.label': 'Update Now', + 'UpdateToLatestButton.label': '今すぐアップデート', 'UpdateToLatestButton.desc': 'Update CATS to the latest version', - 'UpdateToSelectedButton.label': 'Update to Selected version', + 'UpdateToSelectedButton.label': '選択したバージョンに更新', 'UpdateToSelectedButton.desc': 'Update CATS to the selected version', - 'UpdateToDevButton.label': 'Update to Development version', + 'UpdateToDevButton.label': '開発版に更新', 'UpdateToDevButton.desc': 'Update CATS to the Development version', - 'RemindMeLaterButton.label': 'Remind me later', + 'RemindMeLaterButton.label': '後で通知する', 'RemindMeLaterButton.desc': 'This hides the update notification \'til the next Blender restart', 'RemindMeLaterButton.success': 'You will be reminded later', - 'IgnoreThisVersionButton.label': 'Ignore this version', + 'IgnoreThisVersionButton.label': 'このバージョンを無視', 'IgnoreThisVersionButton.desc': 'This ignores this version. You will be reminded again when the next version releases', 'IgnoreThisVersionButton.success': 'Version {name} will be ignored.', - 'ShowPatchnotesPanel.label': 'Patchnotes', + 'ShowPatchnotesPanel.label': 'パッチノート', 'ShowPatchnotesPanel.desc': 'Shows the patchnotes of the selected version', 'ShowPatchnotesPanel.releaseDate': 'Released: {date}', - 'ConfirmUpdatePanel.label': 'Confirm Update', + 'ConfirmUpdatePanel.label': 'アップデートを確認', 'ConfirmUpdatePanel.desc': 'This shows you a panel in which you have to confirm your update choice', 'ConfirmUpdatePanel.warn.dev1': 'Warning:', 'ConfirmUpdatePanel.warn.dev2': ' The development version of CATS if the place where', @@ -1119,17 +1119,17 @@ 'ConfirmUpdatePanel.ShowPatchnotesPanel.label': 'Show Patchnotes', 'ConfirmUpdatePanel.updateNow': 'Update now:', - 'UpdateCompletePanel.label': 'Installation Report', + 'UpdateCompletePanel.label': 'インストールレポート', 'UpdateCompletePanel.desc': 'The update is now complete', 'UpdateCompletePanel.success1': 'CATS was successfully updated.', 'UpdateCompletePanel.success2': 'Restart Blender to complete the update.', 'UpdateCompletePanel.failure1': 'Update failed.', 'UpdateCompletePanel.failure2': 'See Updater Panel for more info.', - 'UpdateNotificationPopup.label': 'Update available', + 'UpdateNotificationPopup.label': 'アップデートが利用可能です', 'UpdateNotificationPopup.desc': 'This shows you that an update is available', 'UpdateNotificationPopup.newUpdate': 'CATS v{name} available!', - 'UpdateNotificationPopup.ShowPatchnotesPanel.label': 'Show Patchnotes', + 'UpdateNotificationPopup.ShowPatchnotesPanel.label': 'パッチノートを見る', 'check_for_update.cantCheck': 'Could not check for updates, try again later', @@ -1139,31 +1139,31 @@ 'draw_update_notification_panel.success': 'Restart Blender to complete update!', 'draw_update_notification_panel.newUpdate': 'CATS v{name} available!', - 'draw_update_notification_panel.UpdateToLatestButton.label': 'Update Now', - 'draw_update_notification_panel.RemindMeLaterButton.label': 'Remind me later', - 'draw_update_notification_panel.IgnoreThisVersionButton.label': 'Ignore this version', + 'draw_update_notification_panel.UpdateToLatestButton.label': '今すぐアップデート', + 'draw_update_notification_panel.RemindMeLaterButton.label': '後で通知する', + 'draw_update_notification_panel.IgnoreThisVersionButton.label': 'このバージョンを無視', 'draw_updater_panel.updateLabel': 'Updates:', 'draw_updater_panel.updateLabel_alt': 'CATS Updater:', 'draw_updater_panel.success': 'Restart Blender to complete update!', - 'draw_updater_panel.CheckForUpdateButton.label': 'Checking..', - 'draw_updater_panel.UpdateToLatestButton.label': 'Update now to {name}', - 'draw_updater_panel.CheckForUpdateButton.label_alt': 'Check now for Update', - 'draw_updater_panel.UpdateToLatestButton.label_alt': 'Up to Date!', - 'draw_updater_panel.UpdateToSelectedButton.label': 'Install version:', - 'draw_updater_panel.UpdateToDevButton.label': 'Install Development Version', + 'draw_updater_panel.CheckForUpdateButton.label': 'チェック中..', + 'draw_updater_panel.UpdateToLatestButton.label': '今すぐ{name}に更新する', + 'draw_updater_panel.CheckForUpdateButton.label_alt': '今すぐアップデートを確認', + 'draw_updater_panel.UpdateToLatestButton.label_alt': '最新の!', + 'draw_updater_panel.UpdateToSelectedButton.label': 'インストールバージョン:', + 'draw_updater_panel.UpdateToDevButton.label': '開発バージョンのインストール', 'draw_updater_panel.currentVersion': 'Current version: {name}', - 'bpy.types.Scene.cats_updater_version_list.label': 'Version', + 'bpy.types.Scene.cats_updater_version_list.label': 'バージョン', 'bpy.types.Scene.cats_updater_version_list.desc': 'Select the version you want to install\n', - 'bpy.types.Scene.cats_update_action.label': 'Choose action', + 'bpy.types.Scene.cats_update_action.label': 'アクションを選択', 'bpy.types.Scene.cats_update_action.desc': 'Action', - 'bpy.types.Scene.cats_update_action.update.label': 'Update Now', + 'bpy.types.Scene.cats_update_action.update.label': '今すぐアップデート', 'bpy.types.Scene.cats_update_action.update.desc': 'Updates now to the latest version', - 'bpy.types.Scene.cats_update_action.ignore.label': 'Ignore this version', + 'bpy.types.Scene.cats_update_action.ignore.label': 'このバージョンを無視', 'bpy.types.Scene.cats_update_action.ignore.desc': 'This ignores this version. You will be reminded again when the next version releases', - 'bpy.types.Scene.cats_update_action.defer.label': 'Remind me later', + 'bpy.types.Scene.cats_update_action.defer.label': '後で通知する', 'bpy.types.Scene.cats_update_action.defer.desc': 'Hides the update notification til the next Blender restart', From 96b8a7d3756384cdddc821bb70f71177f3d28023 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 2 Sep 2020 11:59:15 +0200 Subject: [PATCH 04/64] Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 --- README.md | 3 ++- resources/icons/supporters/Lhun.png | Bin 2296 -> 2808 bytes resources/icons/supporters/~ellie~.png | Bin 0 -> 2215 bytes .../icons/supporters/\342\231\241DAWKY.png" | Bin 0 -> 2367 bytes resources/supporters.json | 6 ++++++ tools/armature_manual.py | 6 +++++- updater.py | 2 +- 7 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 resources/icons/supporters/~ellie~.png create mode 100644 "resources/icons/supporters/\342\231\241DAWKY.png" diff --git a/README.md b/README.md index cb8621b0..e1dac92b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cats Blender Plugin (0.17.0) +# Cats Blender Plugin (0.17.1) A tool designed to shorten steps needed to import and optimize models into VRChat. Compatible models are: MMD, XNALara, Mixamo, Source Engine, Unreal Engine, DAZ/Poser, Blender Rigify, Sims 2, Motion Builder, 3DS Max and potentially more @@ -302,6 +302,7 @@ It checks for a new version automatically once every day. #### 0.17.1 - Changed link to a new vrm importer since the old one dropped support +- Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 #### 0.17.0 - **Cats is now fully compatible with Blender 2.83!** diff --git a/resources/icons/supporters/Lhun.png b/resources/icons/supporters/Lhun.png index ca4034ccf4b4aab13e69f6dfb0e540e42c539468..f095279dfcd74bcc3cb5a7116123441e0fe431ac 100644 GIT binary patch literal 2808 zcmZ`*c{tQ-8~zz&t1)xzl4Zy`!yI&Ml^Kn#3}z@xOk*32$v8}j(&%f5P$XN~4oxZ9 zV#tzll58;{2HCe3pXJzPnUC+k?~m_#-{-ln>%Q;jdY}K^JKoXW`k1J^C;$M*Y;7?3 z15`dNVbDR}m0c)200DnA77YLu55;!91P?_Y8$1>OBGmyPngIa64yfqw0B{Wn06&QU zV44X4G9lS@xYGy15$_Au7=U+p^54BkJzzvaY}~>C0K9rwdx+S#TRk#3uqUhV_T<*nP&FhZu>y>z^|Cj zyj$C|bV_*j+1+JvhwF-?^*h}oBMQQ*HmpDBlT&Hm?6Zj^00L$1^7i*Or)Qc}G!pUr z`6pIxW$619v>PjIzPAnkyyVb)?_teYR?FO{p-K&|d0+7Ljg2vCO?}SX2lMTvXPeV?4exJ?jOj#ytKJ<;X=}ET5S9| z>%eH`3rSuB-Ub3EoAMXR*?mijw`UW0$((Ov`>WK1ZSrGAaVuB7C&X}36-}2M-IO;(yMY-|*Ewd&}!+LxHqcSdVEW>=MXGi+>O=_vq^$9^Qbx{lD0 z!&r&cxu@)aY~%eTJ$l)tkwNnY9pf`!w|*^-jcyNz#dt)q8dLO2gK8w5!!mS_H23^{ zbBVVyJo}1&h~BtfsF}UEIBI(H=hNoqDA@0A1$QYVhBpb<>_u-ftzSbBBlJEO<%j@^+vE%qAf zBXzk%PV&XD`~6hotPg=}L&at0>|gyq-p*>x(BxB#p>imG;c_b)!vDO^+DjvQiaF`7 zuyh~wpFg#*iEOp~-GRuOIJO!Y>we<_j zi-;t#q(2-JjaUFcD)WXNCsPI4RTZ?!AK$Ys>FQ>n665;$t24W3r1{6^7!sW_+&PXz zqE{+1+L9T~rzuq>oD$~+703i@2fC`{DOmrtWwJ0M7Be-1ukt9?P3LBeTA4h^YU;7A zD)~F>K7mLBf|xBKBQ5au+S2Q{K0mPj4$}r%0!0+lzM=-@JdV1-l^Dq{B!pwfo5`Us z=R$IbS9|^$c+?bDyQ#XwvmiIR1(xX^*4-XjYS^YfuGYy3aRYsXQA%mpR7H_7|g$AS@VXi_4g7UUd?`V zeMfZ($E$FNklFpxUDd0o2@oxmzGJ4n9lLNZR*99YGNJgcTb%L5vmFp4@tv4Tv0+sv zM#m8jV!TY$eeH23+i>&Ve!*CnULAX=C3v(G7kIUKn6#_pt!%CxEho zrFR%EE-0Li@S1fm>?L+2%o?OAJ8d&3Mn0?sSC5(67!5Twl&jzshH!R$i*WYT5N! zQSnZyuW#&wrS-n@^vl;0%__B7=)p-_?+xaOd!K{1_p-v-L;L-C*U#5BT!||-8HboP zmP$4}lk>b0{8!@y=7eUDG6vl%HfDMsbOd22?*AU3ki{ak(m(gyZUd8xwrgKhdajJ{ z-I?yz@YZM|Xa^f=k)58DZK3Pj%bq`NPr8tCE!fm#Eg{RVA})W`*`+86MO{|w?0r+S z8?Xx|)6cqaz0b@V7cmxc=5nke_-bRnK4r74P`q@&d{q9}0|X%v`I3?iTvQfX-r=p? z#KgooT&BlAY9F3rMsDBmFJG_XrKEkj7~G>*mhJ$TaXG31{*6!cGL{@(6%J9f>(f%h z^@!M~clU>_dyPbVzO9+mr=#b|4L}3*xu_FEbhLf2X^4_#}{I1H>A1zO=#% z2s|G8j_~QIWBs*4N+B~=PQ+%$@Si6zA5;;R&Y)-j6sH-@ z07nZy@+)TLp7fIBIBI=4A5>xqqzJtBO-Cf;_x_PZo_$l1?WdgIKhA;tV?!4?_+(LW z5J3P3-~yEeQ4f?9dg9u~Wl(=P5K9Erax$GE4{74?AeET7XiXSKz&c4-4bEt?umM>- zlTFSNk%I97`Q=DapbeT83aRohky1vX1!4J~=0phKh&q}mAgl~xXu?EMd~w2oSFy1V zrvKGU;lrUEXEeoN)^t_x{8yj}``8bk;5_lZ;?!zYJJG#^WF|qgbfuxM(E_NUfFs6A z*_leEgejw~k8{Eg@*Z&VQ1=Rg|8@4x$=J6wwziY7E@9*BUgr9Gz9g7~lh089QN1^@s6 delta 2295 zcmVjAQV@$RaJ?krHv}8Dp8dvtx{E0n>GP8X(P3NG^9D&rhoMKgQ!we1PDlJ zVy>V-h>gSH1s~YhCiqO_UY$BOGvVSbz?RuUF#TDdgU9!m zHbRO-p_?W?K5-E51j2ZuZaIQE6+$|N;{yecH!a>sUVmWlUdQPfb0=wM=UREq_9Zpe z`D4L_C(0ILS9Ae>=Z~OLQ}JBqWn?pHsZ0(1IraYVhw{nN&R~UfGXPe z0DpU?4qWZ&_8ZQs!YXJ*mK#SI30c>as1YcZDcpaRVt=Hw(l-9+#p&0Q!tDl-WkK;p6(f* zT+llLG32$cdBc#WkI#_CiS26*^^3)vr+?XCoiPWIDqI#uK1x~OaA1gx5x0;gd@6as z<0?X4S825}+%#i-(2sI&01<~1KH@|Xee-^D7|r6)+4P~K8|KAWP85zRn(ID{PyOKkVKsp^#y(ZX)Gtpk=c z8`cPkJTuN8g^pXQOqDTzX5Ii^dTF=cJK+B5yGNQanq@FB_&Jj4Bm!O^Dq`h`k*%`K4VlOn@dr`t4}T+L>KM$6ioxcO&Km%ChmD84{(9ccqPepLpY~ma zK!kTX16Z^mhRti&A|8#CSH7ra48zd#GCOruB4}Qb(IrcI+Q>XF9X!)I-Oj8^D9Mt- zxp{4QRV_6f3?5na5SFdng;9yanaf=`bfTGz=)?(rJRu5dV+m2v7etscqkkk)f?y~N zh0FddbNIwt_gv^~0IFBV(p5$A%?^{R$&>tU+wd?_Mg`hCP9hjyjD_WO_~hbg2u>GD ziFg9f+aNl7yD>OyrMiIt^$enxYjMTfpZef>(f*OhWdcfNLd-{BG!=fc;dj|%0YJZIBic_uk@+OJ| z2G?%(;Z$1-e*DBPY7<#3tF6WARh!UxJB_l~64b{8gY%7ir+*;b$m-1FObBl)qvrgT zaLUHNy!)xv>!0^Gh*WAzLLsE72OOog>&stShpMtNoNjF)!O`*HvPSf!Nf{~rV&>ZE zQ?d4KAH8;AKjC{lmw(mo83yit*265`ig^9ozrJM~8wmvM1~E+MjjL9XCU{BhG`k=r zx^JLiK_!B09v&{Q`YQrIc4`MnSTDGKtSnWp% zsbFz1XpdXDZZY2i(myhcwvOxY6?xH@8UF0&e|@u)XW4N6j(_pbxope{z^^eioXq?r zEsDG>OITD}2dmjJ#&g~h9qX__)W$1_^*&tf>+1d6hlk%Jjp+6*5AK)y!Q zo@p4bXMF4kvws{CzH3#rJUv@cQ-e?_2$_rvQ~@p0845L=PA5c3q>N}q+pWYa7us*0 zyRX&h02+@iWrufn)Ec7kFf?-;da#}fel_`;<*6lqXz_Av-LMWkm07#!kZ}iA}UMl%wMAX*$nn?)#kht=6)R>uzF z&kjQ}<#6)7<|Wh59@pd8H<&PnCX6>eS?_$nQ}q{W^ev->gP9^r_DU@PRMQJ!%$Yxl z7Ecu&X=+H#@o%gJSpOmRz0SB;m09b^GB5BwI9R@rEuQ#pXLS3EaewL7{{avV$AzV= R%$)!L002ovPDHLkV1mctU6ud< diff --git a/resources/icons/supporters/~ellie~.png b/resources/icons/supporters/~ellie~.png new file mode 100644 index 0000000000000000000000000000000000000000..91333346c8434ee3410fb6591dd24d5bda5b5b91 GIT binary patch literal 2215 zcmZ{mc{J3E8^*sgwm~!qWhrCHlF87BWN9YM3})iL00bcbVAT--G^hX|>|4-k zs(BKyI~!tjf!}|oq`mUdiNg_y!|QP@LZGZN3WD+u@h46-P8Vfi)%`N(O`07>uk|TrDGhxS$7kSM$gN{@o=B% z<+GM%O_Vk;DUh9;*s@yK8fuORKL~p5GkaU`;p9SKpQ#wXy4h2Unw`j?Rg3r6V$zhr zR5k+W5+aVPcR}G%;<(2!UO%lBbf_nh2qS1pSC^of4DY1 zN_JsnH3jp8u zh=rTM$M$m^;}^L>eaCJRsc{;Bki+zqg>HGN7_P3s4g8Iz&~|9eIHs!Y z^E)oP8VS4WCQuc6OxEo@&DH*X>VR$POj=f-0D9nnA}nb6nzIbH<`73x-xCoC&Zl<6 z{3BTEBF_aNDAVlNGG7J>LTA^FMaPfo{Z_q@9VDM3o2sNmZmZp!FZjw@ZANJBMkrb- zDWW@V8#%N{o-yoxq(sNN@kxlM*!+?Yx?&nOP?9HlB2JDH#j))Dz>Jgqm&YnCy_rDV zJ?iab<~~3ghj|lmkZ2F}u~=&xCze-vx*=1&h$@Nj!g+{$Q2BzX?VXi3m<_$^WKE{; zG{138Cw^ssZ=0oI#5)=gx7fOLC3K@Lac@b|Y^u*H?>2t`-BzBh(RQ^h zYF-~HoQiPHMkLD7YMq(p+s}Gl;S?A=HwT4hXdl1$9{im4yff{H6!9koXY2;ll)mtW z;YF8ns(!wka%cKSa4ZsHZCZwfA9uzeJvGXBA#Rwm8SAGen23uidv?Q3%(C`VZ-hKI z97;3wHFe`+eL5Z-j_!zcOf6oG;AP7BEiY9L@Iza=b8~(t!^H?1J7OO^KeZp-=55%b-$5g$YPgq)+2E|H`E)!ZMx1GUz_wfAKoJV{6# zg=AMYe*8Mb5pZvLIpVXvxaK5=>z}#=n*TEn!y zYmK)Q-@02$b!~*4xhtlL&E1Vj9H-*C0jQxlnj|zSBvll8+jDuy3oZZVbTW=XFpGU5oI1R}G znOv{ZY5r4(=`@l`U}p+JAMg^eV#)dzB2Bh`x*&R0#k|}p0q&_!)-SV6t@Ud3YN!h+ zGKgg7v2(MRXgcNDuU9QbSB2Cff5w6yuRkf*(>0p%c@iPscVRRSloeeAy1bQ8j|g2H zQ+RX55Gg9UXp^xbZ?Lo$d{sV9`Sj(PH-$|#+uy%U+F^r3zxQ;Hz3koVWR6ZvGaAJx zP-;br&xEG`HcH5Y`*}?}YRoJ3J}I1`#ERf301-B{&Y;|qB&aB<^ic^sMlL&`)@J26 z+wvJr1fRXRxGy}y7*Eq;;9YZPYP%o$XfcB;qoMqhdtAlfF5h^EiM)HMOz<7mXs@{z z9Jt#QTj6h46SJO$t*2@lBHo56M5UZ6w9b$%@xa$ow*%kCk}zXEBdxto*`_q#1zCM>MTD5j~8I^MBR&Dg?SBGK!98SokJikwAfiV7LX z7O3A7o&HA7cNMy3l223ntfF?MzxsE0T!`BES>BI5ehE&m<>(38q;RqhxDz~f9;DjA zleQi8+&E9S97i$XV%8`RIX?&+UP;sGS|MnY-KeZIktU^Sy|YUNVe8g;g>Xf;3>m5U zPzGrNwe&&yr{3Hoh@T-4WpasjX+Sx)v1f9rxwH$!t~@ zFPVvouL@#g!eQi3B=U8wH1ndqdadVjrPDTd&j!f`^)u3V=$>!IGBk`Z(K_TfG1IwI4Av19XnA6nLxoU+2hCes z18wpheC^kKT4n6*51t3dvOtr;DPXGX?N6>wNc8vKCqB8Vb literal 0 HcmV?d00001 diff --git "a/resources/icons/supporters/\342\231\241DAWKY.png" "b/resources/icons/supporters/\342\231\241DAWKY.png" new file mode 100644 index 0000000000000000000000000000000000000000..a7fdb2dd25aabe770cd8b449c351931c204956e3 GIT binary patch literal 2367 zcmZ{mc{tQ-8^?c=^-z{EQ)8VOCfhJ(ES+(bWtbrp#vpsl*c;1;5)sLa>_oD(NQ%l@ zsgz^SII>ez8e<(qR5&qb&iluEz5l$|b3OO>y1$?2dp+0v*Yhw=*%Ktiq2d4lNLmx| zj(k<&Z?LE^zutB$!}Dc-fSH{c0Mupu_RCj@->di$9qj<%0ulfcQvhIxZzV1OKr|Wv zmV5vJR|Ei%@RAn}fAE3unG*y&!25UIYPp}scSPvcc9tR>5h($6C84S$OTHyQi?k;R zyt@-q%*S7>@n$4q_oF=bP>)&Z?n|GWmq)oDn_8YW$j>yDW~_8XSlF_qY!%F5DdrVm zIXV33(2$5t`VKjpsi}0jOtXM2hdO2M{=oD%Gf90C!dtErI>`_3}JaHtMrR~#}0_! zN*+c=z+0nzy4p_<`+)Q`E+2O;a~;>fy~g+)T#gI}M{+($1Z`<+|EBI-A-69RK=2|2 zUnsTJU$1U$di!YX`$J5TROS;aQD>r?*HexCq|P@eXqae$&6Q2|&@Z995wo5H^r6?g zG3@4m@ic?7<2{hM3!PMN5<_^Z4OI_kP4-XDdBuH=+lbsF+#D&Z%UX_7YW*%`O5kE8 zvW}0Ak2I~kO;mc*gk~R9ry`0a-~+Y(NthF^z3>p@WD?VGe2%&K<8$lguh|er7S!Y& zBOv$}Dd(Q4>wcif2|S_2>tXe?x(Txj!!YUk(*66v7kU$B5pdHBHbU-%9OwbC+KoG^R5WLwBRM<-_XxGJJn^xAvmofzRkf@rk&r-h zi+gDd4GC~39C4F>ukg+RBfoB-HYZO>)dEh2PoNK9b%6?9Gg)NiV45pA|&^g=>anr z46%NG#m0m^+^$^p3Tu31{@BQ|rle~cLpP-=A??EGbq&Mfk%0;(Gc_4sfV=G~<#?X? zyAhLZHu%lIp}~g`%c;8=7+AwDe5XXwHh7o3`Q;Q00TGo2+!GTY^nwHnEC`9dvkfl2 z6a6;5xo)=Arfxbj5#TiU!w(gcv*BVIh60^1Bd*~`6Jds{4ZN>cS}!oM6rj|Xq?nVtr*hWN7`EClqndy2%m?AI z>WB}chaTBrt4G1Xc!`S4gk+Vy9^B9sZI^z@!G?Q+zEvJaU#6ji<~!6rtTiMZ*uPIU z-4atn_7lA_ClKee90jP3RY97*`mQp*Gs8fQIv|uiDOOC(%(QCjN-9J*!NI~b$Vy8; zr@UTl)U{@MVAPF3ecG``6nD-6xwcXno|_JBZBi7{?Kb=6DlXm+ijs+42ET$QYJ}0XHHVPgLsgl{<4^UvqT6IMmLK*;u&2c|?;SFq zn8*+B!4k_rhnIpngcR}!N3{=rnbw}A?A5-=GP*%Iw`*}Rj&^OruHmo3px6b}#b+%k z?@js^Z)lf!Xd&#GPSH=YCwHK%_2aTt#T6zqFh^@N@?rBNeK0>&mX+tKo&U4>^cW7Z^v-WSf~6)r@tDhvGk#`r9%0gg-I3TM^oCAgyQa6{35C{#s&Z!I z`ccWy<&`5-aV5690){-cq|HeMN*ho0@gefzHtej5NgFy|2>6l zC#mx)5E|Tca%@Ycz7SI2ayWq|)_(7<)xM6GGexO?KA34=es0vdFs(XgaMg4P{AqXF zb8}U6qqWaTfoxJz=u{I~ET$oiD^-{MCc9tGBPHA^z1*3UXpNjDlt@^necDAOY$cJq zMlA57*lXy?n}|WWpmT!qbq(|hj zQUkkc=Ky|x=TjSO9Y(P{veY>JEJQB}LxBd(XeyWY<&+t>Y6WuNPkl6xnFc{b$6cZ( zJ>?hQY*=~mhV{U?ZacKEUHU8uFQvO*tGNqH)@#3>@EDK9Gj&SMt?kzg*5jdgL5gNc zwv)E#L9dwnS|~@p^Y3*T)sq79A`mZ!6EpAC=E{`WB?IKai-^DrML2selamFX2{8BR zo+PYRDb zi!iz3atf-WBXtbLXOFYvaBsJcHI>>O6lTyZtLFVz_3Ldq)#db<2MYW9PxZL@M{0Ex zBld@UxXheqL)_LZOXjneLO1H+THKgSGLd7pH2$`oPI zsB};15f=1ul%ga=PCK6$?w* zpqr|$jYDI!(EN%HwA|$XFa9SGM)MD%p8x-W z!-s6I@dGmd-rz)|(qnuhDFEd{KoC{Oj}~^Ty}^}Fwf?0Rw6HMBSvufIi3++v@mHap jRSBfi!=rGzy8k_|8~Gn-9+ZLN#{g?fd;DW_pUeLM*{U`= literal 0 HcmV?d00001 diff --git a/resources/supporters.json b/resources/supporters.json index f88155ba..c2e7293b 100644 --- a/resources/supporters.json +++ b/resources/supporters.json @@ -484,6 +484,12 @@ "startdate": "2020-05-28", "description": "Yes! It is me! That guy who makes all the worlds", "website": "https://twitter.com/LuciferMStarVRC" + },{ + "displayname": "♡DAWKY", + "startdate": "2020-07-16" + },{ + "displayname": "~ellie~", + "startdate": "2020-08-14" } ] } diff --git a/tools/armature_manual.py b/tools/armature_manual.py index 0e5f1a0b..b433288c 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -190,7 +190,11 @@ def pose_to_shapekey(name): # Apply armature mod mod = mesh.modifiers.new(name, 'ARMATURE') mod.object = Common.get_armature() - bpy.ops.object.modifier_apply(apply_as='SHAPE', modifier=mod.name) + + if bpy.app.version < (2, 90): + bpy.ops.object.modifier_apply(apply_as='SHAPE', modifier=mod.name) + else: + bpy.ops.object.modifier_apply_as_shapekey(keep_modifier=False, modifier=mod.name) armature = Common.set_default_stage() Common.switch('POSE') diff --git a/updater.py b/updater.py index bb07852f..797a8c1e 100644 --- a/updater.py +++ b/updater.py @@ -377,7 +377,7 @@ def check_for_update(): print('Checking for Cats update...') # Get all releases from Github - if not get_github_releases('Darkblader24') and not get_github_releases('michaeldegroot'): + if not get_github_releases('Darkblader24') and not get_github_releases('GiveMeAllYourCats'): finish_update_checking(error='Could not check for updates, try again later') return From 1dc29a733d4090d20d7f751c4aad0d8f4550eb3e Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 2 Sep 2020 12:17:52 +0200 Subject: [PATCH 05/64] Fixed more bugs with Blender 2.90 --- README.md | 1 + tools/armature.py | 2 +- tools/armature_manual.py | 6 +----- tools/common.py | 16 +++++++++++++--- tools/decimation.py | 4 ++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e1dac92b..40597a4f 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,7 @@ It checks for a new version automatically once every day. #### 0.17.1 - Changed link to a new vrm importer since the old one dropped support - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 +- More fixes for Blender 2.90 #### 0.17.0 - **Cats is now fully compatible with Blender 2.83!** diff --git a/tools/armature.py b/tools/armature.py index 5b5efb29..cb0be618 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -191,7 +191,7 @@ def execute(self, context): mod = mesh.modifiers.new(morph.name, 'ARMATURE') mod.object = armature - bpy.ops.object.modifier_apply(apply_as='SHAPE', modifier=mod.name) + Common.apply_modifier(mod, as_shapekey=True) wm.progress_end() # Perform source engine specific operations diff --git a/tools/armature_manual.py b/tools/armature_manual.py index b433288c..6848f2fe 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -190,11 +190,7 @@ def pose_to_shapekey(name): # Apply armature mod mod = mesh.modifiers.new(name, 'ARMATURE') mod.object = Common.get_armature() - - if bpy.app.version < (2, 90): - bpy.ops.object.modifier_apply(apply_as='SHAPE', modifier=mod.name) - else: - bpy.ops.object.modifier_apply_as_shapekey(keep_modifier=False, modifier=mod.name) + Common.apply_modifier(mod, as_shapekey=True) armature = Common.set_default_stage() Common.switch('POSE') diff --git a/tools/common.py b/tools/common.py index 287fe109..46987a53 100644 --- a/tools/common.py +++ b/tools/common.py @@ -288,6 +288,16 @@ def set_default_stage(): return armature +def apply_modifier(mod, as_shapekey=False): + if bpy.app.version < (2, 90): + bpy.ops.object.modifier_apply(apply_as='SHAPE' if as_shapekey else 'DATA', modifier=mod.name) + else: + if as_shapekey: + bpy.ops.object.modifier_apply_as_shapekey(keep_modifier=False, modifier=mod.name) + else: + bpy.ops.object.modifier_apply(modifier=mod.name) + + def remove_bone(find_bone): armature = get_armature() switch('EDIT') @@ -793,12 +803,12 @@ def join_meshes(armature_name=None, mode=0, apply_transformations=True, repair_s if has_shapekeys(mesh): bpy.ops.object.shape_key_remove(all=True) - bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) + apply_modifier(mod) elif mod.type == 'SUBSURF': mesh.modifiers.remove(mod) elif mod.type == 'MIRROR': if not has_shapekeys(mesh): - bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) + apply_modifier(mod) # Standardize UV maps name if version_2_79_or_older(): @@ -1739,7 +1749,7 @@ def mix_weights(mesh, vg_from, vg_to, mix_strength=1.0, mix_mode='ADD', delete_o mod.mix_mode = mix_mode mod.mix_set = 'B' mod.mask_constant = mix_strength - bpy.ops.object.modifier_apply(modifier=mod.name) + apply_modifier(mod) if delete_old_vg: mesh.vertex_groups.remove(mesh.vertex_groups.get(vg_from)) mesh.active_shape_key_index = 0 # This line fixes a visual bug in 2.80 which causes random weights to be stuck after being merged diff --git a/tools/decimation.py b/tools/decimation.py index 7f8f5e24..12af043d 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -316,7 +316,7 @@ def decimate(self, context): mod = mesh_obj.modifiers.new("Decimate", 'DECIMATE') mod.ratio = decimation mod.use_collapse_triangulate = True - bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) + Common.apply_modifier(mod) tris_after = len(mesh_obj.data.polygons) print(tris) @@ -353,7 +353,7 @@ def decimate(self, context): # mod = mesh_obj.modifiers.new("Decimate", 'DECIMATE') # mod.ratio = decimation # mod.use_collapse_triangulate = True - # bpy.ops.object.modifier_apply(apply_as='DATA', modifier=mod.name) + # Common.apply_modifier(mod) # # Common.unselect_all() # break From dcdca897ac8c6617704719b34e338e6dc1c3f7b7 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sun, 27 Sep 2020 00:55:51 +0200 Subject: [PATCH 06/64] Made translations reloadable --- README.md | 7 +- __init__.py | 5 +- translations/__init__.py | 3 +- translations/en_US.py | 258 +++++++++++++++++++-------------------- 4 files changed, 140 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 40597a4f..566a866c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cats Blender Plugin (0.17.1) +# Cats Blender Plugin (0.18.0) A tool designed to shorten steps needed to import and optimize models into VRChat. Compatible models are: MMD, XNALara, Mixamo, Source Engine, Unreal Engine, DAZ/Poser, Blender Rigify, Sims 2, Motion Builder, 3DS Max and potentially more @@ -300,7 +300,10 @@ It checks for a new version automatically once every day. ## Changelog -#### 0.17.1 +#### 0.18.0 +- **Added japanese translation!** + - Full credit goes to **Jordo** and **Ruubick**! Thank you so much <3 +- Cats is now fully compatible with Blender 2.90 - Changed link to a new vrm importer since the old one dropped support - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 - More fixes for Blender 2.90 diff --git a/__init__.py b/__init__.py index 2a6a0c0e..3c96faae 100644 --- a/__init__.py +++ b/__init__.py @@ -51,7 +51,6 @@ import requests from . import globs -from .translations import t # Check if cats is reloading or started fresh if "bpy" not in locals(): @@ -65,6 +64,7 @@ # This order is important import mmd_tools_local from . import updater + from . import translations from . import tools from . import ui from . import extentions @@ -72,10 +72,13 @@ import importlib importlib.reload(updater) importlib.reload(mmd_tools_local) + importlib.reload(translations) importlib.reload(tools) importlib.reload(ui) importlib.reload(extentions) +from .translations import t + # How to update mmd_tools: (outdated, no longer used) # Delete mmd_tools_local folder diff --git a/translations/__init__.py b/translations/__init__.py index 0ec2aacd..2c7e2eec 100644 --- a/translations/__init__.py +++ b/translations/__init__.py @@ -7,6 +7,7 @@ else: from .en_US import dictionary + def t(phrase: str, *args, **kwargs): # Translate the given phrase into Blender's current language. try: @@ -22,4 +23,4 @@ def t(phrase: str, *args, **kwargs): return output.format(*args, **kwargs) except KeyError: print('Warning - Text missing for: ' + phrase) - return phrase \ No newline at end of file + return phrase diff --git a/translations/en_US.py b/translations/en_US.py index 57200c2c..5ea17e35 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -6,14 +6,14 @@ 'Main.error.restartAdmin': '\n\nFaulty CATS installation found!' '\nTo fix this restart Blender as admin! ' '\n', - 'Main.error.deleteFollowing': ' ' \ - ' '\ - '\n\nFaulty CATS installation found!' \ - '\nTo fix this delete the following files and folders inside your addons folder:' \ + 'Main.error.deleteFollowing': ' ' + ' ' + '\n\nFaulty CATS installation found!' + '\nTo fix this delete the following files and folders inside your addons folder:' '\n', 'Main.error.installViaPreferences': '\n\nFaulty CATS installation found!' - '\nPlease install CATS via User Preferences and restart Blender!' - '\n', + '\nPlease install CATS via User Preferences and restart Blender!' + '\n', 'Main.error.restartAndEnable': '\n\nFaulty CATS installation was found and fixed!' '\nPlease restart Blender and enable CATS again!' '\n', @@ -168,20 +168,21 @@ # Tools Armature 'FixArmature.label': 'Fix Model', - 'FixArmature.desc': 'Automatically:\n' \ - '- Reparents bones\n' \ - '- Removes unnecessary bones, objects, groups & constraints\n' \ - '- Translates and renames bones & objects\n' \ - '- Merges weight paints\n' \ - '- Corrects the hips\n' \ - '- Joins meshes\n' \ - '- Converts morphs into shapes\n' \ + 'FixArmature.desc': 'Automatically:\n' + '- Reparents bones\n' + '- Removes unnecessary bones, objects, groups & constraints\n' + '- Translates and renames bones & objects\n' + '- Merges weight paints\n' + '- Corrects the hips\n' + '- Joins meshes\n' + '- Converts morphs into shapes\n' '- Corrects shading', 'FixArmature.error.noMesh': ['No mesh inside the armature found!', 'If there are meshes outside of the armature,', 'set the armature as the parent of the meshes.'], - # Format strings? vvvv t(str, fixed_uv_coords) -> The model was successfully fixed, but there were {} faulty UV - 'FixArmature.error.faultyUV1': 'The model was successfully fixed, but there were {uvcoord} faulty UV coordinates.', # 'The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', + # Format strings? vvvv t(str, fixed_uv_coords) -> The model was successfully fixed, but there were {} faulty UV + 'FixArmature.error.faultyUV1': 'The model was successfully fixed, but there were {uvcoord} faulty UV coordinates.', + # 'The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', 'FixArmature.error.faultyUV2': 'This could result in broken textures and you might have to fix them manually.', 'FixArmature.error.faultyUV3': 'This issue is often caused by edits in PMX editor.', 'FixArmature.fixedSuccess': 'Model successfully fixed.', @@ -195,14 +196,14 @@ # Tools Armature Manual 'StartPoseMode.label': 'Start Pose Mode', - 'StartPoseMode.desc': 'Starts the pose mode.\n' \ + 'StartPoseMode.desc': 'Starts the pose mode.\n' 'This lets you test how your model will move', 'StopPoseMode.label': 'Stop Pose Mode', 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', 'PoseToShape.label': 'Pose to Shape Key', - 'PoseToShape.desc': 'This saves your current pose as a new shape key.' \ + 'PoseToShape.desc': 'This saves your current pose as a new shape key.' '\nThe new shape key will be at the bottom of your shape key list of the mesh', 'PoseNamePopup.label': 'Give this shapekey a name:', @@ -210,75 +211,75 @@ 'PoseNamePopup.success': 'Pose successfully saved as shape key.', 'PoseToRest.label': 'Apply as Rest Pose', - 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' \ - '\n' \ - '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' \ + 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' + '\n' + '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' '\nWARNING: This can have unwanted effects on shape keys, so be careful when modifying the head with this', 'PoseToRest.success': 'Pose successfully applied as rest pose.', 'JoinMeshes.label': 'Join Meshes', - 'JoinMeshes.desc': 'Joins all meshes of this model together.' \ - '\nIt also:' \ - '\n - Reorders all shape keys correctly' \ - '\n - Applies all transforms' \ - '\n - Repairs broken armature modifiers' \ - '\n - Applies all decimation and mirror modifiers' \ + 'JoinMeshes.desc': 'Joins all meshes of this model together.' + '\nIt also:' + '\n - Reorders all shape keys correctly' + '\n - Applies all transforms' + '\n - Repairs broken armature modifiers' + '\n - Applies all decimation and mirror modifiers' '\n - Merges UV maps correctly', 'JoinMeshes.failure': 'Meshes could not be joined!', 'JoinMeshes.success': 'Meshes joined.', 'JoinMeshesSelected.label': 'Join Selected Meshes', - 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' \ - '\nIt also:' \ - '\n - Reorders all shape keys correctly' \ - '\n - Applies all transforms' \ - '\n - Repairs broken armature modifiers' \ - '\n - Applies all decimation and mirror modifiers' \ + 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' + '\nIt also:' + '\n - Reorders all shape keys correctly' + '\n - Applies all transforms' + '\n - Repairs broken armature modifiers' + '\n - Applies all decimation and mirror modifiers' '\n - Merges UV maps correctly', 'JoinMeshesSelected.error.noSelect': 'No meshes selected! Please select the meshes you want to join in the hierarchy!', 'JoinMeshesSelected.error.cantJoin': 'Selected meshes could not be joined!', 'JoinMeshesSelected.success': 'Selected meshes joined.', 'SeparateByMaterials.label': 'Separate by Materials', - 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' \ - '\n' \ + 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' + '\n' 'Warning: Never decimate something where you might need the shape keys later (face, mouth, eyes..)', 'SeparateByMaterials.success': 'Successfully separated by materials.', 'SeparateByLooseParts.label': 'Separate by Loose Parts', - 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' \ + 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' 'This acts like separating by materials but creates more meshes for more precision', 'SeparateByLooseParts.success': 'Successfully separated by loose parts.', 'SeparateByShapekeys.label': 'Separate by Shape Keys', - 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' \ - '\ndepending on whether it is effected by a shape key or not.' \ - '\n' \ + 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' + '\ndepending on whether it is effected by a shape key or not.' + '\n' '\nVery 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,' \ - '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ - '\n' \ + 'SeparateByCopyProtection.desc': 'Separates selected mesh into two parts,' + '\ndepending on whether it is effected by the Cats Copy Protection or not.' + '\n' '\nUseful if you have the Copy Protection enabled on multiple selected parts of your model', 'SeparateByCopyProtection.success': 'Successfully separated by shape keys.', 'SeparateByX.error.noMesh': 'No meshes found!', - 'SeparateByX.error.multipleMesh': 'Multiple meshes found!' \ - '\nPlease select the mesh you want to separate!', + 'SeparateByX.error.multipleMesh': 'Multiple meshes found!' + '\nPlease select the mesh you want to separate!', 'SeparateByX.warn.noSeparation': 'No meshes had to be separated!', 'MergeWeights.label': 'Merge Weights to Parent', - 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' \ - '\n' \ + 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' + '\n' '\nOnly available in Edit or Pose Mode with bones selected', 'MergeWeights.success': 'Deleted {number} bones and added their weights to their parents.', 'MergeWeightsToActive.label': 'Merge Weights to Active', - 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' \ - '\nThe active bone is the one you selected last.' \ - '\n' \ + 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' + '\nThe active bone is the one you selected last.' + '\n' '\nOnly available in Edit or Pose Mode with bones selected', 'MergeWeightsToActive.success': 'Deleted {number} bones and added their weights to the active bone.', @@ -291,7 +292,7 @@ 'ApplyAllTransformations.success': 'Transformations applied.', 'RemoveZeroWeightBones.label': 'Remove Zero Weight Bones', - 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' \ + 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' 'Don\'t use this if you plan to use \'Fix Model\'', 'RemoveZeroWeightBones.success': 'Deleted {number} zero weight bones.', @@ -304,25 +305,25 @@ 'RemoveConstraints.success': 'Removed all bone constraints.', 'RecalculateNormals.label': 'Recalculate Normals', - 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' \ - 'Don\'t use this on good looking meshes as this can screw them up.\n' \ + 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' + 'Don\'t use this on good looking meshes as this can screw them up.\n' 'Use this if there are random inverted or darker faces on the mesh', 'RecalculateNormals.success': 'Recalculated all normals.', 'FlipNormals.label': 'Flip Normals', - 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' \ + 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' 'Use this if all normals are inverted', 'FlipNormals.success': 'Flipped all normals.', 'RemoveDoubles.label': 'Remove Doubles', - 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ - '\nThis is more safe than doing it manually:' \ - '\n - leaves shape keys completely untouched' \ + 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' + '\nThis is more safe than doing it manually:' + '\n - leaves shape keys completely untouched' '\n - but removes less doubles overall', 'RemoveDoubles.success': 'Removed {number} vertices.', 'RemoveDoublesNormal.label': 'Remove Doubles Normally', - 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ + 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' '\nThis is exactly like doing it manually', 'RemoveDoublesNormal.success': 'Removed {number} vertices.', @@ -332,9 +333,9 @@ 'FixVRMShapesButton.success': 'Fixed VRM shapekeys.', 'FixFBTButton.label': 'Fix Full Body Tracking', - 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' \ - '\n' \ - '\nApplies a general fix for Full Body Tracking.' \ + 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' + '\n' + '\nApplies a general fix for Full Body Tracking.' '\nIgnore the \"Spine length zero\" warning in Unity', 'FixFBTButton.error.bonesNotFound': 'Required bones could not be found!' '\nPlease make sure that your armature contains the following bones:' @@ -344,9 +345,9 @@ 'FixFBTButton.success': 'Successfully applied the Full Body Tracking fix.', 'RemoveFBTButton.label': 'Remove Full Body Tracking Fix', - 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' \ - '\n' \ - '\nRequires bones:' \ + 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' + '\n' + '\nRequires bones:' '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2', 'RemoveFBTButton.error.bonesNotFound': 'Required bones could not be found!' '\nPlease make sure that your armature contains the following bones:' @@ -361,28 +362,28 @@ # Tools Armature Custom 'MergeArmature.label': 'Merge Armatures', - 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' \ - '\nYou should fix both armatures with Cats first.' \ + 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' + '\nYou should fix both armatures with Cats first.' '\nOnly move the mesh of the merge armature to the desired position, the bones will be moved automatically', 'MergeArmature.error.notFound': 'The armature "{name}" could not be found.', - 'MergeArmature.error.checkTransforms': [ 'Please make sure that the parent of the merge armature has the following transforms:', - ' - Location at 0', - ' - Rotation at 0', - ' - Scale at 1'], - 'MergeArmature.error.pleaseFix': [ 'Please use the "Fix Model" feature on the selected armatures first!', - 'Make sure to select the armature you want to fix above the "Fix Model" button!', - 'After that please only move the mesh (not the armature!) to the desired position.'], + 'MergeArmature.error.checkTransforms': ['Please make sure that the parent of the merge armature has the following transforms:', + ' - Location at 0', + ' - Rotation at 0', + ' - Scale at 1'], + 'MergeArmature.error.pleaseFix': ['Please use the "Fix Model" feature on the selected armatures first!', + 'Make sure to select the armature you want to fix above the "Fix Model" button!', + 'After that please only move the mesh (not the armature!) to the desired position.'], 'MergeArmature.success': 'Armatures successfully joined.', 'AttachMesh.label': 'Attach Mesh', - 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' \ - '\n' \ - '\nINFO: The mesh will only be assigned to the selected bone.' \ + 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' + '\n' + '\nINFO: The mesh will only be assigned to the selected bone.' '\nE.g.: A jacket won\'t work, because it requires multiple bones', 'AttachMesh.success': 'Mesh successfully attached to armature.', 'CustomModelTutorialButton.label': 'Go to Documentation', - 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) + 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) 'CustomModelTutorialButton.success': 'Documentation', 'merge_armatures.error.transformReset': ['If you want to rotate the new part, only modify the mesh instead of the armature,', @@ -420,7 +421,7 @@ # Tools Bonemerge 'BoneMergeButton.label': 'Merge Bones', - 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' \ + 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' 'This is useful to reduce the amount of bones used by Dynamic Bones.', 'BoneMergeButton.success': 'Merged bones.', @@ -429,7 +430,7 @@ # Tools Copy protection 'CopyProtectionEnable.label': 'Enable Protection', - 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' \ + 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' '\nRead the documentation before use', 'CopyProtectionEnable.success': 'Model secured!', @@ -459,40 +460,40 @@ 'ScanButton.desc': 'Separates the mesh.', 'AddShapeButton.label': 'Add', - 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' \ + 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' 'This means that every mesh containing that shape key will be not decimated.', 'AddMeshButton.label': 'Add', - 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' \ + 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' 'This means that this mesh will be not decimated.', 'RemoveShapeButton.label': '', - 'RemoveShapeButton.desc': 'Removes the selected shape key from the whitelist.\n' \ + 'RemoveShapeButton.desc': 'Removes the selected shape key from the whitelist.\n' 'This means that this shape key is no longer decimation safe!', 'RemoveMeshButton.label': '', - 'RemoveMeshButton.desc': 'Removes the selected mesh from the whitelist.\n' \ + 'RemoveMeshButton.desc': 'Removes the selected mesh from the whitelist.\n' 'This means that this mesh will be decimated.', 'AutoDecimateButton.label': 'Quick Decimation', - 'AutoDecimateButton.desc': 'This will automatically decimate your model while preserving the shape keys.\n' \ + 'AutoDecimateButton.desc': 'This will automatically decimate your model while preserving the shape keys.\n' 'You should manually remove unimportant meshes first.', 'AutoDecimateButton.error.noMesh': 'No meshes found!', - 'decimate.cantDecimateWithSettings': 'This model can not be decimated to {number} tris with the specified settings.', + 'decimate.cantDecimateWithSettings': 'This model can not be decimated to {number} tris with the specified settings.', 'decimate.safeTryOptions': 'Try to use Custom, Half or Full Decimation.', 'decimate.halfTryOptions': 'Try to use Custom or Full Decimation.', 'decimate.customTryOptions': 'Select fewer shape keys and/or meshes or use Full Decimation.', 'decimate.disableFingersOrIncrease': 'Disable \'Save Fingers\' or increase the Tris Count.', - 'decimate.disableFingers': 'or disable \'Save Fingers\'.', # This comes after one of the previous xTryOptions + 'decimate.disableFingers': 'or disable \'Save Fingers\'.', # This comes after one of the previous xTryOptions 'decimate.noDecimationNeeded': 'The model already has less than {number} tris. Nothing had to be decimated.', 'decimate.cantDecimate1': 'The model could not be decimated to {number} tris.', 'decimate.cantDecimate2': 'It got decimated as much as possible within the limits.', # Tools Eyetracking 'CreateEyesButton.label': 'Create Eye Tracking', - 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' \ - 'You should do decimation before this operation.\n' \ + 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' + 'You should do decimation before this operation.\n' 'Test the resulting eye movement in the \'Testing\' tab.', 'CreateEyesButton.error.noShapeSelected': 'You have no shape keys selected.' '\nPlease choose a mesh containing shape keys or check "Disable Eye Blinking".', @@ -507,8 +508,8 @@ 'CreateEyesButton.success': 'Created eye tracking!', 'StartTestingButton.label': 'Start Eye Testing', - 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' \ - 'Don\'t forget to stop the Testing process afterwards.\n' \ + 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' + 'Don\'t forget to stop the Testing process afterwards.\n' 'Bones "LeftEye" and "RightEye" are required.', 'StopTestingButton.label': 'Stop Eye Testing', @@ -519,15 +520,15 @@ 'ResetRotationButton.desc': 'This resets the eye positions.', 'AdjustEyesButton.label': 'Set Range', - 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' \ + 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' 'This gets saved', 'AdjustEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' '\nThis might be because you selected the wrong mesh or the wrong bone.' '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.', 'StartIrisHeightButton.label': 'Start Iris Height Adjustment', - 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' \ - 'Use this to fix clipping of the iris into the eye ball.\n' \ + 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' + 'Use this to fix clipping of the iris into the eye ball.\n' 'This gets saved.', 'TestBlinking.label': 'Test', @@ -541,30 +542,30 @@ # Tools Importer 'ImportAnyModel.label': 'Import Any Model', - 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' \ - '\n' \ - '\nSupported types:' \ - '\n- MMD: .pmx/.pmd' \ - '\n- XNALara: .xps/.mesh/.ascii' \ - '\n- Source: .smd/.qc' \ - '\n- VRM: .vrm' \ - '\n- FBX .fbx ' \ - '\n- DAE: .dae ' \ + 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' + '\n' + '\nSupported types:' + '\n- MMD: .pmx/.pmd' + '\n- XNALara: .xps/.mesh/.ascii' + '\n- Source: .smd/.qc' + '\n- VRM: .vrm' + '\n- FBX .fbx ' + '\n- DAE: .dae ' '\n- ZIP: .zip', - 'ImportAnyModel.desc2.8': 'Import a model of any supported type.' \ - '\n' \ - '\nSupported types:' \ - '\n- MMD: .pmx/.pmd' \ - '\n- XNALara: .xps/.mesh/.ascii' \ - '\n- Source: .smd/.qc/.vta/.dmx' \ - '\n- VRM: .vrm' \ - '\n- FBX: .fbx' \ - '\n- DAE: .dae ' \ + 'ImportAnyModel.desc2.8': 'Import a model of any supported type.' + '\n' + '\nSupported types:' + '\n- MMD: .pmx/.pmd' + '\n- XNALara: .xps/.mesh/.ascii' + '\n- Source: .smd/.qc/.vta/.dmx' + '\n- VRM: .vrm' + '\n- FBX: .fbx' + '\n- DAE: .dae ' '\n- ZIP: .zip', 'ImportAnyModel.importantInfo.label': 'IMPORTANT INFO (hover here)', 'ImportAnyModel.importantInfo.desc': 'If you want to modify the import settings, use the button next to the Import button.\n\n', 'ImportAnyModel.error.emptyZip': 'The selected zip file contains no importable models.', - 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' \ + 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' '\nPlease use a tool such as the "Autodesk FBX Converter" to make it compatible.', 'ZipPopup.label': 'Zip Model Selection:', @@ -621,8 +622,8 @@ 'VrmToolsButton.success': 'VRM Importer link opened', 'ExportModel.label': 'Export Model', - 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' \ - '\n' \ + 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' + '\n' 'Automatically sets the optimal export settings', 'ExportModel.error.notEnabled': 'FBX Exporter not enabled! Please enable it in your User Preferences.', @@ -661,35 +662,35 @@ 'OneTexPerMatButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.', 'OneTexPerMatOnlyButton.label': 'One Material Texture', - 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' \ - '\nAlso removes the textures from the material instead of disabling it.' \ + 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' + '\nAlso removes the textures from the material instead of disabling it.' '\nThis 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' \ + 'StandardizeTextures.desc': 'Enables Color and Alpha on every texture, sets the blend method to Multiply' '\nand 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.\n' \ - 'Your avatar should visibly look the same after this operation.\n' \ - 'This is a very important step for optimizing your avatar.\n' \ + 'CombineMaterialsButton.desc': 'Combines similar materials into one, reducing draw calls.\n' + 'Your avatar should visibly look the same after this operation.\n' + 'This is a very important step for optimizing your avatar.\n' 'If you have problems with this, please tell us!\n', 'CombineMaterialsButton.error.noChanges': 'No materials combined.', 'CombineMaterialsButton.success': 'Combined {number} materials!', 'ConvertAllToPngButton.label': 'Convert Textures to PNG', - 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' \ - '\nThis helps with transparency and compatibility issues.' \ + 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' + '\nThis helps with transparency and compatibility issues.' '\n\nThe converted image files will be saved next to the old ones', 'ConvertAllToPngButton.success': 'Converted {number} to PNG files.', # Tools Root bone 'RootButton.label': 'Parent Bones', - 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' \ + 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' 'Very useful for Dynamic Bones.', 'RootButton.success': 'Bones parented!', @@ -707,7 +708,7 @@ 'ResetGoogleDictButton.resetInfo': 'Local Google Dictionary cleared!', 'DebugTranslations.label': 'Debug Google Translations', # DEV ONLY - 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' \ + 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' '\nThis button is only visible in the cats development version', # DEV ONLY 'DebugTranslations.error': 'Errors found, response printed!!', # DEV ONLY 'DebugTranslations.success': 'No issues with Google Translations found, response printed!', # DEV ONLY @@ -721,9 +722,9 @@ 'If you didn\'t change the shape key order, you can revert the shape keys from top to bottom.'], 'ShapeKeyApplier.error.revertCustomBasis.scale': 7.3, 'ShapeKeyApplier.error.revert': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', - 'Start with the reverted shape key that uses the relative key called "Basis".', - '', - "If you didn't change the shape key order, you can revert the shape keys from top to bottom."], + 'Start with the reverted shape key that uses the relative key called "Basis".', + '', + "If you didn't change the shape key order, you can revert the shape keys from top to bottom."], 'ShapeKeyApplier.error.revert.scale': 7.3, 'ShapeKeyApplier.successRemoved': 'Successfully removed shapekey "{name}" from the Basis.', 'ShapeKeyApplier.successSet': 'Successfully set shapekey "{name}" as the new Basis.', @@ -784,7 +785,7 @@ # Tools Viseme 'AutoVisemeButton.label': 'Create Visemes', - 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' \ + 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' 'It will generate 15 shape keys from the 3 shape keys you specify', 'AutoVisemeButton.error.noShapekeys': 'This mesh has no shapekeys!', 'AutoVisemeButton.error.selectShapekeys': 'Please select the correct mouth shapekeys instead of "Basis"!', @@ -1166,5 +1167,4 @@ 'bpy.types.Scene.cats_update_action.defer.label': 'Remind me later', 'bpy.types.Scene.cats_update_action.defer.desc': 'Hides the update notification til the next Blender restart', - -} \ No newline at end of file +} From c6aead4783abff1adaa0744aebd1b15d990066f8 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sun, 27 Sep 2020 01:13:16 +0200 Subject: [PATCH 07/64] Support typo in old vrm importer --- tools/common.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/common.py b/tools/common.py index e54d0d27..b2530e92 100644 --- a/tools/common.py +++ b/tools/common.py @@ -1994,9 +1994,15 @@ def fix_vrm_shader(mesh): 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['NomalmapTexture'].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') + if not node_input: + node_input = 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: From 85731298b3b767f0b9edf12942b492cd129fa183 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sun, 27 Sep 2020 01:31:28 +0200 Subject: [PATCH 08/64] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 566a866c..5641f54f 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ It checks for a new version automatically once every day. #### 0.18.0 - **Added japanese translation!** - - Full credit goes to **Jordo** and **Ruubick**! Thank you so much <3 + - Full credit goes to **Jordo** and **Ruuubick**! Thank you so much <3 - Cats is now fully compatible with Blender 2.90 - Changed link to a new vrm importer since the old one dropped support - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 From c0ed1e4e9be793507865d2221824c5b52bd588cf Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Mon, 26 Oct 2020 18:13:08 -0700 Subject: [PATCH 09/64] Introduce 'Smart Decimation' that preserves shape keys The Decimate modifier cannot be applied to any mesh which has shape keys, but the Decimate mesh operator can. Applying the Decimate mesh operator causes the shape keys to all get jumbled up, but notably jumbled up in __the same way__, so we can exploit this by creating a duplicate Basis key that we let get messed up, and use it as a reference to un-jumble the other shape keys. --- extentions.py | 4 +++- tools/decimation.py | 47 ++++++++++++++++++++++++++++++++++++++----- translations/en_US.py | 20 +++++++++++++++--- ui/decimation.py | 38 ++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/extentions.py b/extentions.py index 868283cb..a4de7b71 100644 --- a/extentions.py +++ b/extentions.py @@ -164,9 +164,11 @@ def register(): ("FULL", t('Scene.decimation_mode.full.label'), t('Scene.decimation_mode.full.desc')), + ("SMART", t('Scene.decimation_mode.smart.label'), t('Scene.decimation_mode.smart.desc')) + ("CUSTOM", t('Scene.decimation_mode.custom.label'), t('Scene.decimation_mode.custom.desc')) ], - default='HALF' + default='SMART' ) Scene.selection_mode = EnumProperty( diff --git a/tools/decimation.py b/tools/decimation.py index 996a0131..e26a4c0d 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -179,6 +179,7 @@ def decimate(self, context): full_decimation = context.scene.decimation_mode == 'FULL' half_decimation = context.scene.decimation_mode == 'HALF' safe_decimation = context.scene.decimation_mode == 'SAFE' + smart_decimation = context.scene.decimation_mode == 'SMART' save_fingers = context.scene.decimate_fingers max_tris = context.scene.max_tris meshes = [] @@ -233,6 +234,17 @@ def decimate(self, context): bpy.ops.object.shape_key_remove(all=True) meshes.append((mesh, tris)) tris_count += tris + elif smart_decimation: + if len(mesh.data.shape_keys.key_blocks) == 1: + bpy.ops.object.shape_key_remove(all=True) + else: + # Add a duplicate basis key which we un-apply to fix shape keys + mesh.active_shape_key_index = 0 + bpy.ops.object.shape_key_add(from_mix=False) + mesh.active_shape_key.name = "CATS Basis" + mesh.active_shape_key_index = 0 + meshes.append((mesh, tris)) + tris_count += tris elif custom_decimation: found = False for shape in ignore_shapes: @@ -273,7 +285,7 @@ def decimate(self, context): elif custom_decimation: message.append(t('decimate.customTryOptions')) if save_fingers: - if full_decimation: + if full_decimation or smart_decimation: message.append(t('decimate.disableFingersOrIncrease')) else: message[1] = message[1][:-1] @@ -309,10 +321,22 @@ def decimate(self, context): print(decimation) # Apply decimation mod - mod = mesh_obj.modifiers.new("Decimate", 'DECIMATE') - mod.ratio = decimation - mod.use_collapse_triangulate = True - Common.apply_modifier(mod) + if not smart_decimation: + mod = mesh_obj.modifiers.new("Decimate", 'DECIMATE') + mod.ratio = decimation + mod.use_collapse_triangulate = True + Common.apply_modifier(mod) + else: + Common.switch('EDIT') + bpy.ops.mesh.select_mode(type="VERT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.mesh.decimate(ratio=decimation, + use_vertex_group=False, + vertex_group_factor=1.0, + invert_vertex_group=False, + use_symmetry=True, + symmetry_axis='X') + Common.switch('OBJECT') tris_after = len(mesh_obj.data.polygons) print(tris) @@ -320,6 +344,19 @@ def decimate(self, context): current_tris_count = current_tris_count - tris + tris_after tris_count = tris_count - tris + # Repair shape keys if SMART mode is enabled + if smart_decimation and len(mesh_obj.data.shape_keys.key_blocks) > 0: + for idx in range(1, len(mesh_obj.data.shape_keys.key_blocks) - 2): + print("Key: " + mesh_obj.data.shape_keys.key_blocks[idx].name) + mesh_obj.active_shape_key_index = idx + Common.switch('EDIT') + bpy.ops.mesh.blend_from_shape(shape="CATS Basis", blend=-1.0, add=True) + Common.switch('OBJECT') + mesh_obj.shape_key_remove(key=mesh_obj.data.shape_keys.key_blocks["CATS Basis"]) + mesh_obj.active_shape_key_index = 0 + + + Common.unselect_all() diff --git a/translations/en_US.py b/translations/en_US.py index 5ea17e35..64793a98 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -92,7 +92,8 @@ 'DecimationPanel.decimationMode': 'Decimation Mode:', 'DecimationPanel.safeModeDesc': ' Decent results - No shape key loss', 'DecimationPanel.halfModeDesc': ' Good results - Minimal shape key loss', - 'DecimationPanel.fullModeDesc': ' Best results - Full shape key loss', + 'DecimationPanel.fullModeDesc': ' Consistent results - Full shape key loss', + 'DecimationPanel.smartModeDesc': ' Best results - Preserve shape keys', 'DecimationPanel.customSeparateMaterials': 'Start by Separating by Materials:', 'DecimationPanel.SeparateByMaterials.label': 'Separate by Materials', 'DecimationPanel.customJoinMeshes': 'Stop by Joining Meshes:', @@ -102,6 +103,14 @@ 'DecimationPanel.warn.noMeshSelected': 'No mesh selected', 'DecimationPanel.warn.emptyList': 'Both lists are empty, this equals Full Decimation!', 'DecimationPanel.warn.correctWhitelist': 'Both whitelists are considered during decimation', + 'DecimationPanel.preset.excellent.label': 'Excellent', + 'DecimationPanel.preset.excellent.description': 'The maximum number of tris you can have for an Excellent rating.', + 'DecimationPanel.preset.good.label': 'Good', + 'DecimationPanel.preset.good.description': 'The maximum number of tris you can have for a Good rating.', + 'DecimationPanel.preset.quest.label': 'Quest', + 'DecimationPanel.preset.quest.description': 'The reccomended number of tris for Quest avatars.\n' \ + 'A hard limit will be established in the future that will not be much more than this.', + # UI Eye tracking 'EyeTrackingPanel.label': 'Eye Tracking', @@ -917,11 +926,16 @@ 'The results are better but you will lose the shape keys in some meshes.\n' 'Eye Tracking and Lip Syncing should still work.', 'Scene.decimation_mode.full.label': 'Full', - 'Scene.decimation_mode.full.desc': 'Best results - full shape key loss\n' + 'Scene.decimation_mode.full.desc': 'Consistent results - full shape key loss\n' '\n' 'This will decimate your whole model deleting all shape keys in the process.\n' - 'This will give the best results but you will lose the ability to add blinking and Lip Syncing.\n' + 'This will give consistent results but you will lose the ability to add blinking and Lip Syncing.\n' 'Eye Tracking will still work if you disable Eye Blinking.', + 'Scene.decimation_mode.smart.label': "Smart", + 'Scene.decimation_mode.smart.desc': 'Best results - repair shape keys after decimation\n' + '\n' + "This will decimate your whole model and attempt to undo the warping caused by Blender's decimation.\n" + "This will give the best results and keep blinking and lip syncing, but may have issues on some models.", 'Scene.decimation_mode.custom.label': 'Custom', 'Scene.decimation_mode.custom.desc': 'Custom results - custom shape key loss\n' '\n' diff --git a/ui/decimation.py b/ui/decimation.py index f5ec3991..f7d2a2e1 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -9,6 +9,38 @@ from ..tools.register import register_wrap from ..translations import t +@register_wrap +class AutoDecimatePresetGood(bpy.types.Operator): + bl_idname = 'cats_decimation.preset_good' + bl_label = t('DecimationPanel.preset.good.label') + bl_description = t('DecimationPanel.preset.good.label') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + bpy.context.scene.max_tris = 70000 + return {'FINISHED'} + +@register_wrap +class AutoDecimatePresetExcellent(bpy.types.Operator): + bl_idname = 'cats_decimation.preset_excellent' + bl_label = t('DecimationPanel.preset.excellent.label') + bl_description = t('DecimationPanel.preset.excellent.label') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + bpy.context.scene.max_tris = 32000 + return {'FINISHED'} + +@register_wrap +class AutoDecimatePresetQuest(bpy.types.Operator): + bl_idname = 'cats_decimation.preset_quest' + bl_label = t('DecimationPanel.preset.quest.label') + bl_description = t('DecimationPanel.preset.quest.label') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + bpy.context.scene.max_tris = 5000 + return {'FINISHED'} @register_wrap class DecimationPanel(ToolPanel, bpy.types.Panel): @@ -39,6 +71,8 @@ def draw(self, context): row.label(text=t('DecimationPanel.halfModeDesc')) elif context.scene.decimation_mode == 'FULL': row.label(text=t('DecimationPanel.fullModeDesc')) + elif context.scene.decimation_mode == 'SMART': + row.label(text=t('DecimationPanel.smartModeDesc')) elif context.scene.decimation_mode == 'CUSTOM': col.separator() @@ -133,6 +167,10 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'decimation_remove_doubles') row = col.row(align=True) + row.operator(AutoDecimatePresetGood.bl_idname) + row.operator(AutoDecimatePresetExcellent.bl_idname) + row.operator(AutoDecimatePresetQuest.bl_idname) + row = col.row(align=True) row.prop(context.scene, 'max_tris') col.separator() row = col.row(align=True) From d608d9566b926d22bd29c7570b4c2ffa7282b238 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 27 Oct 2020 23:08:30 -0700 Subject: [PATCH 10/64] Some fixes --- extentions.py | 2 +- ui/decimation.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extentions.py b/extentions.py index a4de7b71..35cfa1a1 100644 --- a/extentions.py +++ b/extentions.py @@ -164,7 +164,7 @@ def register(): ("FULL", t('Scene.decimation_mode.full.label'), t('Scene.decimation_mode.full.desc')), - ("SMART", t('Scene.decimation_mode.smart.label'), t('Scene.decimation_mode.smart.desc')) + ("SMART", t('Scene.decimation_mode.smart.label'), t('Scene.decimation_mode.smart.desc')), ("CUSTOM", t('Scene.decimation_mode.custom.label'), t('Scene.decimation_mode.custom.desc')) ], diff --git a/ui/decimation.py b/ui/decimation.py index f7d2a2e1..03586228 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -13,7 +13,7 @@ class AutoDecimatePresetGood(bpy.types.Operator): bl_idname = 'cats_decimation.preset_good' bl_label = t('DecimationPanel.preset.good.label') - bl_description = t('DecimationPanel.preset.good.label') + bl_description = t('DecimationPanel.preset.good.description') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -24,7 +24,7 @@ def execute(self, context): class AutoDecimatePresetExcellent(bpy.types.Operator): bl_idname = 'cats_decimation.preset_excellent' bl_label = t('DecimationPanel.preset.excellent.label') - bl_description = t('DecimationPanel.preset.excellent.label') + bl_description = t('DecimationPanel.preset.excellent.description') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -35,7 +35,7 @@ def execute(self, context): class AutoDecimatePresetQuest(bpy.types.Operator): bl_idname = 'cats_decimation.preset_quest' bl_label = t('DecimationPanel.preset.quest.label') - bl_description = t('DecimationPanel.preset.quest.label') + bl_description = t('DecimationPanel.preset.quest.description') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): From e4d2a949503ea917a1e35b89b5d5f346e1525937 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 28 Oct 2020 21:28:27 +0100 Subject: [PATCH 11/64] Updated supporters list --- resources/icons/supporters/Godfall.png | Bin 0 -> 2452 bytes resources/icons/supporters/MizuFox.png | Bin 0 -> 3050 bytes resources/icons/supporters/Vyrei.png | Bin 0 -> 2137 bytes resources/icons/supporters/Wishes.png | Bin 0 -> 1879 bytes resources/supporters.json | 23 ++++++++++++++++++++++- 5 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 resources/icons/supporters/Godfall.png create mode 100644 resources/icons/supporters/MizuFox.png create mode 100644 resources/icons/supporters/Vyrei.png create mode 100644 resources/icons/supporters/Wishes.png diff --git a/resources/icons/supporters/Godfall.png b/resources/icons/supporters/Godfall.png new file mode 100644 index 0000000000000000000000000000000000000000..bc5976bc681d98309f67ff9a9eac2033c18b0a21 GIT binary patch literal 2452 zcmV;F32XL=P)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}000RuNklO3ngX~Zn=^y$!0g3Y?9qQ=k&v-nl(Y0J|EwA-v9G|pZg=UwFXxt z26m#%CdNDg#mIOKiF`ny(I#|S45&xp7%H_$gsucuxdO!n46q7VhKpHJ5h~+=R1kJR z1tJvF4x$riB2GJ}bd3+N@-`CZz7Jk}uLTCVpE!>&kx9~ZKp9|z#@0qGDQ5E13uq0r z(AC{dVL>73mUQ~g^r1AGupBjmj|;lNR;;`V#IKxyfr~y~5ZSB|Hv=~TR;WCBkdNN| zkRxBz;Bcg2x7%rMZbk^f{8Kp=qCYDL#|G)+TQRU}E`L~SkU>FM-#hp3rWgFVB} z%|E^c?|d)D8^#K1#Y9qAgT$%JTfiWbFdvB7l!;$M%n_xbu7PcDZsSi|w_>wev8AMp z?Moqu$K&*c!$hM|+}YXO|G=-fedQe#XBMMuH(?y&-=wk>LEHb{Nh2}n=OrjoPA6F|7kpJ0Zcod0|*09-Z zbX~ecd;3K|fDOc2AfG#l@DO4o0fQXkJSlDi^1lW8ys{EseLbqlgr@7PTfd%ncJ1Pi zTegs$olQ?q&t((HVzJQR^YO)@L+HAmICO!y1At9v6n#q|!fGbs71t(%R99CM85kh0 zY5a1-1|E9&VLToWYu2viFK@m{X=y3Fy<^uKNs@@g<8*X%AW7dET9g6P$t1%11OWpZ zg1f}Ez=-(j>gwXFFTW(FX)Ii{i2FBeK$0Xvp%8(d0OgAp^ZVzYBY&Ki{(*jsG2Ec4 zDm%7qqq?faxQaXp|=flj7GlsUMWE@mYId|p|}$&)7;@AY!;>eZ;KiY&_*hCzxg zh3>9y+RnAGcwQN)rWDXd*Att~#wEX>$2M(Z$M)?+B9R2S>41kKhFB;OV}Tftf;;)K z-|r{b+spEb3M5Hl*Up`o&1Mv2%!3LIe?Lt1qkqL+HAs2J3>=yn2`R}EhGF1HOQWWy zhPt{s%w}`q!U3WPh_gt+U@Cc|edzUi@p?R5HG4LD_Uz$E&0!WUTF4|%5oh)urTxWf zekNUmUCH75{0VIkQWX9nfO<>}rNz7lc03ifz zt*z8IH1g6*FLK??GS0ntkflmFi=JPF))QgiZy%HAaN`SJOs*`$1Tm@BMjmm=ui4F~+Dl&oY0Fl@r5hpZtx8dCSA;EBvrrcg+yB)1Z1LP!} zp`jsia&nlzU_r71D5yYVvRT`ubRT*Ilewx$-jefJQrmOdjiU^5n_9yK5JvrKL=+v-6lD523r zaO`T9>TsZ{D*eOh<8U~L#-a=i4HE1P643?$NO9On&qyEDBL)TrDDZme@9!rZ4kv>L zfjBJ^0v{JgD-Vam)E+<1`KBh$o;d?RZ*MQbV37LLb+k3Nq6~X47K;%IhQLT9VKf@0 ztE-EGf&%Wj=N|sKZy&pN?@m_G4(QY?DAX9|(M2~BnIpymO=H^h>A2l)Hb40!v**mA zu&|Jxo*r^?bIHxkB^r(5_xlNjLNqrup=la{K!DWLRF>R!8$VsKf?Jm?p|i7-=bn8Q z)nsCMMFm8_I14D$NT?jAldp}F$pkw(I{~OzzMKo^&$Hpa`v`}_xZQ5Dva)DyZl=NK z!(y?}+S0=5)2FegrgH0&C2ZTVgMaMY$=$0~p(qNUS5`7%!UR71^i#fUIDye%Ks(K- zR4GV=(b;JnWSUqASP}&4h5=vLvIUpR#pcbMS-W~Q%PK06B?+6&hQr~Yp`n4s#zr1{ z{Bg?4%b7cOE~=Vn_qDaP96WG<%*;$COqjs?AHB=WgR_{?TZ*hH?;9rkiHS(Z%K$af zdUEv}b&ec9!h5@Rv3A`$N=ixq*z?IJ95`@*zHk_?*UOaRVnU%14u^xX@^Z4Xv%j(4 zg$oxrcI+74U0rxQ9-`4Gd-v?&#^pEhlZ<7UUR6Ve!a`)eP7n}4gx^T{yb@d=WYg;# zS$g%4kYyQJQ7A7fBXu~948uUvwB)inLb|)VId}Ff!C;V3Fv#h;I`VS!xPIvk6y+Ad zQ=m8U3@JPfAU}20(}|ZRY6&r|>%ygy>YggXMlW+`&7;1)p8fmx6Y1~AYO{SagpTa- zBP9?B&>ITj^Z97-H86MHT&`Yn4O#Xq)HhW0<9vpl2Z7k=p;RYNXvW9ENx@maJ@Y7P zj>_%V-GRxZVi~TCcDo&$%|?29I+7$YG&Dpg6r!`UlM}VIXqrZr%Y^`3*=`~t!qkGP zDMBEH9pE&ao6l21RM8@!4P{e5wf_hzW6qOp}u{z9X`R@R=KD7n! S1|%o|0000oA;?vO1v zrY!ZNYu7}?5M_*s-*nHp_n!MZ=Xbv6`My8rdA;Ay^PJCLpL7R%OMV`49smINt*www zOqKq*xHy=9Tx{$vQ`iC#b_f7y&gEtJu>XX9R!(*R5UU6PgzEr6XKoQz0U!zr0Bha= zU|0+Qq7mgU9F3TO!}qKu5;*$#5xeUPm^++!YdaL@N1o%X1{!r&&0PV2`;j#g;ezgS zEjpVeF&%Z2gric*?4C}Cd~=?4%AJ%#iKBML(OQy2R$1w=5;5}cTQ4K>K*>N0Zyt&u zFQJ(nA}Pn!74cMS0i^|E$qmg~+U5&Uf(>qA=awDU$1Y{lQCIY*{q;Ru_bSyIrk+|0 zuit7k+$%p~9Kq{rcLtk%0dN+$pKD3D(YI0BO^6kekwMtns+;yOwHH7jC*}diw_glh zrGsL<0)~9#weu>nlY{6d9b8@5UAg?RPI5{Ifn{Z15@0W<+)q#&8Q=5aoAw$apfU>$ zMcp)0Kt~xdoP%#_U^{NwlFhE_%`dj|c2MjF%vTA^CS2Zp%Od3XpuRY$0RGU-VXHwN zv5nwL!sG1t(+Ofwj&j#>9!0CQG{TAHlTJg9d3+p79eJNN^z|%EQBRd8(h7id(XMk9 z+=o74*#vZN>4*<6yhb@0Y~?OpGNIHZ6rre}V`}g1KBhy@qkkS70c|B@iO{B`#ax7S z2<$FegyYd>4!v0&qJxqyNcO;sJf}R!yYePH2nWz(Ar=~umUr3a1|l&xg?w`IeQ~9y zbk!Rl{=yKcVHKI$THRP*U2{2wHpfvRsNfm9+tq2JQ2MWI0woz;=dLyJ_dmts@Z=c2QX6-+;g3cZeGihPz2z}# ztL6`0p>zB$O1Q{*9&VDpwmnW{Z_+{2N0hGG&wZp*N5K{_#l#Y@{;Q}%KCd4}k1j0S z_o_G8hnK5@?Va)yp3AGsyJ<}m^JKh3ATn5f8A9e};_ahDsk&;+r`3QDVptI!0uDqHvafsaFDx6PYoQ-H)_!!HlrD^bGJ^DVK1PE&Fm#mE$q@;wo$F z5hQBQF1E8h=J11Tbi4S7vDQLgidVDL9<)79_+kNPjd8`Sxq!RKlY5r_UbR!SkPPBI z-N14WKvV%Fv%sd7WCKr4^L0tPmKBtI#j8Gmbo26=gzjI7xVMziZ>F7bf@A!Zv~j}g zT!EMkB@8**)9119HljP}q+N5&l@fX_LY^Pu+Vk9)fg=ixxrXj6*L)qzabI7(ngJ3-_#hjNWqF$F3^Hfn?^$$U1-y%1D`WiwHrrB zL(_F$PujGtHB4^6R2HGh9A|=&JkKj~pG{wTk`n&T@szfaq=VY0ZDVP!vS>Y_X8dXJ zYf;aInI&<{ZgUNF^=(?mPS@9oGr#@lW6{n=UHYbQ$6aGwSqj&+2l0I5Tox-10|}%?ivgm0GmPD}7T^ZT_B>4t+#H#EHxnt{TDXH=Kavlp!%q|CdeSDyr zcviuSrAk@`p=*(j_@yxKH&+-r2!e3rE6s8CD6xI$sHfmzkn@3PvOM8_8|S>YPbxaB zNQsjWyZ)o%jOPLQb>qm;h5aeXcbB-Adx`-?LMV@XII$^iodL3BH7vi!W zfu?WOm{J#t_zaJ1+Zc+2E+rKm{o`xLWcpjFGZf-%Kul{d;=rV&;ZP!WSLI&OQO0`) z26?7od9CW1=eyQ{k}h2XEbP6%b`qjrE9>?Z|J4=oE>h>4@1IPdCPyN1vyvYe5{!|R zbgX)OQ&O-V`UlO`c0(<6Cv~JE)ImKlx0A|X->A@sk!xG3jU>Ol;wc#RNYNI-N?J(z zewCkl%X_tj0%KeWN|(6ZXdEH zL$$TIR4eUx!d~;xl!^Unl-_>D4sUCFd&lADzz*7~+TgnfwihvUm&$}a*7y4D>Atz( zGJzc5aX6U(8}x4KE0qMjK7>|J-S=x!$T-$`<7>aHSnms&Ol|%A)zIH5kW-{96>V+p zC$7vZ7|yLcP^zlVPfQ*B=x<}Z9Wx>Ov0>K|+$@-8(2vd%%M!almzLqHN!$2wtsz@$ zz_)S{YT-IeQKQh8B}48EuNV`HO}ry$Q7;aPVnV)2j%H@kp7%S>)V)tkIZxw&mzdXR zjOCA`X{_g)ING=^Nzqw#X)wK$XjiDLnXuBAD|^arytXc{<9)+>~^WuzAv} zRO*Hh%4W2SoZ{8hvze+XH^dn-Pj&cxUe1KZbNi*hO##b;J-C>s&;HAuNi2IgPM(Le z+Z?IImeq^0whf=EyMq!CQj)^*lTTDTvNg1lvll$ck83%~R5>i<)UV&N zg^{|>&QXPg7^`vu^p-*qfPr07pRAhg%|1&5slKJwos$@l%^GpFIl$TSsj~f1Ki#-kK7hXKsHttV8L4AXYwXwu zU$MH?v$&Wxw+JKmgtVZV9a#AKtXB8rI8wlrbj9plpeEsX8;6jXUCnv)`rG_nzG^!WL%K z;SKK)icOn~`wj>E@zeBYy~A zzTk`Pe4WyyVcAz~);ks?ESB?vVPf;9zNG|ftiT-R&STKl0d{r(gjsU|EFc)b!mNHy zlZvUq02@<5%*7lG0RR&l7#yJC7aj@#tV9sN`X^&jBFkSs)>R_wKlRTHs;1!~Qxxzh zG~V9_k1_O%#4rWWf@(q4VNi7~mD z5FCREK_i1hF=4pi@Gt-dg+hlax0oaH|C&7hZA!qXhA>U4KTk3b4`t@@fD;B69Emzga@svs&f%0 z0W%^1Sk#C}#FT`P0_=cxr=TEWM*sv65k8TWQhZ|W00=Mu5iswjrkYa9;V}2E?!Kf- zL@DJg3{C{NnszODz>kOs1PVM5PzrdsM}V2>LEu4;?v8FAt*g4KuIt)+U*jw#55ttD zp2mrwxL?8kY8Qc+q2f+vsF^B-TL)A{Fx%X~n1i|ZC z=Ve*Wt98G)xVpU9nZewcIWYksqKFXuf9|M1iU|4$5kW-bI8M_+Mhhe+feDnQx%$)Nu4b$aj zmr{0zX|!eSZV@g7@dPk|f{3-7BqE@<<{sMmd^&4w3AEjAv&iB8{&YO1l!W+rcmL?o zqeQ&UOIz07TAdeNn-G6^_ugyYZnqcvZOIaVApme&7kEhGX&e%h_1f0iw3VE`{pPRV zef`({W_tPj8JnFB_retBDJ3D+)_ZLnU_fhQk5+42Ru~?5xS4@TiZCb;0U&xm+}$pR z!#IwR^zzwLW>(e1?Mr&Zh+z*Mct`(ft^UT7V{VwHXW*R;u;XD+W z>U?Ua!+JXEx`b;<{GWgL_doyXkMrSP5Yv#SA(QFx_9h~VWJyU|>#d~#0lhW`)_Ivv zXH~_I91ujH_xX5itwqFqKArCmeOZ3;#mn1w@BY%-S6_Yg=<;$soz{8YZnp-|Ct)6B z@Bl(vAIs>fwN`5#7$~I_1V9iDCUQF;?&o=a^ZNCxSFhG}{pa8PUS9sgFbpaD{o6O| zy28D!3+Kr~Lk$20ysN{*J_>VdeOZ=G?3mdI0We&VXc~+5y3D80PHtM)|MZ)6OI>HCK-inte^b?3Yio@I6lf3DYF(}Mh)~sKS!!(n2vn^KmNT|n$*BnMHsfyd z{K;q6+WOnKZ-*?e|MqHK*W=xts_u5XFJ3%9o4H!=9Y93%-oxPTsu~YGT5G*`nF@h= z@1mF2-MdMl$DirV&A{?#pU(Gpzxma3uf3gDTNY~C4u`M5{eD@NfBouTWGG9}{nC|N zXOoi4Xh~+OOCs5143Y!r0FOJ-P%1oL{^EMK zzua#2ySuyBU%%Pv^0<6{vAa%%%_;}-FdB?9m6AC#05c0_CXxh7fQFkX1?)lV95~$F z*JTa$ve|5R7vnfyUOayMWG(m*laThOrEsI0GKe z#KIr|z1C$}c-pwz;c&=FIOQ_tK{5qLx23gk2QVaOp=2o~DnkiIozI8kePJdDKghAv8b64$M$#g6wgxtbwXXawgsyBas z!vU=QYI|9>7Z^t{fk3g&eO=$*e7L>66_MTLCCrcahwH1yeXV_2p~%E+(Q55$-Em!O zeS81m#g|_g?CH~|?_dA@fBx&gw=!Ms_uFY_NG`Jf{IeJT^p96Bo=K>hS(rzJnR!P$_9QZZEgHW^G6^$#}KB{PNk0oC-NswHmP=&#%7y?%lf& zV%^L<0ueCt-dp;J!!$Ee=5XrOv^Oy$*bhJa@aXZiGcj05V0f_IZr{JZ$w~IRi^rcm zyL$F){QL=adyGZQ!ZjWcQAD`AB+A0e@nDXmO0C;meXYK(Zgz8fFXQAX=B1`l6rn_A zv;Fxmf69Sz+~lh#xY#2LRQ0e5fQSG%CvgOSlE`n)$5^V*b1W;jW^=8lQ(M-})r+#* z9hT!7u^&es2Tv()H?MC$6dA6hJj#r5Ko$U!NFvBA9*7i5kW7pKd^45a>l!_JwYu6m z_w(I)p09uQ>*@01t}ScCBvP5?@SbJ3zDgvt-Jl4<&gXRwM`8&lRW%JF%2^~KOx<)l zZOm=Z*0o!&mLNkJfllN}I-$apc?BxKeUB004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x010qNS#tmY4#NNd4#NS*Z>VGd000McNliru2@kD95;P+O2yh9^xZR+ZsR08pFbeDhmVk@5o%}|g z=9*0Opb&e7xJ-1fV;@brX+am2zEx2lQvkphr8odmg8D6aUpl$!x#>(|H0x!gxX_6B zb~LsMxPS)Qh@N$l1!+0%Fl~9F-!9t6g162uipgB;?z_A14Pdiu5*nSIVzCtbskqO|~k z<5Cd1Qs(($3r#=4T_(0uy=FXfOn zTMggj0Rbg2Syo>v(dOAz<`6pDM%wU>G(GoV*LZJsemN}azLC*A?ppMDh39#$vyexy3nb#`QEYiflm4Ek`b3ZFs%eOqQtC(=nym?sJf1x|6FQ+pFMpwIQx2hVk&vC zYhIdoPfno&L7f@oI*tncy2+efZp;*#Jc3&Z>cFeUAczn|dwZ?HZt=T+`e)|i7anWx zNkJ13a>=(nOT`B1j@-$&f`Q)qZ0M(6oDPiG)}d_#1P~wv#fFa#J8xbv7e2r*zLL+U zs1|W5f#n*7irYO~N>SI2O6JnLzhno+QaBA5BI3;-bd!a{02mBRWj%S_eR#jTYmi{b zr@ZIHZ(NDA)_pmPfH2ZSJGxWZ-2=H?Mp}P6cR>(sArRXI2rxi~Xy}?A-l--=sa(^< z9M%fZ#+6j|0U=oh#vOKcd|JY+Hp0K3Z@#ld(^C-A%#%~%dZ%70>Z=tN1fevvp6C*L zy8smnA;@1=c28%%^U{@fUYa=%Z0!$jvGkD?_kXf24ml3Zt{yvx@ZwBxXgDd2PLR@u zZ6{H$tzRmvPcPtSr~Z7kSfy?KX-=?lNh$u#YcwI_b~lUL3!Y`3>1!3IQZuu#_1{|P!%*6De@GmV$7B%V5$78X>cvbvIV`zG6U zVd}Gw<$L!0<&A#}0=G!jGH%`>$4A6>pM3hB$%85^slvN+3%R}{M@4ncF1=jn`~3Lh z->TFqO(Enh1qi^<*!c5bKlal{y8FV@QFTe@_E~ov&G+|0Ynd7JBP3Q{|HHZ9dTCWO z^T2AAT?k=}8M<@et>6EuYjC(42=$Mjd*@#WuT=D|XDz#pEDNS8JMK#Q?@eTD?*W3g zK166arhp&<@XU#0U0u2Q+$?cq_B|2wf6A_2jtXxYkz_Wkjj(NW9DZ~#0?2TaADNaW z8|H(PW5507kKnkD>pG3PQz}{mpP Date: Thu, 29 Oct 2020 00:17:41 +0100 Subject: [PATCH 12/64] Improved translation loading, added smart decimation translations --- README.md | 8 ++++- __init__.py | 3 ++ extentions.py | 6 +--- tools/decimation.py | 32 +++++++++++++++++- translations/__init__.py | 70 ++++++++++++++++++++++++++++++---------- translations/en_US.py | 24 +++++++++----- translations/ja_JP.py | 22 +++++++++++-- ui/decimation.py | 44 ++++--------------------- 8 files changed, 136 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 5641f54f..af27ce84 100644 --- a/README.md +++ b/README.md @@ -301,8 +301,14 @@ It checks for a new version automatically once every day. ## Changelog #### 0.18.0 -- **Added japanese translation!** +- **Added Japanese translation!** + - Cats is now almost fully translated into Japanese + - To use it simply change your Blender language to Japanese and then restart Blender - Full credit goes to **Jordo** and **Ruuubick**! Thank you so much <3 + - If you want to help translating Cats into any language, please us know! +- **Added Smart Decimation!** + - This lets you decimate without loosing any shapekeys! + - Full credit goes to **feilen**! Tons of thanks for this awesome feature <3 - Cats is now fully compatible with Blender 2.90 - Changed link to a new vrm importer since the old one dropped support - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 diff --git a/__init__.py b/__init__.py index 3c96faae..f8d8db5f 100644 --- a/__init__.py +++ b/__init__.py @@ -255,6 +255,9 @@ def register(): # Register Updater and check for CATS update updater.register(bl_info, dev_branch, version_str) + # Check for missing translations + translations.check_missing_translations() + # Set some global settings, first allowed use of globs globs.dev_branch = dev_branch globs.version_str = version_str diff --git a/extentions.py b/extentions.py index 35cfa1a1..d80852bc 100644 --- a/extentions.py +++ b/extentions.py @@ -158,14 +158,10 @@ def register(): name=t('Scene.decimation_mode.label'), description=t('Scene.decimation_mode.desc'), items=[ + ("SMART", t('Scene.decimation_mode.smart.label'), t('Scene.decimation_mode.smart.desc')), ("SAFE", t('Scene.decimation_mode.safe.label'), t('Scene.decimation_mode.safe.desc')), - ("HALF", t('Scene.decimation_mode.half.label'), t('Scene.decimation_mode.half.desc')), - ("FULL", t('Scene.decimation_mode.full.label'), t('Scene.decimation_mode.full.desc')), - - ("SMART", t('Scene.decimation_mode.smart.label'), t('Scene.decimation_mode.smart.desc')), - ("CUSTOM", t('Scene.decimation_mode.custom.label'), t('Scene.decimation_mode.custom.desc')) ], default='SMART' diff --git a/tools/decimation.py b/tools/decimation.py index e26a4c0d..42f88042 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -345,7 +345,7 @@ def decimate(self, context): current_tris_count = current_tris_count - tris + tris_after tris_count = tris_count - tris # Repair shape keys if SMART mode is enabled - if smart_decimation and len(mesh_obj.data.shape_keys.key_blocks) > 0: + if smart_decimation and Common.has_shapekeys(mesh_obj): for idx in range(1, len(mesh_obj.data.shape_keys.key_blocks) - 2): print("Key: " + mesh_obj.data.shape_keys.key_blocks[idx].name) mesh_obj.active_shape_key_index = idx @@ -392,7 +392,37 @@ def decimate(self, context): # break +@register_wrap +class AutoDecimatePresetGood(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): + bpy.context.scene.max_tris = 70000 + return {'FINISHED'} +@register_wrap +class AutoDecimatePresetExcellent(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): + bpy.context.scene.max_tris = 32000 + return {'FINISHED'} + + +@register_wrap +class AutoDecimatePresetQuest(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): + bpy.context.scene.max_tris = 5000 + return {'FINISHED'} diff --git a/translations/__init__.py b/translations/__init__.py index 2c7e2eec..86153697 100644 --- a/translations/__init__.py +++ b/translations/__init__.py @@ -1,26 +1,62 @@ # Thanks to https://www.thegrove3d.com/learn/how-to-translate-a-blender-addon/ for the idea +import os +import importlib +import addon_utils from bpy.app.translations import locale +from .en_US import dictionary as dict_en -if locale in ['ja_JP']: - from .ja_JP import dictionary +# Get package name, important for relative imports +package_name = '' +for mod in addon_utils.modules(): + if mod.bl_info['name'] == 'Cats Blender Plugin': + package_name = mod.__name__ + +# Get all supported languages in translations folder +languages = [] +for file in os.listdir(os.path.dirname(__file__)): + lang_name = os.path.splitext(file)[0] + if len(lang_name) == 5 and lang_name[2] == '_' and lang_name != 'en_US': + languages.append(lang_name) + +# Import the correct language dictionary +if locale in languages: + lang = importlib.import_module(package_name + '.translations.' + locale) + dictionary = lang.dictionary else: - from .en_US import dictionary + dictionary = dict_en def t(phrase: str, *args, **kwargs): # Translate the given phrase into Blender's current language. - try: - output = dictionary[phrase] - if isinstance(output, list): - newList = [] - for string in output: - newList.append(string.format(*args, **kwargs)) - return newList - elif not isinstance(output, str): - return output - else: - return output.format(*args, **kwargs) - except KeyError: - print('Warning - Text missing for: ' + phrase) - return phrase + output = dictionary.get(phrase) + if output is None: + output = dict_en.get(phrase) + if output is None: + print('Warning: Unknown phrase: ' + phrase) + return phrase + + if isinstance(output, list): + newList = [] + for string in output: + newList.append(string.format(*args, **kwargs)) + return newList + elif not isinstance(output, str): + return output + else: + return output.format(*args, **kwargs) + + +def check_missing_translations(): + lang_dicts = [] + for language in languages: + lang = importlib.import_module(package_name + '.translations.' + language) + lang_dicts.append(lang.dictionary) + + for key, value in dict_en.items(): + if value is None: + print('Translations en_US: Value missing for key: ' + key) + + for lang_dict in lang_dicts: + if lang_dict.get(key) is None: + print('Translations ' + lang_dict.get('name') + ': Value missing for key: ' + key) diff --git a/translations/en_US.py b/translations/en_US.py index 64793a98..e2850423 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -2,6 +2,9 @@ # Class.label / Class.desc (tooltip) # Class.property + # Language name + 'name': 'en_US', + # Main file 'Main.error.restartAdmin': '\n\nFaulty CATS installation found!' '\nTo fix this restart Blender as admin! ' @@ -108,10 +111,9 @@ 'DecimationPanel.preset.good.label': 'Good', 'DecimationPanel.preset.good.description': 'The maximum number of tris you can have for a Good rating.', 'DecimationPanel.preset.quest.label': 'Quest', - 'DecimationPanel.preset.quest.description': 'The reccomended number of tris for Quest avatars.\n' \ + 'DecimationPanel.preset.quest.description': 'The recommended number of tris for Quest avatars.\n' 'A hard limit will be established in the future that will not be much more than this.', - # UI Eye tracking 'EyeTrackingPanel.label': 'Eye Tracking', 'EyeTrackingPanel.error.noMesh': 'No meshes found!', @@ -800,7 +802,7 @@ 'AutoVisemeButton.error.selectShapekeys': 'Please select the correct mouth shapekeys instead of "Basis"!', 'AutoVisemeButton.success': 'Created mouth visemes!', - # Extentions + # Extensions 'Scene.armature.label': 'Armature', 'Scene.armature.desc': 'Select the armature which will be used by Cats', @@ -910,9 +912,15 @@ 'Scene.merge_armatures_remove_zero_weight_bones.label': 'Remove Zero Weight Bones', 'Scene.merge_armatures_remove_zero_weight_bones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices.' '\nUncheck this if bones or vertex groups that you want to keep got deleted', + # Decimation 'Scene.decimation_mode.label': 'Decimation Mode', 'Scene.decimation_mode.desc': 'Decimation Mode', + 'Scene.decimation_mode.smart.label': "Smart", + 'Scene.decimation_mode.smart.desc': 'Best results - repair shape keys after decimation\n' + '\n' + "This will decimate your whole model and attempt to undo the warping caused by Blender's decimation.\n" + "This will give the best results and keep blinking and lip syncing, but may have issues on some models.", 'Scene.decimation_mode.safe.label': 'Safe', 'Scene.decimation_mode.safe.desc': 'Decent results - no shape key loss\n' '\n' @@ -931,11 +939,6 @@ 'This will decimate your whole model deleting all shape keys in the process.\n' 'This will give consistent results but you will lose the ability to add blinking and Lip Syncing.\n' 'Eye Tracking will still work if you disable Eye Blinking.', - 'Scene.decimation_mode.smart.label': "Smart", - 'Scene.decimation_mode.smart.desc': 'Best results - repair shape keys after decimation\n' - '\n' - "This will decimate your whole model and attempt to undo the warping caused by Blender's decimation.\n" - "This will give the best results and keep blinking and lip syncing, but may have issues on some models.", 'Scene.decimation_mode.custom.label': 'Custom', 'Scene.decimation_mode.custom.desc': 'Custom results - custom shape key loss\n' '\n' @@ -984,6 +987,7 @@ 'Scene.max_tris.label': 'Tris', 'Scene.max_tris.desc': 'The target amount of tris after decimation', + # Eye Tracking 'Scene.eye_mode.label': 'Eye Mode', 'Scene.eye_mode.desc': 'Mode', @@ -1053,6 +1057,7 @@ 'Scene.mesh_name_viseme.label': 'Mesh', 'Scene.mesh_name_viseme.desc': 'The mesh with the mouth shape keys', + # Visemes 'Scene.mouth_a.label': 'Viseme AA', 'Scene.mouth_a.desc': 'Shape key containing mouth movement that looks like someone is saying "aa".\nDo not put empty shape keys like "Basis" in here', @@ -1065,9 +1070,11 @@ 'Scene.shape_intensity.label': 'Shape Key Mix Intensity', 'Scene.shape_intensity.desc': 'Controls the strength in the creation of the shape keys. Lower for less mouth movement strength', + # Bone Parenting 'Scene.root_bone.label': 'To Parent', 'Scene.root_bone.desc': 'List of bones that look like they could be parented together to a root bone', + # Optimize 'Scene.optimize_mode.label': 'Optimize Mode', 'Scene.optimize_mode.desc': 'Mode', @@ -1077,6 +1084,7 @@ 'Scene.optimize_mode.material.desc': 'Some various options on material manipulation.', 'Scene.optimize_mode.bonemerging.label': 'Bone Merging', 'Scene.optimize_mode.bonemerging.desc': 'Allows child bones to be merged into their parents.', + # Bone Merging 'Scene.merge_ratio.label': 'Merge Ratio', 'Scene.merge_ratio.desc': 'Higher = more bones will be merged\n' diff --git a/translations/ja_JP.py b/translations/ja_JP.py index 5706338d..019fb2cb 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -2,6 +2,9 @@ # Class.label / Class.desc (tooltip) # Class.property + # Language name + 'name': 'ja_JP', + # Main file 'Main.error.restartAdmin': '\n\nFaulty CATS installation found!' '\nTo fix this restart Blender as admin! ' @@ -92,7 +95,8 @@ 'DecimationPanel.decimationMode': 'デシメーションモード:', 'DecimationPanel.safeModeDesc': ' まともな結果 - シェイプキーの損失はありません', 'DecimationPanel.halfModeDesc': ' 良好な結果 - 最小のシェープキー損失', - 'DecimationPanel_fullModeDesc': ' 最良の結果 - フルシェイプキー損失', + 'DecimationPanel.fullModeDesc': ' 一貫した結果 - フルシェイプキー損失', + 'DecimationPanel.smartModeDesc': ' 最良の結果 - 形状キーの保存', 'DecimationPanel.customSeparateMaterials': '材料別に分離して開始:', 'DecimationPanel.SeparateByMaterials.label': '材料別に分離', 'DecimationPanel.customJoinMeshes': 'メッシュに参加して停止:', @@ -102,6 +106,13 @@ 'DecimationPanel.warn.noMeshSelected': 'メッシュが選択されていません', 'DecimationPanel.warn.emptyList': '両方のリストが空で、これは完全なデシメーションに等しい!', 'DecimationPanel.warn.correctWhitelist': '両方のホワイトリストはデシメーション中に考慮されます', + 'DecimationPanel.preset.excellent.label': '優れた', + 'DecimationPanel.preset.excellent.description': '優れた評価を得るために持つことができるトリスの最大数', + 'DecimationPanel.preset.good.label': '良い', + 'DecimationPanel.preset.good.description': 'あなたが良い評価のために持つことができるトリスの最大数', + 'DecimationPanel.preset.quest.label': 'Quest', + 'DecimationPanel.preset.quest.description': 'Questアバターの推奨トリス数.\n' + '将来的には、これをはるかに超えることのない厳しい制限が設定されます。', # UI Eye tracking 'EyeTrackingPanel.label': 'アイトラッキング', @@ -903,6 +914,11 @@ # Decimation 'Scene.decimation_mode.label': '単純化モード', 'Scene.decimation_mode.desc': 'Decimation Mode', + 'Scene.decimation_mode.smart.label': "スマート", + 'Scene.decimation_mode.smart.desc': 'Best results - repair shape keys after decimation\n' + '\n' + "This will decimate your whole model and attempt to undo the warping caused by Blender's decimation.\n" + "This will give the best results and keep blinking and lip syncing, but may have issues on some models.", 'Scene.decimation_mode.safe.label': '安全', 'Scene.decimation_mode.safe.desc': 'Decent results - no shape key loss\n' '\n' @@ -916,10 +932,10 @@ 'The results are better but you will lose the shape keys in some meshes.\n' 'Eye Tracking and Lip Syncing should still work.', 'Scene.decimation_mode.full.label': 'フル', - 'Scene.decimation_mode.full.desc': 'Best results - full shape key loss\n' + 'Scene.decimation_mode.full.desc': 'Consistent results - full shape key loss\n' '\n' 'This will decimate your whole model deleting all shape keys in the process.\n' - 'This will give the best results but you will lose the ability to add blinking and Lip Syncing.\n' + 'This will give consistent results but you will lose the ability to add blinking and Lip Syncing.\n' 'Eye Tracking will still work if you disable Eye Blinking.', 'Scene.decimation_mode.custom.label': 'カスタム', 'Scene.decimation_mode.custom.desc': 'Custom results - custom shape key loss\n' diff --git a/ui/decimation.py b/ui/decimation.py index 03586228..2ad4053d 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -9,38 +9,6 @@ from ..tools.register import register_wrap from ..translations import t -@register_wrap -class AutoDecimatePresetGood(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): - bpy.context.scene.max_tris = 70000 - return {'FINISHED'} - -@register_wrap -class AutoDecimatePresetExcellent(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): - bpy.context.scene.max_tris = 32000 - return {'FINISHED'} - -@register_wrap -class AutoDecimatePresetQuest(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): - bpy.context.scene.max_tris = 5000 - return {'FINISHED'} @register_wrap class DecimationPanel(ToolPanel, bpy.types.Panel): @@ -65,14 +33,14 @@ def draw(self, context): row.prop(context.scene, 'decimation_mode', expand=True) row = col.row(align=True) row.scale_y = 0.7 - if context.scene.decimation_mode == 'SAFE': + if context.scene.decimation_mode == 'SMART': + row.label(text=t('DecimationPanel.smartModeDesc')) + elif 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 == 'SMART': - row.label(text=t('DecimationPanel.smartModeDesc')) elif context.scene.decimation_mode == 'CUSTOM': col.separator() @@ -167,9 +135,9 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'decimation_remove_doubles') row = col.row(align=True) - row.operator(AutoDecimatePresetGood.bl_idname) - row.operator(AutoDecimatePresetExcellent.bl_idname) - row.operator(AutoDecimatePresetQuest.bl_idname) + row.operator(Decimation.AutoDecimatePresetGood.bl_idname) + row.operator(Decimation.AutoDecimatePresetExcellent.bl_idname) + row.operator(Decimation.AutoDecimatePresetQuest.bl_idname) row = col.row(align=True) row.prop(context.scene, 'max_tris') col.separator() From 5aa3088710c9ac7bb814822752bba60a117cf85d Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 29 Oct 2020 00:25:22 +0100 Subject: [PATCH 13/64] Updated readme --- README.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index af27ce84..74e026ff 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ There are a lot of perks like having your name inside the plugin! - Optimizing model with one click! - Creating lip syncing - Creating eye tracking - - Automatic decimation + - Automatic decimation (while keeping shapekeys) - Creating custom models easily - Creating texture atlas - Creating root bones for Dynamic Bones @@ -52,11 +52,11 @@ Join our Discord to report errors, suggestions and make comments! ![](https://i.imgur.com/pJfVsho.png) - - If you need help figuring out how to use the tool: + - If you need help figuring out how to use the tool (very outdated): [![VRChat - Cat's Blender Plugin Overview](https://img.youtube.com/vi/0gu0kEj2xwA/0.jpg)](https://www.youtube.com/watch?v=0gu0kEj2xwA) -Skip the step where he installs "mmd_tools" in the video below, it's not needed anymore! +Skip the step where he installs "mmd_tools" in the video below, it's not needed anymore! (also very outdated) [![VRChat - Importing an MMD to VRChat Megatutorial!](https://img.youtube.com/vi/7P0ljQ6hU0A/0.jpg)](https://www.youtube.com/watch?v=7P0ljQ6hU0A) @@ -65,6 +65,9 @@ Skip the step where he installs "mmd_tools" in the video below, it's not needed - Shotariya - Neitri - Kiraver + - Jordo + - Ruubick + - feilen ## Model @@ -190,10 +193,13 @@ This uses an internal dictionary and Google Translate. ## Decimation -![](https://i.imgur.com/vozxKy9.png) +![](https://i.imgur.com/5u3teLp.png) **Decimate your model automatically.** +##### Smart Decimation +- This will decimate all meshes while keeping every shapekey. + ##### Save Decimation - This will only decimate meshes with no shape keys. @@ -204,7 +210,7 @@ This uses an internal dictionary and Google Translate. - This will decimate your whole model deleting all shape keys in the process. ##### Custom Decimation -- This will let you choose which meshes and shape keys should not be decimated. +- This lets you choose the meshes and shape keys that should not be decimated. ## Eye Tracking @@ -297,6 +303,7 @@ This works by checking all bones and trying to figure out if they can be grouped **This plugin has an auto updater.** It checks for a new version automatically once every day. +--- ## Changelog From 02acd1011d81fa7e6480a506be977920248a0de6 Mon Sep 17 00:00:00 2001 From: Hotox Date: Thu, 29 Oct 2020 20:12:54 +0100 Subject: [PATCH 14/64] Added "Show mmd_tools tabs" option to Settings --- README.md | 3 ++- extentions.py | 6 ++++++ tools/common.py | 37 ++++++++++++++++++++++++++++++++++++- tools/settings.py | 3 ++- translations/en_US.py | 4 ++++ translations/ja_JP.py | 9 +++++++++ ui/settings_updates.py | 2 ++ 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74e026ff..4c124d27 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ There are a lot of perks like having your name inside the plugin! - Optimizing materials - Translating shape keys, bones, materials and meshes - Merging bone groups to reduce overall bone count - - Protecting your avatars from game cache ripping - Auto updater *More to come!* @@ -317,6 +316,8 @@ It checks for a new version automatically once every day. - This lets you decimate without loosing any shapekeys! - Full credit goes to **feilen**! Tons of thanks for this awesome feature <3 - Cats is now fully compatible with Blender 2.90 +- Added "Show mmd_tools tabs" option to Settings + - This allows you show and hide the "MMD" and "Misc" tabs added by the mmd_tools plugin - Changed link to a new vrm importer since the old one dropped support - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 - More fixes for Blender 2.90 diff --git a/extentions.py b/extentions.py index d80852bc..f83769d0 100644 --- a/extentions.py +++ b/extentions.py @@ -446,6 +446,12 @@ def register(): ) # Settings + Scene.show_mmd_tabs = BoolProperty( + name=t('Scene.show_mmd_tabs.label'), + description=t('Scene.show_mmd_tabs.desc'), + default=True, + update=Common.toggle_mmd_tabs + ) Scene.embed_textures = BoolProperty( name=t('Scene.embed_textures.label'), description=t('Scene.embed_textures.desc'), diff --git a/tools/common.py b/tools/common.py index b2530e92..3d929a4f 100644 --- a/tools/common.py +++ b/tools/common.py @@ -39,10 +39,15 @@ from . import decimation as Decimation from . import translate as Translate from . import armature_bones as Bones +from . import settings as Settings from .register import register_wrap -from mmd_tools_local import utils from ..translations import t +from mmd_tools_local import utils +from mmd_tools_local.panels import tool as mmd_tool +from mmd_tools_local.panels import util_tools as mmd_util_tools +from mmd_tools_local.panels import view_prop as mmd_view_prop + # TODO: # - Add check if hips bone really needs to be rotated # - Reset Pivot @@ -2083,6 +2088,36 @@ def fix_twist_bone_names(armature): bone_twist.name = 'z' + bone_twist.name +def toggle_mmd_tabs(self, context): + mmd_cls = [ + mmd_tool.MMDToolsObjectPanel, + mmd_tool.MMDDisplayItemsPanel, + mmd_tool.MMDMorphToolsPanel, + mmd_tool.MMDRigidbodySelectorPanel, + mmd_tool.MMDJointSelectorPanel, + mmd_util_tools.MMDMaterialSorter, + mmd_util_tools.MMDMeshSorter, + mmd_view_prop.MMDViewPanel, + mmd_view_prop.MMDSDEFPanel, + ] + + print('Toggling mmd tabs') + if context.scene.show_mmd_tabs: + for cls in mmd_cls: + try: + bpy.utils.register_class(cls) + except: + pass + else: + for cls in reversed(mmd_cls): + try: + bpy.utils.unregister_class(cls) + except: + pass + + Settings.update_settings(None, None) + + """ HTML <-> text conversions. diff --git a/tools/settings.py b/tools/settings.py index 6744d895..c68b5709 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -49,8 +49,9 @@ # Settings name = [Default Value, Require Blender Restart] settings_default = OrderedDict() -# settings_default['use_custom_mmd_tools'] = [False, True] +settings_default['show_mmd_tabs'] = [True, False] settings_default['embed_textures'] = [False, False] +# settings_default['use_custom_mmd_tools'] = [False, True] lock_settings = False diff --git a/translations/en_US.py b/translations/en_US.py index e2850423..85d947b4 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -1096,6 +1096,10 @@ 'Scene.merge_bone.label': 'To Merge', 'Scene.merge_bone.desc': 'List of bones that look like they could be merged together to reduce overall bones', + # Settings + 'Scene.show_mmd_tabs.label': 'Show mmd_tools tabs', + 'Scene.show_mmd_tabs.desc': 'Allows you to hide/show the mmd_tools tabs "MMD" and "Misc"', + 'Scene.embed_textures.label': 'Embed Textures on Export', 'Scene.embed_textures.desc': 'Enable this to embed the texture files into the FBX file upon export.' '\nUnity will automatically extract these textures and put them into a separate folder.' diff --git a/translations/ja_JP.py b/translations/ja_JP.py index 019fb2cb..e56936e7 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -985,6 +985,7 @@ 'Scene.max_tris.label': 'トリス', 'Scene.max_tris.desc': 'The target amount of tris after decimation', + # Eye Tracking 'Scene.eye_mode.label': 'アイモード', 'Scene.eye_mode.desc': 'Mode', @@ -1054,6 +1055,7 @@ 'Scene.mesh_name_viseme.label': 'メッシュ', 'Scene.mesh_name_viseme.desc': 'The mesh with the mouth shape keys', + # Visemes 'Scene.mouth_a.label': 'バイセム AA', 'Scene.mouth_a.desc': 'Shape key containing mouth movement that looks like someone is saying "aa".\nDo not put empty shape keys like "Basis" in here', @@ -1066,9 +1068,11 @@ 'Scene.shape_intensity.label': 'シェイプキーミックスの強度', 'Scene.shape_intensity.desc': 'Controls the strength in the creation of the shape keys. Lower for less mouth movement strength', + # Bone Parenting 'Scene.root_bone.label': '親に', 'Scene.root_bone.desc': 'List of bones that look like they could be parented together to a root bone', + # Optimize 'Scene.optimize_mode.label': '最適化モード', 'Scene.optimize_mode.desc': 'Mode', @@ -1078,6 +1082,7 @@ 'Scene.optimize_mode.material.desc': 'Some various options on material manipulation.', 'Scene.optimize_mode.bonemerging.label': '骨のマージ', 'Scene.optimize_mode.bonemerging.desc': 'Allows child bones to be merged into their parents.', + # Bone Merging 'Scene.merge_ratio.label': 'マージ率', 'Scene.merge_ratio.desc': 'Higher = more bones will be merged\n' @@ -1089,6 +1094,10 @@ 'Scene.merge_bone.label': 'マージするには', 'Scene.merge_bone.desc': 'List of bones that look like they could be merged together to reduce overall bones', + # Settings + 'Scene.show_mmd_tabs.label': 'Show mmd_tools tabs', + 'Scene.show_mmd_tabs.desc': 'Allows you to hide/show the mmd_tools tabs "MMD" and "Misc"', + 'Scene.embed_textures.label': 'エクスポート時にテクスチャを埋め込む', 'Scene.embed_textures.desc': 'Enable this to embed the texture files into the FBX file upon export.' '\nUnity will automatically extract these textures and put them into a separate folder.' diff --git a/ui/settings_updates.py b/ui/settings_updates.py index 335da1db..6939cf19 100644 --- a/ui/settings_updates.py +++ b/ui/settings_updates.py @@ -24,6 +24,8 @@ def draw(self, context): row.label(text=t('UpdaterPanel.name'), icon=globs.ICON_SETTINGS) col.separator() + row = col.row(align=True) + row.prop(context.scene, 'show_mmd_tabs') row = col.row(align=True) row.prop(context.scene, 'embed_textures') # row = col.row(align=True) From bf2ce0238179d3bbee1c0dfc8ab17adeea8fd8b5 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 31 Oct 2020 12:27:46 +0100 Subject: [PATCH 15/64] Fixed Fix Model error (#209) and fixed Eye Tracking roaming (#207) --- tools/armature.py | 5 ++++- tools/eyetracking.py | 41 +++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/tools/armature.py b/tools/armature.py index 71238041..f604906e 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -241,7 +241,10 @@ def execute(self, context): context.space_data.shading.show_backface_culling = True # Set the Color Management View Transform to "Standard" instead of the Blender default "Filmic" - context.scene.view_settings.view_transform = 'Standard' + try: + context.scene.view_settings.view_transform = 'Standard' + except TypeError: + print('Color Management View Transform "Standard" not found!') # Set shading to 3D view set_material_shading() diff --git a/tools/eyetracking.py b/tools/eyetracking.py index 0556b195..fd4a0dd9 100644 --- a/tools/eyetracking.py +++ b/tools/eyetracking.py @@ -25,8 +25,9 @@ # Edits by: GiveMeAllYourCats, Hotox import bpy -import bmesh +import copy import math +import bmesh from collections import OrderedDict from random import random @@ -475,6 +476,8 @@ def repair_shapekeys_mouth(mesh_name): # TODO Add vertex repairing! eye_right = None eye_left_data = None eye_right_data = None +eye_left_rot = [] +eye_right_rot = [] @register_wrap @@ -498,12 +501,18 @@ def execute(self, context): Common.switch('POSE') armature.data.pose_position = 'POSE' - global eye_left, eye_right, eye_left_data, eye_right_data + global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot eye_left = armature.pose.bones.get('LeftEye') eye_right = armature.pose.bones.get('RightEye') eye_left_data = armature.data.bones.get('LeftEye') eye_right_data = armature.data.bones.get('RightEye') + # Save initial eye rotations + eye_left.rotation_mode = 'XYZ' + eye_left_rot = copy.deepcopy(eye_left.rotation_euler) + eye_right.rotation_mode = 'XYZ' + eye_right_rot = copy.deepcopy(eye_right.rotation_euler) + if eye_left is None or eye_right is None or eye_left_data is None or eye_right_data is None: return {'FINISHED'} @@ -538,7 +547,7 @@ class StopTestingButton(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - global eye_left, eye_right, eye_left_data, eye_right_data + global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot if eye_left: context.scene.eye_rotation_x = 0 context.scene.eye_rotation_y = 0 @@ -566,40 +575,34 @@ def execute(self, context): eye_right = None eye_left_data = None eye_right_data = None + eye_left_rot = [] + eye_right_rot = [] return {'FINISHED'} # This gets called by the eye testing sliders def set_rotation(self, context): - global eye_left, eye_right, eye_left_data, eye_right_data + global eye_left, eye_right if not eye_left: StopTestingButton.execute(self, context) self.report({'ERROR'}, t('StopTestingButton.error.tryAgain')) return None - eye_left_data.select = True - eye_right_data.select = True - - bpy.ops.pose.rot_clear() - - eye_left_data.select = False - eye_right_data.select = False - eye_left.rotation_mode = 'XYZ' - eye_left.rotation_euler.rotate_axis('X', math.radians(context.scene.eye_rotation_x)) - eye_left.rotation_euler.rotate_axis('Y', math.radians(context.scene.eye_rotation_y)) + eye_left.rotation_euler[0] = eye_left_rot[0] + math.radians(context.scene.eye_rotation_x) + eye_left.rotation_euler[1] = eye_left_rot[1] + math.radians(context.scene.eye_rotation_y) eye_right.rotation_mode = 'XYZ' - eye_right.rotation_euler.rotate_axis('X', math.radians(context.scene.eye_rotation_x)) - eye_right.rotation_euler.rotate_axis('Y', math.radians(context.scene.eye_rotation_y)) + eye_right.rotation_euler[0] = eye_right_rot[0] + math.radians(context.scene.eye_rotation_x) + eye_right.rotation_euler[1] = eye_right_rot[1] + math.radians(context.scene.eye_rotation_y) return None def stop_testing(self, context): - global eye_left, eye_right, eye_left_data, eye_right_data - if not eye_left or not eye_right or not eye_left_data or not eye_right_data: + global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot + if not eye_left or not eye_right or not eye_left_data or not eye_right_data or not eye_left_rot or not eye_right_rot: return None armature = Common.set_default_stage() @@ -628,6 +631,8 @@ def stop_testing(self, context): eye_right = None eye_left_data = None eye_right_data = None + eye_left_rot = [] + eye_right_rot = [] return None From 6067a80355842b15636fc784169e03221ee70bcc Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 31 Oct 2020 12:35:33 +0100 Subject: [PATCH 16/64] Fixed draw_type error (#199) --- tools/armature.py | 3 ++- tools/importer.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/armature.py b/tools/armature.py index f604906e..4c8a89e7 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -234,7 +234,8 @@ def execute(self, context): space.show_backface_culling = True # set the viewport shading else: armature.data.display_type = 'OCTAHEDRAL' - armature.draw_type = 'WIRE' + if hasattr(armature, 'draw_type'): + armature.draw_type = 'WIRE' armature.show_in_front = True armature.data.show_bone_custom_shapes = False # context.space_data.overlay.show_transparent_bones = True diff --git a/tools/importer.py b/tools/importer.py index e330ebde..b7c9eaab 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -294,7 +294,8 @@ def fix_armatures_post_import(pre_import_objects): fix_bone_orientations(armature) # Set better bone view - armature.draw_type = 'WIRE' + if hasattr(armature, 'draw_type'): + armature.draw_type = 'WIRE' if version_2_79_or_older(): armature.show_x_ray = True else: From ec9debcd7cd7a4c3153ffbd9ec4ada530c63ca53 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 28 Oct 2020 23:11:08 -0700 Subject: [PATCH 17/64] WIP --- extentions.py | 93 ++++++++++++++ tools/bake.py | 338 +++++++++++++++++++++++++++++++++++++++++++++++++ ui/__init__.py | 4 +- ui/bake.py | 69 ++++++++++ 4 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 tools/bake.py create mode 100644 ui/bake.py diff --git a/extentions.py b/extentions.py index f83769d0..9e5a991d 100644 --- a/extentions.py +++ b/extentions.py @@ -167,6 +167,99 @@ def register(): default='SMART' ) + # Bake + Scene.bake_resolution = IntProperty( + name="Resolution", + description="Output resolution for the textures.\n" \ + "- 2048 is typical for desktop use.\n" \ + "- 1024 is reccomended for the Quest", + default=2048, + min=128, + max=4096 + ) + + Scene.bake_use_decimation = BoolProperty( + name='Decimate', + description='Reduce polycount before baking using decimation settings', + default=True + ) + + Scene.bake_generate_uvmap = BoolProperty( + name='Generate single UVMap', + description="Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ + "Only disable if your UVs are non-overlapping already.\n" \ + "This will leave any map named \"Detail Map\" alone.", + + default=True + ) + + Scene.bake_prioritize_face = BoolProperty( + name='Prioritize Face/Eyes', + description='Scale any UV islands attached to the head/children by a factor of 2', + default=True + ) + + Scene.bake_illuminate_eyes = BoolProperty( + name='Set eyes to full brightness', + description='Relight LeftEye and RightEye to be full brightness.\n' \ + "Without this, the eyes will have the shadow of the surrounding socket baked in,\n" + "which doesn't animate well", + default=True + ) + + Scene.bake_pass_smoothness = BoolProperty( + name='Smoothness', + description='Bakes Roughness and then inverts the values.\n' \ + 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.', + default=True + ) + + Scene.bake_smoothness_diffusepack = BoolProperty( + name='Pack to diffuse alpha', + description='Copies the smoothness map to the alpha channel of the diffuse map.\n' \ + "Make sure to set Smoothness > Source to 'Albedo Alpha' in your Unity material", + default=True + ) + + Scene.bake_pass_diffuse = BoolProperty( + name='Diffuse (Color)', + description='Bakes diffuse, un-lighted color. Usually you will want this.', + default=True + ) + + Scene.bake_pass_normal = BoolProperty( + name='Normal (Bump)', + description="Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" \ + "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" \ + "that makes the low res output look like the high res input.", + default=True + ) + + Scene.bake_pass_ao = BoolProperty( + name='Ambient Occlusion', + description='Bakes Ambient Occlusion, non-projected shadows. Adds a good amount of detail to your model.', + default=True + ) + + Scene.bake_pass_questdiffuse = BoolProperty( + name='Quest Diffuse (Color+AO)', + description='Blends the result of the Diffuse and AO bakes to make Quest-compatible shading.', + default=True + ) + + Scene.bake_questdiffuse_opacity = FloatProperty( + name="AO Opacity", + description="The opacity of the shadows to blend onto the Diffuse map.\n" \ + "This should match the unity slider for AO on the Desktop version.", + default=0.75, + min=0.0, + max=1.0, + step=0.05, + precision=2, + subtype='FACTOR' + ) + + Scene.selection_mode = EnumProperty( name=t('Scene.selection_mode.label'), description=t('Scene.selection_mode.desc'), diff --git a/tools/bake.py b/tools/bake.py new file mode 100644 index 00000000..aa4179e0 --- /dev/null +++ b/tools/bake.py @@ -0,0 +1,338 @@ +# MIT License + +# Copyright (c) 2020 GiveMeAllYourCats + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the 'Software'), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Code author: Feilen +# Edits by: Feilen + +import bpy + +from . import common as Common +from .register import register_wrap +from ..translations import t + +@register_wrap +class BakeButton(bpy.types.Operator): + bl_idname = 'cats_bake.bake' + bl_label = 'Copy and Bake (SLOW!)' + bl_description = "Perform the bake. Warning, this performs an actual render!\n" \ + "This will create a copy of your avatar to leave the original alone.\n" \ + "Depending on your machine, this could take an hour or more." + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + @classmethod + def poll(cls, context): + #if not meshes or len(meshes) == 0: + # return False + return True + + # "Bake pass" function. Run a single bake to ".png" against all selected objects. + # When baking selected to active, only bake two objects at a time. + def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, bake_size, bake_samples, bake_ray_distance, background_color, clear, bake_margin, bake_active=None, bake_multires=False, normal_space='TANGENT'): + bpy.ops.object.select_all(action='DESELECT') + if bake_active is not None: + bake_active.select_set(True) + context.view_layer.objects.active = bake_active + + print("Baking objects: " + ",".join([obj.name for obj in objects])) + + if "SCRIPT_" + bake_name + ".png" not in bpy.data.images: + bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, + generated_type="BLANK", alpha=True) + image = bpy.data.images["SCRIPT_" + bake_name + ".png"] + if clear: + image.alpha_mode = "NONE" + image.generated_color = background_color + image.generated_width=bake_size[0] + image.generated_height=bake_size[1] + image.pixels[:] = background_color * bake_size[0] * bake_size[1] + image.scale(bake_size[0], bake_size[1]) + if bake_type == 'NORMAL' or bake_type == 'ROUGHNESS': + image.colorspace_settings.name = 'Non-Color' + + # Select only objects we're baking + for obj in objects: + obj.select_set(True) + + # For all materials in use, change any value node labeled "bake_" to 1.0, then back to 0.0. + for obj in objects: + for slot in obj.material_slots: + if slot.material: + for node in obj.active_material.node_tree.nodes: + if node.label == "bake_" + bake_name: + node.outputs["Value"].default_value = 1 + + # For all materials in all objects, add or repurpose an image texture node named "SCRIPT_BAKE" + for obj in objects: + for slot in obj.material_slots: + if slot.material: + for node in slot.material.node_tree.nodes: + # Assign bake node + tree = slot.material.node_tree + node = None + if "bake" in tree.nodes: + node = tree.nodes["bake"] + else: + node = tree.nodes.new("ShaderNodeTexImage") + node.name = "bake" + node.label = "Cats bake - do not use" + node.select = True + node.image = bpy.data.images["SCRIPT_" + bake_name + ".png"] + tree.nodes.active = node + + # Run bake. + context.scene.cycles.bake_type = bake_type + if bake_type == 'DIFFUSE': + context.scene.render.bake.use_pass_direct = False + context.scene.render.bake.use_pass_indirect = False + context.scene.render.bake.use_pass_color = True + context.scene.cycles.samples = bake_samples + context.scene.render.bake.use_clear = clear and bake_type == 'NORMAL' + context.scene.render.bake.use_selected_to_active = (bake_active != None) + context.scene.render.bake.margin = bake_margin + context.scene.render.use_bake_multires = bake_multires + context.scene.render.bake.normal_space = normal_space + bpy.ops.object.bake(type=bake_type, + #pass_filter=bake_pass_filter, + use_clear= clear and bake_type == 'NORMAL', + #uv_layer="SCRIPT", + use_selected_to_active=(bake_active != None), + cage_extrusion=bake_ray_distance, + normal_space=normal_space + ) + # For all materials in use, change any value node labeled "bake_" to 1.0, then back to 0.0. + for obj in objects: + for slot in obj.material_slots: + if slot.material: + for node in obj.active_material.node_tree.nodes: + if node.label == "bake_" + bake_name: + node.outputs["Value"].default_value = 0 + + def copy_ob(self, ob, parent, collection): + # copy ob + copy = ob.copy() + copy.data = ob.data.copy() + copy.parent = parent + copy.matrix_parent_inverse = ob.matrix_parent_inverse.copy() + # copy particle settings + for ps in copy.particle_systems: + ps.settings = ps.settings.copy() + collection.objects.link(copy) + return copy + + def tree_copy(self, ob, parent, collection, levels=3): + def recurse(ob, parent, depth): + if depth > levels: + return + copy = self.copy_ob(ob, parent, collection) + + for child in ob.children: + recurse(child, copy, depth + 1) + + return copy + return recurse(ob, ob.parent, 0) + + def execute(self, context): + meshes = Common.get_meshes_objects() + if not meshes or len(meshes) == 0: + self.report({'ERROR'}, "No meshes found!") + return {'FINISHED'} + self.perform_bake(context) + return {'FINISHED'} +# saved_data = Common.SavedData() +# +# if context.scene.decimation_mode != 'CUSTOM': +# mesh = Common.join_meshes(repair_shape_keys=False) +# Common.separate_by_materials(context, mesh) +# +# +# Common.join_meshes() +# +# saved_data.load() + + def perform_bake(self, context): + print('START BAKE') + # Global options + resolution = context.scene.bake_resolution + use_decimation = context.scene.bake_use_decimation + generate_uvmap = context.scene.bake_generate_uvmap + prioritize_face = context.scene.bake_prioritize_face + margin = 0.01 + + # Passes + pass_diffuse = context.scene.bake_pass_diffuse + pass_normal = context.scene.bake_pass_normal + pass_smoothness = context.scene.bake_pass_smoothness + pass_ao = context.scene.bake_pass_ao + pass_questdiffuse = context.scene.bake_pass_questdiffuse + + # Pass options + illuminate_eyes = context.scene.bake_illuminate_eyes + questdiffuse_opacity = context.scene.bake_questdiffuse_opacity + smoothness_diffusepack = context.scene.bake_smoothness_diffusepack + + # Create an output collection + collection = bpy.data.collections.new("CATS Bake") + context.scene.collection.children.link(collection) + + # Tree-copy all meshes + armature = Common.get_armature() + arm_copy = self.tree_copy(armature, None, collection) + + # Make sure all armature modifiers target the new armature + for child in collection.all_objects: + for modifier in child.modifiers: + if modifier.type == "ARMATURE": + modifier.object = arm_copy + + if generate_uvmap: + # Make copies of the currently render-active UV layer, name "CATS UV" + for child in collection.all_objects: + if child.type == "MESH": + child.select_set(True) + bpy.context.view_layer.objects.active = child + bpy.ops.mesh.uv_texture_add() + child.data.uv_layers[-1].name = 'CATS UV' + + # Select all meshes. Select all UVs. Average islands scale + bpy.context.view_layer.objects.active = next(child for child in arm_copy.children if child.type == "MESH") + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.average_islands_scale() # Use blender average so we can make our own tweaks. + bpy.ops.object.mode_set(mode='OBJECT') + + # TODO: Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by 200% if selected + # TODO: Look at all bones hierarchically from 'Head' and select those + + # Pack islands. Optionally use UVPackMaster if it's available + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.select_all(action='SELECT') + if False: #TODO: detect if UVPackMaster installed and configured + context.scene.uvp2_props.normalize_islands = False + context.scene.uvp2_props.lock_overlapping_mode = '0' if use_decimation else '2' + context.scene.uvp2_props.pack_to_others = False + context.scene.uvp2_props.margin = margin + context.scene.uvp2_props.similarity_threshold = 3 + context.scene.uvp2_props.precision = 500 + bpy.ops.uvpackmaster2.uv_pack() + else: + bpy.ops.uv.pack_islands(rotate=True, margin=margin) + + # TODO: ensure render engine is set to Cycles + + # Bake diffuse + if pass_diffuse: + self.bake_pass(context, "diffuse", "DIFFUSE", {"COLOR"}, [obj for obj in collection.all_objects if obj.type == "MESH"], + SCRIPT_BAKE_SIZE, 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + + # Bake roughness, invert + if pass_smoothness: + self.bake_pass(context, "smoothness", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + SCRIPT_BAKE_SIZE, 1, 0, [0.0,0.0,0.0,1.0], True, int(margin * resolution / 2)) + image = bpy.data.images["SCRIPT_smoothness.png"] + for idx, pixel in enumerate(image.pixels): + # invert r, g, b, but not a + if (idx % 4) != 3: + pixel = 1.0 - pixel + + # Pack smoothness to diffuse alpha (if selected) + if smoothness_diffusepack and pass_diffuse and pass_smoothness: + diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] + smoothness_image = bpy.data.images["SCRIPT_smoothness.png"] + for idx, pixel in enumerate(diffuse_image.pixels): + if (idx % 4) == 3: + pixel = smoothness_image.pixels[idx - 3] + + + # TODO: bake emit + + # TODO: advanced: bake alpha from diffuse node setup + + # TODO: advanced: bake metallic from diffuse node setup + + # TODO: advanced: bake detail mask from diffuse node setup + + # Bake AO + if pass_ao: + # TODO: Disable rendering of all objects in the scene except these ones. + bake_pass("ao", "AO", {"AO"}, [obj for obj in collection.all_objects if and obj.type == "MESH"], + SCRIPT_BAKE_SIZE, 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) + # TODO: Re-enable rendering + + # Bake eyes AO to full brightness if selected + + # Blend diffuse and AO to create Quest Diffuse (if selected) + if pass_diffuse and pass_ao and pass_questdiffuse: + if "SCRIPT_questdiffuse.png" not in bpy.data.images: + bpy.ops.image.new(name="SCRIPT_questdiffuse.png", width=resolution, height=resolution, color=background_color, + generated_type="BLANK", alpha=False) + image = bpy.data.images["SCRIPT_questdiffuse.png"] + diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] + ao_image = bpy.data.images["SCRIPT_ao.png"] + image.generated_color = background_color + image.generated_width=resolution + image.generated_height=resolution + image.scale(resolution, resolution) + for idx, pixel in image.pixels: + if (idx % 4 != 3): + pixel = diffuse_image.pixels[idx] * (1.0 - questdiffuse_opacity) * (questdiffuse_opacity * ao_image.pixels[idx]) + else: + pixel = diffuse_image.pixels[idx] + + # Bake highres normals + if not use_decimate: + # Just bake the traditional way + if pass_normal: + self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + SCRIPT_BAKE_SIZE, 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + else: + # Join meshes + Common.join_meshes(armature=arm_copy.name, repair_shape_keys=False) + + # Bake normals in object coordinates + bake_pass("world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + SCRIPT_BAKE_SIZE, 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") + + # Decimate + bpy.ops.cats_decimate.auto_decimate() + + # Remove all other materials + while len(bpy.context.object.material_slots) > 0: + bpy.context.object.active_material_index = 0 #select the top material + bpy.ops.object.material_slot_remove() + + # Apply generated material (object normals -> normal map -> BSDF normal and other textures) + + # Remove old UV maps (if we created new ones) + if generate_uvmap: + for uv_layer in child.data.uv_layers: + if uv_layer.name != "CATS UV" and uv_layer.name != "Detail Map": + child.data.uv_layers.remove(uv_layer) + + # Bake tangent normals + if use_decimate and pass_normal: + self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + SCRIPT_BAKE_SIZE, 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + + + # Update generated material to show tangents diff --git a/ui/__init__.py b/ui/__init__.py index d0ea573b..9fe4f5bd 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -6,6 +6,7 @@ from . import manual from . import custom from . import decimation + from . import bake from . import eye_tracking from . import visemes from . import bone_root @@ -22,6 +23,7 @@ importlib.reload(manual) importlib.reload(custom) importlib.reload(decimation) + importlib.reload(bake) importlib.reload(eye_tracking) importlib.reload(visemes) importlib.reload(bone_root) @@ -29,4 +31,4 @@ importlib.reload(copy_protection) importlib.reload(settings_updates) importlib.reload(supporter) - importlib.reload(credits) \ No newline at end of file + importlib.reload(credits) diff --git a/ui/bake.py b/ui/bake.py new file mode 100644 index 00000000..9d2c326d --- /dev/null +++ b/ui/bake.py @@ -0,0 +1,69 @@ +import bpy + +from .. import globs +from .main import ToolPanel +from .main import layout_split +from ..tools import common as Common +from ..tools import decimation as Decimation +from ..tools import bake as Bake +from ..tools import armature_manual as Armature_manual +from ..tools.register import register_wrap +from ..translations import t + +@register_wrap +class BakePanel(ToolPanel, bpy.types.Panel): + bl_idname = 'VIEW3D_PT_catsbake' + bl_label = 'Bake' + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + box = layout.box() + col = box.column(align=True) + + col.label(text="General options:") + row = col.row(align=True) + row.prop(context.scene, 'bake_resolution', expand=True) + row = col.row(align=True) + row.prop(context.scene, 'bake_use_decimation', expand=True) + row = col.row(align=True) + row.prop(context.scene, 'bake_generate_uvmap', expand=True) + if context.scene.bake_generate_uvmap: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_prioritize_face', expand=True) + col.separator() + row = col.row(align=True) + col.label(text="Bake passes:") + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_diffuse', expand=True) + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_normal', expand=True) + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_smoothness', expand=True) + if context.scene.bake_pass_diffuse and context.scene.bake_pass_smoothness: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_smoothness_diffusepack', expand=True) + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_ao', expand=True) + if context.scene.bake_pass_ao: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_illuminate_eyes', expand=True) + col.separator() + if context.scene.bake_pass_diffuse and context.scene.bake_pass_ao: + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_questdiffuse', expand=True) + col.separator() + if context.scene.bake_pass_questdiffuse: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_questdiffuse_opacity', expand=True) + row = col.row(align=True) + col.separator() + col.separator() + row.operator(Bake.BakeButton.bl_idname, icon='RENDER_STILL') From 7bb7882926ee0578fcff63e1bf4787c21d1ad6de Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 28 Oct 2020 23:51:36 -0700 Subject: [PATCH 18/64] Mostly working! Packing needs some speedup, normals are still incomplete --- tools/bake.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index aa4179e0..9ca27a4b 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -52,7 +52,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba bake_active.select_set(True) context.view_layer.objects.active = bake_active - print("Baking objects: " + ",".join([obj.name for obj in objects])) + print("Baking " + bake_name + " for objects: " + ",".join([obj.name for obj in objects])) if "SCRIPT_" + bake_name + ".png" not in bpy.data.images: bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, @@ -241,14 +241,15 @@ def perform_bake(self, context): # TODO: ensure render engine is set to Cycles # Bake diffuse + Common.switch('OBJECT') if pass_diffuse: self.bake_pass(context, "diffuse", "DIFFUSE", {"COLOR"}, [obj for obj in collection.all_objects if obj.type == "MESH"], - SCRIPT_BAKE_SIZE, 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) # Bake roughness, invert if pass_smoothness: self.bake_pass(context, "smoothness", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - SCRIPT_BAKE_SIZE, 1, 0, [0.0,0.0,0.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 1, 0, [0.0,0.0,0.0,1.0], True, int(margin * resolution / 2)) image = bpy.data.images["SCRIPT_smoothness.png"] for idx, pixel in enumerate(image.pixels): # invert r, g, b, but not a @@ -257,11 +258,15 @@ def perform_bake(self, context): # Pack smoothness to diffuse alpha (if selected) if smoothness_diffusepack and pass_diffuse and pass_smoothness: + print("Packing smoothness to diffuse alpha") diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] smoothness_image = bpy.data.images["SCRIPT_smoothness.png"] - for idx, pixel in enumerate(diffuse_image.pixels): - if (idx % 4) == 3: - pixel = smoothness_image.pixels[idx - 3] + pixel_buffer = list(diffuse_image.pixels) + smoothness_buffer = smoothness_image.pixels[:] + for idx in range(3, len(pixel_buffer), 4): + print(idx) + pixel_buffer[idx] = smoothness_buffer[idx - 1] + diffuse_image.pixels[:] = pixel_buffer # TODO: bake emit @@ -275,8 +280,8 @@ def perform_bake(self, context): # Bake AO if pass_ao: # TODO: Disable rendering of all objects in the scene except these ones. - bake_pass("ao", "AO", {"AO"}, [obj for obj in collection.all_objects if and obj.type == "MESH"], - SCRIPT_BAKE_SIZE, 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) + self.bake_pass(context, "ao", "AO", {"AO"}, [obj for obj in collection.all_objects if obj.type == "MESH"], + (resolution, resolution), 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) # TODO: Re-enable rendering # Bake eyes AO to full brightness if selected @@ -304,14 +309,14 @@ def perform_bake(self, context): # Just bake the traditional way if pass_normal: self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - SCRIPT_BAKE_SIZE, 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) else: # Join meshes Common.join_meshes(armature=arm_copy.name, repair_shape_keys=False) # Bake normals in object coordinates - bake_pass("world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - SCRIPT_BAKE_SIZE, 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") + self.bake_pass(context, "world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") # Decimate bpy.ops.cats_decimate.auto_decimate() @@ -332,7 +337,7 @@ def perform_bake(self, context): # Bake tangent normals if use_decimate and pass_normal: self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - SCRIPT_BAKE_SIZE, 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) # Update generated material to show tangents From 9fdc70d8abce77469732bf7b00967a3061aea759 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Thu, 29 Oct 2020 21:31:26 -0700 Subject: [PATCH 19/64] Significant improvements, approaching general usability --- extentions.py | 8 +++ tools/__init__.py | 2 + tools/bake.py | 167 +++++++++++++++++++++++++++++++++----------- tools/decimation.py | 40 +++++++++-- ui/bake.py | 6 ++ 5 files changed, 176 insertions(+), 47 deletions(-) diff --git a/extentions.py b/extentions.py index 9e5a991d..d10b9ceb 100644 --- a/extentions.py +++ b/extentions.py @@ -227,6 +227,14 @@ def register(): default=True ) + Scene.bake_preserve_seams = BoolProperty( + name="Preserve seams", + description='Forces the Decimate operation to preserve vertices making up seams, preventing hard edges along seams.\n' \ + 'May result in less ideal geometry.\n' \ + "Use if you notice ugly edges along your texture seams.", + default=False + ) + Scene.bake_pass_normal = BoolProperty( name='Normal (Bump)', description="Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" \ diff --git a/tools/__init__.py b/tools/__init__.py index 72884b5d..a9c5feda 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -12,6 +12,7 @@ from . import copy_protection from . import credits from . import decimation + from . import bake from . import eyetracking from . import fbx_patch from . import importer @@ -36,6 +37,7 @@ importlib.reload(copy_protection) importlib.reload(credits) importlib.reload(decimation) + importlib.reload(bake) importlib.reload(eyetracking) importlib.reload(fbx_patch) importlib.reload(importer) diff --git a/tools/bake.py b/tools/bake.py index 9ca27a4b..5dd3591c 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -67,6 +67,8 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba image.scale(bake_size[0], bake_size[1]) if bake_type == 'NORMAL' or bake_type == 'ROUGHNESS': image.colorspace_settings.name = 'Non-Color' + if bake_type == 'DIFFUSE': # For packing smoothness to alpha + image.alpha_mode = 'CHANNEL_PACKED' # Select only objects we're baking for obj in objects: @@ -155,17 +157,13 @@ def execute(self, context): if not meshes or len(meshes) == 0: self.report({'ERROR'}, "No meshes found!") return {'FINISHED'} + if context.scene.render.engine != 'CYCLES': + self.report({'ERROR'}, "You need to set your render engine to Cycles first!") + return {'FINISHED'} +# saved_data = Common.SavedData() + # TODO: Check if any UV islands are self-overlapping, emit an error self.perform_bake(context) return {'FINISHED'} -# saved_data = Common.SavedData() -# -# if context.scene.decimation_mode != 'CUSTOM': -# mesh = Common.join_meshes(repair_shape_keys=False) -# Common.separate_by_materials(context, mesh) -# -# -# Common.join_meshes() -# # saved_data.load() def perform_bake(self, context): @@ -173,10 +171,14 @@ def perform_bake(self, context): # Global options resolution = context.scene.bake_resolution use_decimation = context.scene.bake_use_decimation + preserve_seams = context.scene.bake_preserve_seams generate_uvmap = context.scene.bake_generate_uvmap prioritize_face = context.scene.bake_prioritize_face + prioritize_factor = 2.0 margin = 0.01 + # TODO: Option to seperate by loose parts and bake selected to active + # Passes pass_diffuse = context.scene.bake_pass_diffuse pass_normal = context.scene.bake_pass_normal @@ -204,30 +206,58 @@ def perform_bake(self, context): modifier.object = arm_copy if generate_uvmap: + bpy.ops.object.select_all(action='DESELECT') # Make copies of the currently render-active UV layer, name "CATS UV" for child in collection.all_objects: if child.type == "MESH": child.select_set(True) - bpy.context.view_layer.objects.active = child + context.view_layer.objects.active = child bpy.ops.mesh.uv_texture_add() child.data.uv_layers[-1].name = 'CATS UV' # Select all meshes. Select all UVs. Average islands scale - bpy.context.view_layer.objects.active = next(child for child in arm_copy.children if child.type == "MESH") + context.view_layer.objects.active = next(child for child in arm_copy.children if child.type == "MESH") bpy.ops.object.editmode_toggle() bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.average_islands_scale() # Use blender average so we can make our own tweaks. bpy.ops.object.mode_set(mode='OBJECT') - # TODO: Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by 200% if selected + # Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by 200% if selected # TODO: Look at all bones hierarchically from 'Head' and select those + for obj in collection.all_objects: + if obj.type != "MESH": + continue + context.view_layer.objects.active = obj + for group in ["Head", "LeftEye", "RightEye"]: + if group in obj.vertex_groups: + print("{} found in {}".format(group, obj.name)) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') + # Select all vertices in it + obj.vertex_groups.active = obj.vertex_groups[group] + bpy.ops.object.vertex_group_select() + # Synchronize + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + # Then select all UVs + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + # Then for each UV (cause of the viewport thing) scale up by the selected factor + uv_layer = obj.data.uv_layers["CATS UV"].data + for poly in obj.data.polygons: + for loop in poly.loop_indices: + if uv_layer[loop].select: + uv_layer[loop].uv.x *= prioritize_factor + uv_layer[loop].uv.y *= prioritize_factor # Pack islands. Optionally use UVPackMaster if it's available bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') - if False: #TODO: detect if UVPackMaster installed and configured + try: # detect if UVPackMaster installed and configured context.scene.uvp2_props.normalize_islands = False context.scene.uvp2_props.lock_overlapping_mode = '0' if use_decimation else '2' context.scene.uvp2_props.pack_to_others = False @@ -235,11 +265,9 @@ def perform_bake(self, context): context.scene.uvp2_props.similarity_threshold = 3 context.scene.uvp2_props.precision = 500 bpy.ops.uvpackmaster2.uv_pack() - else: + except AttributeError: bpy.ops.uv.pack_islands(rotate=True, margin=margin) - # TODO: ensure render engine is set to Cycles - # Bake diffuse Common.switch('OBJECT') if pass_diffuse: @@ -249,12 +277,15 @@ def perform_bake(self, context): # Bake roughness, invert if pass_smoothness: self.bake_pass(context, "smoothness", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 1, 0, [0.0,0.0,0.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 1, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) image = bpy.data.images["SCRIPT_smoothness.png"] - for idx, pixel in enumerate(image.pixels): + pixel_buffer = list(image.pixels) + for idx in range(0, len(image.pixels)): # invert r, g, b, but not a if (idx % 4) != 3: - pixel = 1.0 - pixel + pixel_buffer[idx] = 1.0 - pixel_buffer[idx] + image.pixels[:] = pixel_buffer + # Pack smoothness to diffuse alpha (if selected) if smoothness_diffusepack and pass_diffuse and pass_smoothness: @@ -264,8 +295,7 @@ def perform_bake(self, context): pixel_buffer = list(diffuse_image.pixels) smoothness_buffer = smoothness_image.pixels[:] for idx in range(3, len(pixel_buffer), 4): - print(idx) - pixel_buffer[idx] = smoothness_buffer[idx - 1] + pixel_buffer[idx] = smoothness_buffer[idx - 3] diffuse_image.pixels[:] = pixel_buffer @@ -279,65 +309,120 @@ def perform_bake(self, context): # Bake AO if pass_ao: + # TODO: Add modifiers that prevent LeftEye and RightEye being baked # TODO: Disable rendering of all objects in the scene except these ones. self.bake_pass(context, "ao", "AO", {"AO"}, [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) # TODO: Re-enable rendering - # Bake eyes AO to full brightness if selected - # Blend diffuse and AO to create Quest Diffuse (if selected) if pass_diffuse and pass_ao and pass_questdiffuse: if "SCRIPT_questdiffuse.png" not in bpy.data.images: - bpy.ops.image.new(name="SCRIPT_questdiffuse.png", width=resolution, height=resolution, color=background_color, + bpy.ops.image.new(name="SCRIPT_questdiffuse.png", width=resolution, height=resolution, generated_type="BLANK", alpha=False) image = bpy.data.images["SCRIPT_questdiffuse.png"] diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] ao_image = bpy.data.images["SCRIPT_ao.png"] - image.generated_color = background_color image.generated_width=resolution image.generated_height=resolution image.scale(resolution, resolution) - for idx, pixel in image.pixels: + pixel_buffer = list(image.pixels) + diffuse_buffer = diffuse_image.pixels[:] + ao_buffer = ao_image.pixels[:] + for idx in range(0, len(image.pixels)): if (idx % 4 != 3): - pixel = diffuse_image.pixels[idx] * (1.0 - questdiffuse_opacity) * (questdiffuse_opacity * ao_image.pixels[idx]) + # Map range: set the black point up to 1-opacity + pixel_buffer[idx] = diffuse_buffer[idx] * ((1.0 - questdiffuse_opacity) + (questdiffuse_opacity * ao_buffer[idx])) else: - pixel = diffuse_image.pixels[idx] + # Just copy alpha + pixel_buffer[idx] = diffuse_buffer[idx] + image.pixels[:] = pixel_buffer + # Bake highres normals - if not use_decimate: + if not use_decimation: # Just bake the traditional way if pass_normal: self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) else: # Join meshes - Common.join_meshes(armature=arm_copy.name, repair_shape_keys=False) + Common.join_meshes(armature_name=arm_copy.name, repair_shape_keys=False) # Bake normals in object coordinates - self.bake_pass(context, "world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + if pass_normal: + self.bake_pass(context, "world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") - # Decimate - bpy.ops.cats_decimate.auto_decimate() + # Decimate. If 'preserve seams' is selected, forcibly preserve seams (seams from islands, deselect seams) + bpy.ops.cats_decimation.auto_decimate(armature_name=arm_copy.name, preserve_seams=preserve_seams, seperate_materials=False) # Remove all other materials - while len(bpy.context.object.material_slots) > 0: - bpy.context.object.active_material_index = 0 #select the top material + while len(context.object.material_slots) > 0: + context.object.active_material_index = 0 #select the top material bpy.ops.object.material_slot_remove() # Apply generated material (object normals -> normal map -> BSDF normal and other textures) + mat = bpy.data.materials.get("CATS Baked") + if mat is not None: + bpy.data.materials.remove(mat, do_unlink=True) + # create material + mat = bpy.data.materials.new(name="CATS Baked") + mat.use_nodes = True + mat.use_backface_culling = True + # add a normal map and image texture to connect the world texture, if it exists + tree = mat.node_tree + bsdfnode = next(node for node in tree.nodes if node.type == "BSDF_PRINCIPLED") + bsdfnode.inputs["Specular"].default_value = 0 + if pass_normal: + normaltexnode = tree.nodes.new("ShaderNodeTexImage") + if use_decimation: + normaltexnode.image = bpy.data.images["SCRIPT_world.png"] + + normalmapnode = tree.nodes.new("ShaderNodeNormalMap") + normalmapnode.space = "OBJECT" + + tree.links.new(normalmapnode.inputs["Color"], normaltexnode.outputs["Color"]) + tree.links.new(bsdfnode.inputs["Normal"], normalmapnode.outputs["Normal"]) + for child in collection.all_objects: + if child.type == "MESH": + child.data.materials.append(mat) # Remove old UV maps (if we created new ones) if generate_uvmap: - for uv_layer in child.data.uv_layers: - if uv_layer.name != "CATS UV" and uv_layer.name != "Detail Map": - child.data.uv_layers.remove(uv_layer) + for child in collection.all_objects: + if child.type == "MESH": + uv_layers = child.data.uv_layers[:] + while uv_layers: + layer = uv_layers.pop() + if layer.name != "CATS UV" and layer.name != "Detail Map": + print("Removing UV {}".format(layer.name)) + child.data.uv_layers.remove(layer) # Bake tangent normals - if use_decimate and pass_normal: + if use_decimation and pass_normal: self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) - - # Update generated material to show tangents + # Update generated material to preview all of our passes + if pass_normal: + normaltexnode.image = bpy.data.images["SCRIPT_normal.png"] + normalmapnode.space = "TANGENT" + if pass_diffuse: + diffusetexnode = tree.nodes.new("ShaderNodeTexImage") + diffusetexnode.image = bpy.data.images["SCRIPT_diffuse.png"] + tree.links.new(bsdfnode.inputs["Base Color"], diffusetexnode.outputs["Color"]) + if pass_smoothness: + if smoothness_diffusepack and pass_diffuse: + invertnode = tree.nodes.new("ShaderNodeInvert") + tree.links.new(invertnode.inputs["Color"], diffusetexnode.outputs["Alpha"]) + tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) + else: + smoothnesstexnode = tree.nodes.new("ShaderNodeTexImage") + smoothnesstexnode.image = bpy.data.images["SCRIPT_smoothness.png"] + invertnode = tree.nodes.new("ShaderNodeInvert") + tree.links.new(invertnode.inputs["Color"], smoothnesstexnode.outputs["Color"]) + tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) + + # Move armature so we can see it + arm_copy.location.x += arm_copy.dimensions.x diff --git a/tools/decimation.py b/tools/decimation.py index 42f88042..cabe8a21 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -151,6 +151,18 @@ class AutoDecimateButton(bpy.types.Operator): bl_description = t('AutoDecimateButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + armature_name: bpy.props.StringProperty( + name = 'armature_name', + ) + + preserve_seams: bpy.props.BoolProperty( + name = 'preserve_seams', + ) + + seperate_materials: bpy.props.BoolProperty( + name = 'seperate_materials' + ) + def execute(self, context): meshes = Common.get_meshes_objects() if not meshes or len(meshes) == 0: @@ -160,12 +172,13 @@ def execute(self, context): saved_data = Common.SavedData() if context.scene.decimation_mode != 'CUSTOM': - mesh = Common.join_meshes(repair_shape_keys=False) - Common.separate_by_materials(context, mesh) + mesh = Common.join_meshes(repair_shape_keys=False, armature_name=self.armature_name) + if self.seperate_materials: + Common.separate_by_materials(context, mesh) self.decimate(context) - Common.join_meshes() + Common.join_meshes(armature_name=self.armature_name) saved_data.load() @@ -186,7 +199,7 @@ def decimate(self, context): current_tris_count = 0 tris_count = 0 - meshes_obj = Common.get_meshes_objects() + meshes_obj = Common.get_meshes_objects(armature_name=self.armature_name) for mesh in meshes_obj: Common.set_active(mesh) @@ -330,6 +343,20 @@ def decimate(self, context): Common.switch('EDIT') bpy.ops.mesh.select_mode(type="VERT") bpy.ops.mesh.select_all(action="SELECT") + if self.preserve_seams: + bpy.ops.mesh.select_all(action="DESELECT") + bpy.ops.uv.seams_from_islands() + + # select all seams + Common.switch('OBJECT') + me = mesh_obj.data + for edge in me.edges: + if edge.use_seam: + edge.select = True + + Common.switch('EDIT') + bpy.ops.mesh.select_all(action="INVERT") + bpy.ops.mesh.decimate(ratio=decimation, use_vertex_group=False, vertex_group_factor=1.0, @@ -346,8 +373,9 @@ def decimate(self, context): tris_count = tris_count - tris # Repair shape keys if SMART mode is enabled if smart_decimation and Common.has_shapekeys(mesh_obj): - for idx in range(1, len(mesh_obj.data.shape_keys.key_blocks) - 2): - print("Key: " + mesh_obj.data.shape_keys.key_blocks[idx].name) + for idx in range(1, len(mesh_obj.data.shape_keys.key_blocks) - 1): + if "Reverted" in mesh_obj.data.shape_keys.key_blocks[idx].name: + continue mesh_obj.active_shape_key_index = idx Common.switch('EDIT') bpy.ops.mesh.blend_from_shape(shape="CATS Basis", blend=-1.0, add=True) diff --git a/ui/bake.py b/ui/bake.py index 9d2c326d..a802cc1c 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -21,11 +21,17 @@ def draw(self, context): box = layout.box() col = box.column(align=True) + # TODO: Quest (decimate) and Desktop (nodecimate) presets + col.label(text="General options:") row = col.row(align=True) row.prop(context.scene, 'bake_resolution', expand=True) row = col.row(align=True) row.prop(context.scene, 'bake_use_decimation', expand=True) + if context.scene.bake_use_decimation: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_preserve_seams', expand=True) row = col.row(align=True) row.prop(context.scene, 'bake_generate_uvmap', expand=True) if context.scene.bake_generate_uvmap: From 1c102f6ac03a260b7fc712506f3452969c968ef5 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 30 Oct 2020 10:26:18 -0700 Subject: [PATCH 20/64] More progress --- extentions.py | 26 +++++++++--- tools/bake.py | 97 ++++++++++++++++++++++++++------------------- tools/decimation.py | 1 + ui/bake.py | 4 ++ 4 files changed, 82 insertions(+), 46 deletions(-) diff --git a/extentions.py b/extentions.py index d10b9ceb..cc908c0a 100644 --- a/extentions.py +++ b/extentions.py @@ -185,20 +185,32 @@ def register(): ) Scene.bake_generate_uvmap = BoolProperty( - name='Generate single UVMap', + name='Generate UVMap', description="Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ "Only disable if your UVs are non-overlapping already.\n" \ - "This will leave any map named \"Detail Map\" alone.", + "This will leave any map named \"Detail Map\" alone.\n" \ + "Uses UVPackMaster where available for more efficient UVs", default=True ) Scene.bake_prioritize_face = BoolProperty( name='Prioritize Face/Eyes', - description='Scale any UV islands attached to the head/children by a factor of 2', + description='Scale any UV islands attached to the head/eyes by a given factor.', default=True ) + Scene.bake_face_scale = FloatProperty( + name="Face/Eyes Scale", + description="How much to scale up the face/eyes textures.", + default=3.0, + min=0.5, + max=4.0, + step=0.25, + precision=2, + subtype='FACTOR' + ) + Scene.bake_illuminate_eyes = BoolProperty( name='Set eyes to full brightness', description='Relight LeftEye and RightEye to be full brightness.\n' \ @@ -210,7 +222,8 @@ def register(): Scene.bake_pass_smoothness = BoolProperty( name='Smoothness', description='Bakes Roughness and then inverts the values.\n' \ - 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.', + 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.\n' \ + 'Not neccesary if your mesh has a global roughness value', default=True ) @@ -245,8 +258,9 @@ def register(): Scene.bake_pass_ao = BoolProperty( name='Ambient Occlusion', - description='Bakes Ambient Occlusion, non-projected shadows. Adds a good amount of detail to your model.', - default=True + description='Bakes Ambient Occlusion, non-projected shadows. Adds a good amount of detail to your model.\n' \ + 'Takes a fairly long time to bake', + default=False ) Scene.bake_pass_questdiffuse = BoolProperty( diff --git a/tools/bake.py b/tools/bake.py index 5dd3591c..71c1b815 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -29,6 +29,8 @@ from .register import register_wrap from ..translations import t +# TODO: Button to auto-detect bake passes from nodes + @register_wrap class BakeButton(bpy.types.Operator): bl_idname = 'cats_bake.bake' @@ -38,14 +40,7 @@ class BakeButton(bpy.types.Operator): "Depending on your machine, this could take an hour or more." bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - @classmethod - def poll(cls, context): - #if not meshes or len(meshes) == 0: - # return False - return True - # "Bake pass" function. Run a single bake to ".png" against all selected objects. - # When baking selected to active, only bake two objects at a time. def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, bake_size, bake_samples, bake_ray_distance, background_color, clear, bake_margin, bake_active=None, bake_multires=False, normal_space='TANGENT'): bpy.ops.object.select_all(action='DESELECT') if bake_active is not None: @@ -63,8 +58,6 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba image.generated_color = background_color image.generated_width=bake_size[0] image.generated_height=bake_size[1] - image.pixels[:] = background_color * bake_size[0] * bake_size[1] - image.scale(bake_size[0], bake_size[1]) if bake_type == 'NORMAL' or bake_type == 'ROUGHNESS': image.colorspace_settings.name = 'Non-Color' if bake_type == 'DIFFUSE': # For packing smoothness to alpha @@ -174,7 +167,7 @@ def perform_bake(self, context): preserve_seams = context.scene.bake_preserve_seams generate_uvmap = context.scene.bake_generate_uvmap prioritize_face = context.scene.bake_prioritize_face - prioritize_factor = 2.0 + prioritize_factor = context.scene.bake_face_scale margin = 0.01 # TODO: Option to seperate by loose parts and bake selected to active @@ -225,33 +218,34 @@ def perform_bake(self, context): # Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by 200% if selected # TODO: Look at all bones hierarchically from 'Head' and select those - for obj in collection.all_objects: - if obj.type != "MESH": - continue - context.view_layer.objects.active = obj - for group in ["Head", "LeftEye", "RightEye"]: - if group in obj.vertex_groups: - print("{} found in {}".format(group, obj.name)) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.uv.select_all(action='DESELECT') - bpy.ops.mesh.select_all(action='DESELECT') - # Select all vertices in it - obj.vertex_groups.active = obj.vertex_groups[group] - bpy.ops.object.vertex_group_select() - # Synchronize - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='EDIT') - # Then select all UVs - bpy.ops.uv.select_all(action='SELECT') - bpy.ops.object.mode_set(mode='OBJECT') - - # Then for each UV (cause of the viewport thing) scale up by the selected factor - uv_layer = obj.data.uv_layers["CATS UV"].data - for poly in obj.data.polygons: - for loop in poly.loop_indices: - if uv_layer[loop].select: - uv_layer[loop].uv.x *= prioritize_factor - uv_layer[loop].uv.y *= prioritize_factor + if prioritize_face: + for obj in collection.all_objects: + if obj.type != "MESH": + continue + context.view_layer.objects.active = obj + for group in ["Head", "LeftEye", "RightEye"]: + if group in obj.vertex_groups: + print("{} found in {}".format(group, obj.name)) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') + # Select all vertices in it + obj.vertex_groups.active = obj.vertex_groups[group] + bpy.ops.object.vertex_group_select() + # Synchronize + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') + # Then select all UVs + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + # Then for each UV (cause of the viewport thing) scale up by the selected factor + uv_layer = obj.data.uv_layers["CATS UV"].data + for poly in obj.data.polygons: + for loop in poly.loop_indices: + if uv_layer[loop].select: + uv_layer[loop].uv.x *= prioritize_factor + uv_layer[loop].uv.y *= prioritize_factor # Pack islands. Optionally use UVPackMaster if it's available bpy.ops.object.mode_set(mode='EDIT') @@ -301,18 +295,35 @@ def perform_bake(self, context): # TODO: bake emit - # TODO: advanced: bake alpha from diffuse node setup + # TODO: advanced: bake alpha from last bsdf output - # TODO: advanced: bake metallic from diffuse node setup + # TODO: advanced: bake metallic from last bsdf output # TODO: advanced: bake detail mask from diffuse node setup # Bake AO if pass_ao: - # TODO: Add modifiers that prevent LeftEye and RightEye being baked + if illuminate_eyes: + # Add modifiers that prevent LeftEye and RightEye being baked + for obj in collection.all_objects: + if obj.type == "MESH" and "LeftEye" in obj.vertex_groups: + leyemask = obj.modifiers.new(type='MASK', name="leyemask") + leyemask.mode = "VERTEX_GROUP" + leyemask.vertex_group = "LeftEye" + leyemask.invert_vertex_group = True + if obj.type == "MESH" and "RightEye" in obj.vertex_groups: + reyemask = obj.modifiers.new(type='MASK', name="reyemask") + reyemask.mode = "VERTEX_GROUP" + reyemask.vertex_group = "RightEye" + reyemask.invert_vertex_group = True # TODO: Disable rendering of all objects in the scene except these ones. self.bake_pass(context, "ao", "AO", {"AO"}, [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) + if illuminate_eyes: + if "leyemask" in obj.modifiers: + obj.modifiers.remove(leyemask) + if "reyemask" in obj.modifiers: + obj.modifiers.remove(reyemask) # TODO: Re-enable rendering # Blend diffuse and AO to create Quest Diffuse (if selected) @@ -373,6 +384,7 @@ def perform_bake(self, context): # add a normal map and image texture to connect the world texture, if it exists tree = mat.node_tree bsdfnode = next(node for node in tree.nodes if node.type == "BSDF_PRINCIPLED") + # TODO: Copy BSDF default properties from the largest origin mesh which has BSDF bsdfnode.inputs["Specular"].default_value = 0 if pass_normal: normaltexnode = tree.nodes.new("ShaderNodeTexImage") @@ -412,6 +424,7 @@ def perform_bake(self, context): diffusetexnode = tree.nodes.new("ShaderNodeTexImage") diffusetexnode.image = bpy.data.images["SCRIPT_diffuse.png"] tree.links.new(bsdfnode.inputs["Base Color"], diffusetexnode.outputs["Color"]) + # TODO: If AO, blend in AO. If questdiffuse, just use that if pass_smoothness: if smoothness_diffusepack and pass_diffuse: invertnode = tree.nodes.new("ShaderNodeInvert") @@ -426,3 +439,7 @@ def perform_bake(self, context): # Move armature so we can see it arm_copy.location.x += arm_copy.dimensions.x + + # TODO: Optionally cleanup bones as a last step + + print("BAKE COMPLETE!") diff --git a/tools/decimation.py b/tools/decimation.py index cabe8a21..149b9802 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -343,6 +343,7 @@ def decimate(self, context): Common.switch('EDIT') bpy.ops.mesh.select_mode(type="VERT") bpy.ops.mesh.select_all(action="SELECT") + # TODO: Fix decimation calculation when pinning seams if self.preserve_seams: bpy.ops.mesh.select_all(action="DESELECT") bpy.ops.uv.seams_from_islands() diff --git a/ui/bake.py b/ui/bake.py index a802cc1c..6912e2e6 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -38,6 +38,10 @@ def draw(self, context): row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_prioritize_face', expand=True) + if context.scene.bake_prioritize_face: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_face_scale', expand=True) col.separator() row = col.row(align=True) col.label(text="Bake passes:") From 18d36bfaf654c0b8e3ceff89c5fdfe4ccc937fde Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 30 Oct 2020 12:37:53 -0700 Subject: [PATCH 21/64] Add more passes, fix uvpackmaster support --- extentions.py | 39 +++++++++++++++++++++++++++++++++++++-- tools/bake.py | 25 ++++++++++++++++++++++--- ui/bake.py | 6 +++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/extentions.py b/extentions.py index cc908c0a..3e537080 100644 --- a/extentions.py +++ b/extentions.py @@ -189,7 +189,7 @@ def register(): description="Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ "Only disable if your UVs are non-overlapping already.\n" \ "This will leave any map named \"Detail Map\" alone.\n" \ - "Uses UVPackMaster where available for more efficient UVs", + "Uses UVPackMaster where available for more efficient UVs, make sure the window is showing", default=True ) @@ -252,7 +252,8 @@ def register(): name='Normal (Bump)', description="Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" \ "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" \ - "that makes the low res output look like the high res input.", + "that makes the low res output look like the high res input.\n" \ + "Will not work well if you have self-intersecting islands", default=True ) @@ -269,6 +270,40 @@ def register(): default=True ) + Scene.bake_pass_emit = BoolProperty( + name='Emit', + description='Bakes Emit, glowyness', + default=False + ) + + Scene.bake_show_advanced = BoolProperty( + name='Advanced', + description='Show advanced passes. These are not natively bakeable in Blender,\n' \ + 'so they may not work as well', + default=False + ) + + Scene.bake_pass_alpha = BoolProperty( + name='Transparency', + description='Bakes transparency by connecting the last Principled BSDF Alpha input\n' \ + 'to the Base Color input and baking Diffuse', + default=False + ) + + Scene.bake_alpha_diffusepack = BoolProperty( + name='Pack to diffuse alpha', + description='Copies the alpha map to the alpha channel of the diffuse map.\n' \ + "This will override any existing alpha map", + default=True + ) + + Scene.bake_pass_metallic = BoolProperty( + name='Metallic', + description='Bakes metallic by connecting the last Principled BSDF Metallic input\n' \ + 'to the Base Color input and baking Diffuse', + default=False + ) + Scene.bake_questdiffuse_opacity = FloatProperty( name="AO Opacity", description="The opacity of the shadows to blend onto the Diffuse map.\n" \ diff --git a/tools/bake.py b/tools/bake.py index 71c1b815..b92a3c7b 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -178,6 +178,7 @@ def perform_bake(self, context): pass_smoothness = context.scene.bake_pass_smoothness pass_ao = context.scene.bake_pass_ao pass_questdiffuse = context.scene.bake_pass_questdiffuse + pass_emit = context.scene.bake_pass_emit # Pass options illuminate_eyes = context.scene.bake_illuminate_eyes @@ -247,21 +248,35 @@ def perform_bake(self, context): uv_layer[loop].uv.x *= prioritize_factor uv_layer[loop].uv.y *= prioritize_factor + + # UVPackmaster doesn't seem to like huge islands. + bpy.ops.object.mode_set(mode='OBJECT') + for obj in bpy.context.selected_objects: + uv_layer = obj.data.uv_layers["CATS UV"].data + for poly in obj.data.polygons: + for loop in poly.loop_indices: + uv_layer[loop].uv.x *= 0.05 + uv_layer[loop].uv.y *= 0.05 + # Pack islands. Optionally use UVPackMaster if it's available bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') - try: # detect if UVPackMaster installed and configured + + # detect if UVPackMaster installed and configured + try: # TODO: UVP doesn't respect margins when called like this, find out why context.scene.uvp2_props.normalize_islands = False context.scene.uvp2_props.lock_overlapping_mode = '0' if use_decimation else '2' context.scene.uvp2_props.pack_to_others = False context.scene.uvp2_props.margin = margin context.scene.uvp2_props.similarity_threshold = 3 - context.scene.uvp2_props.precision = 500 + context.scene.uvp2_props.precision = 1000 bpy.ops.uvpackmaster2.uv_pack() except AttributeError: bpy.ops.uv.pack_islands(rotate=True, margin=margin) + # TODO: Bake selected to active option. Seperate by materials, then bake selected to active for each part + # Bake diffuse Common.switch('OBJECT') if pass_diffuse: @@ -293,7 +308,10 @@ def perform_bake(self, context): diffuse_image.pixels[:] = pixel_buffer - # TODO: bake emit + # bake emit TODO: test + if pass_emit: + self.bake_pass(context, "emit", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + (resolution, resolution), 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) # TODO: advanced: bake alpha from last bsdf output @@ -366,6 +384,7 @@ def perform_bake(self, context): (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") # Decimate. If 'preserve seams' is selected, forcibly preserve seams (seams from islands, deselect seams) + # TODO: We need to use our own settings: own tri count, and always use Smart decimation mode bpy.ops.cats_decimation.auto_decimate(armature_name=arm_copy.name, preserve_seams=preserve_seams, seperate_materials=False) # Remove all other materials diff --git a/ui/bake.py b/ui/bake.py index 6912e2e6..8031866d 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -23,6 +23,8 @@ def draw(self, context): # TODO: Quest (decimate) and Desktop (nodecimate) presets + col.label(text="Presets:") + col.label(text="General options:") row = col.row(align=True) row.prop(context.scene, 'bake_resolution', expand=True) @@ -68,11 +70,13 @@ def draw(self, context): if context.scene.bake_pass_diffuse and context.scene.bake_pass_ao: row = col.row(align=True) row.prop(context.scene, 'bake_pass_questdiffuse', expand=True) - col.separator() if context.scene.bake_pass_questdiffuse: row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_questdiffuse_opacity', expand=True) + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_emit', expand=True) row = col.row(align=True) col.separator() col.separator() From 7035658e9fce814cc448658e44eab973dca3dccf Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 30 Oct 2020 17:27:09 -0700 Subject: [PATCH 22/64] Bake last, organize nodes, fix object world coordinates --- extentions.py | 6 ++++ tools/__init__.py | 4 +-- tools/bake.py | 91 +++++++++++++++++++++++++++++++++++++++++------ ui/__init__.py | 4 +-- ui/bake.py | 4 +++ ui/decimation.py | 1 + 6 files changed, 95 insertions(+), 15 deletions(-) diff --git a/extentions.py b/extentions.py index 3e537080..68a4ab5b 100644 --- a/extentions.py +++ b/extentions.py @@ -257,6 +257,12 @@ def register(): default=True ) + Scene.bake_normal_apply_trans = BoolProperty( + name='Apply transforms', + description="Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps", + default=True + ) + Scene.bake_pass_ao = BoolProperty( name='Ambient Occlusion', description='Bakes Ambient Occlusion, non-projected shadows. Adds a good amount of detail to your model.\n' \ diff --git a/tools/__init__.py b/tools/__init__.py index a9c5feda..0d6a4811 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -12,12 +12,12 @@ from . import copy_protection from . import credits from . import decimation - from . import bake from . import eyetracking from . import fbx_patch from . import importer from . import material from . import rootbone + from . import bake from . import settings from . import shapekey from . import supporter @@ -37,12 +37,12 @@ importlib.reload(copy_protection) importlib.reload(credits) importlib.reload(decimation) - importlib.reload(bake) importlib.reload(eyetracking) importlib.reload(fbx_patch) importlib.reload(importer) importlib.reload(material) importlib.reload(rootbone) + importlib.reload(bake) importlib.reload(settings) importlib.reload(shapekey) importlib.reload(supporter) diff --git a/tools/bake.py b/tools/bake.py index b92a3c7b..38f5aa88 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -30,6 +30,12 @@ from ..translations import t # TODO: Button to auto-detect bake passes from nodes +# Diffuse: on if >1 material has different color inputs or if any has non-default base color input on bsdf +# Normal: on if any normals connected or if decimating +# Smoothness: similar to diffuse +# Pack to alpha: on unless alpha bake +# AO: on unless a toon bsdf shader node is detected anywhere +# diffuse ao: on if AO on @register_wrap class BakeButton(bpy.types.Operator): @@ -51,7 +57,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba if "SCRIPT_" + bake_name + ".png" not in bpy.data.images: bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, - generated_type="BLANK", alpha=True) + generated_type="BLANK", alpha=True, float_buffer=normal_space=='OBJECT') image = bpy.data.images["SCRIPT_" + bake_name + ".png"] if clear: image.alpha_mode = "NONE" @@ -92,6 +98,8 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba node.select = True node.image = bpy.data.images["SCRIPT_" + bake_name + ".png"] tree.nodes.active = node + node.location.x += 500 + node.location.y -= 500 # Run bake. context.scene.cycles.bake_type = bake_type @@ -166,6 +174,7 @@ def perform_bake(self, context): use_decimation = context.scene.bake_use_decimation preserve_seams = context.scene.bake_preserve_seams generate_uvmap = context.scene.bake_generate_uvmap + # TODO: Option to smart UV project as a last ditch effort prioritize_face = context.scene.bake_prioritize_face prioritize_factor = context.scene.bake_face_scale margin = 0.01 @@ -184,6 +193,7 @@ def perform_bake(self, context): illuminate_eyes = context.scene.bake_illuminate_eyes questdiffuse_opacity = context.scene.bake_questdiffuse_opacity smoothness_diffusepack = context.scene.bake_smoothness_diffusepack + normal_apply_trans = context.scene.bake_normal_apply_trans # Create an output collection collection = bpy.data.collections.new("CATS Bake") @@ -193,6 +203,9 @@ def perform_bake(self, context): armature = Common.get_armature() arm_copy = self.tree_copy(armature, None, collection) + # Move armature so we can see it + arm_copy.location.x += arm_copy.dimensions.x + # Make sure all armature modifiers target the new armature for child in collection.all_objects: for modifier in child.modifiers: @@ -264,7 +277,7 @@ def perform_bake(self, context): bpy.ops.uv.select_all(action='SELECT') # detect if UVPackMaster installed and configured - try: # TODO: UVP doesn't respect margins when called like this, find out why + try: # UVP doesn't respect margins when called like this, find out why context.scene.uvp2_props.normalize_islands = False context.scene.uvp2_props.lock_overlapping_mode = '0' if use_decimation else '2' context.scene.uvp2_props.pack_to_others = False @@ -334,7 +347,6 @@ def perform_bake(self, context): reyemask.mode = "VERTEX_GROUP" reyemask.vertex_group = "RightEye" reyemask.invert_vertex_group = True - # TODO: Disable rendering of all objects in the scene except these ones. self.bake_pass(context, "ao", "AO", {"AO"}, [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) if illuminate_eyes: @@ -342,7 +354,6 @@ def perform_bake(self, context): obj.modifiers.remove(leyemask) if "reyemask" in obj.modifiers: obj.modifiers.remove(reyemask) - # TODO: Re-enable rendering # Blend diffuse and AO to create Quest Diffuse (if selected) if pass_diffuse and pass_ao and pass_questdiffuse: @@ -375,8 +386,19 @@ def perform_bake(self, context): self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) else: - # Join meshes - Common.join_meshes(armature_name=arm_copy.name, repair_shape_keys=False) + if not normal_apply_trans: + # Join meshes + Common.join_meshes(armature_name=arm_copy.name, repair_shape_keys=False) + else: + for obj in collection.all_objects: + # Joining meshes causes issues with materials. Instead. apply location for all meshes, so object and world space are the same + if obj.type == "MESH": + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location = True, scale = True, rotation = True) + + # Bake normals in object coordinates if pass_normal: @@ -409,9 +431,13 @@ def perform_bake(self, context): normaltexnode = tree.nodes.new("ShaderNodeTexImage") if use_decimation: normaltexnode.image = bpy.data.images["SCRIPT_world.png"] + normaltexnode.location.x -= 500 + normaltexnode.location.y -= 200 normalmapnode = tree.nodes.new("ShaderNodeNormalMap") normalmapnode.space = "OBJECT" + normalmapnode.location.x -= 200 + normalmapnode.location.y -= 200 tree.links.new(normalmapnode.inputs["Color"], normaltexnode.outputs["Color"]) tree.links.new(bsdfnode.inputs["Normal"], normalmapnode.outputs["Normal"]) @@ -435,6 +461,16 @@ def perform_bake(self, context): self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + # Blend in original tangent normals + #print("Adding highres tangent normals to lowpoly tangent") + #normal_image = bpy.data.images["SCRIPT_normal.png"] + #origin_image = bpy.data.images["SCRIPT_origin_tangent.png"] + #pixel_buffer = list(normal_image.pixels) + #origin_buffer = origin_image.pixels[:] + #for idx in range(3, len(pixel_buffer), 4): + # pixel_buffer[idx] = max(-1, min(1, pixel_buffer[idx] + origin_buffer[idx])) + #normal_image.pixels[:] = pixel_buffer + # Update generated material to preview all of our passes if pass_normal: normaltexnode.image = bpy.data.images["SCRIPT_normal.png"] @@ -442,11 +478,41 @@ def perform_bake(self, context): if pass_diffuse: diffusetexnode = tree.nodes.new("ShaderNodeTexImage") diffusetexnode.image = bpy.data.images["SCRIPT_diffuse.png"] - tree.links.new(bsdfnode.inputs["Base Color"], diffusetexnode.outputs["Color"]) - # TODO: If AO, blend in AO. If questdiffuse, just use that + diffusetexnode.location.x -= 300 + diffusetexnode.location.y += 500 + # If AO, blend in AO. + if pass_ao: + # AO -> Math (* ao_opacity + (1-ao_opacity)) -> Mix (Math, diffuse) -> Color + aotexnode = tree.nodes.new("ShaderNodeTexImage") + aotexnode.image = bpy.data.images["SCRIPT_ao.png"] + aotexnode.location.x -= 700 + aotexnode.location.y += 800 + + multiplytexnode = tree.nodes.new("ShaderNodeMath") + multiplytexnode.operation = "MULTIPLY_ADD" + multiplytexnode.inputs[1].default_value = questdiffuse_opacity + multiplytexnode.inputs[2].default_value = 1.0 - questdiffuse_opacity + multiplytexnode.location.x -= 400 + multiplytexnode.location.y += 700 + tree.links.new(multiplytexnode.inputs[0], aotexnode.outputs["Color"]) + + mixnode = tree.nodes.new("ShaderNodeMixRGB") + mixnode.blend_type = "MULTIPLY" + mixnode.inputs["Fac"].default_value = 1.0 + mixnode.location.x -= 200 + mixnode.location.y += 700 + tree.links.new(mixnode.inputs["Color1"], multiplytexnode.outputs["Value"]) + tree.links.new(mixnode.inputs["Color2"], diffusetexnode.outputs["Color"]) + + tree.links.new(bsdfnode.inputs["Base Color"], mixnode.outputs["Color"]) + else: + tree.links.new(bsdfnode.inputs["Base Color"], diffusetexnode.outputs["Color"]) if pass_smoothness: if smoothness_diffusepack and pass_diffuse: invertnode = tree.nodes.new("ShaderNodeInvert") + diffusetexnode.location.x -= 200 + invertnode.location.x -= 200 + invertnode.location.y += 200 tree.links.new(invertnode.inputs["Color"], diffusetexnode.outputs["Alpha"]) tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) else: @@ -455,9 +521,12 @@ def perform_bake(self, context): invertnode = tree.nodes.new("ShaderNodeInvert") tree.links.new(invertnode.inputs["Color"], smoothnesstexnode.outputs["Color"]) tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) - - # Move armature so we can see it - arm_copy.location.x += arm_copy.dimensions.x + if pass_emit: + emittexnode = tree.nodes.new("ShaderNodeTexImage") + emittexnode.image = bpy.data.images["SCRIPT_emit.png"] + emittexnode.location.x -= 800 + emittexnode.location.y -= 150 + tree.links.new(bsdfnode.inputs["Emission"], emittexnode.outputs["Color"]) # TODO: Optionally cleanup bones as a last step diff --git a/ui/__init__.py b/ui/__init__.py index 9fe4f5bd..125abcc6 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -6,11 +6,11 @@ from . import manual from . import custom from . import decimation - from . import bake from . import eye_tracking from . import visemes from . import bone_root from . import optimization + from . import bake from . import copy_protection from . import settings_updates from . import supporter @@ -23,11 +23,11 @@ importlib.reload(manual) importlib.reload(custom) importlib.reload(decimation) - importlib.reload(bake) importlib.reload(eye_tracking) importlib.reload(visemes) importlib.reload(bone_root) importlib.reload(optimization) + importlib.reload(bake) importlib.reload(copy_protection) importlib.reload(settings_updates) importlib.reload(supporter) diff --git a/ui/bake.py b/ui/bake.py index 8031866d..b4fc6e27 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -52,6 +52,10 @@ def draw(self, context): col.separator() row = col.row(align=True) row.prop(context.scene, 'bake_pass_normal', expand=True) + if context.scene.bake_pass_normal: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_normal_apply_trans', expand=True) col.separator() row = col.row(align=True) row.prop(context.scene, 'bake_pass_smoothness', expand=True) diff --git a/ui/decimation.py b/ui/decimation.py index 2ad4053d..a6978b2b 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -141,6 +141,7 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'max_tris') col.separator() + col.label(text="Not reccomended if baking!", icon='INFO') row = col.row(align=True) row.scale_y = 1.2 row.operator(Decimation.AutoDecimateButton.bl_idname, icon='MOD_DECIM') From edf8c9b51a9d77bac72f5366132164c2b5566caf Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 30 Oct 2020 19:22:49 -0700 Subject: [PATCH 23/64] Cleanup --- tools/bake.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 38f5aa88..6a7e5bd3 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -57,7 +57,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba if "SCRIPT_" + bake_name + ".png" not in bpy.data.images: bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, - generated_type="BLANK", alpha=True, float_buffer=normal_space=='OBJECT') + generated_type="BLANK", alpha=True) image = bpy.data.images["SCRIPT_" + bake_name + ".png"] if clear: image.alpha_mode = "NONE" @@ -79,6 +79,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba if slot.material: for node in obj.active_material.node_tree.nodes: if node.label == "bake_" + bake_name: + # TODO: restrict to 'Value' type nodes node.outputs["Value"].default_value = 1 # For all materials in all objects, add or repurpose an image texture node named "SCRIPT_BAKE" @@ -127,6 +128,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba if slot.material: for node in obj.active_material.node_tree.nodes: if node.label == "bake_" + bake_name: + # TODO: restrict to 'Value' type nodes node.outputs["Value"].default_value = 0 def copy_ob(self, ob, parent, collection): @@ -321,7 +323,7 @@ def perform_bake(self, context): diffuse_image.pixels[:] = pixel_buffer - # bake emit TODO: test + # bake emit if pass_emit: self.bake_pass(context, "emit", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) @@ -398,8 +400,6 @@ def perform_bake(self, context): context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location = True, scale = True, rotation = True) - - # Bake normals in object coordinates if pass_normal: self.bake_pass(context, "world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], @@ -425,7 +425,6 @@ def perform_bake(self, context): # add a normal map and image texture to connect the world texture, if it exists tree = mat.node_tree bsdfnode = next(node for node in tree.nodes if node.type == "BSDF_PRINCIPLED") - # TODO: Copy BSDF default properties from the largest origin mesh which has BSDF bsdfnode.inputs["Specular"].default_value = 0 if pass_normal: normaltexnode = tree.nodes.new("ShaderNodeTexImage") @@ -461,16 +460,6 @@ def perform_bake(self, context): self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) - # Blend in original tangent normals - #print("Adding highres tangent normals to lowpoly tangent") - #normal_image = bpy.data.images["SCRIPT_normal.png"] - #origin_image = bpy.data.images["SCRIPT_origin_tangent.png"] - #pixel_buffer = list(normal_image.pixels) - #origin_buffer = origin_image.pixels[:] - #for idx in range(3, len(pixel_buffer), 4): - # pixel_buffer[idx] = max(-1, min(1, pixel_buffer[idx] + origin_buffer[idx])) - #normal_image.pixels[:] = pixel_buffer - # Update generated material to preview all of our passes if pass_normal: normaltexnode.image = bpy.data.images["SCRIPT_normal.png"] From c8a598738574dddcf2a3c4d5bdede7d625f5a1f8 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 30 Oct 2020 22:33:14 -0700 Subject: [PATCH 24/64] Fix bug with packing, add ability to smart UV project --- extentions.py | 14 ++++++++++++-- tools/bake.py | 32 +++++++++++++++++++------------- tools/decimation.py | 2 ++ ui/bake.py | 4 ++++ 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/extentions.py b/extentions.py index 68a4ab5b..3dabf143 100644 --- a/extentions.py +++ b/extentions.py @@ -194,6 +194,14 @@ def register(): default=True ) + Scene.bake_smart_uvmap = BoolProperty( + name='Smart UV Project', + description="Generate a new UVMap with Blender's Smart UV Project option.\n" \ + "Will avoid overlaps but doesn't give the best results.\n" \ + "Use if overlaps are hard to avoid", + default=False + ) + Scene.bake_prioritize_face = BoolProperty( name='Prioritize Face/Eyes', description='Scale any UV islands attached to the head/eyes by a given factor.', @@ -259,13 +267,15 @@ def register(): Scene.bake_normal_apply_trans = BoolProperty( name='Apply transforms', - description="Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps", + description="Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps\n" \ + "Turn this off if applying location causes problems with your model", default=True ) Scene.bake_pass_ao = BoolProperty( name='Ambient Occlusion', - description='Bakes Ambient Occlusion, non-projected shadows. Adds a good amount of detail to your model.\n' \ + description='Bakes Ambient Occlusion, non-projected shadows. Adds a significant amount of detail to your model.\n' \ + 'Reccomended for non-toon style avatars.\n' \ 'Takes a fairly long time to bake', default=False ) diff --git a/tools/bake.py b/tools/bake.py index 6a7e5bd3..a970e382 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -31,8 +31,9 @@ # TODO: Button to auto-detect bake passes from nodes # Diffuse: on if >1 material has different color inputs or if any has non-default base color input on bsdf -# Normal: on if any normals connected or if decimating # Smoothness: similar to diffuse +# Emit: similar to diffuse +# Normal: on if any normals connected or if decimating # Pack to alpha: on unless alpha bake # AO: on unless a toon bsdf shader node is detected anywhere # diffuse ao: on if AO on @@ -179,6 +180,7 @@ def perform_bake(self, context): # TODO: Option to smart UV project as a last ditch effort prioritize_face = context.scene.bake_prioritize_face prioritize_factor = context.scene.bake_face_scale + smart_uvmap = context.scene.bake_smart_uvmap margin = 0.01 # TODO: Option to seperate by loose parts and bake selected to active @@ -223,6 +225,18 @@ def perform_bake(self, context): context.view_layer.objects.active = child bpy.ops.mesh.uv_texture_add() child.data.uv_layers[-1].name = 'CATS UV' + if smart_uvmap: + idx = child.data.uv_layers.active_index + child.data.uv_layers.active_index = len(child.data.uv_layers) - 1 + bpy.ops.object.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.smart_project(angle_limit=90.0, island_margin=0.0, user_area_weight=0.0, + use_aspect=True, stretch_to_bounds=True) + bpy.ops.object.editmode_toggle() + child.data.uv_layers.active_index = idx + + # TODO: cleanup, all editmode_toggle -> common.etc # Select all meshes. Select all UVs. Average islands scale context.view_layer.objects.active = next(child for child in arm_copy.children if child.type == "MESH") @@ -232,7 +246,7 @@ def perform_bake(self, context): bpy.ops.uv.average_islands_scale() # Use blender average so we can make our own tweaks. bpy.ops.object.mode_set(mode='OBJECT') - # Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by 200% if selected + # Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by factor if selected # TODO: Look at all bones hierarchically from 'Head' and select those if prioritize_face: for obj in collection.all_objects: @@ -264,20 +278,11 @@ def perform_bake(self, context): uv_layer[loop].uv.y *= prioritize_factor - # UVPackmaster doesn't seem to like huge islands. - bpy.ops.object.mode_set(mode='OBJECT') - for obj in bpy.context.selected_objects: - uv_layer = obj.data.uv_layers["CATS UV"].data - for poly in obj.data.polygons: - for loop in poly.loop_indices: - uv_layer[loop].uv.x *= 0.05 - uv_layer[loop].uv.y *= 0.05 - # Pack islands. Optionally use UVPackMaster if it's available bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') - + bpy.ops.uv.pack_islands(rotate=True, margin=margin) # detect if UVPackMaster installed and configured try: # UVP doesn't respect margins when called like this, find out why context.scene.uvp2_props.normalize_islands = False @@ -288,7 +293,7 @@ def perform_bake(self, context): context.scene.uvp2_props.precision = 1000 bpy.ops.uvpackmaster2.uv_pack() except AttributeError: - bpy.ops.uv.pack_islands(rotate=True, margin=margin) + pass # TODO: Bake selected to active option. Seperate by materials, then bake selected to active for each part @@ -401,6 +406,7 @@ def perform_bake(self, context): bpy.ops.object.transform_apply(location = True, scale = True, rotation = True) # Bake normals in object coordinates + # TODO: 32-bit floats so we don't lose detail if pass_normal: self.bake_pass(context, "world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") diff --git a/tools/decimation.py b/tools/decimation.py index 149b9802..57a1b253 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -358,6 +358,8 @@ def decimate(self, context): Common.switch('EDIT') bpy.ops.mesh.select_all(action="INVERT") + #TODO: If we can create a vertex group with weights roughly equal to 'how likely is this to be animated', + # we can create much better topology by inverse-weighting against it. bpy.ops.mesh.decimate(ratio=decimation, use_vertex_group=False, vertex_group_factor=1.0, diff --git a/ui/bake.py b/ui/bake.py index b4fc6e27..e2ef5162 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -44,6 +44,10 @@ def draw(self, context): row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_face_scale', expand=True) + if context.scene.bake_prioritize_face: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_smart_uvmap', expand=True) col.separator() row = col.row(align=True) col.label(text="Bake passes:") From d165c9ba2fcc53102578110a76c586b2af18b743 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Sun, 1 Nov 2020 20:34:40 -0800 Subject: [PATCH 25/64] Add auto-detect for passes, recursive head check --- extentions.py | 70 ++++++--- tools/bake.py | 365 +++++++++++++++++++++++++++++++++++++------- tools/decimation.py | 2 + ui/bake.py | 42 ++++- ui/decimation.py | 32 ++++ 5 files changed, 427 insertions(+), 84 deletions(-) diff --git a/extentions.py b/extentions.py index 3dabf143..38e68ee4 100644 --- a/extentions.py +++ b/extentions.py @@ -167,6 +167,14 @@ def register(): default='SMART' ) + Scene.bake_max_tris = IntProperty( + name=t('Scene.max_tris.label'), + description=t('Scene.max_tris.desc'), + default=5000, + min=1, + max=70000 + ) + # Bake Scene.bake_resolution = IntProperty( name="Resolution", @@ -180,7 +188,7 @@ def register(): Scene.bake_use_decimation = BoolProperty( name='Decimate', - description='Reduce polycount before baking using decimation settings', + description='Reduce polycount before baking, then use Normal maps to restore detail', default=True ) @@ -203,14 +211,14 @@ def register(): ) Scene.bake_prioritize_face = BoolProperty( - name='Prioritize Face/Eyes', + name='Prioritize Head/Eyes', description='Scale any UV islands attached to the head/eyes by a given factor.', default=True ) Scene.bake_face_scale = FloatProperty( - name="Face/Eyes Scale", - description="How much to scale up the face/eyes textures.", + name="Head/Eyes Scale", + description="How much to scale up the face/eyes portion of the textures.", default=3.0, min=0.5, max=4.0, @@ -219,6 +227,19 @@ def register(): subtype='FACTOR' ) + Scene.bake_quick_compare = BoolProperty( + name='Quick compare', + description='Move output avatar next to existing one to quickly compare', + default=True + ) + + Scene.bake_simplify_armature = BoolProperty( + name='Simplify armature', + description='Merge weights for all non-humanoid bones into their parents.\n' \ + 'Reccomended for Quest avatars with no special AV3 animations', + default=False + ) + Scene.bake_illuminate_eyes = BoolProperty( name='Set eyes to full brightness', description='Relight LeftEye and RightEye to be full brightness.\n' \ @@ -235,16 +256,10 @@ def register(): default=True ) - Scene.bake_smoothness_diffusepack = BoolProperty( - name='Pack to diffuse alpha', - description='Copies the smoothness map to the alpha channel of the diffuse map.\n' \ - "Make sure to set Smoothness > Source to 'Albedo Alpha' in your Unity material", - default=True - ) - Scene.bake_pass_diffuse = BoolProperty( name='Diffuse (Color)', - description='Bakes diffuse, un-lighted color. Usually you will want this.', + description='Bakes diffuse, un-lighted color. Usually you will want this.\n' \ + 'While baking, this temporarily links "Metallic" to "Anisotropic Rotation" as metallic can cause issues.', default=True ) @@ -292,17 +307,33 @@ def register(): default=False ) - Scene.bake_show_advanced = BoolProperty( - name='Advanced', - description='Show advanced passes. These are not natively bakeable in Blender,\n' \ - 'so they may not work as well', - default=False + Scene.bake_diffuse_alpha_pack = EnumProperty( + name="Alpha Channel", + description="What to pack to the Diffuse Alpha channel", + items=[ + ("NONE", "None", "No alpha channel"), + ("TRANSPARENCY", "Transparency", "Pack Transparency"), + ("SMOOTHNESS", "Smoothness", "Pack Smoothness. Most efficient if you don't have transparency or metallic textures."), + ], + default="NONE" + ) + + Scene.bake_metallic_alpha_pack = EnumProperty( + name="Metallic Alpha Channel", + description="What to pack to the Metallic Alpha channel", + items=[ + ("NONE", "None", "No alpha channel"), + ("SMOOTHNESS", "Smoothness", "Pack Smoothness. Use this if your Diffuse alpha channel is already populated with Transparency") + ], + default="NONE" ) Scene.bake_pass_alpha = BoolProperty( name='Transparency', description='Bakes transparency by connecting the last Principled BSDF Alpha input\n' \ - 'to the Base Color input and baking Diffuse', + 'to the Base Color input and baking Diffuse.\n' \ + 'Not a native pass in Blender, results may vary\n' \ + 'Unused if you are baking to Quest', default=False ) @@ -316,7 +347,8 @@ def register(): Scene.bake_pass_metallic = BoolProperty( name='Metallic', description='Bakes metallic by connecting the last Principled BSDF Metallic input\n' \ - 'to the Base Color input and baking Diffuse', + 'to the Base Color input and baking Diffuse.\n' \ + 'Not a native pass in Blender, results may vary', default=False ) diff --git a/tools/bake.py b/tools/bake.py index a970e382..2c2c18a2 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -29,14 +29,135 @@ from .register import register_wrap from ..translations import t -# TODO: Button to auto-detect bake passes from nodes -# Diffuse: on if >1 material has different color inputs or if any has non-default base color input on bsdf -# Smoothness: similar to diffuse -# Emit: similar to diffuse -# Normal: on if any normals connected or if decimating -# Pack to alpha: on unless alpha bake -# AO: on unless a toon bsdf shader node is detected anywhere -# diffuse ao: on if AO on +@register_wrap +class BakePresetDesktop(bpy.types.Operator): + bl_idname = 'cats_bake.preset_desktop' + bl_label = "Desktop" + bl_description = "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" \ + "This will try to automatically detect which bake passes are relevant to your model" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + context.scene.bake_max_tris = 32000 + context.scene.bake_use_decimation = True + context.scene.bake_resolution = 2048 + # Autodetect passes based on BSDF node inputs + bsdf_nodes = [] + objects = Common.get_meshes_objects() + for obj in objects: + for slot in obj.material_slots: + if slot.material: + tree = slot.material.node_tree + for node in tree.nodes: + if node.type == "BSDF_PRINCIPLED": + bsdf_nodes.append(node) + # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf + context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) + # Smoothness: similar to diffuse + context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) + # Emit: similar to diffuse + context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) + + # Transparency: similar to diffuse + context.scene.bake_pass_alpha = (any([node.inputs["Alpha"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Alpha"].default_value for node in bsdf_nodes])) > 1) + + # Metallic: similar to diffuse + context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) + + # Normal: on if any normals connected or if decimating... so, always on for this preset + context.scene.bake_pass_normal = True + + # Apply transforms: on if more than one mesh TODO: with different materials? + context.scene.bake_normal_apply_trans = len(objects) > 1 + + # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? + # diffuse ao: off if desktop + context.scene.bake_pass_questdiffuse = False + + # alpha packs: arrange for maximum efficiency. + # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so + context.scene.bake_diffuse_alpha_pack = "NONE" + context.scene.bake_metallic_alpha_pack = "NONE" + # If 'smoothness' and 'transparency', we need to force metallic to bake so we can pack to it. + if context.scene.bake_pass_smoothness and context.scene.bake_pass_alpha: + context.scene.bake_pass_metallic = True + # If we have transparency, it needs to go in diffuse alpha + if context.scene.bake_pass_alpha: + context.scene.bake_diffuse_alpha_pack = "TRANSPARENCY" + # Smoothness to diffuse is only the most efficient when we don't have metallic or alpha + if context.scene.bake_pass_smoothness and not context.scene.bake_pass_metallic and not context.scene.bake_pass_alpha: + context.scene.bake_diffuse_alpha_pack = "SMOOTHNESS" + if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: + context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + + return {'FINISHED'} + +@register_wrap +class BakePresetQuest(bpy.types.Operator): + bl_idname = 'cats_bake.preset_quest' + bl_label = "Quest" + bl_description = "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" \ + "This will try to automatically detect which bake passes are relevant to your model\n" \ + "If you are not using any special AV3 features, you should manually select non-humanoid bones\n" \ + "and then use Merge Weights in the Model Options panel" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + def execute(self, context): + context.scene.bake_max_tris = 5000 + context.scene.bake_use_decimation = True + context.scene.bake_resolution = 1024 + # Autodetect passes based on BSDF node inputs + bsdf_nodes = [] + objects = Common.get_meshes_objects() + for obj in objects: + for slot in obj.material_slots: + if slot.material: + tree = slot.material.node_tree + for node in tree.nodes: + if node.type == "BSDF_PRINCIPLED": + bsdf_nodes.append(node) + # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf + context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) + # Smoothness: similar to diffuse + context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) + # Emit: similar to diffuse + context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) + + # Transparency: unsupported on quest. Possibly would make better mip maps if manually configured in Unity to have alpha: None + context.scene.bake_pass_alpha = False + + # Metallic: similar to diffuse + context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) + + # Normal: on if any normals connected or if decimating... so, always on for this preset + context.scene.bake_pass_normal = True + + # Apply transforms: on if more than one mesh TODO: with different materials? + context.scene.bake_normal_apply_trans = len(objects) > 1 + + # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? + # diffuse ao: on if AO is on + context.scene.bake_pass_questdiffuse = context.scene.bake_pass_ao + + # alpha packs: arrange for maximum efficiency. + # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so + context.scene.bake_diffuse_alpha_pack = "NONE" + context.scene.bake_metallic_alpha_pack = "NONE" + # If 'smoothness', we need to force metallic to bake so we can pack to it. (smoothness source is not configurable) + if context.scene.bake_pass_smoothness: + context.scene.bake_pass_metallic = True + if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: + context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + return {'FINISHED'} @register_wrap class BakeButton(bpy.types.Operator): @@ -44,9 +165,41 @@ class BakeButton(bpy.types.Operator): bl_label = 'Copy and Bake (SLOW!)' bl_description = "Perform the bake. Warning, this performs an actual render!\n" \ "This will create a copy of your avatar to leave the original alone.\n" \ - "Depending on your machine, this could take an hour or more." + "Depending on your machine and model, this could take an hour or more.\n" \ + "For each pass, any Value node in your materials labeled bake_ will be\n" \ + "set to 1.0, for more granular customization." bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + # Only works between equal data types. + def swap_links(self, objects, input1, input2): + # Find all Principled BSDF. Flip values for input1 and input2 (default_value and connection) + for obj in objects: + for slot in obj.material_slots: + if slot.material: + tree = slot.material.node_tree + for node in tree.nodes: + if node.type == "BSDF_PRINCIPLED": + dv1 = node.inputs[input1].default_value + dv2 = node.inputs[input2].default_value + node.inputs[input2].default_value = dv1 + node.inputs[input1].default_value = dv2 + + alpha_input = None + if node.inputs[input1].is_linked: + alpha_input = node.inputs[input1].links[0].from_socket + tree.links.remove(node.inputs[input1].links[0]) + + color_input = None + if node.inputs[input2].is_linked: + color_input = node.inputs[input2].links[0].from_socket + tree.links.remove(node.inputs[input2].links[0]) + + if color_input: + tree.links.new(node.inputs[input1], color_input) + + if alpha_input: + tree.links.new(node.inputs[input2], alpha_input) + # "Bake pass" function. Run a single bake to ".png" against all selected objects. def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, bake_size, bake_samples, bake_ray_distance, background_color, clear, bake_margin, bake_active=None, bake_multires=False, normal_space='TANGENT'): bpy.ops.object.select_all(action='DESELECT') @@ -56,19 +209,26 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba print("Baking " + bake_name + " for objects: " + ",".join([obj.name for obj in objects])) - if "SCRIPT_" + bake_name + ".png" not in bpy.data.images: + if clear: + if "SCRIPT_" + bake_name + ".png" in bpy.data.images: + image = bpy.data.images["SCRIPT_" + bake_name + ".png"] + image.user_clear() + bpy.data.images.remove(image) + bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, generated_type="BLANK", alpha=True) - image = bpy.data.images["SCRIPT_" + bake_name + ".png"] - if clear: + image = bpy.data.images["SCRIPT_" + bake_name + ".png"] image.alpha_mode = "NONE" image.generated_color = background_color image.generated_width=bake_size[0] image.generated_height=bake_size[1] - if bake_type == 'NORMAL' or bake_type == 'ROUGHNESS': + image.scale(bake_size[0], bake_size[1]) + if bake_type == 'NORMAL': image.colorspace_settings.name = 'Non-Color' - if bake_type == 'DIFFUSE': # For packing smoothness to alpha + if bake_name == 'diffuse' or bake_name == 'metallic': # For packing smoothness to alpha image.alpha_mode = 'CHANNEL_PACKED' + image.pixels[:] = background_color * bake_size[0] * bake_size[1] + image = bpy.data.images["SCRIPT_" + bake_name + ".png"] # Select only objects we're baking for obj in objects: @@ -79,8 +239,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba for slot in obj.material_slots: if slot.material: for node in obj.active_material.node_tree.nodes: - if node.label == "bake_" + bake_name: - # TODO: restrict to 'Value' type nodes + if node.type == "VALUE" and node.label == "bake_" + bake_name: node.outputs["Value"].default_value = 1 # For all materials in all objects, add or repurpose an image texture node named "SCRIPT_BAKE" @@ -128,8 +287,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba for slot in obj.material_slots: if slot.material: for node in obj.active_material.node_tree.nodes: - if node.label == "bake_" + bake_name: - # TODO: restrict to 'Value' type nodes + if node.type == "VALUE" and node.label == "bake_" + bake_name: node.outputs["Value"].default_value = 0 def copy_ob(self, ob, parent, collection): @@ -164,24 +322,42 @@ def execute(self, context): if context.scene.render.engine != 'CYCLES': self.report({'ERROR'}, "You need to set your render engine to Cycles first!") return {'FINISHED'} -# saved_data = Common.SavedData() # TODO: Check if any UV islands are self-overlapping, emit an error + + # Change decimate settings, run bake, change them back + decimation_mode = context.scene.decimation_mode + max_tris = context.scene.max_tris + decimate_fingers = context.scene.decimate_fingers + decimation_remove_doubles = context.scene.decimation_remove_doubles + + context.scene.decimation_mode = "SMART" + context.scene.max_tris = context.scene.bake_max_tris + context.scene.decimate_fingers = False + context.scene.decimation_remove_doubles = True self.perform_bake(context) + + context.scene.decimation_mode = decimation_mode + context.scene.max_tris = max_tris + context.scene.decimate_fingers = decimate_fingers + context.scene.decimation_remove_doubles = decimation_remove_doubles + return {'FINISHED'} -# saved_data.load() def perform_bake(self, context): print('START BAKE') + # TODO: diffuse, emit, alpha, metallic and smoothness all have a very slight difference between sample counts + # default it to something sane, but maybe add a menu later? # Global options resolution = context.scene.bake_resolution use_decimation = context.scene.bake_use_decimation preserve_seams = context.scene.bake_preserve_seams generate_uvmap = context.scene.bake_generate_uvmap - # TODO: Option to smart UV project as a last ditch effort prioritize_face = context.scene.bake_prioritize_face prioritize_factor = context.scene.bake_face_scale smart_uvmap = context.scene.bake_smart_uvmap margin = 0.01 + quick_compare = context.scene.bake_quick_compare + # TODO: Option to seperate by loose parts and bake selected to active @@ -192,12 +368,15 @@ def perform_bake(self, context): pass_ao = context.scene.bake_pass_ao pass_questdiffuse = context.scene.bake_pass_questdiffuse pass_emit = context.scene.bake_pass_emit + pass_alpha = context.scene.bake_pass_alpha + pass_metallic = context.scene.bake_pass_metallic # Pass options illuminate_eyes = context.scene.bake_illuminate_eyes questdiffuse_opacity = context.scene.bake_questdiffuse_opacity - smoothness_diffusepack = context.scene.bake_smoothness_diffusepack normal_apply_trans = context.scene.bake_normal_apply_trans + diffuse_alpha_pack = context.scene.bake_diffuse_alpha_pack + metallic_alpha_pack = context.scene.bake_metallic_alpha_pack # Create an output collection collection = bpy.data.collections.new("CATS Bake") @@ -208,7 +387,8 @@ def perform_bake(self, context): arm_copy = self.tree_copy(armature, None, collection) # Move armature so we can see it - arm_copy.location.x += arm_copy.dimensions.x + if quick_compare: + arm_copy.location.x += arm_copy.dimensions.x # Make sure all armature modifiers target the new armature for child in collection.all_objects: @@ -236,7 +416,7 @@ def perform_bake(self, context): bpy.ops.object.editmode_toggle() child.data.uv_layers.active_index = idx - # TODO: cleanup, all editmode_toggle -> common.etc + # TODO: cleanup, all editmode_toggle -> common.switch # Select all meshes. Select all UVs. Average islands scale context.view_layer.objects.active = next(child for child in arm_copy.children if child.type == "MESH") @@ -244,30 +424,38 @@ def perform_bake(self, context): bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.average_islands_scale() # Use blender average so we can make our own tweaks. - bpy.ops.object.mode_set(mode='OBJECT') + Common.switch('OBJECT') - # Select all islands belonging to 'Head', 'LeftEye' and 'RightEye', separate islands, enlarge by factor if selected - # TODO: Look at all bones hierarchically from 'Head' and select those + # Select all islands belonging to 'Head' and children and enlarge them if prioritize_face: + def recursive_name_get(bone): + ret = set([bone.name]) + if bone.children: + for child in bone.children: + ret.union(recursive_name_get(child)) + return ret + + selected_group_names = recursive_name_get(arm_copy.data.bones["Head"]) + print("Prioritizing vertex groups: " + (", ".join(selected_group_names))) + for obj in collection.all_objects: if obj.type != "MESH": continue context.view_layer.objects.active = obj - for group in ["Head", "LeftEye", "RightEye"]: + for group in selected_group_names: if group in obj.vertex_groups: - print("{} found in {}".format(group, obj.name)) - bpy.ops.object.mode_set(mode='EDIT') + Common.switch('EDIT') bpy.ops.uv.select_all(action='DESELECT') bpy.ops.mesh.select_all(action='DESELECT') # Select all vertices in it obj.vertex_groups.active = obj.vertex_groups[group] bpy.ops.object.vertex_group_select() # Synchronize - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='EDIT') + Common.switch('OBJECT') + Common.switch('EDIT') # Then select all UVs bpy.ops.uv.select_all(action='SELECT') - bpy.ops.object.mode_set(mode='OBJECT') + Common.switch('OBJECT') # Then for each UV (cause of the viewport thing) scale up by the selected factor uv_layer = obj.data.uv_layers["CATS UV"].data @@ -279,7 +467,7 @@ def perform_bake(self, context): # Pack islands. Optionally use UVPackMaster if it's available - bpy.ops.object.mode_set(mode='EDIT') + Common.switch('EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.pack_islands(rotate=True, margin=margin) @@ -291,22 +479,27 @@ def perform_bake(self, context): context.scene.uvp2_props.margin = margin context.scene.uvp2_props.similarity_threshold = 3 context.scene.uvp2_props.precision = 1000 - bpy.ops.uvpackmaster2.uv_pack() + # Give UVP a static number of iterations to do TODO: make this configurable? + for _ in range(1, 10): + bpy.ops.uvpackmaster2.uv_pack() except AttributeError: pass - # TODO: Bake selected to active option. Seperate by materials, then bake selected to active for each part - # Bake diffuse Common.switch('OBJECT') if pass_diffuse: + # Metallic can cause issues baking diffuse, so we put it somewhere typically unused + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic Rotation") + self.bake_pass(context, "diffuse", "DIFFUSE", {"COLOR"}, [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic Rotation") # Bake roughness, invert if pass_smoothness: self.bake_pass(context, "smoothness", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 1, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) image = bpy.data.images["SCRIPT_smoothness.png"] pixel_buffer = list(image.pixels) for idx in range(0, len(image.pixels)): @@ -316,29 +509,66 @@ def perform_bake(self, context): image.pixels[:] = pixel_buffer - # Pack smoothness to diffuse alpha (if selected) - if smoothness_diffusepack and pass_diffuse and pass_smoothness: - print("Packing smoothness to diffuse alpha") - diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] - smoothness_image = bpy.data.images["SCRIPT_smoothness.png"] - pixel_buffer = list(diffuse_image.pixels) - smoothness_buffer = smoothness_image.pixels[:] - for idx in range(3, len(pixel_buffer), 4): - pixel_buffer[idx] = smoothness_buffer[idx - 3] - diffuse_image.pixels[:] = pixel_buffer - # bake emit if pass_emit: self.bake_pass(context, "emit", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 1, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + + # advanced: bake alpha from bsdf output + if pass_alpha: + originals = self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") + + # Run the bake pass (bake roughness) + self.bake_pass(context, "alpha", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) - # TODO: advanced: bake alpha from last bsdf output + # Revert the changes (re-flip) + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") - # TODO: advanced: bake metallic from last bsdf output + # advanced: bake metallic from last bsdf output + if pass_metallic: + # Find all Principled BSDF nodes. Flip Roughness and Metallic (default_value and connection) + originals = self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Roughness") + + # Run the bake pass + self.bake_pass(context, "metallic", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + + # Revert the changes (re-flip) + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Roughness") + + # Pack to diffuse alpha (if selected) + if pass_diffuse and ((diffuse_alpha_pack == "SMOOTHNESS" and pass_smoothness) or + (diffuse_alpha_pack == "TRANSPARENCY" and pass_alpha)): + print("Packing to diffuse alpha") + diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] + alpha_image = None + if diffuse_alpha_pack == "SMOOTHNESS": + alpha_image = bpy.data.images["SCRIPT_smoothness.png"] + elif diffuse_alpha_pack == "TRANSPARENCY": + alpha_image = bpy.data.images["SCRIPT_alpha.png"] + pixel_buffer = list(diffuse_image.pixels) + alpha_buffer = alpha_image.pixels[:] + for idx in range(3, len(pixel_buffer), 4): + pixel_buffer[idx] = alpha_buffer[idx - 3] + diffuse_image.pixels[:] = pixel_buffer + + # Pack to metallic alpha (if selected) + if pass_metallic and (metallic_alpha_pack == "SMOOTHNESS" and pass_smoothness): + print("Packing to metallic alpha") + metallic_image = bpy.data.images["SCRIPT_metallic.png"] + alpha_image = bpy.data.images["SCRIPT_smoothness.png"] + pixel_buffer = list(metallic_image.pixels) + alpha_buffer = alpha_image.pixels[:] + for idx in range(3, len(pixel_buffer), 4): + pixel_buffer[idx] = alpha_buffer[idx - 3] + metallic_image.pixels[:] = pixel_buffer # TODO: advanced: bake detail mask from diffuse node setup + # TODO: specularity? would allow specular setups on pre-existing avatars + # Bake AO if pass_ao: if illuminate_eyes: @@ -381,8 +611,8 @@ def perform_bake(self, context): # Map range: set the black point up to 1-opacity pixel_buffer[idx] = diffuse_buffer[idx] * ((1.0 - questdiffuse_opacity) + (questdiffuse_opacity * ao_buffer[idx])) else: - # Just copy alpha - pixel_buffer[idx] = diffuse_buffer[idx] + # Alpha is unused on quest, set to 1 to make sure unity doesn't keep it + pixel_buffer[idx] = 1.0 image.pixels[:] = pixel_buffer @@ -412,7 +642,6 @@ def perform_bake(self, context): (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") # Decimate. If 'preserve seams' is selected, forcibly preserve seams (seams from islands, deselect seams) - # TODO: We need to use our own settings: own tri count, and always use Smart decimation mode bpy.ops.cats_decimation.auto_decimate(armature_name=arm_copy.name, preserve_seams=preserve_seams, seperate_materials=False) # Remove all other materials @@ -502,20 +731,40 @@ def perform_bake(self, context): tree.links.new(bsdfnode.inputs["Base Color"], mixnode.outputs["Color"]) else: tree.links.new(bsdfnode.inputs["Base Color"], diffusetexnode.outputs["Color"]) + if pass_metallic: + metallictexnode = tree.nodes.new("ShaderNodeTexImage") + metallictexnode.image = bpy.data.images["SCRIPT_metallic.png"] + metallictexnode.location.x -= 300 + metallictexnode.location.y += 200 + tree.links.new(bsdfnode.inputs["Metallic"], metallictexnode.outputs["Color"]) if pass_smoothness: - if smoothness_diffusepack and pass_diffuse: + if pass_diffuse and (diffuse_alpha_pack == "SMOOTHNESS"): invertnode = tree.nodes.new("ShaderNodeInvert") diffusetexnode.location.x -= 200 invertnode.location.x -= 200 invertnode.location.y += 200 tree.links.new(invertnode.inputs["Color"], diffusetexnode.outputs["Alpha"]) tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) + elif pass_metallic and (metallic_alpha_pack == "SMOOTHNESS"): + invertnode = tree.nodes.new("ShaderNodeInvert") + metallictexnode.location.x -= 200 + invertnode.location.x -= 200 + invertnode.location.y += 100 + tree.links.new(invertnode.inputs["Color"], metallictexnode.outputs["Alpha"]) + tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) else: smoothnesstexnode = tree.nodes.new("ShaderNodeTexImage") smoothnesstexnode.image = bpy.data.images["SCRIPT_smoothness.png"] invertnode = tree.nodes.new("ShaderNodeInvert") tree.links.new(invertnode.inputs["Color"], smoothnesstexnode.outputs["Color"]) tree.links.new(bsdfnode.inputs["Roughness"], invertnode.outputs["Color"]) + if pass_alpha: + if pass_diffuse and (diffuse_alpha_pack == "TRANSPARENCY"): + tree.links.new(bsdfnode.inputs["Alpha"], diffusetexnode.outputs["Alpha"]) + else: + alphatexnode = tree.nodes.new("ShaderNodeTexImage") + alphatexnode.image = bpy.data.images["SCRIPT_alpha.png"] + tree.links.new(bsdfnode.inputs["Alpha"], alphatexnode.outputs["Color"]) if pass_emit: emittexnode = tree.nodes.new("ShaderNodeTexImage") emittexnode.image = bpy.data.images["SCRIPT_emit.png"] @@ -524,5 +773,7 @@ def perform_bake(self, context): tree.links.new(bsdfnode.inputs["Emission"], emittexnode.outputs["Color"]) # TODO: Optionally cleanup bones as a last step + # Select all bones which don't fuzzy match a whitelist (Chest, Head, etc) and do Merge Weights to parent on them + # For now, just add a note saying you should merge bones manually print("BAKE COMPLETE!") diff --git a/tools/decimation.py b/tools/decimation.py index 57a1b253..7e2bade6 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -360,6 +360,8 @@ def decimate(self, context): #TODO: If we can create a vertex group with weights roughly equal to 'how likely is this to be animated', # we can create much better topology by inverse-weighting against it. + #TODO: On many meshes, un-subdividing until it's near the target verts and then decimating the rest of the way + # results in MUCH better topology. Something to figure out against 2.93 bpy.ops.mesh.decimate(ratio=decimation, use_vertex_group=False, vertex_group_factor=1.0, diff --git a/ui/bake.py b/ui/bake.py index e2ef5162..2172490f 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -21,16 +21,19 @@ def draw(self, context): box = layout.box() col = box.column(align=True) - # TODO: Quest (decimate) and Desktop (nodecimate) presets - - col.label(text="Presets:") - + col.label(text="Autodetect:") + row = col.row(align=True) + row.operator(Bake.BakePresetDesktop.bl_idname, icon="ANTIALIASED") + row.operator(Bake.BakePresetQuest.bl_idname, icon="ALIASED") col.label(text="General options:") row = col.row(align=True) row.prop(context.scene, 'bake_resolution', expand=True) row = col.row(align=True) row.prop(context.scene, 'bake_use_decimation', expand=True) if context.scene.bake_use_decimation: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_max_tris', expand=True) row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_preserve_seams', expand=True) @@ -48,11 +51,24 @@ def draw(self, context): row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_smart_uvmap', expand=True) + #row = col.row(align=True) + #row.prop(context.scene, 'bake_simplify_armature', expand=True) + row = col.row(align=True) + row.prop(context.scene, 'bake_quick_compare', expand=True) col.separator() row = col.row(align=True) col.label(text="Bake passes:") row = col.row(align=True) row.prop(context.scene, 'bake_pass_diffuse', expand=True) + if context.scene.bake_pass_diffuse and (context.scene.bake_pass_smoothness or context.scene.bake_pass_alpha): + row = col.row(align=True) + row.separator() + row.label(text="Alpha:") + row.prop(context.scene, 'bake_diffuse_alpha_pack', expand=True) + if (context.scene.bake_diffuse_alpha_pack == "TRANSPARENCY") and not context.scene.bake_pass_alpha: + col.label(text="Transparency isn't currently selected!", icon="INFO") + elif (context.scene.bake_diffuse_alpha_pack == "SMOOTHNESS") and not context.scene.bake_pass_smoothness: + col.label(text="Smoothness isn't currently selected!", icon="INFO") col.separator() row = col.row(align=True) row.prop(context.scene, 'bake_pass_normal', expand=True) @@ -63,10 +79,6 @@ def draw(self, context): col.separator() row = col.row(align=True) row.prop(context.scene, 'bake_pass_smoothness', expand=True) - if context.scene.bake_pass_diffuse and context.scene.bake_pass_smoothness: - row = col.row(align=True) - row.separator() - row.prop(context.scene, 'bake_smoothness_diffusepack', expand=True) col.separator() row = col.row(align=True) row.prop(context.scene, 'bake_pass_ao', expand=True) @@ -83,6 +95,20 @@ def draw(self, context): row.separator() row.prop(context.scene, 'bake_questdiffuse_opacity', expand=True) col.separator() + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_alpha', expand=True) + col.separator() + row = col.row(align=True) + row.prop(context.scene, 'bake_pass_metallic', expand=True) + if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: + row = col.row(align=True) + row.separator() + row.label(text="Alpha:") + row.prop(context.scene, 'bake_metallic_alpha_pack', expand=True) + if context.scene.bake_diffuse_alpha_pack == "SMOOTHNESS" and context.scene.bake_metallic_alpha_pack == "SMOOTHNESS": + col.label(text="Smoothness packed in two places!", icon="INFO") + col.separator() + row = col.row(align=True) row.prop(context.scene, 'bake_pass_emit', expand=True) row = col.row(align=True) diff --git a/ui/decimation.py b/ui/decimation.py index a6978b2b..cf54a024 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -9,6 +9,38 @@ from ..tools.register import register_wrap from ..translations import t +@register_wrap +class AutoDecimatePresetGood(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 AutoDecimatePresetExcellent(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 AutoDecimatePresetQuest(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 DecimationPanel(ToolPanel, bpy.types.Panel): From 3371c77997f901bb10224418733b15c03a8dc65e Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Mon, 2 Nov 2020 14:43:57 -0800 Subject: [PATCH 26/64] Update wording, improve Smart UV Project results, better display of alpha values --- extentions.py | 3 +-- tools/bake.py | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extentions.py b/extentions.py index 38e68ee4..beaf2e6d 100644 --- a/extentions.py +++ b/extentions.py @@ -205,8 +205,7 @@ def register(): Scene.bake_smart_uvmap = BoolProperty( name='Smart UV Project', description="Generate a new UVMap with Blender's Smart UV Project option.\n" \ - "Will avoid overlaps but doesn't give the best results.\n" \ - "Use if overlaps are hard to avoid", + "Use if your result has weird shading issues", default=False ) diff --git a/tools/bake.py b/tools/bake.py index 2c2c18a2..17cd0511 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -145,8 +145,8 @@ def execute(self, context): context.scene.bake_normal_apply_trans = len(objects) > 1 # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? - # diffuse ao: on if AO is on - context.scene.bake_pass_questdiffuse = context.scene.bake_pass_ao + # diffuse ao: on, won't do anything unless ao gets checked + context.scene.bake_pass_questdiffuse = True # alpha packs: arrange for maximum efficiency. # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so @@ -411,7 +411,7 @@ def perform_bake(self, context): bpy.ops.object.editmode_toggle() bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') - bpy.ops.uv.smart_project(angle_limit=90.0, island_margin=0.0, user_area_weight=0.0, + bpy.ops.uv.smart_project(angle_limit=66.0, island_margin=0.01, user_area_weight=0.0, use_aspect=True, stretch_to_bounds=True) bpy.ops.object.editmode_toggle() child.data.uv_layers.active_index = idx @@ -517,7 +517,7 @@ def recursive_name_get(bone): # advanced: bake alpha from bsdf output if pass_alpha: - originals = self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") # Run the bake pass (bake roughness) self.bake_pass(context, "alpha", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], @@ -529,7 +529,7 @@ def recursive_name_get(bone): # advanced: bake metallic from last bsdf output if pass_metallic: # Find all Principled BSDF nodes. Flip Roughness and Metallic (default_value and connection) - originals = self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Roughness") + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Roughness") # Run the bake pass self.bake_pass(context, "metallic", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], @@ -765,6 +765,7 @@ def recursive_name_get(bone): alphatexnode = tree.nodes.new("ShaderNodeTexImage") alphatexnode.image = bpy.data.images["SCRIPT_alpha.png"] tree.links.new(bsdfnode.inputs["Alpha"], alphatexnode.outputs["Color"]) + mat.blend_method = 'CLIP' if pass_emit: emittexnode = tree.nodes.new("ShaderNodeTexImage") emittexnode.image = bpy.data.images["SCRIPT_emit.png"] From 5d3bd44c3688645c7416eb6c0a9a9916f4877130 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Mon, 2 Nov 2020 15:08:08 -0800 Subject: [PATCH 27/64] Handle prioritizing head in a smarter way --- tools/bake.py | 44 ++++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 17cd0511..07d22ec9 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -428,43 +428,35 @@ def perform_bake(self, context): # Select all islands belonging to 'Head' and children and enlarge them if prioritize_face: - def recursive_name_get(bone): - ret = set([bone.name]) - if bone.children: - for child in bone.children: - ret.union(recursive_name_get(child)) - return ret - - selected_group_names = recursive_name_get(arm_copy.data.bones["Head"]) + selected_group_names = ["Head"] + selected_group_names.extend([bone.name for bone in arm_copy.data.bones["Head"].children_recursive]) print("Prioritizing vertex groups: " + (", ".join(selected_group_names))) for obj in collection.all_objects: if obj.type != "MESH": continue context.view_layer.objects.active = obj + Common.switch('EDIT') + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') for group in selected_group_names: if group in obj.vertex_groups: - Common.switch('EDIT') - bpy.ops.uv.select_all(action='DESELECT') - bpy.ops.mesh.select_all(action='DESELECT') # Select all vertices in it obj.vertex_groups.active = obj.vertex_groups[group] bpy.ops.object.vertex_group_select() - # Synchronize - Common.switch('OBJECT') - Common.switch('EDIT') - # Then select all UVs - bpy.ops.uv.select_all(action='SELECT') - Common.switch('OBJECT') - - # Then for each UV (cause of the viewport thing) scale up by the selected factor - uv_layer = obj.data.uv_layers["CATS UV"].data - for poly in obj.data.polygons: - for loop in poly.loop_indices: - if uv_layer[loop].select: - uv_layer[loop].uv.x *= prioritize_factor - uv_layer[loop].uv.y *= prioritize_factor - + # Synchronize + Common.switch('OBJECT') + Common.switch('EDIT') + # Then select all UVs + bpy.ops.uv.select_all(action='SELECT') + # Then for each UV (cause of the viewport thing) scale up by the selected factor + Common.switch('OBJECT') + uv_layer = obj.data.uv_layers["CATS UV"].data + for poly in obj.data.polygons: + for loop in poly.loop_indices: + if uv_layer[loop].select: + uv_layer[loop].uv.x *= prioritize_factor + uv_layer[loop].uv.y *= prioritize_factor # Pack islands. Optionally use UVPackMaster if it's available Common.switch('EDIT') From 6249fc118b3178cf505900cec32c7c678f18a612 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 3 Nov 2020 19:27:00 -0800 Subject: [PATCH 28/64] Add a new UV unpinning mode, better quality than smart UV project by unpinning center mirrored islands --- extentions.py | 21 +++++++++------------ tools/bake.py | 17 ++++++++++++----- ui/bake.py | 8 ++++---- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/extentions.py b/extentions.py index beaf2e6d..fa7f4f51 100644 --- a/extentions.py +++ b/extentions.py @@ -202,11 +202,15 @@ def register(): default=True ) - Scene.bake_smart_uvmap = BoolProperty( - name='Smart UV Project', - description="Generate a new UVMap with Blender's Smart UV Project option.\n" \ - "Use if your result has weird shading issues", - default=False + Scene.bake_uv_overlap_correction = EnumProperty( + name="Overlap correction", + description="Method used to prevent overlaps in UVMap", + items=[ + ("NONE", "None", "Leave islands as they are. Use if islands don't self-intersect at all"), + ("UNMIRROR", "Unmirror", "Move all face islands with positive X values over one to un-pin mirrored UVs. Solves most UV pinning issues."), + ("REPROJECT", "Reproject", "Use blender's Smart UV Project to come up with an entirely new island layout. Tends to reduce quality."), + ], + default="UNMIRROR" ) Scene.bake_prioritize_face = BoolProperty( @@ -336,13 +340,6 @@ def register(): default=False ) - Scene.bake_alpha_diffusepack = BoolProperty( - name='Pack to diffuse alpha', - description='Copies the alpha map to the alpha channel of the diffuse map.\n' \ - "This will override any existing alpha map", - default=True - ) - Scene.bake_pass_metallic = BoolProperty( name='Metallic', description='Bakes metallic by connecting the last Principled BSDF Metallic input\n' \ diff --git a/tools/bake.py b/tools/bake.py index 07d22ec9..b986007a 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -102,9 +102,7 @@ class BakePresetQuest(bpy.types.Operator): bl_idname = 'cats_bake.preset_quest' bl_label = "Quest" bl_description = "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" \ - "This will try to automatically detect which bake passes are relevant to your model\n" \ - "If you are not using any special AV3 features, you should manually select non-humanoid bones\n" \ - "and then use Merge Weights in the Model Options panel" + "This will try to automatically detect which bake passes are relevant to your model" bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): @@ -354,7 +352,7 @@ def perform_bake(self, context): generate_uvmap = context.scene.bake_generate_uvmap prioritize_face = context.scene.bake_prioritize_face prioritize_factor = context.scene.bake_face_scale - smart_uvmap = context.scene.bake_smart_uvmap + uv_overlap_correction = context.scene.bake_uv_overlap_correction margin = 0.01 quick_compare = context.scene.bake_quick_compare @@ -405,7 +403,7 @@ def perform_bake(self, context): context.view_layer.objects.active = child bpy.ops.mesh.uv_texture_add() child.data.uv_layers[-1].name = 'CATS UV' - if smart_uvmap: + if uv_overlap_correction == "REPROJECT": idx = child.data.uv_layers.active_index child.data.uv_layers.active_index = len(child.data.uv_layers) - 1 bpy.ops.object.editmode_toggle() @@ -415,6 +413,15 @@ def perform_bake(self, context): use_aspect=True, stretch_to_bounds=True) bpy.ops.object.editmode_toggle() child.data.uv_layers.active_index = idx + elif uv_overlap_correction == "UNMIRROR": + # TODO: issue a warning if any source images don't use 'wrap' + # Select all faces in +X + uv_layer = child.data.uv_layers["CATS UV"].data + for poly in child.data.polygons: + if poly.center[0] > 0: + for loop in poly.loop_indices: + if uv_layer[loop].select: + uv_layer[loop].uv.x += 1 # TODO: cleanup, all editmode_toggle -> common.switch diff --git a/ui/bake.py b/ui/bake.py index 2172490f..e76c0980 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -47,10 +47,10 @@ def draw(self, context): row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_face_scale', expand=True) - if context.scene.bake_prioritize_face: - row = col.row(align=True) - row.separator() - row.prop(context.scene, 'bake_smart_uvmap', expand=True) + row = col.row(align=True) + row.separator() + row.label(text="Overlap fix:") + row.prop(context.scene, 'bake_uv_overlap_correction', expand=True) #row = col.row(align=True) #row.prop(context.scene, 'bake_simplify_armature', expand=True) row = col.row(align=True) From e68e3a373a646fb2f2f198a09d1f2b7b767fdba2 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 3 Nov 2020 19:35:54 -0800 Subject: [PATCH 29/64] More graceful handling of no Head bone, message in UI --- tools/bake.py | 2 +- ui/bake.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/bake.py b/tools/bake.py index b986007a..3cff1fb5 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -434,7 +434,7 @@ def perform_bake(self, context): Common.switch('OBJECT') # Select all islands belonging to 'Head' and children and enlarge them - if prioritize_face: + if prioritize_face and "Head" in arm_copy.data.bones: selected_group_names = ["Head"] selected_group_names.extend([bone.name for bone in arm_copy.data.bones["Head"].children_recursive]) print("Prioritizing vertex groups: " + (", ".join(selected_group_names))) diff --git a/ui/bake.py b/ui/bake.py index e76c0980..5b691794 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -44,9 +44,14 @@ def draw(self, context): row.separator() row.prop(context.scene, 'bake_prioritize_face', expand=True) if context.scene.bake_prioritize_face: + armature = Common.get_armature() row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_face_scale', expand=True) + if armature is None or "Head" not in armature.data.bones: + row = col.row(align=True) + row.separator() + row.label(text="No \"Head\" bone found!", icon="INFO") row = col.row(align=True) row.separator() row.label(text="Overlap fix:") From a760c53a4391357888fe756f3bfafd355764018c Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 3 Nov 2020 19:52:16 -0800 Subject: [PATCH 30/64] Add warnings when rendering disabled or material isn't using nodes --- tools/bake.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/bake.py b/tools/bake.py index 3cff1fb5..661fc321 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -47,10 +47,14 @@ def execute(self, context): for obj in objects: for slot in obj.material_slots: if slot.material: + if not slot.material.use_nodes or not slot.material.node_tree: + self.report({'ERROR'}, "A material in use isn't using Nodes, fix this in the Shading tab.") + return {'FINISHED'} tree = slot.material.node_tree for node in tree.nodes: if node.type == "BSDF_PRINCIPLED": bsdf_nodes.append(node) + # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) @@ -115,6 +119,9 @@ def execute(self, context): for obj in objects: for slot in obj.material_slots: if slot.material: + if not slot.material.use_nodes or not slot.material.node_tree: + self.report({'ERROR'}, "A material in use isn't using Nodes, fix this in the Shading tab.") + return {'FINISHED'} tree = slot.material.node_tree for node in tree.nodes: if node.type == "BSDF_PRINCIPLED": @@ -320,6 +327,9 @@ def execute(self, context): if context.scene.render.engine != 'CYCLES': self.report({'ERROR'}, "You need to set your render engine to Cycles first!") return {'FINISHED'} + if any([obj.hide_render for obj in Common.get_armature().children]): + self.report({'ERROR'}, "One or more of your armature's meshes have rendering disabled!") + return {'FINISHED'} # TODO: Check if any UV islands are self-overlapping, emit an error # Change decimate settings, run bake, change them back @@ -423,6 +433,7 @@ def perform_bake(self, context): if uv_layer[loop].select: uv_layer[loop].uv.x += 1 + # TODO: cleanup, all editmode_toggle -> common.switch # Select all meshes. Select all UVs. Average islands scale From 09b83eed0f7fb1b92b2724038349559dd11507df Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 3 Nov 2020 20:42:06 -0800 Subject: [PATCH 31/64] Automatically detect the largest source BSDF node and copy its default_values to the preview texture --- tools/bake.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 661fc321..35e4ae74 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -330,7 +330,7 @@ def execute(self, context): if any([obj.hide_render for obj in Common.get_armature().children]): self.report({'ERROR'}, "One or more of your armature's meshes have rendering disabled!") return {'FINISHED'} - # TODO: Check if any UV islands are self-overlapping, emit an error + # TODO: Check if any UV islands are self-overlapping, emit a warning # Change decimate settings, run bake, change them back decimation_mode = context.scene.decimation_mode @@ -404,6 +404,20 @@ def perform_bake(self, context): if modifier.type == "ARMATURE": modifier.object = arm_copy + # Copy default values from the largest diffuse BSDF + objs_size_descending = sorted([obj for obj in collection.all_objects if obj.type == "MESH"], + key=lambda obj: obj.dimensions.x * obj.dimensions.y * obj.dimensions.z, + reverse=True) + def first_bsdf(objs): + for obj in objs_size_descending: + for slot in obj.material_slots: + if slot.material: + tree = slot.material.node_tree + for node in tree.nodes: + if node.type == "BSDF_PRINCIPLED": + return node + bsdf_original = first_bsdf(objs_size_descending) + if generate_uvmap: bpy.ops.object.select_all(action='DESELECT') # Make copies of the currently render-active UV layer, name "CATS UV" @@ -670,7 +684,8 @@ def perform_bake(self, context): # add a normal map and image texture to connect the world texture, if it exists tree = mat.node_tree bsdfnode = next(node for node in tree.nodes if node.type == "BSDF_PRINCIPLED") - bsdfnode.inputs["Specular"].default_value = 0 + for bsdfinput in bsdfnode.inputs: + bsdfinput.default_value = bsdf_original.inputs[bsdfinput.identifier].default_value if pass_normal: normaltexnode = tree.nodes.new("ShaderNodeTexImage") if use_decimation: From 67c33545d1a2b0193867b1f6b2d44289366dd5e8 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 4 Nov 2020 21:37:31 -0800 Subject: [PATCH 32/64] Add 'Animation Weighting' and fix an issue with un-mirror. Animation weighting messes with symmetry so off by default --- extentions.py | 38 +++++++++++++++++ tools/bake.py | 18 +++++--- tools/decimation.py | 102 +++++++++++++++++++++++++++++++++++++++++--- ui/bake.py | 7 +++ ui/decimation.py | 7 +++ 5 files changed, 161 insertions(+), 11 deletions(-) diff --git a/extentions.py b/extentions.py index fa7f4f51..e4e772df 100644 --- a/extentions.py +++ b/extentions.py @@ -167,6 +167,25 @@ def register(): default='SMART' ) + Scene.decimation_animation_weighting = BoolProperty( + name="Animation weighting", + description="Weight decimation based on shape keys and vertex group overlap\n" \ + "Results in better animating topology by trading off overall shape accuracy\n" \ + "Appears to mess with Blender's ability to keep things symmetrical, so YMMV", + default=False + ) + + Scene.decimation_animation_weighting_factor = FloatProperty( + name="Factor", + description="How much influence the animation weighting has on the overall shape", + default=0.1, + min=0, + max=1, + step=0.05, + precision=2, + subtype='FACTOR' + ) + Scene.bake_max_tris = IntProperty( name=t('Scene.max_tris.label'), description=t('Scene.max_tris.desc'), @@ -192,6 +211,25 @@ def register(): default=True ) + Scene.bake_animation_weighting = BoolProperty( + name="Animation weighting", + description="Weight decimation based on shape keys and vertex group overlap\n" \ + "Results in better animating topology by trading off overall shape accuracy\n" \ + "Appears to mess with Blender's ability to keep things symmetrical, so YMMV", + default=False + ) + + Scene.bake_animation_weighting_factor = FloatProperty( + name="Factor", + description="How much influence the animation weighting has on the overall shape", + default=0.1, + min=0, + max=1, + step=0.05, + precision=2, + subtype='FACTOR' + ) + Scene.bake_generate_uvmap = BoolProperty( name='Generate UVMap', description="Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ diff --git a/tools/bake.py b/tools/bake.py index 35e4ae74..ab40bb52 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -337,17 +337,23 @@ def execute(self, context): max_tris = context.scene.max_tris decimate_fingers = context.scene.decimate_fingers decimation_remove_doubles = context.scene.decimation_remove_doubles + decimation_animation_weighting = context.scene.decimation_animation_weighting + decimation_animation_weighting_factor = context.scene.decimation_animation_weighting_factor context.scene.decimation_mode = "SMART" context.scene.max_tris = context.scene.bake_max_tris context.scene.decimate_fingers = False context.scene.decimation_remove_doubles = True + context.scene.decimation_animation_weighting = context.scene.bake_animation_weighting + context.scene.decimation_animation_weighting_factor = context.scene.bake_animation_weighting_factor self.perform_bake(context) context.scene.decimation_mode = decimation_mode context.scene.max_tris = max_tris context.scene.decimate_fingers = decimate_fingers context.scene.decimation_remove_doubles = decimation_remove_doubles + context.scene.decimation_animation_weighting = decimation_animation_weighting + context.scene.decimation_animation_weighting_factor = decimation_animation_weighting_factor return {'FINISHED'} @@ -394,10 +400,6 @@ def perform_bake(self, context): armature = Common.get_armature() arm_copy = self.tree_copy(armature, None, collection) - # Move armature so we can see it - if quick_compare: - arm_copy.location.x += arm_copy.dimensions.x - # Make sure all armature modifiers target the new armature for child in collection.all_objects: for modifier in child.modifiers: @@ -440,12 +442,12 @@ def first_bsdf(objs): elif uv_overlap_correction == "UNMIRROR": # TODO: issue a warning if any source images don't use 'wrap' # Select all faces in +X + print("Un-mirroring source CATS UV data") uv_layer = child.data.uv_layers["CATS UV"].data for poly in child.data.polygons: if poly.center[0] > 0: for loop in poly.loop_indices: - if uv_layer[loop].select: - uv_layer[loop].uv.x += 1 + uv_layer[loop].uv.x += 1 # TODO: cleanup, all editmode_toggle -> common.switch @@ -509,6 +511,10 @@ def first_bsdf(objs): except AttributeError: pass + # Move armature so we can see it + if quick_compare: + arm_copy.location.x += arm_copy.dimensions.x + # Bake diffuse Common.switch('OBJECT') if pass_diffuse: diff --git a/tools/decimation.py b/tools/decimation.py index 7e2bade6..448f4ddd 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -25,6 +25,7 @@ # Edits by: import bpy +import math from . import common as Common from . import armature_bones as Bones @@ -194,6 +195,8 @@ def decimate(self, context): safe_decimation = context.scene.decimation_mode == 'SAFE' smart_decimation = context.scene.decimation_mode == 'SMART' save_fingers = context.scene.decimate_fingers + animation_weighting = context.scene.decimation_animation_weighting + animation_weighting_factor = context.scene.decimation_animation_weighting_factor max_tris = context.scene.max_tris meshes = [] current_tris_count = 0 @@ -210,6 +213,93 @@ def decimate(self, context): Common.remove_doubles(mesh, 0.00001, save_shapes=True) current_tris_count += len(mesh.data.polygons) + if animation_weighting: + for mesh in meshes_obj: + # Weight by multiplied bone weights for every pair of bones. + # This is O(n*m^2) for n verts and m bones, generally runs relatively quickly. + weights = dict() + for vertex in mesh.data.vertices: + v_weights = [group.weight for group in vertex.groups] + v_mults = [] + for idx1, w1 in enumerate(vertex.groups): + for idx2, w2 in enumerate(vertex.groups): + if idx1 != idx2: + # Weight [vgroup * vgroup] for index = + if (w1.group, w2.group) not in weights: + weights[(w1.group, w2.group)] = dict() + weights[(w1.group, w2.group)][vertex.index] = w1.weight * w2.weight + + # Normalize per vertex group pair + normalizedweights = dict() + for pair, weighting in weights.items(): + m_min = 1 + m_max = 0 + for _, weight in weighting.items(): + m_min = min(m_min, weight) + m_max = max(m_max, weight) + + if pair not in normalizedweights: + normalizedweights[pair] = dict() + for v_index, weight in weighting.items(): + try: + normalizedweights[pair][v_index] = (weight - m_min) / (m_max - m_min) + except ZeroDivisionError: + normalizedweights[pair][v_index] = weight + + newweights = dict() + for pair, weighting in normalizedweights.items(): + for v_index, weight in weighting.items(): + try: + newweights[v_index] = max(newweights[v_index], weight) + except KeyError: + newweights[v_index] = weight + + s_weights = dict() + + # Weight by relative shape key movement. This is kind of slow, but not too bad. It's O(n*m) for n verts and m shape keys, + # but shape keys contain every vert (not just the ones they impact) + # For shape key in shape keys: + for key_block in mesh.data.shape_keys.key_blocks[1:]: + basis = mesh.data.shape_keys.key_blocks[0] + s_weights[key_block.name] = dict() + + for idx, vert in enumerate(key_block.data): + s_weights[key_block.name][idx] = math.sqrt(math.pow(basis.data[idx].co[0] - vert.co[0], 2.0) + + math.pow(basis.data[idx].co[1] - vert.co[1], 2.0) + + math.pow(basis.data[idx].co[2] - vert.co[2], 2.0)) + + # normalize min/max vert movement + s_normalizedweights = dict() + for keyname, weighting in s_weights.items(): + m_min = math.inf + m_max = 0 + for _, weight in weighting.items(): + m_min = min(m_min, weight) + m_max = max(m_max, weight) + + if keyname not in s_normalizedweights: + s_normalizedweights[keyname] = dict() + for v_index, weight in weighting.items(): + try: + s_normalizedweights[keyname][v_index] = (weight - m_min) / (m_max - m_min) + except ZeroDivisionError: + s_normalizedweights[keyname][v_index] = weight + + # find max normalized movement over all shape keys + for pair, weighting in s_normalizedweights.items(): + for v_index, weight in weighting.items(): + try: + newweights[v_index] = max(newweights[v_index], weight) + except KeyError: + newweights[v_index] = weight + + # TODO: ignore shape keys which move very little? + context.view_layer.objects.active = mesh + bpy.ops.object.vertex_group_add() + mesh.vertex_groups[-1].name = "CATS Animation" + for idx, weight in newweights.items(): + mesh.vertex_groups[-1].add([idx], weight, "REPLACE") + if save_fingers: for mesh in meshes_obj: if len(mesh.vertex_groups) > 0: @@ -338,6 +428,10 @@ def decimate(self, context): mod = mesh_obj.modifiers.new("Decimate", 'DECIMATE') mod.ratio = decimation mod.use_collapse_triangulate = True + if animation_weighting: + mod.vertex_group = "CATS Animation" + mod.vertex_group_factor = animation_weighting_factor + mod.invert_vertex_group = True Common.apply_modifier(mod) else: Common.switch('EDIT') @@ -358,14 +452,12 @@ def decimate(self, context): Common.switch('EDIT') bpy.ops.mesh.select_all(action="INVERT") - #TODO: If we can create a vertex group with weights roughly equal to 'how likely is this to be animated', - # we can create much better topology by inverse-weighting against it. #TODO: On many meshes, un-subdividing until it's near the target verts and then decimating the rest of the way # results in MUCH better topology. Something to figure out against 2.93 bpy.ops.mesh.decimate(ratio=decimation, - use_vertex_group=False, - vertex_group_factor=1.0, - invert_vertex_group=False, + use_vertex_group=animation_weighting, + vertex_group_factor=animation_weighting_factor, + invert_vertex_group=True, use_symmetry=True, symmetry_axis='X') Common.switch('OBJECT') diff --git a/ui/bake.py b/ui/bake.py index 5b691794..412b5d94 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -37,6 +37,13 @@ def draw(self, context): row = col.row(align=True) row.separator() row.prop(context.scene, 'bake_preserve_seams', expand=True) + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_animation_weighting', expand=True) + if context.scene.bake_animation_weighting: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'bake_animation_weighting_factor', expand=True) row = col.row(align=True) row.prop(context.scene, 'bake_generate_uvmap', expand=True) if context.scene.bake_generate_uvmap: diff --git a/ui/decimation.py b/ui/decimation.py index cf54a024..e6ec5fa6 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -167,6 +167,13 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'decimation_remove_doubles') row = col.row(align=True) + row.prop(context.scene, 'decimation_animation_weighting', expand=True) + if context.scene.decimation_animation_weighting: + row = col.row(align=True) + row.separator() + row.prop(context.scene, 'decimation_animation_weighting_factor', expand=True) + col.separator() + row = col.row(align=True) row.operator(Decimation.AutoDecimatePresetGood.bl_idname) row.operator(Decimation.AutoDecimatePresetExcellent.bl_idname) row.operator(Decimation.AutoDecimatePresetQuest.bl_idname) From 31f60491ea601458b458d0cf71ea476595cbaf7e Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 4 Nov 2020 21:54:41 -0800 Subject: [PATCH 33/64] Resolve issues with mirroring --- tools/bake.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index ab40bb52..8a07f7b7 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -511,10 +511,6 @@ def first_bsdf(objs): except AttributeError: pass - # Move armature so we can see it - if quick_compare: - arm_copy.location.x += arm_copy.dimensions.x - # Bake diffuse Common.switch('OBJECT') if pass_diffuse: @@ -804,6 +800,10 @@ def first_bsdf(objs): emittexnode.location.y -= 150 tree.links.new(bsdfnode.inputs["Emission"], emittexnode.outputs["Color"]) + # Move armature so we can see it + if quick_compare: + arm_copy.location.x += arm_copy.dimensions.x + # TODO: Optionally cleanup bones as a last step # Select all bones which don't fuzzy match a whitelist (Chest, Head, etc) and do Merge Weights to parent on them # For now, just add a note saying you should merge bones manually From 4124fad0957c8c02a20208606ba44b9f9c3b548c Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 4 Nov 2020 21:57:23 -0800 Subject: [PATCH 34/64] Update wording --- extentions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extentions.py b/extentions.py index e4e772df..3efac0e3 100644 --- a/extentions.py +++ b/extentions.py @@ -171,7 +171,7 @@ def register(): name="Animation weighting", description="Weight decimation based on shape keys and vertex group overlap\n" \ "Results in better animating topology by trading off overall shape accuracy\n" \ - "Appears to mess with Blender's ability to keep things symmetrical, so YMMV", + "Use if your elbows/joints end up with bad topology" default=False ) @@ -214,8 +214,8 @@ def register(): Scene.bake_animation_weighting = BoolProperty( name="Animation weighting", description="Weight decimation based on shape keys and vertex group overlap\n" \ - "Results in better animating topology by trading off overall shape accuracy\n" \ - "Appears to mess with Blender's ability to keep things symmetrical, so YMMV", + "Results in better animating topology by trading off overall shape accuracy.\n" \ + "Use if your elbows/joints end up with bad topology" default=False ) From 24ffef7be97c466d04cd23d15fdd744a67985b1d Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 4 Nov 2020 22:07:35 -0800 Subject: [PATCH 35/64] Missing comma :v --- extentions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extentions.py b/extentions.py index 3efac0e3..4b418d22 100644 --- a/extentions.py +++ b/extentions.py @@ -171,7 +171,7 @@ def register(): name="Animation weighting", description="Weight decimation based on shape keys and vertex group overlap\n" \ "Results in better animating topology by trading off overall shape accuracy\n" \ - "Use if your elbows/joints end up with bad topology" + "Use if your elbows/joints end up with bad topology", default=False ) @@ -215,7 +215,7 @@ def register(): name="Animation weighting", description="Weight decimation based on shape keys and vertex group overlap\n" \ "Results in better animating topology by trading off overall shape accuracy.\n" \ - "Use if your elbows/joints end up with bad topology" + "Use if your elbows/joints end up with bad topology", default=False ) From f8765fbc78dd7fea639e6627b1ca7705610d9fb8 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 4 Nov 2020 22:27:13 -0800 Subject: [PATCH 36/64] Adjust defaults for animation weighting, 0.25 seems to be the sweet spot --- extentions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extentions.py b/extentions.py index 4b418d22..f696f3c8 100644 --- a/extentions.py +++ b/extentions.py @@ -178,7 +178,7 @@ def register(): Scene.decimation_animation_weighting_factor = FloatProperty( name="Factor", description="How much influence the animation weighting has on the overall shape", - default=0.1, + default=0.25, min=0, max=1, step=0.05, @@ -222,7 +222,7 @@ def register(): Scene.bake_animation_weighting_factor = FloatProperty( name="Factor", description="How much influence the animation weighting has on the overall shape", - default=0.1, + default=0.25, min=0, max=1, step=0.05, From ebd8b242c7e27933295f176933c5f4f27f8a63b5 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 6 Nov 2020 23:02:47 -0800 Subject: [PATCH 37/64] Fix roughness baking, emit black by default --- tools/bake.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 8a07f7b7..79e134d4 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -228,7 +228,7 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba image.generated_width=bake_size[0] image.generated_height=bake_size[1] image.scale(bake_size[0], bake_size[1]) - if bake_type == 'NORMAL': + if bake_type == 'NORMAL' or bake_type == 'ROUGHNESS': image.colorspace_settings.name = 'Non-Color' if bake_name == 'diffuse' or bake_name == 'metallic': # For packing smoothness to alpha image.alpha_mode = 'CHANNEL_PACKED' @@ -421,6 +421,8 @@ def first_bsdf(objs): bsdf_original = first_bsdf(objs_size_descending) if generate_uvmap: + # TODO: automagically detect if meshes even have a uv map, smart UV project them if not + bpy.ops.object.select_all(action='DESELECT') # Make copies of the currently render-active UV layer, name "CATS UV" for child in collection.all_objects: @@ -511,6 +513,8 @@ def first_bsdf(objs): except AttributeError: pass + # TODO: many things don't bake well with alpha != 1, turn it to that before starting? + # Bake diffuse Common.switch('OBJECT') if pass_diffuse: @@ -539,7 +543,7 @@ def first_bsdf(objs): # bake emit if pass_emit: self.bake_pass(context, "emit", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0,0,0,1.0], True, int(margin * resolution / 2)) # advanced: bake alpha from bsdf output if pass_alpha: From d83f278cdc6e2ff495b411507643cb85a737b663 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Sun, 8 Nov 2020 17:07:50 -0800 Subject: [PATCH 38/64] Fix issues with metallic/alpha baking --- tools/bake.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/bake.py b/tools/bake.py index 79e134d4..a6ca92e1 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -80,6 +80,7 @@ def execute(self, context): context.scene.bake_normal_apply_trans = len(objects) > 1 # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? + # TODO: If mesh is manifold and non-intersecting, turn on AO. Otherwise, leave it alone # diffuse ao: off if desktop context.scene.bake_pass_questdiffuse = False @@ -177,10 +178,15 @@ class BakeButton(bpy.types.Operator): # Only works between equal data types. def swap_links(self, objects, input1, input2): + already_swapped = set() # Find all Principled BSDF. Flip values for input1 and input2 (default_value and connection) for obj in objects: for slot in obj.material_slots: if slot.material: + if slot.material.name in already_swapped: + continue + else: + already_swapped.add(slot.material.name) tree = slot.material.node_tree for node in tree.nodes: if node.type == "BSDF_PRINCIPLED": @@ -515,6 +521,8 @@ def first_bsdf(objs): # TODO: many things don't bake well with alpha != 1, turn it to that before starting? + # TODO: Option to apply current shape keys, otherwise normals bake weird + # Bake diffuse Common.switch('OBJECT') if pass_diffuse: @@ -563,7 +571,7 @@ def first_bsdf(objs): # Run the bake pass self.bake_pass(context, "metallic", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0,0,0,1.0], True, int(margin * resolution / 2)) # Revert the changes (re-flip) self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Roughness") From 8d986af606e165cc248feb86a20abef8444ccae9 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Sun, 8 Nov 2020 19:26:19 -0800 Subject: [PATCH 39/64] Improve robustness of alpha and diffuse baking --- tools/bake.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tools/bake.py b/tools/bake.py index a6ca92e1..9fde6fe6 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -211,6 +211,21 @@ def swap_links(self, objects, input1, input2): if alpha_input: tree.links.new(node.inputs[input2], alpha_input) + def set_values(self, objects, input_name, input_value): + already_set = set() + # Find all Principled BSDF. Flip values for input1 and input2 (default_value and connection) + for obj in objects: + for slot in obj.material_slots: + if slot.material: + if slot.material.name in already_set: + continue + else: + already_set.add(slot.material.name) + tree = slot.material.node_tree + for node in tree.nodes: + if node.type == "BSDF_PRINCIPLED": + node.inputs[input_name].default_value = input_value + # "Bake pass" function. Run a single bake to ".png" against all selected objects. def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, bake_size, bake_samples, bake_ray_distance, background_color, clear, bake_margin, bake_active=None, bake_multires=False, normal_space='TANGENT'): bpy.ops.object.select_all(action='DESELECT') @@ -528,6 +543,7 @@ def first_bsdf(objs): if pass_diffuse: # Metallic can cause issues baking diffuse, so we put it somewhere typically unused self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic Rotation") + self.set_values([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", 0.0) self.bake_pass(context, "diffuse", "DIFFUSE", {"COLOR"}, [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) @@ -555,13 +571,18 @@ def first_bsdf(objs): # advanced: bake alpha from bsdf output if pass_alpha: + # when baking alpha as roughness, the -real- alpha needs to be set to 1 to avoid issues + # this will clobber whatever's in Anisotropic Rotation! self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Anisotropic Rotation") + self.set_values([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", 1.0) # Run the bake pass (bake roughness) self.bake_pass(context, "alpha", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) # Revert the changes (re-flip) + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Anisotropic Rotation") self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") # advanced: bake metallic from last bsdf output From f0682af5fb63109e3598dda1f518511f54e4f852 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Mon, 9 Nov 2020 00:28:19 -0800 Subject: [PATCH 40/64] Fix baking without decimating --- tools/bake.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/bake.py b/tools/bake.py index 9fde6fe6..6541feae 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -571,17 +571,21 @@ def first_bsdf(objs): # advanced: bake alpha from bsdf output if pass_alpha: + #TODO: still causes issues on chakdal # when baking alpha as roughness, the -real- alpha needs to be set to 1 to avoid issues # this will clobber whatever's in Anisotropic Rotation! self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Anisotropic Rotation") self.set_values([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", 1.0) + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic") + self.set_values([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", 0) # Run the bake pass (bake roughness) self.bake_pass(context, "alpha", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [1,1,1,1.0], True, int(margin * resolution / 2)) # Revert the changes (re-flip) + self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic") self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Anisotropic Rotation") self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") @@ -703,6 +707,10 @@ def first_bsdf(objs): # Decimate. If 'preserve seams' is selected, forcibly preserve seams (seams from islands, deselect seams) bpy.ops.cats_decimation.auto_decimate(armature_name=arm_copy.name, preserve_seams=preserve_seams, seperate_materials=False) + # join meshes here if we didn't decimate + if not use_decimation: + Common.join_meshes(armature_name=arm_copy.name, repair_shape_keys=False) + # Remove all other materials while len(context.object.material_slots) > 0: context.object.active_material_index = 0 #select the top material From 392324af8445e78bbc04196ad7a3b4f5e411ab24 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Mon, 9 Nov 2020 21:21:20 -0800 Subject: [PATCH 41/64] More accurate decimation, autodetect if bake decimation is needed --- tools/bake.py | 23 ++++++++++++++++------- tools/common.py | 10 ++++++++++ tools/decimation.py | 4 ++-- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 6541feae..1c28eaec 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -39,7 +39,6 @@ class BakePresetDesktop(bpy.types.Operator): def execute(self, context): context.scene.bake_max_tris = 32000 - context.scene.bake_use_decimation = True context.scene.bake_resolution = 2048 # Autodetect passes based on BSDF node inputs bsdf_nodes = [] @@ -55,6 +54,11 @@ def execute(self, context): if node.type == "BSDF_PRINCIPLED": bsdf_nodes.append(node) + # Decimate if we're over the limit + total_tricount = sum([Common.get_tricount(obj) for obj in objects]) + context.scene.bake_use_decimation = total_tricount > 32000 + context.scene.bake_uv_overlap_correction = 'UNMIRROR' if context.scene.bake_use_decimation else "NONE" + # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) @@ -74,7 +78,8 @@ def execute(self, context): or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) # Normal: on if any normals connected or if decimating... so, always on for this preset - context.scene.bake_pass_normal = True + context.scene.bake_pass_normal = (context.scene.bake_use_decimation + or any([node.inputs["Normal"].is_linked for node in bsdf_nodes])) # Apply transforms: on if more than one mesh TODO: with different materials? context.scene.bake_normal_apply_trans = len(objects) > 1 @@ -112,7 +117,6 @@ class BakePresetQuest(bpy.types.Operator): def execute(self, context): context.scene.bake_max_tris = 5000 - context.scene.bake_use_decimation = True context.scene.bake_resolution = 1024 # Autodetect passes based on BSDF node inputs bsdf_nodes = [] @@ -127,6 +131,12 @@ def execute(self, context): for node in tree.nodes: if node.type == "BSDF_PRINCIPLED": bsdf_nodes.append(node) + + # Decimate if we're over the limit + total_tricount = sum([Common.get_tricount(obj) for obj in objects]) + context.scene.bake_use_decimation = total_tricount > 5000 + context.scene.bake_uv_overlap_correction = 'UNMIRROR' if context.scene.bake_use_decimation else "NONE" + # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) @@ -144,8 +154,9 @@ def execute(self, context): context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) - # Normal: on if any normals connected or if decimating... so, always on for this preset - context.scene.bake_pass_normal = True + # Normal: on if any normals connected or if decimating + context.scene.bake_pass_normal = (context.scene.bake_use_decimation + or any([node.inputs["Normal"].is_linked for node in bsdf_nodes])) # Apply transforms: on if more than one mesh TODO: with different materials? context.scene.bake_normal_apply_trans = len(objects) > 1 @@ -534,8 +545,6 @@ def first_bsdf(objs): except AttributeError: pass - # TODO: many things don't bake well with alpha != 1, turn it to that before starting? - # TODO: Option to apply current shape keys, otherwise normals bake weird # Bake diffuse diff --git a/tools/common.py b/tools/common.py index 3d929a4f..32747422 100644 --- a/tools/common.py +++ b/tools/common.py @@ -26,6 +26,7 @@ import re import os import bpy +import bmesh import time from math import degrees @@ -1713,6 +1714,15 @@ def remove_doubles(mesh, threshold, save_shapes=True): return pre_tris - len(mesh.data.polygons) +def get_tricount(obj): + # Triangulates with Bmesh to avoid messing with the original geometry + bmesh_mesh = bmesh.new() + bmesh_mesh.from_mesh(obj.data) + + bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces[:]) + return len(bmesh_mesh.faces) + + def get_bone_orientations(armature): x_cord = 0 y_cord = 1 diff --git a/tools/decimation.py b/tools/decimation.py index 448f4ddd..7417db91 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -211,7 +211,7 @@ def decimate(self, context): Common.switch('OBJECT') if context.scene.decimation_remove_doubles: Common.remove_doubles(mesh, 0.00001, save_shapes=True) - current_tris_count += len(mesh.data.polygons) + current_tris_count += Common.get_tricount(mesh.data.polygons) if animation_weighting: for mesh in meshes_obj: @@ -326,7 +326,7 @@ def decimate(self, context): for mesh in meshes_obj: Common.set_active(mesh) - tris = len(mesh.data.polygons) + tris = Common.get_tricount(mesh) if custom_decimation and mesh.name in ignore_meshes: Common.unselect_all() From 5bdbb8a9135577820586d42e2870e9da968ecb4d Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 10 Nov 2020 21:04:49 -0800 Subject: [PATCH 42/64] Export relevant images and model to an output directory during bake --- tools/bake.py | 53 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 1c28eaec..0b0beca3 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -24,6 +24,7 @@ # Edits by: Feilen import bpy +import os from . import common as Common from .register import register_wrap @@ -255,7 +256,8 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, generated_type="BLANK", alpha=True) image = bpy.data.images["SCRIPT_" + bake_name + ".png"] - image.alpha_mode = "NONE" + image.filepath = bpy.path.abspath("//CATS Bake/" + "SCRIPT_" + bake_name + ".png") + image.alpha_mode = "STRAIGHT" image.generated_color = background_color image.generated_width=bake_size[0] image.generated_height=bake_size[1] @@ -580,7 +582,6 @@ def first_bsdf(objs): # advanced: bake alpha from bsdf output if pass_alpha: - #TODO: still causes issues on chakdal # when baking alpha as roughness, the -real- alpha needs to be set to 1 to avoid issues # this will clobber whatever's in Anisotropic Rotation! self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Alpha", "Roughness") @@ -850,12 +851,52 @@ def first_bsdf(objs): emittexnode.location.y -= 150 tree.links.new(bsdfnode.inputs["Emission"], emittexnode.outputs["Color"]) - # Move armature so we can see it - if quick_compare: - arm_copy.location.x += arm_copy.dimensions.x - # TODO: Optionally cleanup bones as a last step # Select all bones which don't fuzzy match a whitelist (Chest, Head, etc) and do Merge Weights to parent on them # For now, just add a note saying you should merge bones manually + # Export the model to the bake dir + bpy.ops.object.select_all(action='DESELECT') + for obj in collection.all_objects: + obj.select_set(True) + if obj.type == "MESH": + obj.name = "Body" + elif obj.type == "ARMATURE": + obj.name = "Armature" + bpy.ops.export_scene.fbx(filepath=bpy.path.abspath("//CATS Bake/Bake.fbx"), check_existing=False, filter_glob='*.fbx', + use_selection=True, + use_active_collection=False, global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', + bake_space_transform=False, object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'}, + use_mesh_modifiers=True, use_mesh_modifiers_render=True, mesh_smooth_type='OFF', use_subsurf=False, + use_mesh_edges=False, use_tspace=False, use_custom_props=False, add_leaf_bones=True, primary_bone_axis='Y', + secondary_bone_axis='X', use_armature_deform_only=False, armature_nodetype='NULL', bake_anim=True, + bake_anim_use_all_bones=True, bake_anim_use_nla_strips=True, bake_anim_use_all_actions=True, + bake_anim_force_startend_keying=True, bake_anim_step=1.0, bake_anim_simplify_factor=1.0, path_mode='AUTO', + embed_textures=False, batch_mode='OFF', use_batch_own_dir=True, use_metadata=True, + axis_forward='-Z', axis_up='Y') + + # Try to only output what you'll end up importing into unity. + if pass_diffuse: + bpy.data.images["SCRIPT_diffuse.png"].save() + if pass_normal: + bpy.data.images["SCRIPT_normal.png"].save() + if pass_smoothness and (diffuse_alpha_pack != "SMOOTHNESS") and (metallic_alpha_pack != "SMOOTHNESS"): + bpy.data.images["SCRIPT_smoothness.png"].save() + if pass_ao: + bpy.data.images["SCRIPT_ao.png"].save() + if pass_diffuse and pass_ao and pass_questdiffuse: + bpy.data.images["SCRIPT_questdiffuse.png"].save() + if pass_emit: + bpy.data.images["SCRIPT_emit.png"].save() + if pass_alpha and (diffuse_alpha_pack != "TRANSPARENCY"): + bpy.data.images["SCRIPT_alpha.png"].save() + if pass_metallic: + bpy.data.images["SCRIPT_metallic.png"].save() + + self.report({'INFO'}, "Success! Textures and model saved to \'CATS Bake\' folder next to your .blend file.") + + # Move armature so we can see it + if quick_compare: + arm_copy.location.x += arm_copy.dimensions.x + print("BAKE COMPLETE!") From 200e297cff35f8c933169307572c94971d85f0e2 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 10 Nov 2020 22:56:30 -0800 Subject: [PATCH 43/64] Move strings to en_US.py --- extentions.py | 171 +++++++++++--------------------- tools/bake.py | 221 +++++++++++++++--------------------------- translations/en_US.py | 118 ++++++++++++++++++++++ ui/bake.py | 20 ++-- ui/decimation.py | 2 +- 5 files changed, 268 insertions(+), 264 deletions(-) diff --git a/extentions.py b/extentions.py index f696f3c8..6e443281 100644 --- a/extentions.py +++ b/extentions.py @@ -168,16 +168,14 @@ def register(): ) Scene.decimation_animation_weighting = BoolProperty( - name="Animation weighting", - description="Weight decimation based on shape keys and vertex group overlap\n" \ - "Results in better animating topology by trading off overall shape accuracy\n" \ - "Use if your elbows/joints end up with bad topology", + name=t('Scene.decimation_animation_weighting.label'), + description=t('Scene.decimation_animation_weighting.desc'), default=False ) Scene.decimation_animation_weighting_factor = FloatProperty( - name="Factor", - description="How much influence the animation weighting has on the overall shape", + name=t('Scene.decimation_animation_weighting_factor.label'), + description=t('Scene.decimation_animation_weighting_factor.desc'), default=0.25, min=0, max=1, @@ -196,70 +194,45 @@ def register(): # Bake Scene.bake_resolution = IntProperty( - name="Resolution", - description="Output resolution for the textures.\n" \ - "- 2048 is typical for desktop use.\n" \ - "- 1024 is reccomended for the Quest", + name=t('Scene.bake_resolution.label'), + description=t('Scene.bake_resolution.desc'), default=2048, min=128, max=4096 ) Scene.bake_use_decimation = BoolProperty( - name='Decimate', - description='Reduce polycount before baking, then use Normal maps to restore detail', + name=t('Scene.bake_use_decimation.label'), + description=t('Scene.bake_use_decimation.desc'), default=True ) - Scene.bake_animation_weighting = BoolProperty( - name="Animation weighting", - description="Weight decimation based on shape keys and vertex group overlap\n" \ - "Results in better animating topology by trading off overall shape accuracy.\n" \ - "Use if your elbows/joints end up with bad topology", - default=False - ) - - Scene.bake_animation_weighting_factor = FloatProperty( - name="Factor", - description="How much influence the animation weighting has on the overall shape", - default=0.25, - min=0, - max=1, - step=0.05, - precision=2, - subtype='FACTOR' - ) - Scene.bake_generate_uvmap = BoolProperty( - name='Generate UVMap', - description="Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ - "Only disable if your UVs are non-overlapping already.\n" \ - "This will leave any map named \"Detail Map\" alone.\n" \ - "Uses UVPackMaster where available for more efficient UVs, make sure the window is showing", - + name=t('Scene.bake_generate_uvmap.label'), + description=t('Scene.bake_generate_uvmap.desc'), default=True ) Scene.bake_uv_overlap_correction = EnumProperty( - name="Overlap correction", - description="Method used to prevent overlaps in UVMap", + name=t('Scene.bake_uv_overlap_correction.label'), + description=t('Scene.bake_uv_overlap_correction.desc'), items=[ - ("NONE", "None", "Leave islands as they are. Use if islands don't self-intersect at all"), - ("UNMIRROR", "Unmirror", "Move all face islands with positive X values over one to un-pin mirrored UVs. Solves most UV pinning issues."), - ("REPROJECT", "Reproject", "Use blender's Smart UV Project to come up with an entirely new island layout. Tends to reduce quality."), + ("NONE", t("Scene.bake_uv_overlap_correction.none.label"), t("Scene.bake_uv_overlap_correction.none.desc")), + ("UNMIRROR", t("Scene.bake_uv_overlap_correction.unmirror.label"), t("Scene.bake_uv_overlap_correction.unmirror.desc")), + ("REPROJECT", t("Scene.bake_uv_overlap_correction.reproject.label"), t("Scene.bake_uv_overlap_correction.reproject.desc")), ], default="UNMIRROR" ) Scene.bake_prioritize_face = BoolProperty( - name='Prioritize Head/Eyes', - description='Scale any UV islands attached to the head/eyes by a given factor.', + name=t('Scene.bake_prioritize_face.label'), + description=t('Scene.bake_prioritize_face.desc'), default=True ) Scene.bake_face_scale = FloatProperty( - name="Head/Eyes Scale", - description="How much to scale up the face/eyes portion of the textures.", + name=t('Scene.bake_face_scale.label'), + description=t('Scene.bake_face_scale.desc'), default=3.0, min=0.5, max=4.0, @@ -269,127 +242,101 @@ def register(): ) Scene.bake_quick_compare = BoolProperty( - name='Quick compare', - description='Move output avatar next to existing one to quickly compare', + name=t('Scene.bake_quick_compare.label'), + description=t('Scene.bake_quick_compare.desc'), default=True ) - Scene.bake_simplify_armature = BoolProperty( - name='Simplify armature', - description='Merge weights for all non-humanoid bones into their parents.\n' \ - 'Reccomended for Quest avatars with no special AV3 animations', - default=False - ) - Scene.bake_illuminate_eyes = BoolProperty( - name='Set eyes to full brightness', - description='Relight LeftEye and RightEye to be full brightness.\n' \ - "Without this, the eyes will have the shadow of the surrounding socket baked in,\n" - "which doesn't animate well", + name=t('Scene.bake_illuminate_eyes.label'), + description=t('Scene.bake_illuminate_eyes.desc'), default=True ) Scene.bake_pass_smoothness = BoolProperty( - name='Smoothness', - description='Bakes Roughness and then inverts the values.\n' \ - 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.\n' \ - 'Not neccesary if your mesh has a global roughness value', + name=t('Scene.bake_pass_smoothness.label'), + description=t('Scene.bake_pass_smoothness.desc'), default=True ) Scene.bake_pass_diffuse = BoolProperty( - name='Diffuse (Color)', - description='Bakes diffuse, un-lighted color. Usually you will want this.\n' \ - 'While baking, this temporarily links "Metallic" to "Anisotropic Rotation" as metallic can cause issues.', + name=t('Scene.bake_pass_diffuse.label'), + description=t('Scene.bake_pass_diffuse.desc'), default=True ) Scene.bake_preserve_seams = BoolProperty( - name="Preserve seams", - description='Forces the Decimate operation to preserve vertices making up seams, preventing hard edges along seams.\n' \ - 'May result in less ideal geometry.\n' \ - "Use if you notice ugly edges along your texture seams.", + name=t('Scene.bake_preserve_seams.label'), + description=t('Scene.bake_preserve_seams.desc'), default=False ) Scene.bake_pass_normal = BoolProperty( - name='Normal (Bump)', - description="Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" \ - "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" \ - "that makes the low res output look like the high res input.\n" \ - "Will not work well if you have self-intersecting islands", + name=t('Scene.bake_pass_normal.label'), + description=t('Scene.bake_pass_normal.desc'), default=True ) Scene.bake_normal_apply_trans = BoolProperty( - name='Apply transforms', - description="Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps\n" \ - "Turn this off if applying location causes problems with your model", + name=t('Scene.bake_normal_apply_trans.label'), + description=t('Scene.bake_normal_apply_trans.desc'), default=True ) Scene.bake_pass_ao = BoolProperty( - name='Ambient Occlusion', - description='Bakes Ambient Occlusion, non-projected shadows. Adds a significant amount of detail to your model.\n' \ - 'Reccomended for non-toon style avatars.\n' \ - 'Takes a fairly long time to bake', + name=t('Scene.bake_pass_ao.label'), + description=t('Scene.bake_pass_ao.desc'), default=False ) Scene.bake_pass_questdiffuse = BoolProperty( - name='Quest Diffuse (Color+AO)', - description='Blends the result of the Diffuse and AO bakes to make Quest-compatible shading.', + name=t('Scene.bake_pass_questdiffuse.label'), + description=t('Scene.bake_pass_questdiffuse.desc'), default=True ) Scene.bake_pass_emit = BoolProperty( - name='Emit', - description='Bakes Emit, glowyness', + name=t('Scene.bake_pass_emit.label'), + description=t('Scene.bake_pass_emit.desc'), default=False ) Scene.bake_diffuse_alpha_pack = EnumProperty( - name="Alpha Channel", - description="What to pack to the Diffuse Alpha channel", + name=t('Scene.bake_diffuse_alpha_pack.label'), + description=t('Scene.bake_diffuse_alpha_pack.desc'), items=[ - ("NONE", "None", "No alpha channel"), - ("TRANSPARENCY", "Transparency", "Pack Transparency"), - ("SMOOTHNESS", "Smoothness", "Pack Smoothness. Most efficient if you don't have transparency or metallic textures."), + ("NONE", t("Scene.bake_diffuse_alpha_pack.none.label"), t("Scene.bake_diffuse_alpha_pack.none.desc")), + ("TRANSPARENCY", t("Scene.bake_diffuse_alpha_pack.transparency.label"), t("Scene.bake_diffuse_alpha_pack.transparency.desc")), + ("SMOOTHNESS", t("Scene.bake_diffuse_alpha_pack.smoothness.label"), t("Scene.bake_diffuse_alpha_pack.smoothness.desc")), ], default="NONE" ) Scene.bake_metallic_alpha_pack = EnumProperty( - name="Metallic Alpha Channel", - description="What to pack to the Metallic Alpha channel", + name=t('Scene.bake_metallic_alpha_pack.label'), + description=t('Scene.bake_metallic_alpha_pack.desc'), items=[ - ("NONE", "None", "No alpha channel"), - ("SMOOTHNESS", "Smoothness", "Pack Smoothness. Use this if your Diffuse alpha channel is already populated with Transparency") + ("NONE", t("Scene.bake_metallic_alpha_pack.none.label"), t("Scene.bake_metallic_alpha_pack.none.desc")), + ("SMOOTHNESS", t("Scene.bake_metallic_alpha_pack.smoothness.label"), t("Scene.bake_metallic_alpha_pack.smoothness.desc")) ], default="NONE" ) Scene.bake_pass_alpha = BoolProperty( - name='Transparency', - description='Bakes transparency by connecting the last Principled BSDF Alpha input\n' \ - 'to the Base Color input and baking Diffuse.\n' \ - 'Not a native pass in Blender, results may vary\n' \ - 'Unused if you are baking to Quest', + name=t('Scene.bake_pass_alpha.label'), + description=t('Scene.bake_pass_alpha.desc'), default=False ) Scene.bake_pass_metallic = BoolProperty( - name='Metallic', - description='Bakes metallic by connecting the last Principled BSDF Metallic input\n' \ - 'to the Base Color input and baking Diffuse.\n' \ - 'Not a native pass in Blender, results may vary', + name=t('Scene.bake_pass_metallic.label'), + description=t('Scene.bake_pass_metallic.desc'), default=False ) Scene.bake_questdiffuse_opacity = FloatProperty( - name="AO Opacity", - description="The opacity of the shadows to blend onto the Diffuse map.\n" \ - "This should match the unity slider for AO on the Desktop version.", + name=t('Scene.bake_questdiffuse_opacity.label'), + description=t('Scene.bake_questdiffuse_opacity.desc'), default=0.75, min=0.0, max=1.0, @@ -636,7 +583,7 @@ def register(): # Atlas # Material.add_to_atlas = BoolProperty( - # description='Add this material to the atlas', + # description=t('Add this material to the atlas') # default=False # ) @@ -649,7 +596,7 @@ def register(): # ) # Scene.clear_materials = BoolProperty( - # description='Clear materials checkbox', + # description=t('Clear materials checkbox') # default=True # ) @@ -704,7 +651,7 @@ def register(): ) # Scene.disable_vrchat_features = BoolProperty( - # name='Disable VRChat Only Features', + # name=t('Disable VRChat Only Features') # description='This will disable features which are solely used for VRChat.' # '\nThe following will be disabled:' # '\n- Eye Tracking' @@ -715,8 +662,8 @@ def register(): # Copy Protection - obsolete # Scene.protection_mode = EnumProperty( - # name="Randomization Level", - # description="Randomization Level", + # name=t("Randomization Level") + # description=t("Randomization Level") # items=[ # ("FULL", "Full", "This will randomize every vertex of your model and it will be completely unusable for thieves.\n" # 'However this method might cause problems with the Outline option from Cubed shader.\n' diff --git a/tools/bake.py b/tools/bake.py index 0b0beca3..ea4e4a60 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -30,162 +30,101 @@ from .register import register_wrap from ..translations import t +def autodetect_passes(context, tricount, is_desktop): + context.scene.bake_max_tris = tricount + context.scene.bake_resolution = 2048 if is_desktop else 1024 + # Autodetect passes based on BSDF node inputs + bsdf_nodes = [] + objects = Common.get_meshes_objects() + for obj in objects: + for slot in obj.material_slots: + if slot.material: + if not slot.material.use_nodes or not slot.material.node_tree: + self.report({'ERROR'}, t('cats_bake.warn_missing_nodes')) + return {'FINISHED'} + tree = slot.material.node_tree + for node in tree.nodes: + if node.type == "BSDF_PRINCIPLED": + bsdf_nodes.append(node) + + # Decimate if we're over the limit + total_tricount = sum([Common.get_tricount(obj) for obj in objects]) + context.scene.bake_use_decimation = total_tricount > tricount + context.scene.bake_uv_overlap_correction = 'UNMIRROR' if context.scene.bake_use_decimation else "NONE" + + # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf + context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) + # Smoothness: similar to diffuse + context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) + # Emit: similar to diffuse + context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) + + # Transparency: similar to diffuse + context.scene.bake_pass_alpha = is_desktop and (any([node.inputs["Alpha"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Alpha"].default_value for node in bsdf_nodes])) > 1) + + # Metallic: similar to diffuse + context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) + or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) + + # Normal: on if any normals connected or if decimating... so, always on for this preset + context.scene.bake_pass_normal = (context.scene.bake_use_decimation + or any([node.inputs["Normal"].is_linked for node in bsdf_nodes])) + + # Apply transforms: on if more than one mesh TODO: with different materials? + context.scene.bake_normal_apply_trans = len(objects) > 1 + + # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? + # TODO: If mesh is manifold and non-intersecting, turn on AO. Otherwise, leave it alone + # diffuse ao: off if desktop + context.scene.bake_pass_questdiffuse = not is_desktop + + # alpha packs: arrange for maximum efficiency. + # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so + context.scene.bake_diffuse_alpha_pack = "NONE" + context.scene.bake_metallic_alpha_pack = "NONE" + # If 'smoothness' and 'transparency', we need to force metallic to bake so we can pack to it. + if context.scene.bake_pass_smoothness and context.scene.bake_pass_alpha: + context.scene.bake_pass_metallic = True + # If we have transparency, it needs to go in diffuse alpha + if context.scene.bake_pass_alpha: + context.scene.bake_diffuse_alpha_pack = "TRANSPARENCY" + # Smoothness to diffuse is only the most efficient when we don't have metallic or alpha + if context.scene.bake_pass_smoothness and not context.scene.bake_pass_metallic and not context.scene.bake_pass_alpha: + context.scene.bake_diffuse_alpha_pack = "SMOOTHNESS" + if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: + context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + @register_wrap class BakePresetDesktop(bpy.types.Operator): bl_idname = 'cats_bake.preset_desktop' - bl_label = "Desktop" - bl_description = "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" \ - "This will try to automatically detect which bake passes are relevant to your model" + bl_label = t('cats_bake.preset_desktop.label') + bl_description = t('cats_bake.preset_desktop.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - context.scene.bake_max_tris = 32000 - context.scene.bake_resolution = 2048 - # Autodetect passes based on BSDF node inputs - bsdf_nodes = [] - objects = Common.get_meshes_objects() - for obj in objects: - for slot in obj.material_slots: - if slot.material: - if not slot.material.use_nodes or not slot.material.node_tree: - self.report({'ERROR'}, "A material in use isn't using Nodes, fix this in the Shading tab.") - return {'FINISHED'} - tree = slot.material.node_tree - for node in tree.nodes: - if node.type == "BSDF_PRINCIPLED": - bsdf_nodes.append(node) - - # Decimate if we're over the limit - total_tricount = sum([Common.get_tricount(obj) for obj in objects]) - context.scene.bake_use_decimation = total_tricount > 32000 - context.scene.bake_uv_overlap_correction = 'UNMIRROR' if context.scene.bake_use_decimation else "NONE" - - # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf - context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) - # Smoothness: similar to diffuse - context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) - # Emit: similar to diffuse - context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) - - # Transparency: similar to diffuse - context.scene.bake_pass_alpha = (any([node.inputs["Alpha"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Alpha"].default_value for node in bsdf_nodes])) > 1) - - # Metallic: similar to diffuse - context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) - - # Normal: on if any normals connected or if decimating... so, always on for this preset - context.scene.bake_pass_normal = (context.scene.bake_use_decimation - or any([node.inputs["Normal"].is_linked for node in bsdf_nodes])) - - # Apply transforms: on if more than one mesh TODO: with different materials? - context.scene.bake_normal_apply_trans = len(objects) > 1 - - # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? - # TODO: If mesh is manifold and non-intersecting, turn on AO. Otherwise, leave it alone - # diffuse ao: off if desktop - context.scene.bake_pass_questdiffuse = False - - # alpha packs: arrange for maximum efficiency. - # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so - context.scene.bake_diffuse_alpha_pack = "NONE" - context.scene.bake_metallic_alpha_pack = "NONE" - # If 'smoothness' and 'transparency', we need to force metallic to bake so we can pack to it. - if context.scene.bake_pass_smoothness and context.scene.bake_pass_alpha: - context.scene.bake_pass_metallic = True - # If we have transparency, it needs to go in diffuse alpha - if context.scene.bake_pass_alpha: - context.scene.bake_diffuse_alpha_pack = "TRANSPARENCY" - # Smoothness to diffuse is only the most efficient when we don't have metallic or alpha - if context.scene.bake_pass_smoothness and not context.scene.bake_pass_metallic and not context.scene.bake_pass_alpha: - context.scene.bake_diffuse_alpha_pack = "SMOOTHNESS" - if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: - context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" - + autodetect_passes(context, 32000, True) return {'FINISHED'} @register_wrap class BakePresetQuest(bpy.types.Operator): bl_idname = 'cats_bake.preset_quest' - bl_label = "Quest" - bl_description = "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" \ - "This will try to automatically detect which bake passes are relevant to your model" + bl_label = t('cats_bake.preset_quest.label') + bl_description = t('cats_bake.preset_quest.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - context.scene.bake_max_tris = 5000 - context.scene.bake_resolution = 1024 - # Autodetect passes based on BSDF node inputs - bsdf_nodes = [] - objects = Common.get_meshes_objects() - for obj in objects: - for slot in obj.material_slots: - if slot.material: - if not slot.material.use_nodes or not slot.material.node_tree: - self.report({'ERROR'}, "A material in use isn't using Nodes, fix this in the Shading tab.") - return {'FINISHED'} - tree = slot.material.node_tree - for node in tree.nodes: - if node.type == "BSDF_PRINCIPLED": - bsdf_nodes.append(node) - - # Decimate if we're over the limit - total_tricount = sum([Common.get_tricount(obj) for obj in objects]) - context.scene.bake_use_decimation = total_tricount > 5000 - context.scene.bake_uv_overlap_correction = 'UNMIRROR' if context.scene.bake_use_decimation else "NONE" - - # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf - context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) - # Smoothness: similar to diffuse - context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) - # Emit: similar to diffuse - context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) - - # Transparency: unsupported on quest. Possibly would make better mip maps if manually configured in Unity to have alpha: None - context.scene.bake_pass_alpha = False - - # Metallic: similar to diffuse - context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Metallic"].default_value for node in bsdf_nodes])) > 1) - - # Normal: on if any normals connected or if decimating - context.scene.bake_pass_normal = (context.scene.bake_use_decimation - or any([node.inputs["Normal"].is_linked for node in bsdf_nodes])) - - # Apply transforms: on if more than one mesh TODO: with different materials? - context.scene.bake_normal_apply_trans = len(objects) > 1 - - # AO: up to user, don't override as part of this. Possibly detect if using a toon shader in the future? - # diffuse ao: on, won't do anything unless ao gets checked - context.scene.bake_pass_questdiffuse = True - - # alpha packs: arrange for maximum efficiency. - # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so - context.scene.bake_diffuse_alpha_pack = "NONE" - context.scene.bake_metallic_alpha_pack = "NONE" - # If 'smoothness', we need to force metallic to bake so we can pack to it. (smoothness source is not configurable) - if context.scene.bake_pass_smoothness: - context.scene.bake_pass_metallic = True - if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: - context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + autodetect_passes(context, 5000, False) return {'FINISHED'} @register_wrap class BakeButton(bpy.types.Operator): bl_idname = 'cats_bake.bake' - bl_label = 'Copy and Bake (SLOW!)' - bl_description = "Perform the bake. Warning, this performs an actual render!\n" \ - "This will create a copy of your avatar to leave the original alone.\n" \ - "Depending on your machine and model, this could take an hour or more.\n" \ - "For each pass, any Value node in your materials labeled bake_ will be\n" \ - "set to 1.0, for more granular customization." + bl_label = t('cats_bake.bake.label') + bl_description = t('cats_bake.bake.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} # Only works between equal data types. @@ -356,13 +295,13 @@ def recurse(ob, parent, depth): def execute(self, context): meshes = Common.get_meshes_objects() if not meshes or len(meshes) == 0: - self.report({'ERROR'}, "No meshes found!") + self.report({'ERROR'}, t('cats_bake.error.no_meshes')) return {'FINISHED'} if context.scene.render.engine != 'CYCLES': - self.report({'ERROR'}, "You need to set your render engine to Cycles first!") + self.report({'ERROR'}, t('cats_bake.error.no_meshes')) return {'FINISHED'} if any([obj.hide_render for obj in Common.get_armature().children]): - self.report({'ERROR'}, "One or more of your armature's meshes have rendering disabled!") + self.report({'ERROR'}, t('cats_bake.error.render_disabled')) return {'FINISHED'} # TODO: Check if any UV islands are self-overlapping, emit a warning @@ -893,7 +832,7 @@ def first_bsdf(objs): if pass_metallic: bpy.data.images["SCRIPT_metallic.png"].save() - self.report({'INFO'}, "Success! Textures and model saved to \'CATS Bake\' folder next to your .blend file.") + self.report({'INFO'}, t('cats_bake.info.success')) # Move armature so we can see it if quick_compare: diff --git a/translations/en_US.py b/translations/en_US.py index 85d947b4..72941a59 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -113,6 +113,7 @@ 'DecimationPanel.preset.quest.label': 'Quest', 'DecimationPanel.preset.quest.description': 'The recommended number of tris for Quest avatars.\n' 'A hard limit will be established in the future that will not be much more than this.', + 'DecimationPanel.warn.notIfBaking': "Not reccomended if baking!", # UI Eye tracking 'EyeTrackingPanel.label': 'Eye Tracking', @@ -156,6 +157,17 @@ 'CopyProtectionPanel.desc2': 'This protection is not 100% safe!', 'CopyProtectionPanel.desc3': 'Before use: Read the documentation!', + # UI Bake + 'BakePanel.autodetectlabel': 'Autodetect:', + 'BakePanel.generaloptionslabel': "General options:", + 'BakePanel.noheadfound': "No \"Head\" bone found!", + 'BakePanel.overlapfixlabel': "Overlap fix:", + 'BakePanel.bakepasseslabel': "Bake passes:", + 'BakePanel.alphalabel': "Alpha:", + 'BakePanel.transparencywarning': "Transparency isn't currently selected!", + 'BakePanel.smoothnesswarning': "Smoothness isn't currently selected!", + 'BakePanel.doublepackwarning': "Smoothness packed in two places!", + # UI Settings & Updates 'UpdaterPanel.label': 'Settings & Updates', 'UpdaterPanel.name': 'Settings:', @@ -984,6 +996,12 @@ 'Scene.decimation_remove_doubles.label': 'Remove Doubles', 'Scene.decimation_remove_doubles.desc': 'Uncheck this if you got issues with with this checked', + 'Scene.decimation_animation_weighting.label': "Animation weighting", + 'Scene.decimation_animation_weighting.desc': "Weight decimation based on shape keys and vertex group overlap\n" \ + "Results in better animating topology by trading off overall shape accuracy\n" \ + "Use if your elbows/joints end up with bad topology", + 'Scene.decimation_animation_weighting_factor.label': "Factor", + 'Scene.decimation_animation_weighting_factor.desc': "How much influence the animation weighting has on the overall shape", 'Scene.max_tris.label': 'Tris', 'Scene.max_tris.desc': 'The target amount of tris after decimation', @@ -1111,6 +1129,106 @@ 'Scene.debug_translations.label': 'Debug Google Translations', 'Scene.debug_translations.desc': 'Tests the Google Translations and prints the Google response in case of error', + # Bake + 'Scene.bake_resolution.label': "Resolution", + 'Scene.bake_resolution.desc': "Output resolution for the textures.\n" \ + "- 2048 is typical for desktop use.\n" \ + "- 1024 is reccomended for the Quest", + 'Scene.bake_use_decimation.label': 'Decimate', + 'Scene.bake_use_decimation.desc': 'Reduce polycount before baking, then use Normal maps to restore detail', + 'Scene.bake_generate_uvmap.label': 'Generate UVMap', + 'Scene.bake_generate_uvmap.desc': "Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ + "Only disable if your UVs are non-overlapping already.\n" \ + "This will leave any map named \"Detail Map\" alone.\n" \ + "Uses UVPackMaster where available for more efficient UVs, make sure the window is showing", + 'Scene.bake_uv_overlap_correction.label': "Overlap correction", + 'Scene.bake_uv_overlap_correction.desc': "Method used to prevent overlaps in UVMap", + 'Scene.bake_prioritize_face.label':'Prioritize Head/Eyes', + 'Scene.bake_prioritize_face.desc': 'Scale any UV islands attached to the head/eyes by a given factor.', + 'Scene.bake_face_scale.label': "Head/Eyes Scale", + 'Scene.bake_face_scale.desc': "How much to scale up the face/eyes portion of the textures.", + 'Scene.bake_quick_compare.label': 'Quick compare', + 'Scene.bake_quick_compare.desc': 'Move output avatar next to existing one to quickly compare', + 'Scene.bake_illuminate_eyes.label': 'Set eyes to full brightness', + 'Scene.bake_illuminate_eyes.desc': 'Relight LeftEye and RightEye to be full brightness.\n' \ + "Without this, the eyes will have the shadow of the surrounding socket baked in,\n" + "which doesn't animate well", + 'Scene.bake_pass_smoothness.label': 'Smoothness', + 'Scene.bake_pass_smoothness.desc': 'Bakes Roughness and then inverts the values.\n' \ + 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.\n' \ + 'Not neccesary if your mesh has a global roughness value', + 'Scene.bake_pass_diffuse.label': 'Diffuse (Color)', + 'Scene.bake_pass_diffuse.desc': 'Bakes diffuse, un-lighted color. Usually you will want this.\n' \ + 'While baking, this temporarily links "Metallic" to "Anisotropic Rotation" as metallic can cause issues.', + 'Scene.bake_preserve_seams.label': "Preserve seams", + 'Scene.bake_preserve_seams.desc': 'Forces the Decimate operation to preserve vertices making up seams, preventing hard edges along seams.\n' \ + 'May result in less ideal geometry.\n' \ + "Use if you notice ugly edges along your texture seams.", + 'Scene.bake_pass_normal.label': 'Normal (Bump)', + 'Scene.bake_pass_normal.desc': "Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" \ + "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" \ + "that makes the low res output look like the high res input.\n" \ + "Will not work well if you have self-intersecting islands", + 'Scene.bake_normal_apply_trans.label': 'Apply transforms', + 'Scene.bake_normal_apply_trans.desc': "Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps\n" \ + "Turn this off if applying location causes problems with your model", + 'Scene.bake_pass_ao.label': 'Ambient Occlusion', + 'Scene.bake_pass_ao.desc': 'Bakes Ambient Occlusion, non-projected shadows. Adds a significant amount of detail to your model.\n' \ + 'Reccomended for non-toon style avatars.\n' \ + 'Takes a fairly long time to bake', + 'Scene.bake_pass_questdiffuse.label': 'Quest Diffuse (Color+AO)', + 'Scene.bake_pass_questdiffuse.desc': 'Blends the result of the Diffuse and AO bakes to make Quest-compatible shading.', + 'Scene.bake_pass_emit.label': 'Emit', + 'Scene.bake_pass_emit.desc': 'Bakes Emit, glowyness', + 'Scene.bake_diffuse_alpha_pack.label': "Alpha Channel", + 'Scene.bake_diffuse_alpha_pack.desc': "What to pack to the Diffuse Alpha channel", + 'Scene.bake_metallic_alpha_pack.label': "Metallic Alpha Channel", + 'Scene.bake_metallic_alpha_pack.desc': "What to pack to the Metallic Alpha channel", + 'Scene.bake_pass_alpha.label': 'Transparency', + 'Scene.bake_pass_alpha.desc': 'Bakes transparency by connecting the last Principled BSDF Alpha input\n' \ + 'to the Base Color input and baking Diffuse.\n' \ + 'Not a native pass in Blender, results may vary\n' \ + 'Unused if you are baking to Quest', + 'Scene.bake_pass_metallic.label': 'Metallic', + 'Scene.bake_pass_metallic.desc': 'Bakes metallic by connecting the last Principled BSDF Metallic input\n' \ + 'to the Base Color input and baking Diffuse.\n' \ + 'Not a native pass in Blender, results may vary', + 'Scene.bake_questdiffuse_opacity.label': "AO Opacity", + 'Scene.bake_questdiffuse_opacity.desc': "The opacity of the shadows to blend onto the Diffuse map.\n" \ + "This should match the unity slider for AO on the Desktop version.", + + "Scene.bake_uv_overlap_correction.none.label": "None", + "Scene.bake_uv_overlap_correction.none.desc": "Leave islands as they are. Use if islands don't self-intersect at all", + "Scene.bake_uv_overlap_correction.unmirror.label": "Unmirror", + "Scene.bake_uv_overlap_correction.unmirror.desc": "Move all face islands with positive X values over one to un-pin mirrored UVs. Solves most UV pinning issues.", + "Scene.bake_uv_overlap_correction.reproject.label": "Reproject", + "Scene.bake_uv_overlap_correction.reproject.desc": "Use blender's Smart UV Project to come up with an entirely new island layout. Tends to reduce quality.", + + "Scene.bake_diffuse_alpha_pack.none.label": "None", + "Scene.bake_diffuse_alpha_pack.none.desc": "No alpha channel", + "Scene.bake_diffuse_alpha_pack.transparency.label": "Transparency", + "Scene.bake_diffuse_alpha_pack.transparency.desc": "Pack Transparency", + "Scene.bake_diffuse_alpha_pack.smoothness.label": "Smoothness", + "Scene.bake_diffuse_alpha_pack.smoothness.desc": "Pack Smoothness. Most efficient if you don't have transparency or metallic textures.", + + "cats_bake.warn_missing_nodes": "A material in use isn't using Nodes, fix this in the Shading tab.", + "cats_bake.preset_desktop.label": "Desktop", + "cats_bake.preset_desktop.desc": "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" \ + "This will try to automatically detect which bake passes are relevant to your model", + "cats_bake.preset_quest.label": 'Quest', + "cats_bake.preset_quest.desc": "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" \ + "This will try to automatically detect which bake passes are relevant to your model", + 'cats_bake.bake.label': 'Copy and Bake (SLOW!)', + 'cats_bake.bake.desc': "Perform the bake. Warning, this performs an actual render!\n" \ + "This will create a copy of your avatar to leave the original alone.\n" \ + "Depending on your machine and model, this could take an hour or more.\n" \ + "For each pass, any Value node in your materials labeled bake_ will be\n" \ + "set to 1.0, for more granular customization.", + 'cats_bake.error.no_meshes': "No meshes found!", + 'cats_bake.error.render_engine': "You need to set your render engine to Cycles first!", + 'cats_bake.error.render_disabled': "One or more of your armature's meshes have rendering disabled!", + 'cats_bake.info.success': "Success! Textures and model saved to \'CATS Bake\' folder next to your .blend file.", + # Updater 'CheckForUpdateButton.label': 'Check now for Update', 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', diff --git a/ui/bake.py b/ui/bake.py index 412b5d94..c8fba79f 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -21,11 +21,11 @@ def draw(self, context): box = layout.box() col = box.column(align=True) - col.label(text="Autodetect:") + col.label(text=t('BakePanel.autodetectlabel')) row = col.row(align=True) row.operator(Bake.BakePresetDesktop.bl_idname, icon="ANTIALIASED") row.operator(Bake.BakePresetQuest.bl_idname, icon="ALIASED") - col.label(text="General options:") + col.label(text=t('BakePanel.generaloptionslabel')) row = col.row(align=True) row.prop(context.scene, 'bake_resolution', expand=True) row = col.row(align=True) @@ -58,10 +58,10 @@ def draw(self, context): if armature is None or "Head" not in armature.data.bones: row = col.row(align=True) row.separator() - row.label(text="No \"Head\" bone found!", icon="INFO") + row.label(text=t('BakePanel.noheadfound'), icon="INFO") row = col.row(align=True) row.separator() - row.label(text="Overlap fix:") + row.label(text=t('BakePanel.overlapfixlabel')) row.prop(context.scene, 'bake_uv_overlap_correction', expand=True) #row = col.row(align=True) #row.prop(context.scene, 'bake_simplify_armature', expand=True) @@ -69,18 +69,18 @@ def draw(self, context): row.prop(context.scene, 'bake_quick_compare', expand=True) col.separator() row = col.row(align=True) - col.label(text="Bake passes:") + col.label(text=t('BakePanel.bakepasseslabel')) row = col.row(align=True) row.prop(context.scene, 'bake_pass_diffuse', expand=True) if context.scene.bake_pass_diffuse and (context.scene.bake_pass_smoothness or context.scene.bake_pass_alpha): row = col.row(align=True) row.separator() - row.label(text="Alpha:") + row.label(text=t('BakePanel.alphalabel')) row.prop(context.scene, 'bake_diffuse_alpha_pack', expand=True) if (context.scene.bake_diffuse_alpha_pack == "TRANSPARENCY") and not context.scene.bake_pass_alpha: - col.label(text="Transparency isn't currently selected!", icon="INFO") + col.label(text=t('BakePanel.transparencywarning'), icon="INFO") elif (context.scene.bake_diffuse_alpha_pack == "SMOOTHNESS") and not context.scene.bake_pass_smoothness: - col.label(text="Smoothness isn't currently selected!", icon="INFO") + col.label(text=t('BakePanel.smoothnesswarning'), icon="INFO") col.separator() row = col.row(align=True) row.prop(context.scene, 'bake_pass_normal', expand=True) @@ -115,10 +115,10 @@ def draw(self, context): if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: row = col.row(align=True) row.separator() - row.label(text="Alpha:") + row.label(text=t('BakePanel.alphalabel')) row.prop(context.scene, 'bake_metallic_alpha_pack', expand=True) if context.scene.bake_diffuse_alpha_pack == "SMOOTHNESS" and context.scene.bake_metallic_alpha_pack == "SMOOTHNESS": - col.label(text="Smoothness packed in two places!", icon="INFO") + col.label(text=t('BakePanel.doublepackwarning'), icon="INFO") col.separator() row = col.row(align=True) diff --git a/ui/decimation.py b/ui/decimation.py index e6ec5fa6..fbdc8f9d 100644 --- a/ui/decimation.py +++ b/ui/decimation.py @@ -180,7 +180,7 @@ def draw(self, context): row = col.row(align=True) row.prop(context.scene, 'max_tris') col.separator() - col.label(text="Not reccomended if baking!", icon='INFO') + col.label(text=t('DecimationPanel.warn.notIfBaking'), icon='INFO') row = col.row(align=True) row.scale_y = 1.2 row.operator(Decimation.AutoDecimateButton.bl_idname, icon='MOD_DECIM') From 7a9d1e7f8145681a01fc12e9dd165f7aca6e474c Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Tue, 10 Nov 2020 23:03:44 -0800 Subject: [PATCH 44/64] restore accidentally deleted property --- extentions.py | 17 +++++++++++++++++ translations/en_US.py | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/extentions.py b/extentions.py index 6e443281..5ec6a009 100644 --- a/extentions.py +++ b/extentions.py @@ -184,6 +184,23 @@ def register(): subtype='FACTOR' ) + Scene.bake_animation_weighting = BoolProperty( + name=t('Scene.decimation_animation_weighting.label'), + description=t('Scene.decimation_animation_weighting.desc'), + default=False + ) + + Scene.bake_animation_weighting_factor = FloatProperty( + name=t('Scene.decimation_animation_weighting_factor.label'), + description=t('Scene.decimation_animation_weighting_factor.desc'), + default=0.25, + min=0, + max=1, + step=0.05, + precision=2, + subtype='FACTOR' + ) + Scene.bake_max_tris = IntProperty( name=t('Scene.max_tris.label'), description=t('Scene.max_tris.desc'), diff --git a/translations/en_US.py b/translations/en_US.py index 72941a59..59bd3a90 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -1211,6 +1211,11 @@ "Scene.bake_diffuse_alpha_pack.smoothness.label": "Smoothness", "Scene.bake_diffuse_alpha_pack.smoothness.desc": "Pack Smoothness. Most efficient if you don't have transparency or metallic textures.", + "Scene.bake_metallic_alpha_pack.none.label": "None", + "Scene.bake_metallic_alpha_pack.none.desc": "No alpha channel", + "Scene.bake_metallic_alpha_pack.smoothness.label": "Smoothness", + "Scene.bake_metallic_alpha_pack.smoothness.desc": "Pack Smoothness. Use this if your Diffuse alpha channel is already populated with Transparency", + "cats_bake.warn_missing_nodes": "A material in use isn't using Nodes, fix this in the Shading tab.", "cats_bake.preset_desktop.label": "Desktop", "cats_bake.preset_desktop.desc": "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" \ From 4cf64d97e6c50dec1f1905c7ccd58e7992e7d4f8 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 11 Nov 2020 00:06:32 -0800 Subject: [PATCH 45/64] Missed when simplifying --- tools/bake.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index ea4e4a60..d60282af 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -86,17 +86,27 @@ def autodetect_passes(context, tricount, is_desktop): # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so context.scene.bake_diffuse_alpha_pack = "NONE" context.scene.bake_metallic_alpha_pack = "NONE" - # If 'smoothness' and 'transparency', we need to force metallic to bake so we can pack to it. - if context.scene.bake_pass_smoothness and context.scene.bake_pass_alpha: - context.scene.bake_pass_metallic = True - # If we have transparency, it needs to go in diffuse alpha - if context.scene.bake_pass_alpha: - context.scene.bake_diffuse_alpha_pack = "TRANSPARENCY" - # Smoothness to diffuse is only the most efficient when we don't have metallic or alpha - if context.scene.bake_pass_smoothness and not context.scene.bake_pass_metallic and not context.scene.bake_pass_alpha: - context.scene.bake_diffuse_alpha_pack = "SMOOTHNESS" - if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: - context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + if is_desktop: + # If 'smoothness' and 'transparency', we need to force metallic to bake so we can pack to it. + if context.scene.bake_pass_smoothness and context.scene.bake_pass_alpha: + context.scene.bake_pass_metallic = True + # If we have transparency, it needs to go in diffuse alpha + if context.scene.bake_pass_alpha: + context.scene.bake_diffuse_alpha_pack = "TRANSPARENCY" + # Smoothness to diffuse is only the most efficient when we don't have metallic or alpha + if context.scene.bake_pass_smoothness and not context.scene.bake_pass_metallic and not context.scene.bake_pass_alpha: + context.scene.bake_diffuse_alpha_pack = "SMOOTHNESS" + if context.scene.bake_pass_metallic and context.scene.bake_pass_smoothness: + context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + else: + # alpha packs: arrange for maximum efficiency. + # Its important to leave Diffuse alpha alone if we're not using it, as Unity will try to use 4bpp if so + context.scene.bake_diffuse_alpha_pack = "NONE" + context.scene.bake_metallic_alpha_pack = "NONE" + # If 'smoothness', we need to force metallic to bake so we can pack to it. (smoothness source is not configurable) + if context.scene.bake_pass_smoothness: + context.scene.bake_pass_metallic = True + context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" @register_wrap class BakePresetDesktop(bpy.types.Operator): @@ -802,6 +812,8 @@ def first_bsdf(objs): obj.name = "Body" elif obj.type == "ARMATURE": obj.name = "Armature" + if not os.path.exists(bpy.path.abspath("//CATS Bake/")): + os.mkdir(bpy.path.abspath("//CATS Bake/")) bpy.ops.export_scene.fbx(filepath=bpy.path.abspath("//CATS Bake/Bake.fbx"), check_existing=False, filter_glob='*.fbx', use_selection=True, use_active_collection=False, global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', From 9df6581a7f8d10b2d877f6e5fe6db19e5eeba6e9 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 11 Nov 2020 00:28:39 -0800 Subject: [PATCH 46/64] Fix questdiffuse baking --- tools/bake.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index d60282af..60a8da26 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -616,10 +616,14 @@ def first_bsdf(objs): # Blend diffuse and AO to create Quest Diffuse (if selected) if pass_diffuse and pass_ao and pass_questdiffuse: - if "SCRIPT_questdiffuse.png" not in bpy.data.images: - bpy.ops.image.new(name="SCRIPT_questdiffuse.png", width=resolution, height=resolution, - generated_type="BLANK", alpha=False) + if "SCRIPT_questdiffuse.png" in bpy.data.images: + image = bpy.data.images["SCRIPT_questdiffuse.png"] + image.user_clear() + bpy.data.images.remove(image) + bpy.ops.image.new(name="SCRIPT_questdiffuse.png", width=resolution, height=resolution, + generated_type="BLANK", alpha=False) image = bpy.data.images["SCRIPT_questdiffuse.png"] + image.filepath = bpy.path.abspath("//CATS Bake/" + "SCRIPT_questdiffuse.png") diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] ao_image = bpy.data.images["SCRIPT_ao.png"] image.generated_width=resolution From d9eb46dc722dead187ae9a1b7cbb40073f8509cc Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 11 Nov 2020 12:42:33 -0800 Subject: [PATCH 47/64] Update copyright message --- tools/bake.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 60a8da26..8608497d 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -1,6 +1,6 @@ # MIT License -# Copyright (c) 2020 GiveMeAllYourCats +# Copyright (c) 2020 Feilen # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the 'Software'), to deal @@ -21,7 +21,6 @@ # SOFTWARE. # Code author: Feilen -# Edits by: Feilen import bpy import os From d2991207df73b4630d00fd344030f3e703d6dc97 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Thu, 12 Nov 2020 01:13:29 -0800 Subject: [PATCH 48/64] fix a typo, better handling for missing bsdf --- tools/bake.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 8608497d..91527205 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -307,7 +307,7 @@ def execute(self, context): self.report({'ERROR'}, t('cats_bake.error.no_meshes')) return {'FINISHED'} if context.scene.render.engine != 'CYCLES': - self.report({'ERROR'}, t('cats_bake.error.no_meshes')) + self.report({'ERROR'}, t('cats_bake.error.render_engine')) return {'FINISHED'} if any([obj.hide_render for obj in Common.get_armature().children]): self.report({'ERROR'}, t('cats_bake.error.render_disabled')) @@ -689,8 +689,9 @@ def first_bsdf(objs): # add a normal map and image texture to connect the world texture, if it exists tree = mat.node_tree bsdfnode = next(node for node in tree.nodes if node.type == "BSDF_PRINCIPLED") - for bsdfinput in bsdfnode.inputs: - bsdfinput.default_value = bsdf_original.inputs[bsdfinput.identifier].default_value + if bsdf_original is not None: + for bsdfinput in bsdfnode.inputs: + bsdfinput.default_value = bsdf_original.inputs[bsdfinput.identifier].default_value if pass_normal: normaltexnode = tree.nodes.new("ShaderNodeTexImage") if use_decimation: From 77e7affa7f6570bb0e4c7f1d493033d1fd8c0d51 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sun, 22 Nov 2020 19:31:49 +0100 Subject: [PATCH 49/64] Fixed Google Translations randomly not working --- .gitignore | 1 + README.md | 1 + googletrans/gtoken.py | 98 ++++++++++++++++++++---- resources/icons/supporters/Cicieaaa.png | Bin 0 -> 2286 bytes resources/icons/supporters/Mordae.png | Bin 0 -> 2601 bytes resources/supporters.json | 10 ++- tools/translate.py | 77 +++++++++++-------- 7 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 resources/icons/supporters/Cicieaaa.png create mode 100644 resources/icons/supporters/Mordae.png diff --git a/.gitignore b/.gitignore index 9125d9c5..25df6d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ resources/downloads resources/settings.json resources/dictionary_google.json resources/google-response.txt +resources/google-response-readable.txt resources/ignore_version.txt resources/no_auto_ver_check.txt .coverage diff --git a/README.md b/README.md index 4c124d27..74b9a511 100644 --- a/README.md +++ b/README.md @@ -319,6 +319,7 @@ It checks for a new version automatically once every day. - Added "Show mmd_tools tabs" option to Settings - This allows you show and hide the "MMD" and "Misc" tabs added by the mmd_tools plugin - Changed link to a new vrm importer since the old one dropped support +- Fixed Google Translations randomly not working - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 - More fixes for Blender 2.90 diff --git a/googletrans/gtoken.py b/googletrans/gtoken.py index 2722ed36..3ca18640 100644 --- a/googletrans/gtoken.py +++ b/googletrans/gtoken.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- +import re +import os import ast +import bpy import math -import re import time - -import requests - -import bpy -import os import pathlib - - +import requests from .compat import PY3 -from .compat import unicode from .utils import rshift +from .compat import unicode +from html.parser import HTMLParser +from html.entities import name2codepoint class TokenAcquirer(object): @@ -63,6 +61,7 @@ def _update(self): r = self.session.get(self.host, verify=False) # This prints the google response if the button in the cats settings is pressed + # Prints the response from google into a text file inside cats/resources/google-response.txt print_response(r.text) rawtkk = self.RE_RAWTKK.search(r.text) @@ -76,10 +75,14 @@ def _update(self): return # this will be the same as python code after stripping out a reserved word 'var' - if self.RE_TKK.search(r.text): - code = unicode(self.RE_TKK.search(r.text).group(1)).replace('var ', '') - else: - code = unicode(self.RE_TKK2.search(r.text).group(1)).replace('var ', '') + try: + if self.RE_TKK.search(r.text): + code = unicode(self.RE_TKK.search(r.text).group(1)).replace('var ', '') + else: + code = unicode(self.RE_TKK2.search(r.text).group(1)).replace('var ', '') + except AttributeError as e: + print_response(r.text, force_print=True) + raise AttributeError(e) # unescape special ascii characters such like a \x3d(=) if PY3: # pragma: no cover @@ -206,12 +209,75 @@ def do(self, text): return tk -def print_response(text): - if not bpy.context.scene.debug_translations: +def print_response(text, force_print=False): + if not force_print and not bpy.context.scene.debug_translations: return # Prints the response from google into a textfile inside cats/resources/google-response.txt main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve() resources_dir = os.path.join(str(main_dir), "resources") output_file = os.path.join(resources_dir, "google-response.txt") + output_file_readable = os.path.join(resources_dir, "google-response-readable.txt") with open(output_file, 'w', encoding="utf8") as outfile: - outfile.write(text) \ No newline at end of file + outfile.write(text) + with open(output_file_readable, 'w', encoding="utf8") as outfile: + outfile.write(html_to_text(text)) + + +""" +HTML <-> text conversions. +http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python +""" + + +class _HTMLToText(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self._buf = [] + self.hide_output = False + + def handle_starttag(self, tag, attrs): + if tag in ('p', 'br') and not self.hide_output: + self._buf.append('\n') + elif tag in ('script', 'style'): + self.hide_output = True + + def handle_startendtag(self, tag, attrs): + if tag == 'br': + self._buf.append('\n') + + def handle_endtag(self, tag): + if tag == 'p': + self._buf.append('\n') + elif tag in ('script', 'style'): + self.hide_output = False + + def handle_data(self, text): + if text and not self.hide_output: + self._buf.append(re.sub(r'\s+', ' ', text)) + + def handle_entityref(self, name): + if name in name2codepoint and not self.hide_output: + c = chr(name2codepoint[name]) + self._buf.append(c) + + def handle_charref(self, name): + if not self.hide_output: + n = int(name[1:], 16) if name.startswith('x') else int(name) + self._buf.append(chr(n)) + + def get_text(self): + return re.sub(r' +', ' ', ''.join(self._buf)) + + +def html_to_text(html): + """ + Given a piece of HTML, return the plain text it contains. + This handles entities and char refs, but not javascript and stylesheets. + """ + parser = _HTMLToText() + try: + parser.feed(html) + parser.close() + except: # HTMLParseError: No good replacement? + pass + return parser.get_text() \ No newline at end of file diff --git a/resources/icons/supporters/Cicieaaa.png b/resources/icons/supporters/Cicieaaa.png new file mode 100644 index 0000000000000000000000000000000000000000..486aad2a8fed7a6b0bd381a4b5ab3e83edaedf89 GIT binary patch literal 2286 zcmVjXldiTpQs-J*tkXn~q9W=*)czj`pb6;akjI(jtaTRleBCj- zabw@qmd4Yq+enc72;F)T?IfYS_`t2xH((OTHEEP=P@XR$xuj5|S!@y8_~ z2>4Z%ph+aGfF^?4AI9YI{#Yqth+p+ZnB45;@~UoTzUL8(R$scN+g`8tE&bbCYL8r) zrk}k>*R9{`Pd-(zqwA++iF>Q` z$(id6ByT?dW-+NYZvldv7ca^@DR~q{TeniUb0<9+nRI1j(VCY}?(W^p`1I3JKVj5A z4#RW5aB(IW2e8)WpD$XP>?P!RIFOxiN>wh)5)qRLO_nhl4bhSyoSK5Qv2oNVq{k&a z76-EuAU}Y)#g;D%9zzmp2){u*;XgkLugQSo^^sz;V39;TVGF9JAr1@-b%Y>Ly5B`s zi=U4dPaKLLqpDvK!J>mtGRufsBwFfX*|J?(U=`3c`#{;2%qOka5^Ux7|3xET52FhtB*Q?tJ|fy58AAZ*m{bi!UJZ zz6ViKkR%E5{wah$XhFGl7muVHn7JU6JGQj5u`(Nl#w9W>{fDXVi4Fp4ytu2RzJ zA5r+k2@J2lik5BR+5t;jo9$WfMu80Jqk-dF{fX_N&;@PogV~jzHH$BlEWQkT4jg-)0z@e^h~E z`*8UC8M~c4zG@#{1FU>^X>6=u-j`6&2vsI{wgDEWLynB$#yY65K&Qs!Id1|%R(~LZK zb3t5JjoosG5{T$AXP;5z)oXT9UgPG$drPS;OJdU%)vQ`Gg+s+&E^#z+=}R4G5fyRc zjFCx#p&*K^knC01=5vEpVQ-q1>(-aYlOG2|T@cbxLK^Q~oy}KsQ@MQ8EQ|q-zyEwE z5B;f_L&X*tP`UbL1<`1v_BN4&9gdON3`+F1EI_LPN2&_OF+?Xgq>OZdy(`GxnP%4h zp*Z%ObHvMs7o}4@(>5frQ{wf7Ie04I%!UTqd)ko)HEdQZPM4GY&LdQ8=%Hc$s4}N= zn3iKrDnLjP;?`-?74at|cdmiWxhZV!Xyuk?US>j~iceASd%W~3UQ%1boKriW&2JQ( zl>C^V1Q|_~5F$iGA>Lz5u|$qWsL2EpRa||2cq9qkY9*4L$n77bv2W2#c27O&>m_!U zMABpJAnN9z8;^`Br2RNeeav(8!(v9Vs7QiDia$V?!9!MiGI>oC*iz{~DR~seC|C3p zZ_>P?C%5+?O(srnJ+>eGO1+r3o z>{^mSq_-1ghsG@H;F!Cj5Ib_JK8=!Q56h~$nX}0a!903aKXq@oa9g9$s!hZ#1d(33 zf7NNvEqs(iK~VgV5dt-O`HWsd$4DtkAc-JrF+x2oA!r(CM|!MkjuW9hoJ~NGsq39fQgumDPIc;X+u^kHk~-8v{8A!f^!hn=Ed_KzL^w(q z-34Pw9;f;=QNySWy3+?81BKn+be`jQy`{AOK%s39$qDWJ5BF!Uq{wS?GXMYp07*qo IM6N<$g4arM00000 literal 0 HcmV?d00001 diff --git a/resources/icons/supporters/Mordae.png b/resources/icons/supporters/Mordae.png new file mode 100644 index 0000000000000000000000000000000000000000..c498073eb0da7b1d8282dbbb7328047d8db62264 GIT binary patch literal 2601 zcmZ`*c{JPG8vR8KsktbRYl@;ZM<@|fxv05rSS?DMm_tpKph1NyZBawgASJjhs)p7) zD@}264XNT*YN`^YRf?MR#rxx}_5OJ4tnYkl?S1w-d!4h+cfJQsj+X>@rFa1V5U{bf zK(Vv@NkF*Sa|D5~#ZH|5W)5ZmP?N!Th~YXRy{%CW06xV`KySpdIIMo~u@iH)2Jvv!LB0KvB>0eY+^_!}GK!Pz)i@_gnK z7ZE;J*Dqxb01g_(;hz3Op%n%JUbpXv9%hlhucJDCxo8B9aLQN9#cvzr=4TuNT` zRss%6mLiIW7%!&jlVf|e2ifiRNsI72$vi%ksm_18ASjN1?T! zKO-ak{iFP5n&Q98jz7D23!mM8({Gj)XR;aldSsK=q$FsNqLD<$T#wzb)C-=%IXAd> zm=-vCyG^LkSc~>!M((PQRa>j;qQ1`zhEJ7c#WEX%Efh|Nbnf}ZE%c@{larJ0=$n`v zS5a94N))YM0iWKL$Q*&ehNAsGE&)<)CgN(xg>RWnp*$CD`@6b;FSz~gtgI|!C1%Lj z@6Zo<*3zpJ+s;oL;|gVXCHX?yH->5R^Q0$_WELRnp7Of0pEFGM&nII>nam0SLBUc2 zfq>jUIC$-yGZ?%fj~@S8+34rv_00FDi=6fABB# zJu&*Q=aV-hK-W1p&#k@aI%~dMig@L~a>uUfJ!C;fRQfc)}+#Gxnp zb+5`;ISkjYqXVc1Rn@8Ehp!E1dp5fKw*9Y!T|FioyX_!A(oL0bNNthd^h6%W@gY8$ zZHtP0<&fo=06$513C;jTfrAp%*U!w|D~1wcV)n)dwXU`y4JWtK)2j$Y?me8`2`@^E zuWU1ZP15aYCWBpb@^!Wts$U#qYil+1t=HHQ+wSH(MiXqr#Scm)N?N#;5g{HoM)TL6fqF?F(Uj zk7T|wo4!}j$WPSz6~}rM{*VX%3-x0yRA{cLB`v-c95ZW@zj4>lYw^d{o{MVK%3UFNvD5i*&zOU0jx;3_De18} z5C2?;6J)XCL$hSw{l0~#6ki8rQvcCj>~VihO^w?mUQ$jj)I}ahhQ=Dlp5<<`BC4hv zRDpENVVRCPnIl@2I9&Uyx3x*x?^JRI7shQZDYb4UH$z!JYqVD!UaITy$G>P>J!r9C z%Al4S*s4gT_V|^Rl}!gW*aiiu=|;#Wh^cR2e8tFo3R9dFU?or1k&>O0{>O_JDs|W= zVd~;`2b~$7n8)HLA&ma@VIG)uu1|1 z|FN5^=T>X*lawivdA^FYP>8Nx4Vn+L9@8bG#CW-SdwavAdEBbtqiKfx{CsADiMAzg z7*8*~{O28%sJI@Q+bkmj5(Whh4T**QQ=4Cm!d`YA7%z8OsiU#x5rXF&^NTz*^$BtT z;OpGO9Hqi$=fza}ylN_j$-=rzTie1SnW&+hI}r&SA$bKdM}6z-g?z@wJNYJYRQIZ6 zvdAQ!p(~)Gq%&jyXQZ78#S0^( z{d>cuJtli#{N}~Ix=r`c9uP6K8^ykCEM~IW(-7@8(Ev)cY7wXkUtl0iz(wzy7lhsi z8ml<`rp-?Wjp7S@;9uM+7lhNTv=l#WWLIMYs8&Hit3F1u@n7b;@0xX7f8EGg3(ACC zFrEX~C>KmFC%x{NnnG&BwKwW)8>kF?LA24DJ7pV(ESPyp=4Cc^JVlkqUV}i5Mw}ai z)n<3TEH5_`xM)GHV8?6%HC+7E0;Hc(HSXId-Xw@}bpywxH{py?wK z2*fk2`yUYKGcvmtaXe_5Xe;G3<~E)L#O7f%nI6Yvi237;qyb8yi5(%R{d z!@aK>4{WYZMX6R$`)bH=Uff>iWrTDmvJ2nkz0M_cCI4<5etoNS50Hjlzs-axtI_;V zK}U53W_Nh5UAxAEDi`zg^aOT03-g(}ki`3}+rLGweN?Gl1bsUzjL$uU$nIcz0iBQP z>3kQMuon^W(}Y(cV&e~FAm-O$+5jpM-4QWVslGv zE?wf84?_e}mvSgtH;&8L)qU?^=X)P4ExUTvm12o3D6OhWcTpXGH!(pGNJOtZJOj+6 zcE^CxVqMXKfo;(90Jue(f=Meum6J-bezhMe|MIKD57s`iQxOWJD!aS8FR!lBYb%R= z;MjAde)hw4E_WGNU0&X8Apb~PKzIlcHINB zqxcTW2rETM8iDqQOFkoi-n>ml#ky7i|PmTMwzDqXvT^VKBEl%P;;% z5FF}*^}GH51?SH?iL(W2Cp-Ke8XW8!f&);#5mz>% literal 0 HcmV?d00001 diff --git a/resources/supporters.json b/resources/supporters.json index c69d0092..faaecc47 100644 --- a/resources/supporters.json +++ b/resources/supporters.json @@ -509,8 +509,16 @@ "displayname": "Vyrei", "startdate": "2020-10-21", "description": "thank yu for this convenient application ! lots of luv <3\n", - "website": "https://discord.gg/H8kCtxs", + "website": "https://discord.gg/H8kCtxs" + },{ + "displayname": "Cicieaaa", + "startdate": "2020-11-03", + "description": "I am making avatars with Cats Plugin!", + "website": "https://discord.gg/a9VmagS", "tier": 1 + },{ + "displayname": "Mordae", + "startdate": "2020-11-22" } ] } diff --git a/tools/translate.py b/tools/translate.py index 1199a1eb..3faf1bd3 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -29,6 +29,7 @@ import copy import json import pathlib +import traceback import collections import requests.exceptions @@ -428,39 +429,53 @@ def update_dictionary(to_translate_list, translating_shapes=False, self=None): # Translate the rest with google translate print('GOOGLE DICT UPDATE!') translator = Translator() - try: - translations = translator.translate(google_input) - except requests.exceptions.ConnectionError: - print('CONNECTION TO GOOGLE FAILED!') - if self: - self.report({'ERROR'}, t('update_dictionary.error.cantConnect')) - return - except json.JSONDecodeError: - if self: - self.report({'ERROR'}, t('update_dictionary.error.temporaryBan') + t('update_dictionary.error.catsTranslated')) - print('YOU GOT BANNED BY GOOGLE!') - return - except RuntimeError as e: - error = Common.html_to_text(str(e)) - if self: - if 'Please try your request again later' in error: + token_tries = 0 + while True: + try: + translations = translator.translate(google_input) + break + except requests.exceptions.ConnectionError: + print('CONNECTION TO GOOGLE FAILED!') + if self: + self.report({'ERROR'}, t('update_dictionary.error.cantConnect')) + return + except json.JSONDecodeError: + if self: self.report({'ERROR'}, t('update_dictionary.error.temporaryBan') + t('update_dictionary.error.catsTranslated')) - print('YOU GOT BANNED BY GOOGLE!') - return - - if 'Error 403' in error: - self.report({'ERROR'}, t('update_dictionary.error.cantAccess') + t('update_dictionary.error.catsTranslated')) - print('NO PERMISSION TO USE GOOGLE TRANSLATE!') - return + print('YOU GOT BANNED BY GOOGLE!') + return + except RuntimeError as e: + error = Common.html_to_text(str(e)) + if self: + if 'Please try your request again later' in error: + self.report({'ERROR'}, t('update_dictionary.error.temporaryBan') + t('update_dictionary.error.catsTranslated')) + print('YOU GOT BANNED BY GOOGLE!') + return + + if 'Error 403' in error: + self.report({'ERROR'}, t('update_dictionary.error.cantAccess') + t('update_dictionary.error.catsTranslated')) + print('NO PERMISSION TO USE GOOGLE TRANSLATE!') + return + + self.report({'ERROR'}, t('update_dictionary.error.errorMsg') + t('update_dictionary.error.catsTranslated') + '\n' + '\nGoogle: ' + error) + print('', 'You got an error message from Google:', error, '') + return + except AttributeError as e: + # If the translator wasn't able to create a stable connection to Google, just retry it again + # This is an issue with Google since Nov 2020: https://github.com/ssut/py-googletrans/issues/234 + token_tries += 1 + if token_tries < 20: + print('RETRY', token_tries) + translator = Translator() + continue - self.report({'ERROR'}, t('update_dictionary.error.errorMsg') + t('update_dictionary.error.catsTranslated') + '\n' + '\nGoogle: ' + error) - print('', 'You got an error message from Google:', error, '') - return - except AttributeError: - if self: - self.report({'ERROR'}, t('update_dictionary.error.apiChanged')) - print('GOOGLE API CHANGED') - return + # If if didn't work after 20 tries, just quit + # The response from Google was printed into "cats/resources/google-response.txt" + if self: + self.report({'ERROR'}, t('update_dictionary.error.apiChanged')) + print('ERROR: GOOGLE API CHANGED!') + print(traceback.format_exc()) + return # Update the dictionaries for i, translation in enumerate(translations): From 639bbc6e7ec56d96810c42e041f35a3c291ca267 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 28 Nov 2020 12:02:04 +0100 Subject: [PATCH 50/64] Removed mmd_tools when on Linux and Blender 2.90 or higher --- README.md | 63 +++------- __init__.py | 11 +- extentions.py | 10 +- tools/armature.py | 66 +++++----- tools/armature_manual.py | 7 -- tools/bake.py | 85 +++++++------ tools/common.py | 14 ++- tools/importer.py | 12 +- tools/translate.py | 12 +- translations/__init__.py | 57 ++++++--- translations/en_US.py | 3 +- translations/ja_JP.py | 258 +++++++++++++++++++-------------------- ui/credits.py | 5 +- 13 files changed, 311 insertions(+), 292 deletions(-) diff --git a/README.md b/README.md index 74b9a511..785e6e42 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,15 @@ This works by checking all bones and trying to figure out if they can be grouped - Starts the merge process +## Bake + +![](https://user-images.githubusercontent.com/1109288/97830517-147d1500-1c82-11eb-8b20-feba732ad672.png) + +**This is a non-destructive way to produce an optimized variant of (almost) any avatar!** + +For more information please visit the **[Bake Panel Wiki Page](https://github.com/GiveMeAllYourCats/cats-blender-plugin/wiki/Bake)**. + + ## Shape Key ![](https://i.imgur.com/LgFK4KO.png) @@ -315,13 +324,17 @@ It checks for a new version automatically once every day. - **Added Smart Decimation!** - This lets you decimate without loosing any shapekeys! - Full credit goes to **feilen**! Tons of thanks for this awesome feature <3 -- Cats is now fully compatible with Blender 2.90 -- Added "Show mmd_tools tabs" option to Settings - - This allows you show and hide the "MMD" and "Misc" tabs added by the mmd_tools plugin -- Changed link to a new vrm importer since the old one dropped support -- Fixed Google Translations randomly not working -- Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 -- More fixes for Blender 2.90 +- **Added Bake Panel!** + - This is a non-destructive way to produce an optimized variant of (almost) any avatar! + - Full credit goes to **feilen**! Thanks so much for this awesome feature as well <3 +- **General:** + - Cats is now fully compatible with Blender 2.90 and 2.91 + - Added "Show mmd_tools tabs" option to Settings + - This allows you show and hide the "MMD" and "Misc" tabs added by the mmd_tools plugin + - Changed link to a new vrm importer since the old one dropped support + - Fixed Google Translations randomly not working + - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 + - More fixes for Blender 2.90 #### 0.17.0 - **Cats is now fully compatible with Blender 2.83!** @@ -352,42 +365,6 @@ It checks for a new version automatically once every day. - Fixed objects getting unhidden when doing any cats operation in 2.80+ - Updated mmd_tools -#### 0.16.1 -- Fixed export warning bug - -#### 0.16.0 -- **Cats is now fully compatible with Blender 2.81!** -- **Importer**: - - Added support for ZIP files - - It will only extract the zip if importable models are found - - If multiple models are found in the zip, you can select the one you want in a popup window - - Japanese zip files will be extracted with the correct encoding - - Models can now be imported with Cats via the Windows command shell -- **Fix Model**: - - Hips bone will now be larger than before, to comply with the VRChat recommendations - - Read through them here: https://docs.vrchat.com/docs/full-body-tracking - - FFXIV models are now compatible - - Added "Fix Materials" option in Blender 2.80 and higher - - This will apply some VRChat related fixes to materials - - This has always been done in Fix Model but now you can turn it off -- **Model Options**: - - Remove Doubles no longer effects meshes with no shapekeys -- **Custom Model Creation**: - - Added "Join Meshes" option - - Merge Armatures and Attach Mesh no longer require a mesh on the armature - - Fixed bones from the merge armature sometimes getting unintentionally deleted -- **Optimization**: - - Added manual download button if Material Combiner is outdated -- **Copy Protection**: - - Removed Copy Protection panel - - It is no longer a good method for protecting against cache ripping - and it can cause performance and lighting issues -- **General**: - - Armatures will no longer be forced into rest position after any action - - Fixed armatures sometimes not getting detected - - Small bug fixes - - Updated mmd_tools - Read the full changelog [here](https://github.com/michaeldegroot/cats-blender-plugin/releases). diff --git a/__init__.py b/__init__.py index f8d8db5f..9061260d 100644 --- a/__init__.py +++ b/__init__.py @@ -49,6 +49,7 @@ import shutil import pathlib import requests +import platform from . import globs @@ -59,10 +60,14 @@ else: is_reloading = True +# Only load mmd_tools if it's not on linux and 2.90 or higher since it causes Blender to crash +if platform.system() != "Linux" or bpy.app.version < (2, 90): + import mmd_tools_local + # Load or reload all cats modules if not is_reloading: # This order is important - import mmd_tools_local + # import mmd_tools_local from . import updater from . import translations from . import tools @@ -71,7 +76,7 @@ else: import importlib importlib.reload(updater) - importlib.reload(mmd_tools_local) + # importlib.reload(mmd_tools_local) importlib.reload(translations) importlib.reload(tools) importlib.reload(ui) @@ -255,7 +260,7 @@ def register(): # Register Updater and check for CATS update updater.register(bl_info, dev_branch, version_str) - # Check for missing translations + # Load translations and check for missing translations translations.check_missing_translations() # Set some global settings, first allowed use of globs diff --git a/extentions.py b/extentions.py index 5ec6a009..a0a02e34 100644 --- a/extentions.py +++ b/extentions.py @@ -600,7 +600,7 @@ def register(): # Atlas # Material.add_to_atlas = BoolProperty( - # description=t('Add this material to the atlas') + # description=t('Add this material to the atlas'), # default=False # ) @@ -613,7 +613,7 @@ def register(): # ) # Scene.clear_materials = BoolProperty( - # description=t('Clear materials checkbox') + # description=t('Clear materials checkbox'), # default=True # ) @@ -668,7 +668,7 @@ def register(): ) # Scene.disable_vrchat_features = BoolProperty( - # name=t('Disable VRChat Only Features') + # name=t('Disable VRChat Only Features'), # description='This will disable features which are solely used for VRChat.' # '\nThe following will be disabled:' # '\n- Eye Tracking' @@ -679,8 +679,8 @@ def register(): # Copy Protection - obsolete # Scene.protection_mode = EnumProperty( - # name=t("Randomization Level") - # description=t("Randomization Level") + # name=t("Randomization Level"), + # description=t("Randomization Level"), # items=[ # ("FULL", "Full", "This will randomize every vertex of your model and it will be completely unusable for thieves.\n" # 'However this method might cause problems with the Outline option from Cubed shader.\n' diff --git a/tools/armature.py b/tools/armature.py index 4c8a89e7..e12f7392 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -29,19 +29,21 @@ import bpy import copy import math +import platform from mathutils import Matrix from . import common as Common from . import translate as Translate -from . import supporter as Supporter from . import armature_bones as Bones from .common import version_2_79_or_older from .register import register_wrap -from mmd_tools_local.operators import morph as Morph from ..translations import t - -mmd_tools_installed = True +# Only load mmd_tools if it's not on linux and 2.90 or higher since it causes Blender to crash +mmd_tools_installed = False +if platform.system() != "Linux" or bpy.app.version < (2, 90): + from mmd_tools_local.operators import morph as Morph + mmd_tools_installed = True @register_wrap @@ -150,40 +152,41 @@ def execute(self, context): print('DOUBLES END') # Check if model is mmd model - mmd_root = None - try: - mmd_root = armature.parent.mmd_root - except AttributeError: - pass + if mmd_tools_installed: + mmd_root = None + try: + mmd_root = armature.parent.mmd_root + except AttributeError: + pass - # Perform mmd specific operations - if mmd_root: + # Perform mmd specific operations + if mmd_root: - # Set correct mmd shading - mmd_root.use_toon_texture = False - mmd_root.use_sphere_texture = False + # Set correct mmd shading + mmd_root.use_toon_texture = False + mmd_root.use_sphere_texture = False - # Convert mmd bone morphs into shape keys - if len(mmd_root.bone_morphs) > 0: + # Convert mmd bone morphs into shape keys + if len(mmd_root.bone_morphs) > 0: - current_step = 0 - wm.progress_begin(current_step, len(mmd_root.bone_morphs)) + current_step = 0 + wm.progress_begin(current_step, len(mmd_root.bone_morphs)) - armature.data.pose_position = 'POSE' - for index, morph in enumerate(mmd_root.bone_morphs): - current_step += 1 - wm.progress_update(current_step) + armature.data.pose_position = 'POSE' + for index, morph in enumerate(mmd_root.bone_morphs): + current_step += 1 + wm.progress_update(current_step) - armature.parent.mmd_root.active_morph = index - Morph.ViewBoneMorph.execute(None, context) + armature.parent.mmd_root.active_morph = index + Morph.ViewBoneMorph.execute(None, context) - mesh = Common.get_meshes_objects()[0] - Common.set_active(mesh) + mesh = Common.get_meshes_objects()[0] + Common.set_active(mesh) - mod = mesh.modifiers.new(morph.name, 'ARMATURE') - mod.object = armature - Common.apply_modifier(mod, as_shapekey=True) - wm.progress_end() + mod = mesh.modifiers.new(morph.name, 'ARMATURE') + mod.object = armature + Common.apply_modifier(mod, as_shapekey=True) + wm.progress_end() # Perform source engine specific operations # Check if model is source engine model @@ -404,7 +407,8 @@ def execute(self, context): if context.scene.fix_materials: # Make materials exportable in Blender 2.80 and remove glossy mmd shader look # Common.remove_toon_shader(mesh) - Common.fix_mmd_shader(mesh) + if mmd_tools_installed: + Common.fix_mmd_shader(mesh) Common.fix_vrm_shader(mesh) Common.add_principled_shader(mesh) diff --git a/tools/armature_manual.py b/tools/armature_manual.py index 0861613e..883d6961 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -32,13 +32,6 @@ from .register import register_wrap from ..translations import t -mmd_tools_installed = False -try: - import mmd_tools_local - mmd_tools_installed = True -except: - pass - @register_wrap class StartPoseMode(bpy.types.Operator): diff --git a/tools/bake.py b/tools/bake.py index 91527205..aef8ed33 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -29,7 +29,8 @@ from .register import register_wrap from ..translations import t -def autodetect_passes(context, tricount, is_desktop): + +def autodetect_passes(self, context, tricount, is_desktop): context.scene.bake_max_tris = tricount context.scene.bake_resolution = 2048 if is_desktop else 1024 # Autodetect passes based on BSDF node inputs @@ -54,16 +55,18 @@ def autodetect_passes(context, tricount, is_desktop): # Diffuse: on if >1 unique color input or if any has non-default base color input on bsdf context.scene.bake_pass_diffuse = (any([node.inputs["Base Color"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Base Color"].default_value for node in bsdf_nodes])) > 1) + # Smoothness: similar to diffuse context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) + # Emit: similar to diffuse context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) # Transparency: similar to diffuse context.scene.bake_pass_alpha = is_desktop and (any([node.inputs["Alpha"].is_linked for node in bsdf_nodes]) - or len(set([node.inputs["Alpha"].default_value for node in bsdf_nodes])) > 1) + or len(set([node.inputs["Alpha"].default_value for node in bsdf_nodes])) > 1) # Metallic: similar to diffuse context.scene.bake_pass_metallic = (any([node.inputs["Metallic"].is_linked for node in bsdf_nodes]) @@ -107,6 +110,7 @@ def autodetect_passes(context, tricount, is_desktop): context.scene.bake_pass_metallic = True context.scene.bake_metallic_alpha_pack = "SMOOTHNESS" + @register_wrap class BakePresetDesktop(bpy.types.Operator): bl_idname = 'cats_bake.preset_desktop' @@ -115,9 +119,10 @@ class BakePresetDesktop(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - autodetect_passes(context, 32000, True) + autodetect_passes(self, context, 32000, True) return {'FINISHED'} + @register_wrap class BakePresetQuest(bpy.types.Operator): bl_idname = 'cats_bake.preset_quest' @@ -126,9 +131,10 @@ class BakePresetQuest(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} def execute(self, context): - autodetect_passes(context, 5000, False) + autodetect_passes(self, context, 5000, False) return {'FINISHED'} + @register_wrap class BakeButton(bpy.types.Operator): bl_idname = 'cats_bake.bake' @@ -184,10 +190,11 @@ def set_values(self, objects, input_name, input_value): tree = slot.material.node_tree for node in tree.nodes: if node.type == "BSDF_PRINCIPLED": - node.inputs[input_name].default_value = input_value + node.inputs[input_name].default_value = input_value # "Bake pass" function. Run a single bake to ".png" against all selected objects. - def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, bake_size, bake_samples, bake_ray_distance, background_color, clear, bake_margin, bake_active=None, bake_multires=False, normal_space='TANGENT'): + def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, bake_size, bake_samples, bake_ray_distance, background_color, clear, bake_margin, bake_active=None, bake_multires=False, + normal_space='TANGENT'): bpy.ops.object.select_all(action='DESELECT') if bake_active is not None: bake_active.select_set(True) @@ -202,17 +209,17 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba bpy.data.images.remove(image) bpy.ops.image.new(name="SCRIPT_" + bake_name + ".png", width=bake_size[0], height=bake_size[1], color=background_color, - generated_type="BLANK", alpha=True) + generated_type="BLANK", alpha=True) image = bpy.data.images["SCRIPT_" + bake_name + ".png"] image.filepath = bpy.path.abspath("//CATS Bake/" + "SCRIPT_" + bake_name + ".png") image.alpha_mode = "STRAIGHT" image.generated_color = background_color - image.generated_width=bake_size[0] - image.generated_height=bake_size[1] + image.generated_width = bake_size[0] + image.generated_height = bake_size[1] image.scale(bake_size[0], bake_size[1]) if bake_type == 'NORMAL' or bake_type == 'ROUGHNESS': image.colorspace_settings.name = 'Non-Color' - if bake_name == 'diffuse' or bake_name == 'metallic': # For packing smoothness to alpha + if bake_name == 'diffuse' or bake_name == 'metallic': # For packing smoothness to alpha image.alpha_mode = 'CHANNEL_PACKED' image.pixels[:] = background_color * bake_size[0] * bake_size[1] image = bpy.data.images["SCRIPT_" + bake_name + ".png"] @@ -262,13 +269,13 @@ def bake_pass(self, context, bake_name, bake_type, bake_pass_filter, objects, ba context.scene.render.use_bake_multires = bake_multires context.scene.render.bake.normal_space = normal_space bpy.ops.object.bake(type=bake_type, - #pass_filter=bake_pass_filter, - use_clear= clear and bake_type == 'NORMAL', - #uv_layer="SCRIPT", - use_selected_to_active=(bake_active != None), - cage_extrusion=bake_ray_distance, - normal_space=normal_space - ) + # pass_filter=bake_pass_filter, + use_clear=clear and bake_type == 'NORMAL', + # uv_layer="SCRIPT", + use_selected_to_active=(bake_active != None), + cage_extrusion=bake_ray_distance, + normal_space=normal_space + ) # For all materials in use, change any value node labeled "bake_" to 1.0, then back to 0.0. for obj in objects: for slot in obj.material_slots: @@ -299,6 +306,7 @@ def recurse(ob, parent, depth): recurse(child, copy, depth + 1) return copy + return recurse(ob, ob.parent, 0) def execute(self, context): @@ -354,7 +362,6 @@ def perform_bake(self, context): margin = 0.01 quick_compare = context.scene.bake_quick_compare - # TODO: Option to seperate by loose parts and bake selected to active # Passes @@ -374,7 +381,7 @@ def perform_bake(self, context): diffuse_alpha_pack = context.scene.bake_diffuse_alpha_pack metallic_alpha_pack = context.scene.bake_metallic_alpha_pack - # Create an output collection + # Create an output collection collection = bpy.data.collections.new("CATS Bake") context.scene.collection.children.link(collection) @@ -392,6 +399,7 @@ def perform_bake(self, context): objs_size_descending = sorted([obj for obj in collection.all_objects if obj.type == "MESH"], key=lambda obj: obj.dimensions.x * obj.dimensions.y * obj.dimensions.z, reverse=True) + def first_bsdf(objs): for obj in objs_size_descending: for slot in obj.material_slots: @@ -400,6 +408,7 @@ def first_bsdf(objs): for node in tree.nodes: if node.type == "BSDF_PRINCIPLED": return node + bsdf_original = first_bsdf(objs_size_descending) if generate_uvmap: @@ -433,7 +442,6 @@ def first_bsdf(objs): for loop in poly.loop_indices: uv_layer[loop].uv.x += 1 - # TODO: cleanup, all editmode_toggle -> common.switch # Select all meshes. Select all UVs. Average islands scale @@ -441,7 +449,7 @@ def first_bsdf(objs): bpy.ops.object.editmode_toggle() bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') - bpy.ops.uv.average_islands_scale() # Use blender average so we can make our own tweaks. + bpy.ops.uv.average_islands_scale() # Use blender average so we can make our own tweaks. Common.switch('OBJECT') # Select all islands belonging to 'Head' and children and enlarge them @@ -482,7 +490,7 @@ def first_bsdf(objs): bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.pack_islands(rotate=True, margin=margin) # detect if UVPackMaster installed and configured - try: # UVP doesn't respect margins when called like this, find out why + try: # UVP doesn't respect margins when called like this, find out why context.scene.uvp2_props.normalize_islands = False context.scene.uvp2_props.lock_overlapping_mode = '0' if use_decimation else '2' context.scene.uvp2_props.pack_to_others = False @@ -505,14 +513,14 @@ def first_bsdf(objs): self.set_values([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", 0.0) self.bake_pass(context, "diffuse", "DIFFUSE", {"COLOR"}, [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [0.5,0.5,0.5,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0.5, 0.5, 0.5, 1.0], True, int(margin * resolution / 2)) self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic Rotation") # Bake roughness, invert if pass_smoothness: self.bake_pass(context, "smoothness", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [1.0, 1.0, 1.0, 1.0], True, int(margin * resolution / 2)) image = bpy.data.images["SCRIPT_smoothness.png"] pixel_buffer = list(image.pixels) for idx in range(0, len(image.pixels)): @@ -521,12 +529,10 @@ def first_bsdf(objs): pixel_buffer[idx] = 1.0 - pixel_buffer[idx] image.pixels[:] = pixel_buffer - - # bake emit if pass_emit: self.bake_pass(context, "emit", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [0,0,0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0, 0, 0, 1.0], True, int(margin * resolution / 2)) # advanced: bake alpha from bsdf output if pass_alpha: @@ -540,7 +546,7 @@ def first_bsdf(objs): # Run the bake pass (bake roughness) self.bake_pass(context, "alpha", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [1,1,1,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [1, 1, 1, 1.0], True, int(margin * resolution / 2)) # Revert the changes (re-flip) self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Anisotropic") @@ -554,7 +560,7 @@ def first_bsdf(objs): # Run the bake pass self.bake_pass(context, "metallic", "ROUGHNESS", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 32, 0, [0,0,0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 32, 0, [0, 0, 0, 1.0], True, int(margin * resolution / 2)) # Revert the changes (re-flip) self.swap_links([obj for obj in collection.all_objects if obj.type == "MESH"], "Metallic", "Roughness") @@ -606,7 +612,7 @@ def first_bsdf(objs): reyemask.vertex_group = "RightEye" reyemask.invert_vertex_group = True self.bake_pass(context, "ao", "AO", {"AO"}, [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 512, 0, [1.0,1.0,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 512, 0, [1.0, 1.0, 1.0, 1.0], True, int(margin * resolution / 2)) if illuminate_eyes: if "leyemask" in obj.modifiers: obj.modifiers.remove(leyemask) @@ -620,13 +626,13 @@ def first_bsdf(objs): image.user_clear() bpy.data.images.remove(image) bpy.ops.image.new(name="SCRIPT_questdiffuse.png", width=resolution, height=resolution, - generated_type="BLANK", alpha=False) + generated_type="BLANK", alpha=False) image = bpy.data.images["SCRIPT_questdiffuse.png"] image.filepath = bpy.path.abspath("//CATS Bake/" + "SCRIPT_questdiffuse.png") diffuse_image = bpy.data.images["SCRIPT_diffuse.png"] ao_image = bpy.data.images["SCRIPT_ao.png"] - image.generated_width=resolution - image.generated_height=resolution + image.generated_width = resolution + image.generated_height = resolution image.scale(resolution, resolution) pixel_buffer = list(image.pixels) diffuse_buffer = diffuse_image.pixels[:] @@ -640,13 +646,12 @@ def first_bsdf(objs): pixel_buffer[idx] = 1.0 image.pixels[:] = pixel_buffer - # Bake highres normals if not use_decimation: # Just bake the traditional way if pass_normal: self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2)) else: if not normal_apply_trans: # Join meshes @@ -658,13 +663,13 @@ def first_bsdf(objs): bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) context.view_layer.objects.active = obj - bpy.ops.object.transform_apply(location = True, scale = True, rotation = True) + bpy.ops.object.transform_apply(location=True, scale=True, rotation=True) # Bake normals in object coordinates # TODO: 32-bit floats so we don't lose detail if pass_normal: self.bake_pass(context, "world", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") + (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2), normal_space="OBJECT") # Decimate. If 'preserve seams' is selected, forcibly preserve seams (seams from islands, deselect seams) bpy.ops.cats_decimation.auto_decimate(armature_name=arm_copy.name, preserve_seams=preserve_seams, seperate_materials=False) @@ -675,7 +680,7 @@ def first_bsdf(objs): # Remove all other materials while len(context.object.material_slots) > 0: - context.object.active_material_index = 0 #select the top material + context.object.active_material_index = 0 # select the top material bpy.ops.object.material_slot_remove() # Apply generated material (object normals -> normal map -> BSDF normal and other textures) @@ -695,7 +700,7 @@ def first_bsdf(objs): if pass_normal: normaltexnode = tree.nodes.new("ShaderNodeTexImage") if use_decimation: - normaltexnode.image = bpy.data.images["SCRIPT_world.png"] + normaltexnode.image = bpy.data.images["SCRIPT_world.png"] normaltexnode.location.x -= 500 normaltexnode.location.y -= 200 @@ -724,7 +729,7 @@ def first_bsdf(objs): # Bake tangent normals if use_decimation and pass_normal: self.bake_pass(context, "normal", "NORMAL", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], - (resolution, resolution), 128, 0, [0.5,0.5,1.0,1.0], True, int(margin * resolution / 2)) + (resolution, resolution), 128, 0, [0.5, 0.5, 1.0, 1.0], True, int(margin * resolution / 2)) # Update generated material to preview all of our passes if pass_normal: diff --git a/tools/common.py b/tools/common.py index 32747422..12e4d541 100644 --- a/tools/common.py +++ b/tools/common.py @@ -23,11 +23,12 @@ # Code author: GiveMeAllYourCats # Repo: https://github.com/michaeldegroot/cats-blender-plugin # Edits by: GiveMeAllYourCats, Hotox + import re -import os import bpy -import bmesh import time +import bmesh +import platform from math import degrees from mathutils import Vector @@ -44,10 +45,11 @@ from .register import register_wrap from ..translations import t -from mmd_tools_local import utils -from mmd_tools_local.panels import tool as mmd_tool -from mmd_tools_local.panels import util_tools as mmd_util_tools -from mmd_tools_local.panels import view_prop as mmd_view_prop +if platform.system() != "Linux" or bpy.app.version < (2, 90): + from mmd_tools_local import utils + from mmd_tools_local.panels import tool as mmd_tool + from mmd_tools_local.panels import util_tools as mmd_util_tools + from mmd_tools_local.panels import view_prop as mmd_view_prop # TODO: # - Add check if hips bone really needs to be rotated diff --git a/tools/importer.py b/tools/importer.py index b7c9eaab..6dca5fa1 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -27,6 +27,7 @@ import bpy import copy import zipfile +import platform import webbrowser import addon_utils import bpy_extras.io_utils @@ -41,11 +42,12 @@ from ..translations import t mmd_tools_installed = False -try: - import mmd_tools_local - mmd_tools_installed = True -except: - pass +if platform.system() != "Linux" or bpy.app.version < (2, 90): + try: + import mmd_tools_local + mmd_tools_installed = True + except: + pass current_blender_version = str(bpy.app.version[:2])[1:-1].replace(', ', '.') diff --git a/tools/translate.py b/tools/translate.py index 3faf1bd3..2492f5dc 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -29,6 +29,7 @@ import copy import json import pathlib +import platform import traceback import collections import requests.exceptions @@ -40,9 +41,11 @@ from .register import register_wrap from .. import globs from ..googletrans import Translator -from mmd_tools_local import translations from ..translations import t +if platform.system() != "Linux" or bpy.app.version < (2, 90): + from mmd_tools_local import translations as mmd_translations + dictionary = None dictionary_google = None @@ -552,9 +555,10 @@ def translate(to_translate, add_space=False, translating_shapes=False): def fix_jp_chars(name): - for values in translations.jp_half_to_full_tuples: - if values[0] in name: - name = name.replace(values[0], values[1]) + if platform.system() != "Linux" or bpy.app.version < (2, 90): + for values in mmd_translations.jp_half_to_full_tuples: + if values[0] in name: + name = name.replace(values[0], values[1]) return name diff --git a/translations/__init__.py b/translations/__init__.py index 86153697..8280c887 100644 --- a/translations/__init__.py +++ b/translations/__init__.py @@ -4,7 +4,6 @@ import importlib import addon_utils from bpy.app.translations import locale -from .en_US import dictionary as dict_en # Get package name, important for relative imports package_name = '' @@ -12,35 +11,54 @@ if mod.bl_info['name'] == 'Cats Blender Plugin': package_name = mod.__name__ -# Get all supported languages in translations folder languages = [] -for file in os.listdir(os.path.dirname(__file__)): - lang_name = os.path.splitext(file)[0] - if len(lang_name) == 5 and lang_name[2] == '_' and lang_name != 'en_US': - languages.append(lang_name) +dictionary = {} +dictionary_en = {} +lang_module = None +lang_en_module = None -# Import the correct language dictionary -if locale in languages: - lang = importlib.import_module(package_name + '.translations.' + locale) - dictionary = lang.dictionary -else: - dictionary = dict_en + +def load_translations(): + global languages, dictionary, dictionary_en, lang_module, lang_en_module + languages = [] + dictionary = {} + dictionary_en = {} + + # Get all supported languages in translations folder + for file in os.listdir(os.path.dirname(__file__)): + lang_name = os.path.splitext(file)[0] + if len(lang_name) == 5 and lang_name[2] == '_' and lang_name != 'en_US': + languages.append(lang_name) + + # Import the correct language dictionary + if locale in languages: + if lang_module: + importlib.reload(lang_module) + lang_module = importlib.import_module(package_name + '.translations.' + locale) + dictionary = lang_module.dictionary + + if lang_en_module: + importlib.reload(lang_en_module) + lang_en_module = importlib.import_module(package_name + '.translations.en_US') + dictionary_en = lang_en_module.dictionary + print('LOADED TRANSLATIONS') + print(dictionary_en.get('CreditsPanel.descContributors2')) def t(phrase: str, *args, **kwargs): # Translate the given phrase into Blender's current language. output = dictionary.get(phrase) if output is None: - output = dict_en.get(phrase) + output = dictionary_en.get(phrase) if output is None: print('Warning: Unknown phrase: ' + phrase) return phrase if isinstance(output, list): - newList = [] + new_list = [] for string in output: - newList.append(string.format(*args, **kwargs)) - return newList + new_list.append(string.format(*args, **kwargs)) + return new_list elif not isinstance(output, str): return output else: @@ -53,10 +71,15 @@ def check_missing_translations(): lang = importlib.import_module(package_name + '.translations.' + language) lang_dicts.append(lang.dictionary) - for key, value in dict_en.items(): + for key, value in dictionary_en.items(): if value is None: print('Translations en_US: Value missing for key: ' + key) for lang_dict in lang_dicts: if lang_dict.get(key) is None: print('Translations ' + lang_dict.get('name') + ': Value missing for key: ' + key) + + del lang_dicts + + +load_translations() \ No newline at end of file diff --git a/translations/en_US.py b/translations/en_US.py index 59bd3a90..e228fd90 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -186,7 +186,8 @@ 'CreditsPanel.desc1': 'Cats Blender Plugin (', 'CreditsPanel.desc2': 'Created by Hotox and GiveMeAllYourCats', 'CreditsPanel.desc3': 'For the awesome VRChat community <3', - 'CreditsPanel.desc4': 'Special thanks to: Shotariya and Neitri', + 'CreditsPanel.desc4': 'Special thanks to:', + 'CreditsPanel.descContributors': 'Feilen, Jordo, Ruubick, Shotariya and Neitri', 'CreditsPanel.desc5': 'Do you need help or found a bug?', # Tools Armature diff --git a/translations/ja_JP.py b/translations/ja_JP.py index e56936e7..3229f2e3 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -9,14 +9,14 @@ 'Main.error.restartAdmin': '\n\nFaulty CATS installation found!' '\nTo fix this restart Blender as admin! ' '\n', - 'Main.error.deleteFollowing': ' ' \ - ' '\ - '\n\nFaulty CATS installation found!' \ - '\nTo fix this delete the following files and folders inside your addons folder:' \ + 'Main.error.deleteFollowing': ' ' + ' ' + '\n\nFaulty CATS installation found!' + '\nTo fix this delete the following files and folders inside your addons folder:' '\n', 'Main.error.installViaPreferences': '\n\nFaulty CATS installation found!' - '\nPlease install CATS via User Preferences and restart Blender!' - '\n', + '\nPlease install CATS via User Preferences and restart Blender!' + '\n', 'Main.error.restartAndEnable': '\n\nFaulty CATS installation was found and fixed!' '\nPlease restart Blender and enable CATS again!' '\n', @@ -179,20 +179,21 @@ # Tools Armature 'FixArmature.label': 'モデル修正', - 'FixArmature.desc': 'Automatically:\n' \ - '- Reparents bones\n' \ - '- Removes unnecessary bones, objects, groups & constraints\n' \ - '- Translates and renames bones & objects\n' \ - '- Merges weight paints\n' \ - '- Corrects the hips\n' \ - '- Joins meshes\n' \ - '- Converts morphs into shapes\n' \ + 'FixArmature.desc': 'Automatically:\n' + '- Reparents bones\n' + '- Removes unnecessary bones, objects, groups & constraints\n' + '- Translates and renames bones & objects\n' + '- Merges weight paints\n' + '- Corrects the hips\n' + '- Joins meshes\n' + '- Converts morphs into shapes\n' '- Corrects shading', 'FixArmature.error.noMesh': ['No mesh inside the armature found!', 'If there are meshes outside of the armature,', 'set the armature as the parent of the meshes.'], - # Format strings? vvvv t(str, fixed_uv_coords) -> The model was successfully fixed, but there were {} faulty UV - 'FixArmature.error.faultyUV1': 'The model was successfully fixed, but there were {uvcoord} faulty UV coordinates.', # 'The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', + # Format strings? vvvv t(str, fixed_uv_coords) -> The model was successfully fixed, but there were {} faulty UV + 'FixArmature.error.faultyUV1': 'The model was successfully fixed, but there were {uvcoord} faulty UV coordinates.', + # 'The model was successfully fixed, but there were ' + str(fixed_uv_coords) + ' faulty UV coordinates.', 'FixArmature.error.faultyUV2': 'This could result in broken textures and you might have to fix them manually.', 'FixArmature.error.faultyUV3': 'This issue is often caused by edits in PMX editor.', 'FixArmature.fixedSuccess': 'Model successfully fixed.', @@ -206,14 +207,14 @@ # Tools Armature Manual 'StartPoseMode.label': 'ポーズモードを開始', - 'StartPoseMode.desc': 'Starts the pose mode.\n' \ + 'StartPoseMode.desc': 'Starts the pose mode.\n' 'This lets you test how your model will move', 'StopPoseMode.label': 'ポーズモードを停止する', 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', 'PoseToShape.label': 'シェイプキーへのポーズ', - 'PoseToShape.desc': 'This saves your current pose as a new shape key.' \ + 'PoseToShape.desc': 'This saves your current pose as a new shape key.' '\nThe new shape key will be at the bottom of your shape key list of the mesh', 'PoseNamePopup.label': 'このシェイプキーに名前を付ける:', @@ -221,75 +222,75 @@ 'PoseNamePopup.success': 'Pose successfully saved as shape key.', 'PoseToRest.label': 'レストポーズとして適用する', - 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' \ - '\n' \ - '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' \ + 'PoseToRest.desc': 'This applies the current pose position as the new rest position.' + '\n' + '\nIf you scale the bones equally on each axis the shape keys will be scaled correctly as well!' '\nWARNING: This can have unwanted effects on shape keys, so be careful when modifying the head with this', 'PoseToRest.success': 'Pose successfully applied as rest pose.', 'JoinMeshes.label': 'メッシュに参加する', - 'JoinMeshes.desc': 'Joins all meshes of this model together.' \ - '\nIt also:' \ - '\n - Reorders all shape keys correctly' \ - '\n - Applies all transforms' \ - '\n - Repairs broken armature modifiers' \ - '\n - Applies all decimation and mirror modifiers' \ + 'JoinMeshes.desc': 'Joins all meshes of this model together.' + '\nIt also:' + '\n - Reorders all shape keys correctly' + '\n - Applies all transforms' + '\n - Repairs broken armature modifiers' + '\n - Applies all decimation and mirror modifiers' '\n - Merges UV maps correctly', 'JoinMeshes.failure': 'Meshes could not be joined!', 'JoinMeshes.success': 'Meshes joined.', 'JoinMeshesSelected.label': '選択したメッシュを結合する', - 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' \ - '\nIt also:' \ - '\n - Reorders all shape keys correctly' \ - '\n - Applies all transforms' \ - '\n - Repairs broken armature modifiers' \ - '\n - Applies all decimation and mirror modifiers' \ + 'JoinMeshesSelected.desc': 'Joins all selected meshes of this model together.' + '\nIt also:' + '\n - Reorders all shape keys correctly' + '\n - Applies all transforms' + '\n - Repairs broken armature modifiers' + '\n - Applies all decimation and mirror modifiers' '\n - Merges UV maps correctly', 'JoinMeshesSelected.error.noSelect': 'No meshes selected! Please select the meshes you want to join in the hierarchy!', 'JoinMeshesSelected.error.cantJoin': 'Selected meshes could not be joined!', 'JoinMeshesSelected.success': 'Selected meshes joined.', 'SeparateByMaterials.label': '材料別に分離する', - 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' \ - '\n' \ + 'SeparateByMaterials.desc': 'Separates selected mesh by materials.\n' + '\n' 'Warning: Never decimate something where you might need the shape keys later (face, mouth, eyes..)', 'SeparateByMaterials.success': 'Successfully separated by materials.', 'SeparateByLooseParts.label': 'ルーズパーツに分離する', - 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' \ + 'SeparateByLooseParts.desc': 'Separates selected mesh by loose parts.\n' 'This acts like separating by materials but creates more meshes for more precision', 'SeparateByLooseParts.success': 'Successfully separated by loose parts.', 'SeparateByShapekeys.label': 'シェイプキーに区切る', - 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' \ - '\ndepending on whether it is effected by a shape key or not.' \ - '\n' \ + 'SeparateByShapekeys.desc': 'Separates selected mesh into two parts,' + '\ndepending on whether it is effected by a shape key or not.' + '\n' '\nVery useful for manual decimation', 'SeparateByShapekeys.success': 'Successfully separated by shape keys.', 'SeparateByCopyProtection.label': 'コピープロテクションに分離する', - 'SeparateByCopyProtection.desc': 'Separates selected mesh into two parts,' \ - '\ndepending on whether it is effected by the Cats Copy Protection or not.' \ - '\n' \ + 'SeparateByCopyProtection.desc': 'Separates selected mesh into two parts,' + '\ndepending on whether it is effected by the Cats Copy Protection or not.' + '\n' '\nUseful if you have the Copy Protection enabled on multiple selected parts of your model', 'SeparateByCopyProtection.success': 'Successfully separated by shape keys.', 'SeparateByX.error.noMesh': 'No meshes found!', - 'SeparateByX.error.multipleMesh': 'Multiple meshes found!' \ - '\nPlease select the mesh you want to separate!', + 'SeparateByX.error.multipleMesh': 'Multiple meshes found!' + '\nPlease select the mesh you want to separate!', 'SeparateByX.warn.noSeparation': 'No meshes had to be separated!', 'MergeWeights.label': '親に重みをマージする', - 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' \ - '\n' \ + 'MergeWeights.desc': 'Deletes the selected bones and adds their weight to their respective parents.' + '\n' '\nOnly available in Edit or Pose Mode with bones selected', 'MergeWeights.success': 'Deleted {number} bones and added their weights to their parents.', 'MergeWeightsToActive.label': 'ウェイトをアクティブ にマージする', - 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' \ - '\nThe active bone is the one you selected last.' \ - '\n' \ + 'MergeWeightsToActive.desc': 'Deletes the selected bones except the active one and adds their weights to the active bone.' + '\nThe active bone is the one you selected last.' + '\n' '\nOnly available in Edit or Pose Mode with bones selected', 'MergeWeightsToActive.success': 'Deleted {number} bones and added their weights to the active bone.', @@ -302,7 +303,7 @@ 'ApplyAllTransformations.success': 'Transformations applied.', 'RemoveZeroWeightBones.label': 'ゼロ ウェイト ボーンを削除する', - 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' \ + 'RemoveZeroWeightBones.desc': 'Cleans up the bones hierarchy, deleting all bones that don\'t directly affect any vertices\n' 'Don\'t use this if you plan to use \'Fix Model\'', 'RemoveZeroWeightBones.success': 'Deleted {number} zero weight bones.', @@ -315,25 +316,25 @@ 'RemoveConstraints.success': 'Removed all bone constraints.', 'RecalculateNormals.label': '法線を再計算する', - 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' \ - 'Don\'t use this on good looking meshes as this can screw them up.\n' \ + 'RecalculateNormals.desc': 'Makes normals point inside of the selected mesh.\n\n' + 'Don\'t use this on good looking meshes as this can screw them up.\n' 'Use this if there are random inverted or darker faces on the mesh', 'RecalculateNormals.success': 'Recalculated all normals.', 'FlipNormals.label': '法線を反転', - 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' \ + 'FlipNormals.desc': 'Flips the direction of the faces\' normals of the selected mesh.\n' 'Use this if all normals are inverted', 'FlipNormals.success': 'Flipped all normals.', 'RemoveDoubles.label': 'ダブルスを削除する', - 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ - '\nThis is more safe than doing it manually:' \ - '\n - leaves shape keys completely untouched' \ + 'RemoveDoubles.desc': 'Merges duplicated faces and vertices of the selected meshes.' + '\nThis is more safe than doing it manually:' + '\n - leaves shape keys completely untouched' '\n - but removes less doubles overall', 'RemoveDoubles.success': 'Removed {number} vertices.', 'RemoveDoublesNormal.label': 'ダブルを通常どおりに削除する', - 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' \ + 'RemoveDoublesNormal.desc': 'Merges duplicated faces and vertices of the selected meshes.' '\nThis is exactly like doing it manually', 'RemoveDoublesNormal.success': 'Removed {number} vertices.', @@ -343,9 +344,9 @@ 'FixVRMShapesButton.success': 'Fixed VRM shapekeys.', 'FixFBTButton.label': '全身追跡を修正', - 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' \ - '\n' \ - '\nApplies a general fix for Full Body Tracking.' \ + 'FixFBTButton.desc': 'WARNING: This fix is no longer needed for VRChat, you should not use it!' + '\n' + '\nApplies a general fix for Full Body Tracking.' '\nIgnore the \"Spine length zero\" warning in Unity', 'FixFBTButton.error.bonesNotFound': 'Required bones could not be found!' '\nPlease make sure that your armature contains the following bones:' @@ -355,9 +356,9 @@ 'FixFBTButton.success': 'Successfully applied the Full Body Tracking fix.', 'RemoveFBTButton.label': '全身追跡の修正を削除', - 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' \ - '\n' \ - '\nRequires bones:' \ + 'RemoveFBTButton.desc': 'Removes the fix for Full Body Tracking, since it is no longer advised to use it.' + '\n' + '\nRequires bones:' '\n - Hips, Spine, Left leg, Right leg, Left leg 2, Right leg 2', 'RemoveFBTButton.error.bonesNotFound': 'Required bones could not be found!' '\nPlease make sure that your armature contains the following bones:' @@ -372,28 +373,28 @@ # Tools Armature Custom 'MergeArmature.label': 'マージアーマチュア', - 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' \ - '\nYou should fix both armatures with Cats first.' \ + 'MergeArmature.desc': 'Merges the selected merge armature into the base armature.' + '\nYou should fix both armatures with Cats first.' '\nOnly move the mesh of the merge armature to the desired position, the bones will be moved automatically', 'MergeArmature.error.notFound': 'The armature "{name}" could not be found.', - 'MergeArmature.error.checkTransforms': [ 'Please make sure that the parent of the merge armature has the following transforms:', - ' - Location at 0', - ' - Rotation at 0', - ' - Scale at 1'], - 'MergeArmature.error.pleaseFix': [ 'Please use the "Fix Model" feature on the selected armatures first!', - 'Make sure to select the armature you want to fix above the "Fix Model" button!', - 'After that please only move the mesh (not the armature!) to the desired position.'], + 'MergeArmature.error.checkTransforms': ['Please make sure that the parent of the merge armature has the following transforms:', + ' - Location at 0', + ' - Rotation at 0', + ' - Scale at 1'], + 'MergeArmature.error.pleaseFix': ['Please use the "Fix Model" feature on the selected armatures first!', + 'Make sure to select the armature you want to fix above the "Fix Model" button!', + 'After that please only move the mesh (not the armature!) to the desired position.'], 'MergeArmature.success': 'Armatures successfully joined.', 'AttachMesh.label': 'メッシュをアタッチ', - 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' \ - '\n' \ - '\nINFO: The mesh will only be assigned to the selected bone.' \ + 'AttachMesh.desc': 'Attaches the selected mesh to the selected bone of the selected armature.' + '\n' + '\nINFO: The mesh will only be assigned to the selected bone.' '\nE.g.: A jacket won\'t work, because it requires multiple bones', 'AttachMesh.success': 'Mesh successfully attached to armature.', 'CustomModelTutorialButton.label': 'ドキュメントに移動', - 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) + 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) 'CustomModelTutorialButton.success': 'Documentation', 'merge_armatures.error.transformReset': ['If you want to rotate the new part, only modify the mesh instead of the armature,', @@ -431,7 +432,7 @@ # Tools Bonemerge 'BoneMergeButton.label': 'ボーンをマージする', - 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' \ + 'BoneMergeButton.desc': 'Merges the given percentage of bones together.\n' 'This is useful to reduce the amount of bones used by Dynamic Bones.', 'BoneMergeButton.success': 'Merged bones.', @@ -440,7 +441,7 @@ # Tools Copy protection 'CopyProtectionEnable.label': '保護を有効にする', - 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' \ + 'CopyProtectionEnable.desc': 'Protects your model from piracy. NOT a 100% safe protection!' '\nRead the documentation before use', 'CopyProtectionEnable.success': 'Model secured!', @@ -470,40 +471,40 @@ 'ScanButton.desc': 'Separates the mesh.', 'AddShapeButton.label': '追加', - 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' \ + 'AddShapeButton.desc': 'Adds the selected shape key to the whitelist.\n' 'This means that every mesh containing that shape key will be not decimated.', 'AddMeshButton.label': '追加', - 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' \ + 'AddMeshButton.desc': 'Adds the selected mesh to the whitelist.\n' 'This means that this mesh will be not decimated.', 'RemoveShapeButton.label': '', - 'RemoveShapeButton.desc': 'Removes the selected shape key from the whitelist.\n' \ + 'RemoveShapeButton.desc': 'Removes the selected shape key from the whitelist.\n' 'This means that this shape key is no longer decimation safe!', 'RemoveMeshButton.label': '', - 'RemoveMeshButton.desc': 'Removes the selected mesh from the whitelist.\n' \ + 'RemoveMeshButton.desc': 'Removes the selected mesh from the whitelist.\n' 'This means that this mesh will be decimated.', 'AutoDecimateButton.label': 'Quick Decimation', - 'AutoDecimateButton.desc': 'This will automatically decimate your model while preserving the shape keys.\n' \ + 'AutoDecimateButton.desc': 'This will automatically decimate your model while preserving the shape keys.\n' 'You should manually remove unimportant meshes first.', 'AutoDecimateButton.error.noMesh': 'No meshes found!', - 'decimate.cantDecimateWithSettings': 'This model can not be decimated to {number} tris with the specified settings.', + 'decimate.cantDecimateWithSettings': 'This model can not be decimated to {number} tris with the specified settings.', 'decimate.safeTryOptions': 'Try to use Custom, Half or Full Decimation.', 'decimate.halfTryOptions': 'Try to use Custom or Full Decimation.', 'decimate.customTryOptions': 'Select fewer shape keys and/or meshes or use Full Decimation.', 'decimate.disableFingersOrIncrease': 'Disable \'Save Fingers\' or increase the Tris Count.', - 'decimate.disableFingers': 'or disable \'Save Fingers\'.', # This comes after one of the previous xTryOptions + 'decimate.disableFingers': 'or disable \'Save Fingers\'.', # This comes after one of the previous xTryOptions 'decimate.noDecimationNeeded': 'The model already has less than {number} tris. Nothing had to be decimated.', 'decimate.cantDecimate1': 'The model could not be decimated to {number} tris.', 'decimate.cantDecimate2': 'It got decimated as much as possible within the limits.', # Tools Eyetracking 'CreateEyesButton.label': 'アイトラッキングを作成する', - 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' \ - 'You should do decimation before this operation.\n' \ + 'CreateEyesButton.desc': 'This will let you track someone when they come close to you and it enables blinking.\n' + 'You should do decimation before this operation.\n' 'Test the resulting eye movement in the \'Testing\' tab.', 'CreateEyesButton.error.noShapeSelected': 'You have no shape keys selected.' '\nPlease choose a mesh containing shape keys or check "Disable Eye Blinking".', @@ -518,8 +519,8 @@ 'CreateEyesButton.success': 'Created eye tracking!', 'StartTestingButton.label': 'スタートアイテスト', - 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' \ - 'Don\'t forget to stop the Testing process afterwards.\n' \ + 'StartTestingButton.desc': 'This will let you test how the eye movement will look ingame.\n' + 'Don\'t forget to stop the Testing process afterwards.\n' 'Bones "LeftEye" and "RightEye" are required.', 'StopTestingButton.label': 'ストップアイテスト', @@ -530,15 +531,15 @@ 'ResetRotationButton.desc': 'This resets the eye positions.', 'AdjustEyesButton.label': '目の範囲を設定', - 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' \ + 'AdjustEyesButton.desc': 'Lets you re-adjust the movement range of the eyes.\n' 'This gets saved', 'AdjustEyesButton.error.noVertex': 'The bone "{bone}" has no existing vertex group or no vertices assigned to it.' '\nThis might be because you selected the wrong mesh or the wrong bone.' '\nAlso make sure to join your meshes before creating eye tracking and make sure that the eye bones actually move the eyes in pose mode.', 'StartIrisHeightButton.label': 'アイリスの高さの調整を開始', - 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' \ - 'Use this to fix clipping of the iris into the eye ball.\n' \ + 'StartIrisHeightButton.desc': 'Lets you readjust the distance of the iris from the eye ball.\n' + 'Use this to fix clipping of the iris into the eye ball.\n' 'This gets saved.', 'TestBlinking.label': 'テスト', @@ -552,30 +553,30 @@ # Tools Importer 'ImportAnyModel.label': '任意のモデルをインポート', - 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' \ - '\n' \ - '\nSupported types:' \ - '\n- MMD: .pmx/.pmd' \ - '\n- XNALara: .xps/.mesh/.ascii' \ - '\n- Source: .smd/.qc' \ - '\n- VRM: .vrm' \ - '\n- FBX .fbx ' \ - '\n- DAE: .dae ' \ + 'ImportAnyModel.desc2.79': 'Import a model of any supported type.' + '\n' + '\nSupported types:' + '\n- MMD: .pmx/.pmd' + '\n- XNALara: .xps/.mesh/.ascii' + '\n- Source: .smd/.qc' + '\n- VRM: .vrm' + '\n- FBX .fbx ' + '\n- DAE: .dae ' '\n- ZIP: .zip', - 'ImportAnyModel.desc2.8': 'Import a model of any supported type.' \ - '\n' \ - '\nSupported types:' \ - '\n- MMD: .pmx/.pmd' \ - '\n- XNALara: .xps/.mesh/.ascii' \ - '\n- Source: .smd/.qc/.vta/.dmx' \ - '\n- VRM: .vrm' \ - '\n- FBX: .fbx' \ - '\n- DAE: .dae ' \ + 'ImportAnyModel.desc2.8': 'Import a model of any supported type.' + '\n' + '\nSupported types:' + '\n- MMD: .pmx/.pmd' + '\n- XNALara: .xps/.mesh/.ascii' + '\n- Source: .smd/.qc/.vta/.dmx' + '\n- VRM: .vrm' + '\n- FBX: .fbx' + '\n- DAE: .dae ' '\n- ZIP: .zip', 'ImportAnyModel.importantInfo.label': '重要な情報(ここにホバー)', 'ImportAnyModel.importantInfo.desc': 'If you want to modify the import settings, use the button next to the Import button.\n\n', 'ImportAnyModel.error.emptyZip': 'The selected zip file contains no importable models.', - 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' \ + 'ImportAnyModel.error.unsupportedFBX': 'The FBX file version is unsupported!' '\nPlease use a tool such as the "Autodesk FBX Converter" to make it compatible.', 'ZipPopup.label': '圧縮モデルの選択:', @@ -632,8 +633,8 @@ 'VrmToolsButton.success': 'VRM Importer link opened', 'ExportModel.label': 'モデルのエクスポート', - 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' \ - '\n' \ + 'ExportModel.desc': 'Export this model as .fbx for Unity.\n' + '\n' 'Automatically sets the optimal export settings', 'ExportModel.error.notEnabled': 'FBX Exporter not enabled! Please enable it in your User Preferences.', @@ -672,35 +673,35 @@ 'OneTexPerMatButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.', 'OneTexPerMatOnlyButton.label': '1つのマテリアルテクスチャ', - 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' \ - '\nAlso removes the textures from the material instead of disabling it.' \ + 'OneTexPerMatOnlyButton.desc': 'Have all material slots ignore extra texture slots as these are not used by VRChat.' + '\nAlso removes the textures from the material instead of disabling it.' '\nThis 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': 'テクスチャを標準化する', - 'StandardizeTextures.desc': 'Enables Color and Alpha on every texture, sets the blend method to Multiply' \ + 'StandardizeTextures.desc': 'Enables Color and Alpha on every texture, sets the blend method to Multiply' '\nand changes the materials transparency to Z-Transparency', 'StandardizeTextures.success': 'All textures are now standardized.', 'CombineMaterialsButton.label': '同じマテリアルを組み合わせる', - 'CombineMaterialsButton.desc': 'Combines similar materials into one, reducing draw calls.\n' \ - 'Your avatar should visibly look the same after this operation.\n' \ - 'This is a very important step for optimizing your avatar.\n' \ + 'CombineMaterialsButton.desc': 'Combines similar materials into one, reducing draw calls.\n' + 'Your avatar should visibly look the same after this operation.\n' + 'This is a very important step for optimizing your avatar.\n' 'If you have problems with this, please tell us!\n', 'CombineMaterialsButton.error.noChanges': 'No materials combined.', 'CombineMaterialsButton.success': 'Combined {number} materials!', 'ConvertAllToPngButton.label': 'テクスチャをPNGに変換', - 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' \ - '\nThis helps with transparency and compatibility issues.' \ + 'ConvertAllToPngButton.desc': 'Converts all texture files into PNG files.' + '\nThis helps with transparency and compatibility issues.' '\n\nThe converted image files will be saved next to the old ones', 'ConvertAllToPngButton.success': 'Converted {number} to PNG files.', # Tools Root bone 'RootButton.label': '親のボーンに接続する', - 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' \ + 'RootButton.desc': 'This will duplicate the parent of the bones and reparent them to the duplicate.\n' 'Very useful for Dynamic Bones.', 'RootButton.success': 'Bones parented!', @@ -718,7 +719,7 @@ 'ResetGoogleDictButton.resetInfo': 'Local Google Dictionary cleared!', 'DebugTranslations.label': 'Google翻訳をデバッグ', # DEV ONLY - 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' \ + 'DebugTranslations.desc': 'Tests Google translations and prints the response into a file called \'google-response.txt\' located in the cats addon folder > resources' '\nThis button is only visible in the cats development version', # DEV ONLY 'DebugTranslations.error': 'Errors found, response printed!!', # DEV ONLY 'DebugTranslations.success': 'No issues with Google Translations found, response printed!', # DEV ONLY @@ -732,9 +733,9 @@ 'If you didn\'t change the shape key order, you can revert the shape keys from top to bottom.'], 'ShapeKeyApplier.error.revertCustomBasis.scale': 7.3, 'ShapeKeyApplier.error.revert': ['To revert the shape keys, please apply the "Reverted" shape keys in reverse order.', - 'Start with the reverted shape key that uses the relative key called "Basis".', - '', - "If you didn't change the shape key order, you can revert the shape keys from top to bottom."], + 'Start with the reverted shape key that uses the relative key called "Basis".', + '', + "If you didn't change the shape key order, you can revert the shape keys from top to bottom."], 'ShapeKeyApplier.error.revert.scale': 7.3, 'ShapeKeyApplier.successRemoved': 'Successfully removed shapekey "{name}" from the Basis.', 'ShapeKeyApplier.successSet': 'Successfully set shapekey "{name}" as the new Basis.', @@ -795,7 +796,7 @@ # Tools Viseme 'AutoVisemeButton.label': 'バイセムを作成する', - 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' \ + 'AutoVisemeButton.desc': 'This will give your avatar the ability to mimic each sound that comes from your mouth by blending between various shapes to mimic your actual voice.\n' 'It will generate 15 shape keys from the 3 shape keys you specify', 'AutoVisemeButton.error.noShapekeys': 'This mesh has no shapekeys!', 'AutoVisemeButton.error.selectShapekeys': 'Please select the correct mouth shapekeys instead of "Basis"!', @@ -1191,5 +1192,4 @@ 'bpy.types.Scene.cats_update_action.defer.label': '後で通知する', 'bpy.types.Scene.cats_update_action.defer.desc': 'Hides the update notification til the next Blender restart', - -} \ No newline at end of file +} diff --git a/ui/credits.py b/ui/credits.py index 564b2a2d..b5b72160 100644 --- a/ui/credits.py +++ b/ui/credits.py @@ -24,11 +24,14 @@ def draw(self, context): row = col.row(align=True) row.label(text=t('CreditsPanel.desc2')) row = col.row(align=True) - row.label(text=t('CreditsPanel.desc3')) row.scale_y = 0.5 + row.label(text=t('CreditsPanel.desc3')) col.separator() row = col.row(align=True) row.label(text=t('CreditsPanel.desc4')) + row = col.row(align=True) + row.scale_y = 0.5 + row.label(text=t('CreditsPanel.descContributors')) col.separator() row = col.row(align=True) row.label(text=t('CreditsPanel.desc5')) From 78ae1103cbc9494d600c9114933edb1f0c8a7f7f Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 28 Nov 2020 12:06:16 +0100 Subject: [PATCH 51/64] Added another linux check --- tools/common.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tools/common.py b/tools/common.py index 12e4d541..8eb666c9 100644 --- a/tools/common.py +++ b/tools/common.py @@ -2114,18 +2114,19 @@ def toggle_mmd_tabs(self, context): ] print('Toggling mmd tabs') - if context.scene.show_mmd_tabs: - for cls in mmd_cls: - try: - bpy.utils.register_class(cls) - except: - pass - else: - for cls in reversed(mmd_cls): - try: - bpy.utils.unregister_class(cls) - except: - pass + if platform.system() != "Linux" or bpy.app.version < (2, 90): + if context.scene.show_mmd_tabs: + for cls in mmd_cls: + try: + bpy.utils.register_class(cls) + except: + pass + else: + for cls in reversed(mmd_cls): + try: + bpy.utils.unregister_class(cls) + except: + pass Settings.update_settings(None, None) From 6fdc7ad84aa0f650d3cd5a8a58d6eda881ab07b4 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 2 Dec 2020 01:49:23 +0100 Subject: [PATCH 52/64] Added button to "Start/Stop Pose Mode" which starts/stops pose mode without resetting the current pose --- README.md | 1 + tools/armature.py | 2 +- tools/armature_manual.py | 166 +++++++++++++++++++++++++-------------- tools/common.py | 1 - tools/decimation.py | 14 ++-- translations/en_US.py | 3 + ui/armature.py | 14 +++- 7 files changed, 131 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 785e6e42..d3217eff 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,7 @@ It checks for a new version automatically once every day. - Cats is now fully compatible with Blender 2.90 and 2.91 - Added "Show mmd_tools tabs" option to Settings - This allows you show and hide the "MMD" and "Misc" tabs added by the mmd_tools plugin + - Added button to "Start/Stop Pose Mode" which starts/stops pose mode without resetting the current pose - Changed link to a new vrm importer since the old one dropped support - Fixed Google Translations randomly not working - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 diff --git a/tools/armature.py b/tools/armature.py index e12f7392..016ea2c3 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -167,7 +167,7 @@ def execute(self, context): mmd_root.use_sphere_texture = False # Convert mmd bone morphs into shape keys - if len(mmd_root.bone_morphs) > 0: + if hasattr(mmd_root, 'bone_morphs') and len(mmd_root.bone_morphs) > 0: current_step = 0 wm.progress_begin(current_step, len(mmd_root.bone_morphs)) diff --git a/tools/armature_manual.py b/tools/armature_manual.py index 883d6961..feac47b3 100644 --- a/tools/armature_manual.py +++ b/tools/armature_manual.py @@ -47,54 +47,77 @@ def poll(cls, context): return True def execute(self, context): - saved_data = Common.SavedData() + start_pose_mode(reset_pose=True) + return {'FINISHED'} - current = "" - if bpy.context.active_object and bpy.context.active_object.mode == 'EDIT' and bpy.context.active_object.type == 'ARMATURE' and len( - bpy.context.selected_editable_bones) > 0: - current = bpy.context.selected_editable_bones[0].name - if version_2_79_or_older(): - bpy.context.space_data.use_pivot_point_align = False - bpy.context.space_data.show_manipulator = True - else: - pass - # TODO +@register_wrap +class StartPoseModeNoReset(bpy.types.Operator): + bl_idname = 'cats_manual.start_pose_mode_no_reset' + bl_label = t('StartPoseMode.label') + bl_description = t('StartPoseModeNoReset.desc') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - armature = Common.set_default_stage() - Common.switch('POSE') - armature.data.pose_position = 'POSE' + @classmethod + def poll(cls, context): + if Common.get_armature() is None: + return False + return True - for mesh in Common.get_meshes_objects(): - if Common.has_shapekeys(mesh): - for shape_key in mesh.data.shape_keys.key_blocks: - shape_key.value = 0 + def execute(self, context): + start_pose_mode(reset_pose=False) + return {'FINISHED'} - for pb in armature.data.bones: - pb.select = True + +def start_pose_mode(reset_pose=True): + saved_data = Common.SavedData() + + current = "" + if bpy.context.active_object and bpy.context.active_object.mode == 'EDIT' and bpy.context.active_object.type == 'ARMATURE' and len( + bpy.context.selected_editable_bones) > 0: + current = bpy.context.selected_editable_bones[0].name + + if version_2_79_or_older(): + bpy.context.space_data.use_pivot_point_align = False + bpy.context.space_data.show_manipulator = True + else: + pass + # TODO + + armature = Common.set_default_stage() + Common.switch('POSE') + armature.data.pose_position = 'POSE' + + for mesh in Common.get_meshes_objects(): + if Common.has_shapekeys(mesh): + for shape_key in mesh.data.shape_keys.key_blocks: + shape_key.value = 0 + + for pb in armature.data.bones: + pb.select = True + + if reset_pose: bpy.ops.pose.rot_clear() bpy.ops.pose.scale_clear() bpy.ops.pose.transforms_clear() - bone = armature.data.bones.get(current) - if bone is not None: - for pb in armature.data.bones: - if bone.name != pb.name: - pb.select = False - else: - for index, pb in enumerate(armature.data.bones): - if index != 0: - pb.select = False - - if version_2_79_or_older(): - bpy.context.space_data.transform_manipulators = {'ROTATE'} - else: - bpy.ops.wm.tool_set_by_id(name="builtin.rotate") + bone = armature.data.bones.get(current) + if bone is not None: + for pb in armature.data.bones: + if bone.name != pb.name: + pb.select = False + else: + for index, pb in enumerate(armature.data.bones): + if index != 0: + pb.select = False - saved_data.load(hide_only=True) - Common.hide(armature, False) + if version_2_79_or_older(): + bpy.context.space_data.transform_manipulators = {'ROTATE'} + else: + bpy.ops.wm.tool_set_by_id(name="builtin.rotate") - return {'FINISHED'} + saved_data.load(hide_only=True) + Common.hide(armature, False) @register_wrap @@ -111,41 +134,64 @@ def poll(cls, context): return True def execute(self, context): - saved_data = Common.SavedData() - armature = Common.get_armature() - Common.set_active(armature) + stop_pose_mode(reset_pose=True) + return {'FINISHED'} - # Make all objects visible - bpy.ops.object.hide_view_clear() - for pb in armature.data.bones: - pb.hide = False - pb.select = True +@register_wrap +class StopPoseModeNoReset(bpy.types.Operator): + bl_idname = 'cats_manual.stop_pose_mode_no_reset' + bl_label = t('StopPoseMode.label') + bl_description = t('StopPoseModeNoReset.desc') + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + + @classmethod + def poll(cls, context): + if Common.get_armature() is None: + return False + return True + + def execute(self, context): + stop_pose_mode(reset_pose=False) + return {'FINISHED'} + + +def stop_pose_mode(reset_pose=True): + saved_data = Common.SavedData() + armature = Common.get_armature() + Common.set_active(armature) + + # Make all objects visible + bpy.ops.object.hide_view_clear() + + for pb in armature.data.bones: + pb.hide = False + pb.select = True + if reset_pose: bpy.ops.pose.rot_clear() bpy.ops.pose.scale_clear() bpy.ops.pose.transforms_clear() - for pb in armature.data.bones: - pb.select = False - armature = Common.set_default_stage() - # armature.data.pose_position = 'REST' + for pb in armature.data.bones: + pb.select = False - for mesh in Common.get_meshes_objects(): - if Common.has_shapekeys(mesh): - for shape_key in mesh.data.shape_keys.key_blocks: - shape_key.value = 0 + armature = Common.set_default_stage() + # armature.data.pose_position = 'REST' - if version_2_79_or_older(): - bpy.context.space_data.transform_manipulators = {'TRANSLATE'} - else: - bpy.ops.wm.tool_set_by_id(name="builtin.select_box") + for mesh in Common.get_meshes_objects(): + if Common.has_shapekeys(mesh): + for shape_key in mesh.data.shape_keys.key_blocks: + shape_key.value = 0 - Eyetracking.eye_left = None + if version_2_79_or_older(): + bpy.context.space_data.transform_manipulators = {'TRANSLATE'} + else: + bpy.ops.wm.tool_set_by_id(name="builtin.select_box") - saved_data.load(hide_only=True) + Eyetracking.eye_left = None - return {'FINISHED'} + saved_data.load(hide_only=True) @register_wrap diff --git a/tools/common.py b/tools/common.py index 8eb666c9..5441cda5 100644 --- a/tools/common.py +++ b/tools/common.py @@ -2092,7 +2092,6 @@ def fix_twist_bones(mesh, bones_to_delete): def fix_twist_bone_names(armature): # This will fix MMD twist bone names after the vertex groups have been fixed - for bone_type in ['Hand', 'Arm']: for suffix in ['L', 'R']: bone_twist = armature.data.edit_bones.get(bone_type + 'Twist_' + suffix) diff --git a/tools/decimation.py b/tools/decimation.py index 7417db91..5a0e00cc 100644 --- a/tools/decimation.py +++ b/tools/decimation.py @@ -123,6 +123,7 @@ class RemoveShapeButton(bpy.types.Operator): bl_label = t('RemoveShapeButton.label') bl_description = t('RemoveShapeButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + shape_name = bpy.props.StringProperty() def execute(self, context): @@ -137,6 +138,7 @@ class RemoveMeshButton(bpy.types.Operator): bl_label = t('RemoveMeshButton.label') bl_description = t('RemoveMeshButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + mesh_name = bpy.props.StringProperty() def execute(self, context): @@ -152,16 +154,16 @@ class AutoDecimateButton(bpy.types.Operator): bl_description = t('AutoDecimateButton.desc') bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - armature_name: bpy.props.StringProperty( - name = 'armature_name', + armature_name = bpy.props.StringProperty( + name='armature_name', ) - preserve_seams: bpy.props.BoolProperty( - name = 'preserve_seams', + preserve_seams = bpy.props.BoolProperty( + name='preserve_seams', ) - seperate_materials: bpy.props.BoolProperty( - name = 'seperate_materials' + seperate_materials = bpy.props.BoolProperty( + name='seperate_materials' ) def execute(self, context): diff --git a/translations/en_US.py b/translations/en_US.py index e228fd90..86d60135 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -222,9 +222,12 @@ 'StartPoseMode.label': 'Start Pose Mode', 'StartPoseMode.desc': 'Starts the pose mode.\n' 'This lets you test how your model will move', + 'StartPoseModeNoReset.desc': 'Starts the pose mode without resetting the pose.\n' + 'This lets you test how your model will move', 'StopPoseMode.label': 'Stop Pose Mode', 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', + 'StopPoseModeNoReset.desc': 'Stops the pose mode and keeps the current pose', 'PoseToShape.label': 'Pose to Shape Key', 'PoseToShape.desc': 'This saves your current pose as a new shape key.' diff --git a/ui/armature.py b/ui/armature.py index 3ad77377..5ca70d16 100644 --- a/ui/armature.py +++ b/ui/armature.py @@ -166,13 +166,23 @@ def draw(self, context): armature_obj = Common.get_armature() if not armature_obj or armature_obj.mode != 'POSE': - row = col.row(align=True) + split = col.row(align=True) + row = split.row(align=True) row.scale_y = 1.1 row.operator(Armature_manual.StartPoseMode.bl_idname, icon='POSE_HLT') + row = split.row(align=True) + row.alignment = 'RIGHT' + row.scale_y = 1.1 + row.operator(Armature_manual.StartPoseModeNoReset.bl_idname, text="", icon='POSE_HLT') else: - row = col.row(align=True) + split = col.row(align=True) + row = split.row(align=True) row.scale_y = 1.1 row.operator(Armature_manual.StopPoseMode.bl_idname, icon=globs.ICON_POSE_MODE) + row = split.row(align=True) + row.alignment = 'RIGHT' + row.scale_y = 1.1 + row.operator(Armature_manual.StopPoseModeNoReset.bl_idname, text='', icon=globs.ICON_POSE_MODE) if not Eyetracking.eye_left: row = col.row(align=True) row.scale_y = 0.9 From dcb2dc23dcb802d40f3acc65a69522815812b047 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sat, 5 Dec 2020 22:36:54 +0100 Subject: [PATCH 53/64] Switched to a different google translation library since the old one stopped working --- README.md | 2 +- __init__.py | 10 +- extern_tools/google_trans_new/LICENSE | 21 + extern_tools/google_trans_new/README.md | 138 ++++ extern_tools/google_trans_new/constant.py | 179 +++++ .../google_trans_new/google_trans_new.py | 251 ++++++ extern_tools/google_trans_new/test.txt | 720 ++++++++++++++++++ tools/settings.py | 5 +- tools/translate.py | 126 +-- 9 files changed, 1387 insertions(+), 65 deletions(-) create mode 100644 extern_tools/google_trans_new/LICENSE create mode 100644 extern_tools/google_trans_new/README.md create mode 100644 extern_tools/google_trans_new/constant.py create mode 100644 extern_tools/google_trans_new/google_trans_new.py create mode 100644 extern_tools/google_trans_new/test.txt diff --git a/README.md b/README.md index d3217eff..69aa387c 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ It checks for a new version automatically once every day. - This allows you show and hide the "MMD" and "Misc" tabs added by the mmd_tools plugin - Added button to "Start/Stop Pose Mode" which starts/stops pose mode without resetting the current pose - Changed link to a new vrm importer since the old one dropped support - - Fixed Google Translations randomly not working + - Fixed Google Translations no longer working - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 - More fixes for Blender 2.90 diff --git a/__init__.py b/__init__.py index 9061260d..b8425a83 100644 --- a/__init__.py +++ b/__init__.py @@ -76,7 +76,7 @@ else: import importlib importlib.reload(updater) - # importlib.reload(mmd_tools_local) + importlib.reload(mmd_tools_local) importlib.reload(translations) importlib.reload(tools) importlib.reload(ui) @@ -93,7 +93,13 @@ # Search for "show_backface_culling" and set it to False in view.py # Done -# How to update googletrans: +# How to update google_trans_new: +# In google_trans.py comment out everything that has to do with urllib3 +# This is done because 3.5 doesn't have urllib3 by default and it is only used +# to suppress debug logs in the console +# Done + +# How to update googletrans: (outdated since a new googletrans is used Todo remove this) # in the gtoken.py on line 57 update this line to include "verify=False": # r = self.session.get(self.host, verify=False) # In client.py on line 42 remove the Hyper part, it's not faster at all! diff --git a/extern_tools/google_trans_new/LICENSE b/extern_tools/google_trans_new/LICENSE new file mode 100644 index 00000000..1d7f8dd2 --- /dev/null +++ b/extern_tools/google_trans_new/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 lushan88a + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extern_tools/google_trans_new/README.md b/extern_tools/google_trans_new/README.md new file mode 100644 index 00000000..02e2c9b8 --- /dev/null +++ b/extern_tools/google_trans_new/README.md @@ -0,0 +1,138 @@ +# google_trans_new +### Version 1.1.9 + +A free and unlimited python API for google translate. +It's very easy to use and solve the problem that the old api which use tk value cannot be used. +This interface is for academic use only, please do not use it for commercial use. + +Version 1.1.9 have fixed url translate. +Ps: +If your get translations for different genders, it will return a list. +https://support.google.com/translate/answer/9179237?p=gendered_translations&hl=zh-Hans&visit_id=637425624803913067-1347870216&rd=1 +*** + + +Installation +==== +``` +pip install google_trans_new +``` +*** + + +Basic Usage +===== +### Translate +```python +from google_trans_new import google_translator + +translator = google_translator() +translate_text = translator.translate('สวัสดีจีน',lang_tgt='en') +print(translate_text) +-> Hello china +``` +*** + +Advanced Usage +===== +### Translate +```python +from google_trans_new import google_translator + +# You can set the url_suffix according to your country. You can set url_suffix="hk" if you are in hong kong,url_suffix use in https://translate.google.{url_suffix}/ +# If you want use proxy, you can set proxies like proxies={'http':'xxx.xxx.xxx.xxx:xxxx','https':'xxx.xxx.xxx.xxx:xxxx'} +translator = google_translator(url_suffix="hk",timeout=5,proxies={'http':'xxx.xxx.xxx.xxx:xxxx','https':'xxx.xxx.xxx.xxx:xxxx'}) +# +# default parameter : url_suffix="cn" timeout=5 proxies={} +translate_text = translator.translate('สวัสดีจีน',lang_tgt='zh') +# +# default parameter : lang_src=auto lang_tgt=auto +# API can automatically identify the src translation language, so you don’t need to set lang_src +print(translate_text) +-> 你好中国 +``` +### Multithreading Translate +```python +from google_trans_new import google_translator +from multiprocessing.dummy import Pool as ThreadPool +import time + +pool = ThreadPool(8) # Threads + +def request(text): + lang = "zh" + t = google_translator(timeout=5) + translate_text = t.translate(text.strip(), lang) + return translate_text + +if __name__ == "__main__" : + time1 = time.time() + with open("./test.txt",'r') as f_p: + texts = f_p.readlines() + try: + results = pool.map(request, texts) + except Exception as e: + raise e + pool.close() + pool.join() + + time2 = time.time() + print("Translating %s sentences, a total of %s s"%(len(texts),time2 - time1)) +-> Translating 720 sentences, a total of 25.89591908454895 s +``` +### Detect +```python +from google_trans_new import google_translator + +detector = google_translator() +detect_result = detector.detect('สวัสดีจีน') +# +print(detect_result) +-> ['th', 'thai'] +``` +### Pronounce +```python +from google_trans_new import google_translator + +translator = google_translator() +Pronounce = translator.transalate('สวัสดีจีน',lang_src='th',lang_tgt='zh',pronounce=True) +print(Pronounce) +-> ['你好中国 ', 'S̄wạs̄dī cīn', 'Nǐ hǎo zhōngguó'] +``` +*** + +Prerequisites +==== +* **Python >=3.6** +* **requests** +* **six** +*** + + +License +==== +google_trans_new is licensed under the MIT License. The terms are as follows: + +``` +MIT License + +Copyright (c) 2020 lushan88a + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/extern_tools/google_trans_new/constant.py b/extern_tools/google_trans_new/constant.py new file mode 100644 index 00000000..3a4a63fa --- /dev/null +++ b/extern_tools/google_trans_new/constant.py @@ -0,0 +1,179 @@ +LANGUAGES = { + 'af': 'afrikaans', + 'sq': 'albanian', + 'am': 'amharic', + 'ar': 'arabic', + 'hy': 'armenian', + 'az': 'azerbaijani', + 'eu': 'basque', + 'be': 'belarusian', + 'bn': 'bengali', + 'bs': 'bosnian', + 'bg': 'bulgarian', + 'ca': 'catalan', + 'ceb': 'cebuano', + 'ny': 'chichewa', + 'zh-cn': 'chinese (simplified)', + 'zh-tw': 'chinese (traditional)', + 'co': 'corsican', + 'hr': 'croatian', + 'cs': 'czech', + 'da': 'danish', + 'nl': 'dutch', + 'en': 'english', + 'eo': 'esperanto', + 'et': 'estonian', + 'tl': 'filipino', + 'fi': 'finnish', + 'fr': 'french', + 'fy': 'frisian', + 'gl': 'galician', + 'ka': 'georgian', + 'de': 'german', + 'el': 'greek', + 'gu': 'gujarati', + 'ht': 'haitian creole', + 'ha': 'hausa', + 'haw': 'hawaiian', + 'iw': 'hebrew', + 'he': 'hebrew', + 'hi': 'hindi', + 'hmn': 'hmong', + 'hu': 'hungarian', + 'is': 'icelandic', + 'ig': 'igbo', + 'id': 'indonesian', + 'ga': 'irish', + 'it': 'italian', + 'ja': 'japanese', + 'jw': 'javanese', + 'kn': 'kannada', + 'kk': 'kazakh', + 'km': 'khmer', + 'ko': 'korean', + 'ku': 'kurdish (kurmanji)', + 'ky': 'kyrgyz', + 'lo': 'lao', + 'la': 'latin', + 'lv': 'latvian', + 'lt': 'lithuanian', + 'lb': 'luxembourgish', + 'mk': 'macedonian', + 'mg': 'malagasy', + 'ms': 'malay', + 'ml': 'malayalam', + 'mt': 'maltese', + 'mi': 'maori', + 'mr': 'marathi', + 'mn': 'mongolian', + 'my': 'myanmar (burmese)', + 'ne': 'nepali', + 'no': 'norwegian', + 'or': 'odia', + 'ps': 'pashto', + 'fa': 'persian', + 'pl': 'polish', + 'pt': 'portuguese', + 'pa': 'punjabi', + 'ro': 'romanian', + 'ru': 'russian', + 'sm': 'samoan', + 'gd': 'scots gaelic', + 'sr': 'serbian', + 'st': 'sesotho', + 'sn': 'shona', + 'sd': 'sindhi', + 'si': 'sinhala', + 'sk': 'slovak', + 'sl': 'slovenian', + 'so': 'somali', + 'es': 'spanish', + 'su': 'sundanese', + 'sw': 'swahili', + 'sv': 'swedish', + 'tg': 'tajik', + 'ta': 'tamil', + 'tt': 'tatar', + 'te': 'telugu', + 'th': 'thai', + 'tr': 'turkish', + 'tk': 'turkmen', + 'uk': 'ukrainian', + 'ur': 'urdu', + 'ug': 'uyghur', + 'uz': 'uzbek', + 'vi': 'vietnamese', + 'cy': 'welsh', + 'xh': 'xhosa', + 'yi': 'yiddish', + 'yo': 'yoruba', + 'zu': 'zulu', +} + +DEFAULT_SERVICE_URLS = ('translate.google.ac','translate.google.ad','translate.google.ae', + 'translate.google.al','translate.google.am','translate.google.as', + 'translate.google.at','translate.google.az','translate.google.ba', + 'translate.google.be','translate.google.bf','translate.google.bg', + 'translate.google.bi','translate.google.bj','translate.google.bs', + 'translate.google.bt','translate.google.by','translate.google.ca', + 'translate.google.cat','translate.google.cc','translate.google.cd', + 'translate.google.cf','translate.google.cg','translate.google.ch', + 'translate.google.ci','translate.google.cl','translate.google.cm', + 'translate.google.cn','translate.google.co.ao','translate.google.co.bw', + 'translate.google.co.ck','translate.google.co.cr','translate.google.co.id', + 'translate.google.co.il','translate.google.co.in','translate.google.co.jp', + 'translate.google.co.ke','translate.google.co.kr','translate.google.co.ls', + 'translate.google.co.ma','translate.google.co.mz','translate.google.co.nz', + 'translate.google.co.th','translate.google.co.tz','translate.google.co.ug', + 'translate.google.co.uk','translate.google.co.uz','translate.google.co.ve', + 'translate.google.co.vi','translate.google.co.za','translate.google.co.zm', + 'translate.google.co.zw','translate.google.co','translate.google.com.af', + 'translate.google.com.ag','translate.google.com.ai','translate.google.com.ar', + 'translate.google.com.au','translate.google.com.bd','translate.google.com.bh', + 'translate.google.com.bn','translate.google.com.bo','translate.google.com.br', + 'translate.google.com.bz','translate.google.com.co','translate.google.com.cu', + 'translate.google.com.cy','translate.google.com.do','translate.google.com.ec', + 'translate.google.com.eg','translate.google.com.et','translate.google.com.fj', + 'translate.google.com.gh','translate.google.com.gi','translate.google.com.gt', + 'translate.google.com.hk','translate.google.com.jm','translate.google.com.kh', + 'translate.google.com.kw','translate.google.com.lb','translate.google.com.lc', + 'translate.google.com.ly','translate.google.com.mm','translate.google.com.mt', + 'translate.google.com.mx','translate.google.com.my','translate.google.com.na', + 'translate.google.com.ng','translate.google.com.ni','translate.google.com.np', + 'translate.google.com.om','translate.google.com.pa','translate.google.com.pe', + 'translate.google.com.pg','translate.google.com.ph','translate.google.com.pk', + 'translate.google.com.pr','translate.google.com.py','translate.google.com.qa', + 'translate.google.com.sa','translate.google.com.sb','translate.google.com.sg', + 'translate.google.com.sl','translate.google.com.sv','translate.google.com.tj', + 'translate.google.com.tr','translate.google.com.tw','translate.google.com.ua', + 'translate.google.com.uy','translate.google.com.vc','translate.google.com.vn', + 'translate.google.com','translate.google.cv','translate.google.cx', + 'translate.google.cz','translate.google.de','translate.google.dj', + 'translate.google.dk','translate.google.dm','translate.google.dz', + 'translate.google.ee','translate.google.es','translate.google.eu', + 'translate.google.fi','translate.google.fm','translate.google.fr', + 'translate.google.ga','translate.google.ge','translate.google.gf', + 'translate.google.gg','translate.google.gl','translate.google.gm', + 'translate.google.gp','translate.google.gr','translate.google.gy', + 'translate.google.hn','translate.google.hr','translate.google.ht', + 'translate.google.hu','translate.google.ie','translate.google.im', + 'translate.google.io','translate.google.iq','translate.google.is', + 'translate.google.it','translate.google.je','translate.google.jo', + 'translate.google.kg','translate.google.ki','translate.google.kz', + 'translate.google.la','translate.google.li','translate.google.lk', + 'translate.google.lt','translate.google.lu','translate.google.lv', + 'translate.google.md','translate.google.me','translate.google.mg', + 'translate.google.mk','translate.google.ml','translate.google.mn', + 'translate.google.ms','translate.google.mu','translate.google.mv', + 'translate.google.mw','translate.google.ne','translate.google.nf', + 'translate.google.nl','translate.google.no','translate.google.nr', + 'translate.google.nu','translate.google.pl','translate.google.pn', + 'translate.google.ps','translate.google.pt','translate.google.ro', + 'translate.google.rs','translate.google.ru','translate.google.rw', + 'translate.google.sc','translate.google.se','translate.google.sh', + 'translate.google.si','translate.google.sk','translate.google.sm', + 'translate.google.sn','translate.google.so','translate.google.sr', + 'translate.google.st','translate.google.td','translate.google.tg', + 'translate.google.tk','translate.google.tl','translate.google.tm', + 'translate.google.tn','translate.google.to','translate.google.tt', + 'translate.google.us','translate.google.vg','translate.google.vu','translate.google.ws') diff --git a/extern_tools/google_trans_new/google_trans_new.py b/extern_tools/google_trans_new/google_trans_new.py new file mode 100644 index 00000000..7950ff40 --- /dev/null +++ b/extern_tools/google_trans_new/google_trans_new.py @@ -0,0 +1,251 @@ +# coding:utf-8 +# author LuShan +# version : 1.1.9 +import json, requests, random, re +from urllib.parse import quote +# import urllib3 +import logging +from .constant import LANGUAGES, DEFAULT_SERVICE_URLS + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +# urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +URLS_SUFFIX = [re.search('translate.google.(.*)', url.strip()).group(1) for url in DEFAULT_SERVICE_URLS] +URL_SUFFIX_DEFAULT = 'cn' + + +class google_new_transError(Exception): + """Exception that uses context to present a meaningful error message""" + + def __init__(self, msg=None, **kwargs): + self.tts = kwargs.pop('tts', None) + self.rsp = kwargs.pop('response', None) + if msg: + self.msg = msg + elif self.tts is not None: + self.msg = self.infer_msg(self.tts, self.rsp) + else: + self.msg = None + super(google_new_transError, self).__init__(self.msg) + + def infer_msg(self, tts, rsp=None): + cause = "Unknown" + + if rsp is None: + premise = "Failed to connect" + + return "{}. Probable cause: {}".format(premise, "timeout") + # if tts.tld != 'com': + # host = _translate_url(tld=tts.tld) + # cause = "Host '{}' is not reachable".format(host) + + else: + status = rsp.status_code + reason = rsp.reason + + premise = "{:d} ({}) from TTS API".format(status, reason) + + if status == 403: + cause = "Bad token or upstream API changes" + elif status == 200 and not tts.lang_check: + cause = "No audio stream in response. Unsupported language '%s'" % self.tts.lang + elif status >= 500: + cause = "Uptream API error. Try again later." + + return "{}. Probable cause: {}".format(premise, cause) + + +class google_translator: + ''' + You can use 108 language in target and source,details view LANGUAGES. + Target language: like 'en'、'zh'、'th'... + + :param url_suffix: The source text(s) to be translated. Batch translation is supported via sequence input. + The value should be one of the url_suffix listed in : `DEFAULT_SERVICE_URLS` + :type url_suffix: UTF-8 :class:`str`; :class:`unicode`; string sequence (list, tuple, iterator, generator) + + :param text: The source text(s) to be translated. + :type text: UTF-8 :class:`str`; :class:`unicode`; + + :param lang_tgt: The language to translate the source text into. + The value should be one of the language codes listed in : `LANGUAGES` + :type lang_tgt: :class:`str`; :class:`unicode` + + :param lang_src: The language of the source text. + The value should be one of the language codes listed in :const:`googletrans.LANGUAGES` + If a language is not specified, + the system will attempt to identify the source language automatically. + :type lang_src: :class:`str`; :class:`unicode` + + :param timeout: Timeout Will be used for every request. + :type timeout: number or a double of numbers + + :param proxies: proxies Will be used for every request. + :type proxies: class : dict; like: {'http': 'http:171.112.169.47:19934/', 'https': 'https:171.112.169.47:19934/'} + + ''' + + def __init__(self, url_suffix="cn", timeout=5, proxies=None): + self.proxies = proxies + if url_suffix not in URLS_SUFFIX: + self.url_suffix = URL_SUFFIX_DEFAULT + else: + self.url_suffix = url_suffix + url_base = "https://translate.google.{}".format(self.url_suffix) + self.url = url_base + "/_/TranslateWebserverUi/data/batchexecute" + self.timeout = timeout + + def _package_rpc(self, text, lang_src='auto', lang_tgt='auto'): + GOOGLE_TTS_RPC = ["MkEWBc"] + parameter = [[text.strip(), lang_src, lang_tgt, True], [1]] + escaped_parameter = json.dumps(parameter, separators=(',', ':')) + rpc = [[[random.choice(GOOGLE_TTS_RPC), escaped_parameter, None, "generic"]]] + espaced_rpc = json.dumps(rpc, separators=(',', ':')) + # text_urldecode = quote(text.strip()) + freq_initial = "f.req={}&".format(quote(espaced_rpc)) + freq = freq_initial + return freq + + def translate(self, text, lang_tgt='auto', lang_src='auto', pronounce=False): + try: + lang = LANGUAGES[lang_src] + except: + lang_src = 'auto' + try: + lang = LANGUAGES[lang_tgt] + except: + lang_src = 'auto' + text = str(text) + if len(text) >= 5000: + return "Warning: Can only detect less than 5000 characters" + if len(text) == 0: + return "" + headers = { + "Referer": "http://translate.google.{}/".format(self.url_suffix), + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; WOW64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/47.0.2526.106 Safari/537.36", + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" + } + freq = self._package_rpc(text, lang_src, lang_tgt) + response = requests.Request(method='POST', + url=self.url, + data=freq, + headers=headers, + ) + try: + if self.proxies == None or type(self.proxies) != dict: + self.proxies = {} + with requests.Session() as s: + s.proxies = self.proxies + r = s.send(request=response.prepare(), + verify=False, + timeout=self.timeout) + for line in r.iter_lines(chunk_size=1024): + decoded_line = line.decode('utf-8') + if "MkEWBc" in decoded_line: + try: + response = (decoded_line + ']') + response = json.loads(response) + response = list(response) + response = json.loads(response[0][2]) + response_ = list(response) + response = response_[1][0] + if len(response) == 1: + if len(response[0]) > 5: + sentences = response[0][5] + else: ## only url + sentences = response[0][0] + if pronounce == False: + return sentences + elif pronounce == True: + return [sentences,None,None] + translate_text = "" + for sentence in sentences: + sentence = sentence[0] + translate_text += sentence.strip() + ' ' + translate_text = translate_text + if pronounce == False: + return translate_text + elif pronounce == True: + pronounce_src = (response_[0][0]) + pronounce_tgt = (response_[1][0][0][1]) + return [translate_text, pronounce_src, pronounce_tgt] + elif len(response) == 2: + sentences = [] + for i in response: + sentences.append(i[0]) + if pronounce == False: + return sentences + elif pronounce == True: + pronounce_src = (response_[0][0]) + pronounce_tgt = (response_[1][0][0][1]) + return [sentences, pronounce_src, pronounce_tgt] + except Exception as e: + raise e + r.raise_for_status() + except requests.exceptions.ConnectTimeout as e: + raise e + except requests.exceptions.HTTPError as e: + # Request successful, bad response + raise google_new_transError(tts=self, response=r) + except requests.exceptions.RequestException as e: + # Request failed + raise google_new_transError(tts=self) + + def detect(self, text): + text = str(text) + if len(text) >= 5000: + return log.debug("Warning: Can only detect less than 5000 characters") + if len(text) == 0: + return "" + headers = { + "Referer": "http://translate.google.{}/".format(self.url_suffix), + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; WOW64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/47.0.2526.106 Safari/537.36", + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" + } + freq = self._package_rpc(text) + response = requests.Request(method='POST', + url=self.url, + data=freq, + headers=headers) + try: + if self.proxies == None or type(self.proxies) != dict: + self.proxies = {} + with requests.Session() as s: + s.proxies = self.proxies + r = s.send(request=response.prepare(), + verify=False, + timeout=self.timeout) + + for line in r.iter_lines(chunk_size=1024): + decoded_line = line.decode('utf-8') + if "MkEWBc" in decoded_line: + # regex_str = r"\[\[\"wrb.fr\",\"MkEWBc\",\"\[\[(.*).*?,\[\[\[" + try: + # data_got = re.search(regex_str,decoded_line).group(1) + response = (decoded_line + ']') + response = json.loads(response) + response = list(response) + response = json.loads(response[0][2]) + response = list(response) + detect_lang = response[0][2] + except Exception: + raise Exception + # data_got = data_got.split('\\\"]')[0] + return [detect_lang, LANGUAGES[detect_lang.lower()]] + r.raise_for_status() + except requests.exceptions.HTTPError as e: + # Request successful, bad response + log.debug(str(e)) + raise google_new_transError(tts=self, response=r) + except requests.exceptions.RequestException as e: + # Request failed + log.debug(str(e)) + raise google_new_transError(tts=self) diff --git a/extern_tools/google_trans_new/test.txt b/extern_tools/google_trans_new/test.txt new file mode 100644 index 00000000..e898bea7 --- /dev/null +++ b/extern_tools/google_trans_new/test.txt @@ -0,0 +1,720 @@ +China (Chinese: 中国; pinyin: Zhōngguó; lit.: 'Central State; Middle Earth; Midland'), officially the People's Republic of China (PRC; Chinese: 中华人民共和国; pinyin: Zhōnghuá Rénmín Gònghéguó), is a country in East Asia. + It is the world's most populous country, with a population of around 1.4 billion in 2019. + Covering approximately 9.6 million square kilometres (3.7 million mi), it is the world's third or fourth largest country by area. + Governed by the Communist Party of China, the state exercises jurisdiction over 22 provinces, five autonomous regions, four direct-controlled municipalities (Beijing, Tianjin, Shanghai, and Chongqing), and the special administrative regions of Hong Kong and Macau. +China emerged as one of the world's first civilizations, in the fertile basin of the Yellow River in the North China Plain. + For millennia, China's political system was based on hereditary monarchies, or dynasties, beginning with the semi-mythical Xia dynasty in 21st century BCE. + Since then, China has expanded, fractured, and re-unified numerous times. + In the 3rd century BCE, the Qin reunited core China and established the first Chinese empire. + The succeeding Han dynasty, which ruled from 206 BCE until 220 CE, saw some of the most advanced technology at that time, including papermaking and the compass, along with agricultural and medical improvements. + The invention of gunpowder and movable type in the Tang dynasty (618–907) and Northern Song (960–1127) completed the Four Great Inventions. + Tang culture spread widely in Asia, as the new Silk Route brought traders to as far as Mesopotamia and the Horn of Africa. + Dynastic rule ended in 1912 with the Xinhai Revolution, when the Republic of China (ROC) replaced the Qing dynasty. + The subsequent Chinese Civil War resulted in a division of territory in 1949 when the Communist Party of China led by Mao Zedong established the People's Republic of China on mainland China while the Kuomintang-led nationalist government retreated to the island of Taiwan where it governed until 1996 when Taiwan transitioned to democracy. + The political status of Taiwan remains disputed to this day. +China is a unitary one-party socialist republic and is one of the few existing socialist states. + Political dissidents and human rights groups have denounced and criticized the Chinese government for widespread human rights abuses, including suppression of religious and ethnic minorities, censorship and mass surveillance, and cracking down on protests such as in 1989. + The Chinese government says that the right to subsistence and economic development is a prerequisite to other types of human rights and that the notion of human rights should take into account a country's present economic level. +Since the introduction of economic reforms in 1978, China's economy has been one of the world's fastest-growing with annual growth rates consistently above 6 percent. + According to the World Bank, China's GDP grew from $150 billion in 1978 to $12.24 trillion by 2017. + According to official data, China's GDP in 2018 was 90 trillion Yuan ($13.5 trillion). + Since 2010, China has been the world's second-largest economy by nominal GDP, and since 2014, the largest economy in the world by PPP. + China is also the world's largest exporter and second-largest importer of goods. + China is a recognized nuclear weapons state and has the world's largest standing army, the People's Liberation Army, and the second-largest defense budget. + The PRC is a permanent member of the United Nations Security Council as it replaced the ROC in 1971, as well as an active global partner of ASEAN Plus mechanism. + China has been characterized as a emerging superpower, mainly because of its massive population, large and rapidly-growing economy, and powerful military. +The word "China" has been used in English since the 16th century. + However, it was not a word used by the Chinese themselves during the period. + Its origin has been traced through Portuguese, Malay, and Persian back to the Sanskrit word , used in ancient India. +"China" appears in Richard Eden's 1555 translation of the 1516 journal of the Portuguese explorer Duarte Barbosa. + Barbosa's usage was derived from Persian (چین), which was in turn derived from Sanskrit (चीन). + was first used in early Hindu scripture, including the (5th century ) and the (2nd century ). + In 1655, Martino Martini suggested that the word China is derived ultimately from the name of the Qin dynasty (221–206 BCE). + Although this derivation is still given in various sources, the origin of the Sanskrit word is a matter of debate, according to the . + Alternative suggestions include the names for Yelang and the Jing or Chu state. +The official name of the modern state is the "People's Republic of China" (Chinese: 中华人民共和国; pinyin: ). + The shorter form is "China" from ("central") and ("state"), a term which developed under the Western Zhou dynasty in reference to its royal demesne. + It was then applied to the area around Luoyi (present-day Luoyang) during the Eastern Zhou and then to China's Central Plain before being used as an occasional synonym for the state under the Qing. + It was often used as a cultural concept to distinguish the Huaxia people from perceived "barbarians". + The name is also translated as in English. +Archaeological evidence suggests that early hominids inhabited China between 2.24 million and 250,000 years ago. + The hominid fossils of Peking Man, a who used fire, were discovered in a cave at Zhoukoudian near Beijing; they have been dated to between 680,000 and 780,000 years ago. + The fossilized teeth of (dated to 125,000–80,000 years ago) have been discovered in Fuyan Cave in Dao County, Hunan. + Chinese proto-writing existed in Jiahu around 7000 , Damaidi around 6000 , Dadiwan from 5800–5400 , and Banpo dating from the 5th millennium . + Some scholars have suggested that the Jiahu symbols (7th millennium ) constituted the earliest Chinese writing system. +According to Chinese tradition, the first dynasty was the Xia, which emerged around 2100 . + Xia dynasty marked the beginning of China's political system based on hereditary monarchies, or dynasties, which lasted for a millennia. + The dynasty was considered mythical by historians until scientific excavations found early Bronze Age sites at Erlitou, Henan in 1959. + It remains unclear whether these sites are the remains of the Xia dynasty or of another culture from the same period. + The succeeding Shang dynasty is the earliest to be confirmed by contemporary records. + The Shang ruled the plain of the Yellow River in eastern China from the 17th to the 11th century . + Their oracle bone script (from  ) represents the oldest form of Chinese writing yet found, and is a direct ancestor of modern Chinese characters. +The Shang was conquered by the Zhou, who ruled between the 11th and 5th centuries , though centralized authority was slowly eroded by feudal warlords. + Some principalities eventually emerged from the weakened Zhou, no longer fully obeyed the Zhou king and continually waged war with each other in the 300-year Spring and Autumn period. + By the time of the Warring States period of the 5th–3rd centuries , there were only seven powerful states left. +The Warring States period ended in 221  after the state of Qin conquered the other six kingdoms, reunited China and established the dominant order of autocracy. + King Zheng of Qin proclaimed himself the First Emperor of the Qin dynasty. + He enacted Qin's legalist reforms throughout China, notably the forced standardization of Chinese characters, measurements, road widths (i.e., cart axles' length), and currency. + His dynasty also conquered the Yue tribes in Guangxi, Guangdong, and Vietnam. + The Qin dynasty lasted only fifteen years, falling soon after the First Emperor's death, as his harsh authoritarian policies led to widespread rebellion. +Following a widespread civil war during which the imperial library at Xianyang was burned, the Han dynasty emerged to rule China between 206  and  220, creating a cultural identity among its populace still remembered in the ethnonym of the Han Chinese. + The Han expanded the empire's territory considerably, with military campaigns reaching Central Asia, Mongolia, South Korea, and Yunnan, and the recovery of Guangdong and northern Vietnam from Nanyue. + Han involvement in Central Asia and Sogdia helped establish the land route of the Silk Road, replacing the earlier path over the Himalayas to India. + Han China gradually became the largest economy of the ancient world. + Despite the Han's initial decentralization and the official abandonment of the Qin philosophy of Legalism in favor of Confucianism, Qin's legalist institutions and policies continued to be employed by the Han government and its successors. +After the end of the Han dynasty, a period of strife known as Three Kingdoms followed, whose central figures were later immortalized in one of the Four Classics of Chinese literature. + At its end, Wei was swiftly overthrown by the Jin dynasty. + The Jin fell to civil war upon the ascension of a developmentally-disabled emperor; the Five Barbarians then invaded and ruled northern China as the Sixteen States. + The Xianbei unified them as the Northern Wei, whose Emperor Xiaowen reversed his predecessors' apartheid policies and enforced a drastic sinification on his subjects, largely integrating them into Chinese culture. + In the south, the general Liu Yu secured the abdication of the Jin in favor of the Liu Song. + The various successors of these states became known as the Northern and Southern dynasties, with the two areas finally reunited by the Sui in 581. + The Sui restored the Han to power through China, reformed its agriculture, economy and imperial examination system, constructed the Grand Canal, and patronized Buddhism. + However, they fell quickly when their conscription for public works and a failed war in northern Korea provoked widespread unrest. +Under the succeeding Tang and Song dynasties, Chinese economy, technology, and culture entered a golden age. + The Tang Empire returned control of the Western Regions and the Silk Road, brought traders to as far as Mesopotamia and the Horn of Africa, and made the capital Chang'an a cosmopolitan urban center. + However, it was devastated and weakened by the An Shi Rebellion in the 8th century. + In 907, the Tang disintegrated completely when the local military governors became ungovernable. + The Song dynasty ended the separatist situation in 960, leading to a balance of power between the Song and Khitan Liao. + The Song was the first government in world history to issue paper money and the first Chinese polity to establish a permanent standing navy which was supported by the developed shipbuilding industry along with the sea trade. +Between the 10th and 11th centuries, the population of China doubled in size to around 100 million people, mostly because of the expansion of rice cultivation in central and southern China, and the production of abundant food surpluses. + The Song dynasty also saw a revival of Confucianism, in response to the growth of Buddhism during the Tang, and a flourishing of philosophy and the arts, as landscape art and porcelain were brought to new levels of maturity and complexity. + However, the military weakness of the Song army was observed by the Jurchen Jin dynasty. + In 1127, Emperor Huizong of Song and the capital Bianjing were captured during the Jin–Song Wars. + The remnants of the Song retreated to southern China. +The 13th century brought the Mongol conquest of China. + In 1271, the Mongol leader Kublai Khan established the Yuan dynasty; the Yuan conquered the last remnant of the Song dynasty in 1279. + Before the Mongol invasion, the population of Song China was 120 million citizens; this was reduced to 60 million by the time of the census in 1300. + A peasant named Zhu Yuanzhang overthrew the Yuan in 1368 and founded the Ming dynasty as the Hongwu Emperor. + Under the Ming dynasty, China enjoyed another golden age, developing one of the strongest navies in the world and a rich and prosperous economy amid a flourishing of art and culture. + It was during this period that admiral Zheng He led the Ming treasure voyages throughout the Indian Ocean, reaching as far as East Africa.In the early years of the Ming dynasty, China's capital was moved from Nanjing to Beijing. + With the budding of capitalism, philosophers such as Wang Yangming further critiqued and expanded Neo-Confucianism with concepts of individualism and equality of four occupations. + The scholar-official stratum became a supporting force of industry and commerce in the tax boycott movements, which, together with the famines and defense against Japanese invasions of Korea (1592–1598) and Manchu invasions led to an exhausted treasury. +In 1644, Beijing was captured by a coalition of peasant rebel forces led by Li Zicheng. + The Chongzhen Emperor committed suicide when the city fell. + The Manchu Qing dynasty, then allied with Ming dynasty general Wu Sangui, overthrew Li's short-lived Shun dynasty and subsequently seized control of Beijing, which became the new capital of the Qing dynasty. +The Qing dynasty, which lasted from 1644 until 1912, was the last imperial dynasty of China. + Its conquest of the Ming (1618–1683) cost 25 million lives and the economy of China shrank drastically. + After the Southern Ming ended, the further conquest of the Dzungar Khanate added Mongolia, Tibet and Xinjiang to the empire. + The centralized autocracy was strengthened to crack down on anti-Qing sentiment with the policy of valuing agriculture and restraining commerce, the ("sea ban"), and ideological control as represented by the literary inquisition, causing social and technological stagnation. + In the mid-19th century, the dynasty experienced Western imperialism in the Opium Wars with Britain and France. + China was forced to pay compensation, open treaty ports, allow extraterritoriality for foreign nationals, and cede Hong Kong to the British under the 1842 Treaty of Nanking, the first of the Unequal Treaties. + The First Sino-Japanese War (1894–95) resulted in Qing China's loss of influence in the Korean Peninsula, as well as the cession of Taiwan to Japan. +The Qing dynasty also began experiencing internal unrest in which tens of millions of people died, especially in the White Lotus Rebellion, the failed Taiping Rebellion that ravaged southern China in the 1850s and 1860s and the Dungan Revolt (1862–77) in the northwest. + The initial success of the Self-Strengthening Movement of the 1860s was frustrated by a series of military defeats in the 1880s and 1890s. +In the 19th century, the great Chinese diaspora began. + Losses due to emigration were added to by conflicts and catastrophes such as the Northern Chinese Famine of 1876–79, in which between 9 and 13 million people died. + The Guangxu Emperor drafted a reform plan in 1898 to establish a modern constitutional monarchy, but these plans were thwarted by the Empress Dowager Cixi. + The ill-fated anti-foreign Boxer Rebellion of 1899–1901 further weakened the dynasty. + Although Cixi sponsored a program of reforms, the Xinhai Revolution of 1911–12 brought an end to the Qing dynasty and established the Republic of China. +On 1 January 1912, the Republic of China was established, and Sun Yat-sen of the Kuomintang (the KMT or Nationalist Party) was proclaimed provisional president. + However, the presidency was later given to Yuan Shikai, a former Qing general who in 1915 proclaimed himself Emperor of China. + In the face of popular condemnation and opposition from his own Beiyang Army, he was forced to abdicate and re-establish the republic. +After Yuan Shikai's death in 1916, China was politically fragmented. + Its Beijing-based government was internationally recognized but virtually powerless; regional warlords controlled most of its territory. + In the late 1920s, the Kuomintang, under Chiang Kai-shek, the then Principal of the Republic of China Military Academy, was able to reunify the country under its own control with a series of deft military and political manoeuvrings, known collectively as the Northern Expedition. + The Kuomintang moved the nation's capital to Nanjing and implemented "political tutelage", an intermediate stage of political development outlined in Sun Yat-sen's San-min program for transforming China into a modern democratic state. + The political division in China made it difficult for Chiang to battle the communist People's Liberation Army (PLA), against whom the Kuomintang had been warring since 1927 in the Chinese Civil War. + This war continued successfully for the Kuomintang, especially after the PLA retreated in the Long March, until Japanese aggression and the 1936 Xi'an Incident forced Chiang to confront Imperial Japan. +The Second Sino-Japanese War (1937–1945), a theater of World War II, forced an uneasy alliance between the Kuomintang and the PLA. + Japanese forces committed numerous war atrocities against the civilian population; in all, as many as 20 million Chinese civilians died. + An estimated 40,000 to 300,000 Chinese were massacred in the city of Nanjing alone during the Japanese occupation. + During the war, China, along with the UK, the US, and the Soviet Union, were referred to as "trusteeship of the powerful" and were recognized as the Allied "Big Four" in the Declaration by United Nations. + Along with the other three great powers, China was one of the four major Allies of World War II, and was later considered one of the primary victors in the war. + After the surrender of Japan in 1945, Taiwan, including the Pescadores, was returned to Chinese control. + China emerged victorious but war-ravaged and financially drained. + The continued distrust between the Kuomintang and the Communists led to the resumption of civil war. + Constitutional rule was established in 1947, but because of the ongoing unrest, many provisions of the ROC constitution were never implemented in mainland China. +Major combat in the Chinese Civil War ended in 1949 with the Communist Party in control of most of mainland China, and the Kuomintang retreating offshore, reducing its territory to only Taiwan, Hainan, and their surrounding islands. + On 21 September 1949, Communist Party Chairman Mao Zedong proclaimed the establishment of the People's Republic of China with a speech at the First Plenary Session of the Chinese People's Political Consultative Conference. + This was followed by a mass celebration in Tiananmen Square on 1 October, at which the proclamation was made publicly by Mao at the Tiananmen Gate, the date becoming the new country's first National Day. + In 1950, the People's Liberation Army captured Hainan from the ROC and incorporated Tibet. + However, remaining Kuomintang forces continued to wage an insurgency in western China throughout the 1950s. +The regime consolidated its popularity among the peasants through land reform, which included the execution of between 1 and 2 million landlords. + China developed an independent industrial system and its own nuclear weapons. + The Chinese population increased from 550 million in 1950 to 900 million in 1974. + However, the Great Leap Forward, an idealistic massive reform project, resulted in an estimated 15 to 35 million deaths between 1958 and 1961, mostly from starvation. + In 1966, Mao and his allies launched the Cultural Revolution, sparking a decade of political recrimination and social upheaval that lasted until Mao's death in 1976. + In October 1971, the PRC replaced the Republic in the United Nations, and took its seat as a permanent member of the Security Council. +After Mao's death, the Gang of Four was quickly arrested and held responsible for the excesses of the Cultural Revolution. + Deng Xiaoping took power in 1978, and instituted significant economic reforms. + The Party loosened governmental control over citizens' personal lives, and the communes were gradually disbanded in favor of working contracted to households. + This marked China's transition from a planned economy to a mixed economy with an increasingly open-market environment. + China adopted its current constitution on 4 December 1982. + In 1989, the violent suppression of student protests in Tiananmen Square brought sanctions against the Chinese government from various countries. +Jiang Zemin, Li Peng and Zhu Rongji led the nation in the 1990s. + Under their administration, China's economic performance pulled an estimated 150 million peasants out of poverty and sustained an average annual gross domestic product growth rate of 11.2%. + The country joined the World Trade Organization in 2001, and maintained its high rate of economic growth under Hu Jintao and Wen Jiabao's leadership in the 2000s. + However, the growth also severely impacted the country's resources and environment, and caused major social displacement. + Living standards continued to improve rapidly despite the late-2000s recession, but political control remained tight. +Preparations for a decadal leadership change in 2012 were marked by factional disputes and political scandals. + During the 18th National Communist Party Congress in November 2012, Hu Jintao was replaced as General Secretary of the Communist Party by Xi Jinping. + Under Xi, the Chinese government began large-scale efforts to reform its economy, which has suffered from structural instabilities and slowing growth. + The Xi–Li Administration also announced major reforms to the one-child policy and prison system. +In December 2019, a new strain of coronavirus, later identified as COVID-19, broke out in Wuhan, Hubei, then spread to other provinces of China, and then worldwide, becoming the COVID-19 pandemic. + As of June 2020, more than 7.24million cases of COVID-19 have been reported in more than 188 countries and territories, resulting in more than 411,000 deaths and then 3.37million recovered people. +China's landscape is vast and diverse, ranging from the Gobi and Taklamakan Deserts in the arid north to the subtropical forests in the wetter south. + The Himalaya, Karakoram, Pamir and Tian Shan mountain ranges separate China from much of South and Central Asia. + The Yangtze and Yellow Rivers, the third- and sixth-longest in the world, respectively, run from the Tibetan Plateau to the densely populated eastern seaboard. + China's coastline along the Pacific Ocean is 14,500 kilometers (9,000 mi) long and is bounded by the Bohai, Yellow, East China and South China seas. + China connects through the Kazakh border to the Eurasian Steppe which has been an artery of communication between East and West since the Neolithic through the Steppe route – the ancestor of the terrestrial Silk Road(s). +The territory of China lies between latitudes 18° and 54° N, and longitudes 73° and 135° E. + The geographical center of China is marked by the Center of the Country Monument at . + China's landscapes vary significantly across its vast territory. + In the east, along the shores of the Yellow Sea and the East China Sea, there are extensive and densely populated alluvial plains, while on the edges of the Inner Mongolian plateau in the north, broad grasslands predominate. + Southern China is dominated by hills and low mountain ranges, while the central-east hosts the deltas of China's two major rivers, the Yellow River and the Yangtze River. + Other major rivers include the Xi, Mekong, Brahmaputra and Amur. + To the west sit major mountain ranges, most notably the Himalayas. + High plateaus feature among the more arid landscapes of the north, such as the Taklamakan and the Gobi Desert. + The world's highest point, Mount Everest (8,848 m), lies on the Sino-Nepalese border. + The country's lowest point, and the world's third-lowest, is the dried lake bed of Ayding Lake (−154m) in the Turpan Depression. +China's climate is mainly dominated by dry seasons and wet monsoons, which lead to pronounced temperature differences between winter and summer. + In the winter, northern winds coming from high-latitude areas are cold and dry; in summer, southern winds from coastal areas at lower latitudes are warm and moist. + The climate in China differs from region to region because of the country's highly complex topography. +A major environmental issue in China is the continued expansion of its deserts, particularly the Gobi Desert. + Although barrier tree lines planted since the 1970s have reduced the frequency of sandstorms, prolonged drought and poor agricultural practices have resulted in dust storms plaguing northern China each spring, which then spread to other parts of East Asia, including Japan and Korea. + China's environmental watchdog, SEPA, stated in 2007 that China is losing 4,000 km (1,500 sq mi) per year to desertification. + Water quality, erosion, and pollution control have become important issues in China's relations with other countries. + Melting glaciers in the Himalayas could potentially lead to water shortages for hundreds of millions of people. +China has a very agriculturally suitable climate and has been the largest producer of rice, wheat, tomatoes, brinjal, grapes, water melon, spinach in the world. +China is one of 17 megadiverse countries, lying in two of the world's major biogeographic realms: the Palearctic and the Indomalayan. + By one measure, China has over 34,687 species of animals and vascular plants, making it the third-most biodiverse country in the world, after Brazil and Colombia. + The country signed the Rio de Janeiro Convention on Biological Diversity on 11 June 1992, and became a party to the convention on 5 January 1993. + It later produced a National Biodiversity Strategy and Action Plan, with one revision that was received by the convention on 21 September 2010. +China is home to at least 551 species of mammals (the third-highest such number in the world), 1,221 species of birds (eighth), 424 species of reptiles (seventh) and 333 species of amphibians (seventh). + Wildlife in China share habitat with and bear acute pressure from the world's largest population of . + At least 840 animal species are threatened, vulnerable or in danger of local extinction in China, due mainly to human activity such as habitat destruction, pollution and poaching for food, fur and ingredients for traditional Chinese medicine. + Endangered wildlife is protected by law, and as of 2005, the country has over 2,349 nature reserves, covering a total area of 149.95 million hectares, 15 percent of China's total land area. + The Baiji was confirmed extinct on 12 December 2006. +China has over 32,000 species of vascular plants, and is home to a variety of forest types. + Cold coniferous forests predominate in the north of the country, supporting animal species such as moose and Asian black bear, along with over 120 bird species. + The understorey of moist conifer forests may contain thickets of bamboo. + In higher montane stands of juniper and yew, the bamboo is replaced by rhododendrons. + Subtropical forests, which are predominate in central and southern China, support as many as 146,000 species of flora. + Tropical and seasonal rainforests, though confined to Yunnan and Hainan Island, contain a quarter of all the animal and plant species found in China. + China has over 10,000 recorded species of fungi, and of them, nearly 6,000 are higher fungi. +In recent decades, China has suffered from severe environmental deterioration and pollution. + While regulations such as the 1979 Environmental Protection Law are fairly stringent, they are poorly enforced, as they are frequently disregarded by local communities and government officials in favor of rapid economic development. + Urban air pollution is a severe health issue in the country, yet matters have been improving in recent years; the World Bank estimated in 2016 that only 1 of the world's 20 most-polluted cities is located in China, making significant improvements since previous rankings. + China is the country with the second highest death toll because of air pollution, after India. + There are approximately 1 million deaths caused by exposure to ambient air pollution. + China is the world's largest carbon dioxide emitter. + The country also has significant water pollution problems: 8.2% of China's rivers had been polluted by industrial and agricultural waste in 2019, and were unfit for use. + In 2014, the internal freshwater resources per capita of China reduced to 2,062m, and it was below 500m in the North China Plain, while 5,920m in the world. +In China, heavy metals also cause environmental pollution. + Heavy metal pollution is an inorganic chemical hazard, which is mainly caused by lead (Pb), chromium (Cr), arsenic (As), cadmium (Cd), mercury (Hg), zinc (Zn), copper (Cu), cobalt (Co), and nickel (Ni). + Five metals among them, Pb, Cr, As, Cd, and Hg, are the key heavy metal pollutants in China. + Heavy metal pollutants mainly come from mining, sewage irrigation, the manufacturing of metal-containing products, and other related production activities. + High level of heavy metal exposure can also cause permanent intellectual and developmental disabilities, including reading and learning disabilities, behavioral problems, hearing loss, attention problems, and disruption in the development of visual and motor function. + According to the data of a national census of pollution, China has more than 1.5 million sites of heavy metals exposure. + The total volume of discharged heavy metals in the waste water, waste gas and solid wastes are around 900,000 tons each year from 2005–2011. +However, China is the world's leading investor in renewable energy and its commercialization, with $52 billion invested in 2011 alone; it is a major manufacturer of renewable energy technologies and invests heavily in local-scale renewable energy projects. + By 2015, over 24% of China's energy was derived from renewable sources, while most notably from hydroelectric power: a total installed capacity of 197 GW makes China the largest hydroelectric power producer in the world. + China also has the largest power capacity of installed solar photovoltaics system and wind power system in the world. + In 2011, the Chinese government announced plans to invest four trillion yuan (US$619 billion) in water infrastructure and desalination projects over a ten-year period, and to complete construction of a flood prevention and anti-drought system by 2020. + In 2013, China began a five-year, US$277 billion effort to reduce air pollution, particularly in the north of the country. +The People's Republic of China is the second-largest country in the world by land area after Russia, and is the third largest by total area, after Russia and Canada. + China's total area is generally stated as being approximately 9,600,000 km (3,700,000 sq mi). + Specific area figures range from 9,572,900 km (3,696,100 sq mi) according to the , to 9,596,961 km (3,705,407 sq mi) according to the UN Demographic Yearbook, and the CIA World Factbook. +China has the longest combined land border in the world, measuring 22,117 km (13,743 mi) from the mouth of the Yalu River (Amnok River) to the Gulf of Tonkin. + China borders 14 nations, more than any other country except Russia, which also borders 14. + China extends across much of East Asia, bordering Vietnam, Laos, and Myanmar (Burma) in Southeast Asia; India, Bhutan, Nepal, Afghanistan, and Pakistan in South Asia; Tajikistan, Kyrgyzstan and Kazakhstan in Central Asia; and Russia, Mongolia, and North Korea in Inner Asia and Northeast Asia. + Additionally, China shares maritime boundaries with South Korea, Japan, Vietnam, and the Philippines. +China's constitution states that The People's Republic of China "is a socialist state under the people's democratic dictatorship led by the working class and based on the alliance of workers and peasants," and that the state organs "apply the principle of democratic centralism." The PRC is one of the world's only socialist states explicitly aiming to build communism. + The Chinese government has been variously described as communist and socialist, but also as authoritarian and corporatist, with heavy restrictions in many areas, most notably against free access to the Internet, freedom of the press, freedom of assembly, the right to have children, free formation of social organizations and freedom of religion. + Its current political, ideological and economic system has been termed by its leaders as a "consultative democracy" "people's democratic dictatorship", "socialism with Chinese characteristics" (which is Marxism adapted to Chinese circumstances) and the "socialist market economy" respectively. + According to Lutgard Lams, "President Xi is making great attempts to 'Sinicize' Marxist–Leninist Thought 'with Chinese characteristics' in the political sphere." +Since 2018, the main body of the Chinese constitution declares that "the defining feature of socialism with Chinese characteristics is the leadership of the Communist Party of China (CPC)." The 2018 amendments constitutionalized the one-party state status of China, wherein the General Secretary (party leader) holds ultimate power and authority over state and government and serves as the paramount leader of China. + The electoral system is pyramidal. + Local People's Congresses are directly elected, and higher levels of People's Congresses up to the National People's Congress (NPC) are indirectly elected by the People's Congress of the level immediately below. + The political system is decentralized, and provincial and sub-provincial leaders have a significant amount of autonomy. + Another eight political parties, have representatives in the NPC and the Chinese People's Political Consultative Conference (CPPCC). + China supports the Leninist principle of "democratic centralism", but critics describe the elected National People's Congress as a "rubber stamp" body. +The President is the titular head of state, elected by the National People's Congress. + The Premier is the head of government, presiding over the State Council composed of four vice premiers and the heads of ministries and commissions. + The incumbent president is Xi Jinping, who is also the General Secretary of the Communist Party of China and the Chairman of the Central Military Commission, making him China's paramount leader. + The incumbent premier is Li Keqiang, who is also a senior member of the CPC Politburo Standing Committee, China's top decision-making body. +There have been some moves toward political liberalization, in that open contested elections are now held at the village and town levels. + However, the party retains effective control over government appointments: in the absence of meaningful opposition, the CPC wins by default most of the time. + In 2017, Xi called on the communist party to further tighten its grip on the country, to uphold the unity of the party leadership, and achieve the "Chinese Dream of national rejuvenation". + Political concerns in China include the growing gap between rich and poor and government corruption. + Nonetheless, the level of public support for the government and its management of the nation is high, with 80–95% of Chinese citizens expressing satisfaction with the central government, according to a 2011 survey. +The People's Republic of China is divided into 22 provinces, five autonomous regions (each with a designated minority group), and four municipalities—collectively referred to as "mainland China"—as well as the special administrative regions (SARs) of Hong Kong and Macau. + Geographically, all 31 provincial divisions of mainland China can be grouped into six regions: North China, Northeast China, East China, South Central China, Southwest China, and Northwest China. +China considers Taiwan to be its 23rd province, although Taiwan is governed by the Republic of China (ROC), which rejects the PRC's claim. + Conversely, the ROC claims sovereignty over all divisions governed by the PRC. +The PRC has diplomatic relations with 175 countries and maintains embassies in 162. + Its legitimacy is disputed by the Republic of China and a few other countries; it is thus the largest and most populous state with limited recognition. + In 1971, the PRC replaced the Republic of China as the sole representative of China in the United Nations and as one of the five permanent members of the United Nations Security Council. + China was also a former member and leader of the Non-Aligned Movement, and still considers itself an advocate for developing countries. + Along with Brazil, Russia, India and South Africa, China is a member of the BRICS group of emerging major economies and hosted the group's third official summit at Sanya, Hainan in April 2011. +Under its interpretation of the One-China policy, Beijing has made it a precondition to establishing diplomatic relations that the other country acknowledges its claim to Taiwan and severs official ties with the government of the Republic of China. + Chinese officials have protested on numerous occasions when foreign countries have made diplomatic overtures to Taiwan, especially in the matter of armament sales. +Much of current Chinese foreign policy is reportedly based on Premier Zhou Enlai's Five Principles of Peaceful Coexistence, and is also driven by the concept of "harmony without uniformity", which encourages diplomatic relations between states despite ideological differences. + This policy may have led China to support states that are regarded as dangerous or repressive by Western nations, such as Zimbabwe, North Korea and Iran. + China has a close economic and military relationship with Russia, and the two states often vote in unison in the UN Security Council. +China became the world's largest trading nation in 2013, as measured by the sum of imports and exports. + By 2016, China was the largest trading partner of 124 other countries. + In 2019, China's exports of goods and services was $2.5 trillion and imports was $2.1 trillion, thus resulting in $4.6 trillion of foreign trade. + In recent decades, China has played an increasing role in calling for free trade areas and security pacts amongst its Asia-Pacific neighbours. + China became a member of the World Trade Organization (WTO) on 11 December 2001. + In 2004, it proposed an entirely new East Asia Summit (EAS) framework as a forum for regional security issues. + The EAS, which includes ASEAN Plus Three, India, Australia and New Zealand, held its inaugural summit in 2005. + China is also a founding member of the Shanghai Cooperation Organization (SCO), along with Russia and the Central Asian republics. +China has had a long and complex trade relationship with the United States. + In 2000, the United States Congress approved "permanent normal trade relations" (PNTR) with China, allowing Chinese exports in at the same low tariffs as goods from most other countries. + China has a significant trade surplus with the United States, its most important export market. + In the early 2010s, US politicians argued that the Chinese yuan was significantly undervalued, giving China an unfair trade advantage. +Since the turn of the century, China has followed a policy of engaging with African nations for trade and bilateral co-operation; in 2012, Sino-African trade totalled over US$160 billion. + China maintains healthy and highly diversified trade links with the European Union. + China has furthermore strengthened its ties with major South American economies, becoming the largest trading partner of Brazil and building strategic links with Argentina. +China's Belt and Road Initiative has expanded significantly over the last six years and, as of 2019, includes 137 countries and 30 international organizations. +Ever since its establishment after the second Chinese Civil War, the PRC has claimed the territories governed by the Republic of China (ROC), a separate political entity today commonly known as Taiwan, as a part of its territory. + It regards the island of Taiwan as its Taiwan Province, Kinmen and Matsu as a part of Fujian Province and islands the ROC controls in the South China Sea as a part of Hainan Province and Guangdong Province. + These claims are controversial because of the complicated Cross-Strait relations, with the PRC treating the One-China policy as one of its most important diplomatic principles. +In addition to Taiwan, China is also involved in other international territorial disputes. + Since the 1990s, China has been involved in negotiations to resolve its disputed land borders, including a disputed border with India and an undefined border with Bhutan. + China is additionally involved in multilateral disputes over the ownership of several small islands in the East and South China Seas, such as the Senkaku Islands and the Scarborough Shoal. + On 21 May 2014 Xi Jinping, speaking at a conference in Shanghai, pledged to settle China's territorial disputes peacefully. + "China stays committed to seeking peaceful settlement of disputes with other countries over territorial sovereignty and maritime rights and interests", he said. +China is regularly hailed as a potential new superpower, with certain commentators citing its rapid economic progress, growing military might, very large population, and increasing international influence as signs that it will play a prominent global role in the 21st century. + Others, however, warn that economic bubbles and demographic imbalances could slow or even halt China's growth as the century progresses.Some authors also question the definition of "superpower", arguing that China's large economy alone would not qualify it as a superpower, and noting that it lacks the military power and cultural influence of the United States. + Others – namely, IRENA – have argued that China's investment in renewable energy has positioned it to be a likely superpower in that area, especially due to a shift in global trade relations. +The Chinese democracy movement, social activists, and some members of the Communist Party of China believe in the need for social and political reform. + While economic and social controls have been significantly relaxed in China since the 1970s, political freedom is still tightly restricted. + The Constitution of the People's Republic of China states that the "fundamental rights" of citizens include freedom of speech, freedom of the press, the right to a fair trial, freedom of religion, universal suffrage, and property rights. + However, in practice, these provisions do not afford significant protection against criminal prosecution by the state. + Although some criticisms of government policies and the ruling Communist Party are tolerated, censorship of political speech and information, most notably on the Internet, are routinely used to prevent collective action. + By 2020, China plans to give all its citizens a personal "Social Credit" score based on how they behave. + The Social Credit System, now being piloted in a number of Chinese cities, is considered a form of mass surveillance which uses big data analysis technology. + In 2005, Reporters Without Borders ranked China 159th out of 167 states in its Annual World Press Freedom Index, which indicates a very low level of press freedom. + In 2014, China ranked 175th out of 180 countries. +Rural migrants to China's cities often find themselves treated as second class citizens by the household registration system, which controls access to state benefits. + Property rights are often poorly protected, However, a number of rural taxes have been reduced or abolished since the early 2000s, and additional social services provided to rural dwellers. +A number of foreign governments, foreign press agencies, and NGOs have criticized China's human rights record, alleging widespread civil rights violations such as detention without trial, forced abortions, forced confessions, torture, restrictions of fundamental rights, and excessive use of the death penalty. + The government suppresses popular protests and demonstrations that it considers a potential threat to "social stability", as was the case with the Tiananmen Square protests of 1989. +Falun Gong was first taught publicly in 1992. + In 1999, when there were about 70 million practitioners, the persecution of Falun Gong began, resulting in mass arrests, extralegal detention, and alleged reports of torture and deaths in custody. + The Chinese state is regularly accused of large-scale repression and human rights abuses in Tibet and Xinjiang, including violent police crackdowns and religious suppression. + At least one million members of China's Muslim Uyghur minority have been detained in mass detention camps, termed "Vocational Education and Training Centers", aimed at changing the political thinking of detainees, their identities, and their religious beliefs. + The camps were established under General Secretary Xi Jinping's administration. + In January 2019 the United Nations asked for direct access to the detention camps after a panel said it had received "credible reports" that 1.1 million Uygur, Kazakhs, Hui and other ethnic minorities had been detained in these camps. + The state has also sought to control offshore reporting of tensions in Xinjiang, intimidating foreign-based reporters by detaining their family members. +The Chinese government has responded to foreign criticism by arguing that the right to subsistence and economic development is a prerequisite to other types of human rights, and that the notion of human rights should take into account a country's present level of economic development. + It emphasizes the rise in the Chinese standard of living, literacy rate, and average life expectancy since the 1970s, as well as improvements in workplace safety and efforts to combat natural disasters such as the perennial Yangtze River floods. + Furthermore, some Chinese politicians have spoken out in support of democratization, although others remain more conservative. + Some major reform efforts have been conducted. + For instance, in November 2013 the government announced plans to relax the one-child policy and abolish the much-criticized re-education through labour program, although human rights groups note that reforms to the latter have been largely cosmetic. + During the 2000s and early 2010s, the Chinese government was increasingly tolerant of NGOs that offer practical, efficient solutions to social problems, but such "third sector" activity remained heavily regulated. + After Xi Jinping succeeded General Secretary of the Communist Party of China in 2012, human rights in China have become worse. +The Global Slavery Index estimated that in 2016 more than 3.8 million people were living in "conditions of modern slavery", or 0.25% of the population, including victims of human trafficking, forced labor, forced marriage, child labor, and state-imposed forced labor. + All except the last category are illegal. + The state-imposed forced system was formally abolished in 2013 but it is not clear the extent to which its various practices have stopped. + The Chinese penal system includes labor prison factories, detention centers, and re-education camps, which fall under the heading Laogai ("reform through labor"). + The Laogai Research Foundation in the United States estimated that there were over a thousand slave labour prisons and camps, known collectively as the Laogai. + Prisoners are not paid at all, and need their families to send money to them. + Prisoners who refuse to work are beaten, and some are beaten to death. + Many of the prisoners are political or religious dissidents, and some are recognized internationally as prisoners of conscience. + A Chinese leader said that they want to see two products coming out of the prisons: the man who has been reformed, and the product made by the man. + Harry Wu, himself a former prisoner of the Laogai, filmed undercover footage of the Laogai, and was charged with stealing state secrets. + For this, Harry Wu was sentenced to 15 years in prison, but only served 66 days before being deported to the United States. +In 2019 a world-first study called for the mass retraction of more than 400 scientific papers on organ transplantation, because of fears the organs were obtained unethically from Chinese prisoners. + The study was published in the medical journal BMJ Open. + A report published in 2016 found a large discrepancy between official transplant figures from the Chinese government and the number of transplants reported by hospitals. + While the government says 10,000 transplants occur each year, hospital data shows between 60,000 and 100,000 organs are transplanted each year. + The report provided evidence that this gap is being made up by executed prisoners of conscience. +With 2.3 million active troops, the People's Liberation Army (PLA) is the largest standing military force in the world, commanded by the Central Military Commission (CMC). + China has the second-biggest military reserve force, only behind North Korea. + The PLA consists of the Ground Force (PLAGF), the Navy (PLAN), the Air Force (PLAAF), and the People's Liberation Army Rocket Force (PLARF). + According to the Chinese government, China's military budget for 2017 totalled US$151.5 billion, constituting the world's second-largest military budget, although the military expenditures-GDP ratio with 1.3% of GDP is below world average. + However, many authorities – including SIPRI and the U.S. + Office of the Secretary of Defense – argue that China does not report its real level of military spending, which is allegedly much higher than the official budget. +As a recognized nuclear weapons state, China is considered both a major regional military power and a potential military superpower. + According to a 2013 report by the US Department of Defense, China fields between 50 and 75 nuclear ICBMs, along with a number of SRBMs. + However, compared with the other four UN Security Council Permanent Members, China has relatively limited power projection capabilities. + To offset this, it has developed numerous power projection assets since the early 2000s – its first aircraft carrier entered service in 2012, and it maintains a substantial fleet of submarines, including several nuclear-powered attack and ballistic missile submarines. + China has furthermore established a network of foreign military relationships along critical sea lanes. +China has made significant progress in modernising its air force in recent decades, purchasing Russian fighter jets such as the Sukhoi Su-30, and also manufacturing its own modern fighters, most notably the Chengdu J-10, J-20 and the Shenyang J-11, J-15, J-16, and J-31. + China is furthermore engaged in developing an indigenous stealth aircraft and numerous combat drones. + Air and Sea denial weaponry advances have increased the regional threat from the perspective of Japan as well as Washington. + China has also updated its ground forces, replacing its ageing Soviet-derived tank inventory with numerous variants of the modern Type 99 tank, and upgrading its battlefield C3I and C4I systems to enhance its network-centric warfare capabilities. + In addition, China has developed or acquired numerous advanced missile systems, including anti-satellite missiles, cruise missiles and submarine-launched nuclear ICBMs. + According to the Stockholm International Peace Research Institute's data, China became the world's third largest exporter of major arms in 2010–14, an increase of 143 percent from the period 2005–09. + SIPRI also calculated that China surpassed Russia to become the world's second largest arms exporter by 2020. + Chinese officials stated that spending on the military will rise to U.S. + $173B in 2018. + In the period of 2014–2018, China was the fifth largest exporter of major arms in the world, with an increase of 2.7% compared to the previous period.In August 2018, China tested its first hypersonic flight. + The China Academy of Aerospace Aerodynamics (CAAA) claims to have successfully conducted the test with the aircraft Starry Sky-2 that touched a speed of Mach 6 – which is six times the speed of sound, that can carry nuclear missiles. +Since 2010, China had the world's second-largest economy in terms of nominal GDP, totaling approximately US$13.5 trillion (90 trillion Yuan) as of 2018. + In terms of purchasing power parity (PPP GDP), China's economy has been the largest in the world since 2014, according to the World Bank. + According to the World Bank, China's GDP grew from $150 billion in 1978 to $13.6 trillion by 2018. + China's economic growth has been consistently above 6 percent since the introduction of economic reforms in 1978. + China is also the world's largest exporter and second-largest importer of goods. + Between 2010 and 2019, China's contribution to global GDP growth has been 25% to 39%. +China had the largest economy in the world for most of the past two thousand years, during which it has seen cycles of prosperity and decline. + Since economic reforms began in 1978, China has developed into a highly diversified economy and one of the most consequential players in international trade. + Major sectors of competitive strength include manufacturing, retail, mining, steel, textiles, automobiles, energy generation, green energy, banking, electronics, telecommunications, real estate, e-commerce, and tourism. + China has three out of the ten largest stock exchanges in the world—Shanghai, Hong Kong and Shenzhen—that together have a market capitalization of over $10 trillion, as of 2019. +China has been the world's No. + 1 manufacturer since 2010, after overtaking the US, which had been No. + 1 for the previous hundred years. + China has also been No. + 2 in high-tech manufacturing since 2012, according to US National Science Foundation. + China is the second largest retail market in the world, next to the United States. + China leads the world in e-commerce, accounting for 40% of the global market share in 2016 and more than 50% of the global market share in 2019. + China is the world's leader in electric vehicles, manufacturing and buying half of all the plug-in electric cars (BEV and PHEV) in the world in 2018. + China had 174 GW of installed solar capacity by the end of 2018, which amounts to more than 40% of the global solar capacity. +As of 2018, China was first in the world in total number of billionaires and second in millionaires—there were 658 Chinese billionaires and 3.5 million millionaires. + However, it ranks behind over 70 countries (out of around 180) in per capita economic output, making it a middle income country. + Additionally, its development is highly uneven. + Its major cities and coastal areas are far more prosperous compared to rural and interior regions. + China brought more people out of extreme poverty than any other country in history—between 1978 and 2018, China reduced extreme poverty by 800 million. + China reduced the extreme poverty rate—per international standard, it refers to an income of less than $1.90/day—from 88% in 1981 to 1.85% by 2013. + According to the World Bank, the number of Chinese in extreme poverty fell from 756 million to 25 million between 1990 and 2013. + China's own national poverty standards are higher and thus the national poverty rates were 3.1% in 2017 and 1% in 2018. +In 2019, China overtook the US as the home to the highest number of rich people in the world, according to the global wealth report by Credit Suisse. + In other words, as of 2019, 100 million Chinese are in the top 10% of the wealthiest individuals in the world—those who have a net personal wealth of at least $110,000. +From its founding in 1949 until late 1978, the People's Republic of China was a Soviet-style centrally planned economy. + Following Mao's death in 1976 and the consequent end of the Cultural Revolution, Deng Xiaoping and the new Chinese leadership began to reform the economy and move towards a more market-oriented mixed economy under one-party rule. + Agricultural collectivization was dismantled and farmlands privatized, while foreign trade became a major new focus, leading to the creation of Special Economic Zones (SEZs). + Inefficient state-owned enterprises (SOEs) were restructured and unprofitable ones were closed outright, resulting in massive job losses. + Modern-day China is mainly characterized as having a market economy based on private property ownership, and is one of the leading examples of state capitalism. + The state still dominates in strategic "pillar" sectors such as energy production and heavy industries, but private enterprise has expanded enormously, with around 30 million private businesses recorded in 2008. + In 2018, private enterprises in China accounted for 60% of GDP, 80% of urban employment and 90% of new jobs. +In 2015, China's Middle Class became the largest in the world. + By 2019, there were more Chinese than Americans in the richest 10% of the people in the world. + Since economic liberalization began in 1978, China has been among the world's fastest-growing economies, relying originally on investment- and export-led growth, but now shifting more to consumption. + In 2018, 76% of growth in China's GDP came from consumption. + According to the IMF, China's annual average GDP growth between 2001 and 2010 was 10.5%. + In the years immediately following the financial crisis of 2007, China's economic growth rate was equivalent to all of the G7 countries' growth combined. + According to the Global Growth Generators index announced by Citigroup in February 2011, China has a very high 3G growth rating. + Its high productivity, low labor costs and relatively good infrastructure have made it a global leader in manufacturing. +China ranks No. + 1 in the production of steel, aluminum and automobiles—China's global market shares are 50% in steel, 50% in aluminum and 30% in automobile manufacturing. + China has also been increasingly turning to automation, becoming the world's largest market for industrial robots in 2013. + Between 2010 and 2015, China installed 90,000 industrial robots, or one-third of the world's total. + In 2017, China bought 36% of all the new industrial robots in the world. + China's plan is to also domestically design and manufacture 100,000 industrial robots by 2020. + However, the Chinese economy is highly energy-intensive and inefficient; China became the world's largest energy consumer in 2010, relies on coal to supply over 70% of its energy needs, and surpassed the US to become the world's largest oil importer in 2013. +In the last decade, China has become #1 in the world in terms of installed solar power capacity, hydro-power and wind power. + According to the World Economic Forum, China will account for 40% of the global renewable energy by 2022. + In addition, official GDP figures are seen as unreliable and there have been several well-publicized cases of data manipulation. + In the early 2010s, China's economic growth rate began to slow amid domestic credit troubles, weakening international demand for Chinese exports and fragility in the global economy. + China's GDP was smaller than Germany's in 2007; however, by 2017, China's $12.2 trillion-economy became larger than those of Germany, UK, France and Italy combined. + In 2018, the IMF reiterated its forecast that China will overtake the US in terms of nominal GDP by the year 2030. + Economists also expect China's middle class to expand to 600 million people by 2025. +Tourism is a major contributor to the economy. + In 2017, this sector contributed about CNY 8.77 trillion (US$1.35 trillion), 11.04% of the GDP, and contributed direct and indirect employment of up to 28.25 million people. + There were 139.48 million inbound trips and five billion domestic trips. + China is now No. + 1 in the number of skyscrapers (buildings taller than 200m), accounting for about 50% of world's total. + In four years—2015 through 2018—China built 310 skyscrapers, while the corresponding number for the US was 33. +China is one of the world's most technologically advanced nations. + It is the world's largest e-commerce market, amounting to 42% of the global market by 2016 and is expected to account for 55% of global e-commerce retail sales in 2019 (more than three times as large as the US market). + China's e-commerce market had online sales of more than $1 trillion in 2018, according to PWC and is expected to be just under $2 trillion in 2019. + China's e-commerce industry took off in 2009, marked by the growth of internet giants Tencent and Alibaba – purveyors of products such as WeChat and Tmall – that have become ubiquitous in contemporary Chinese life. + Tencent's WeChat Pay and Alibaba's Ali Pay have helped China become a world leader in mobile payments, which amounted to about $30 trillion in China in 2017 and more than $40 trillion in 2018. +China is also second only to the United States in venture capital activity and is home to a large number of unicorn startup companies. + In 2018, China attracted $105 billion of venture capital investments, amounting to 38% of global VC investments that year. + In late 2018, the world's most valuable startup was ByteDance, a Chinese company; and the two most valuable artificial intelligence (AI) startups in the world were SenseTime and Face++, both from China. + In 2017, China's State Council released its Artificial Intelligence Development Plan, which declared AI technology a priority economic growth and investment sector. + In 2018, China created 97 "unicorns" – startups that are worth more than $1 billion – which amounted to 1 unicorn every 3.8 days. + Chinese smartphone brands – Huawei, Xiaomi, Oppo, Vivo, OnePlus etc. + – have captured more than 40% of the global market. + By 2019, Huawei had become the world's largest telecom infrastructure provider, surpassing Nokia and Ericsson, and had taken the lead in 5G technology. + The company also entered the consumer smartphone and enterprise services markets, and is the world's third-largest smartphone company, after Apple and Samsung. +Other Chinese tech giants include DJI (world's largest drone maker with 70% market share), BOE (world's largest flat-panel display maker), Didi (world's largest and most valuable ride-sharing company) and BYD (world's largest plug-in vehicle maker—including both BEV and PHEV) China also the most number of supercomputers—227 out of the Top 500 (as of 2019). +China is also the world leader in patents, accounting for 1.54 million patents or almost half of all global patents in 2018. + China's spending on R&D has been growing rapidly in the last decade, amounting to $277 billion in 2017. +China is a member of the WTO and is the world's largest trading power, with a total international trade value of US$4.62 trillion in 2018. + Its foreign exchange reserves reached US$3.1 trillion as of 2019, making its reserves by far the world's largest. + In 2012, China was the world's largest recipient of inward foreign direct investment (FDI), attracting $253 billion. + In 2014, China's foreign exchange remittances were $US64 billion making it the second largest recipient of remittances in the world. + China also invests abroad, with a total outward FDI of $62.4 billion in 2012, and a number of major takeovers of foreign firms by Chinese companies. + China is a major owner of US public debt, holding trillions of dollars worth of U.S. + Treasury bonds. + China's undervalued exchange rate has caused friction with other major economies, and it has also been widely criticized for manufacturing large quantities of counterfeit goods. +China ranks 17th in the world in Global Innovation Index, not too far from the US, which ranks 6th. + China ranks 27th out of 137 countries in the 2017–2018 Global Competitiveness Index, above many advanced economies and making it by far the most competitive major emerging economy. + This is largely owing to its strength in infrastructure and wide adoption of communication and information technology. + However, it lags behind advanced economies in labor market efficiency, institutional strength, and openness of market competition, especially for foreign players attempting to enter the domestic market. + In 2018, s Global 500 list of the world's largest corporations included 120 Chinese companies. + Many of the largest public companies in the world were Chinese, including the world's largest bank by total assets, the Industrial and Commercial Bank of China. +Following the 2007–08 financial crisis, Chinese authorities sought to actively wean off of its dependence on the U.S. + Dollar as a result of perceived weaknesses of the international monetary system. + To achieve those ends, China took a series of actions to further the internationalization of the Renminbi. + In 2008, China established dim sum bond market and expanded the Cross-Border Trade RMB Settlement Pilot Project, which helps establish pools of offshore RMB liquidity. + This was followed with bilateral agreements to settle trades directly in renminbi with Russia, Japan, Australia, Singapore, the United Kingdom, and Canada. + As a result of the rapid internationalization of the renminbi, it became the eighth-most-traded currency in the world, an emerging international reserve currency, and a component of the IMF's special drawing rights; however, partly due to capital controls that make the renminbi fall short of being a fully convertible currency, it remains far behind the Euro, Dollar and Japanese Yen in international trade volumes. +China has had the world's largest middle class population since 2015, and the middle class grew to a size of 400 million by 2018. + China's middle-class population (if defined as those with annual income of between US$10,000 and US$60,000) had reached more than 300 million by 2012. + Wages in China have grown exponentially in the last 40 years—real (inflation-adjusted) wages grew seven-fold from 1978 to 2007. + By 2018, median wages in Chinese cities such as Shanghai were about the same as or higher than the wages in Eastern European countries. + More than 75 percent of China's urban consumers are expected to earn between 60,000 and 229,000 RMB per year by 2022. + China has the world's second-highest number of billionaires, with nearly 400 as of 2018, increasing at the rate of roughly two per week. + China's domestic retail market was worth over 20 trillion yuan (US$3.2 trillion) in 2012 and is growing at over 12% annually as of 2013, while the country's luxury goods market has expanded immensely, with 27.5% of the global share. + However, in recent years, China's rapid economic growth has contributed to severe consumer inflation, leading to increased government regulation. + China has a high level of economic inequality, which has increased in the past few decades. + In 2012, China's official Gini coefficient was 0.474. + A study conducted by Southwestern University of Finance and Economics showed that China's Gini coefficient actually had reached 0.61 in 2012, and top 1% Chinese held more than 25% of China's wealth. + By 2018, China's GINI index had risen to 0.467, according to the World Bank. +China was once a world leader in science and technology up until the Ming dynasty. + Ancient Chinese discoveries and inventions, such as papermaking, printing, the compass, and gunpowder (the Four Great Inventions), became widespread across East Asia, the Middle East and later to Europe. + Chinese mathematicians were the first to use negative numbers. + By the 17th century, Europe and the Western world surpassed China in scientific and technological advancement. + The causes of this early modern Great Divergence continue to be debated by scholars to this day. +After repeated military defeats by the European colonial powers and Japan in the 19th century, Chinese reformers began promoting modern science and technology as part of the Self-Strengthening Movement. + After the Communists came to power in 1949, efforts were made to organize science and technology based on the model of the Soviet Union, in which scientific research was part of central planning. + After Mao's death in 1976, science and technology was established as one of the Four Modernizations, and the Soviet-inspired academic system was gradually reformed. +Since the end of the Cultural Revolution, China has made significant investments in scientific research and is quickly catching up with the US in R&D spending. + In 2017, China spent $279 billion on scientific research and development. + According to OECD, China spent 2.11% of its GDP on Research and Development (R&D) in 2016. + Science and technology are seen as vital for achieving China's economic and political goals, and are held as a source of national pride to a degree sometimes described as "techno-nationalism". + Nonetheless, China's investment in basic and applied scientific research remains behind that of leading technological powers such as the United States and Japan. + According to the US National Science Board, China had, for the first time, more science and engineering publications than the US, in 2016. + Also, in 2016, China spent $409 billion (by PPP) on Research and Development. + In 2018, China is estimated to have spent $475 billion (by PPP), second only to the USA. + In 2017, China was No. + 2 in international patents application, behind the US but ahead of Japan. + Chinese tech companies Huawei and ZTE were the top 2 filers of international patents in 2017. + Chinese-born scientists have won the Nobel Prize in Physics four times, the Nobel Prize in Chemistry and Physiology or Medicine once respectively, though most of these scientists conducted their Nobel-winning research in western nations. +China is developing its education system with an emphasis on science, mathematics and engineering; in 2009, China graduated over 10,000 PhD engineers, and as many as 500,000 BSc graduates, more than any other country. + In 2016, there were 4.7 million STEM (Science, Technology, Engineering and Mathematics) graduates in China, which was more than eight times the corresponding number for the US. + China also became the world's largest publisher of scientific papers, by 2016. + Chinese technology companies such as Huawei and Lenovo have become world leaders in telecommunications and personal computing, and Chinese supercomputers are consistently ranked among the world's most powerful. + China is also expanding its use of industrial robots; from 2008 to 2011, the installation of multi-role robots in Chinese factories rose by 136 percent. + China has been the world's largest market for industrial robots since 2013 and will account for 45% of newly installed robots from 2019–2021. +The Chinese space program is one of the world's most active, and is a major source of national pride. + In 2018, China successfully launched more satellites (35) than any other country, including the USA (30). + In 1970, China launched its first satellite, Dong Fang Hong I, becoming the fifth country to do so independently. + In 2003, China became the third country to independently send humans into space, with Yang Liwei's spaceflight aboard Shenzhou 5; as of 2015, ten Chinese nationals have journeyed into space, including two women. + In 2011, China's first space station module, Tiangong-1, was launched, marking the first step in a project to assemble a large manned station by the early 2020s. + In 2013, China successfully landed the Chang'e 3 lander and Yutu rover onto the lunar surface. + In 2016, China's 2nd space station module, Tiangong-2, was launched from Jiuquan aboard a Long March 2F rocket on 15 September 2016. + Then Shenzhou 11 successfully docked with Tiangong-2 on 19 October 2016. + In 2019, China became the first country to land a probe—Chang'e 4—on the far side of the moon. +A 2016 report by McKinsey consulting group, revealed that China has been annually spending more on infrastructure than North America and Western Europe combined. +China is the largest telecom market in the world and currently has the largest number of active cellphones of any country in the world, with over 1.5 billion subscribers, as of 2018. + It also has the world's largest number of internet and broadband users, with over 800 million Internet users as of 2018—equivalent to around 60% of its population—and almost all of them being mobile as well. + Almost entire China's population had access to 4G network by 2017. + By 2018, China had more than 1 billion 4G users, accounting for 40% of world's total. + In terms of unique mobile subscribers as percentage of population, China came in at 82%, placing the country No. + 3 in the world (as of 2018). + As of early 2019, the average mobile connection speed in China was 30 Mbit/s (megabits per second), which is 9% slower than the US. + As for fixed broadband in China, the average download speed was 76 Mbit/s; and 60% of fixed broadband Chinese users (or 200 million Chinese households) were able to access the Internet at 100 Mbit/s or higher (as of 2018). + China is making rapid progress in 1 Gbit/s (1000 Mbit/s) internet, and 42% of Chinese homes are expected to have 1 Gbit/s broadband link by 2023. + In 2018, China had 378 million fixed broadband users and 87% of them were fiber-optic users, making China No. + 1 in the world in deployment of fiber-optic cables for broadband. + By the end of 2017, China had 29 million kilometers of fiber-optic cable. + In 2019, China is expected to account for 24% of the world's spending on IoT or internet-connected devices. + Since 2011 China has been the nation with the most installed telecommunication bandwidth in the world. + By 2014, China hosted more than twice as much national bandwidth potential than the U.S., the historical leader in terms of installed telecommunication bandwidth (China: 29% versus US:13% of the global total). + China is making rapid advances in 5G—by late 2018, China had started large-scale and commercial 5G trials. + In early 2019, Shanghai railway station introduced 5G WiFi that has an internet speed of 1,200 Mbit/s. +China Mobile, China Unicom and China Telecom, are the three large providers of mobile and internet in China. + China Telecom alone served more than 145 million broadband subscribers and 300 million mobile users; China Unicom had about 300 million subscribers; and China Mobile, the biggest of them all, had 925 million users, as of 2018. + Combined, the three operators had over 3.4 million 4G base-stations in China. + Several Chinese telecommunications companies, most notably Huawei and ZTE, have been accused of spying for the Chinese military. + British intelligence—GCHQ and NCSC—said in 2019 that there have been no evidence of malicious activity or spying by Huawei. +China is developing its own satellite navigation system, dubbed Beidou, which began offering commercial navigation services across Asia in 2012 and it started providing global services by the end of 2018. + Now China belongs to the elite group of three countries—US and Russia being the other two members—that provide global satellite navigation. +Since the late 1990s, China's national road network has been significantly expanded through the creation of a network of national highways and expressways. + In 2018, China's highways had reached a total length of 142,500 km (88,500 mi), making it the longest highway system in the world; and China's railways reached a total length of 127,000 km by 2017. + By the end of 2018, China's high-speed railway network reached a length of 29,000 km, representing more than 60% of the world's total. + In 1991, there were only six bridges across the main stretch of the Yangtze River, which bisects the country into northern and southern halves. + By October 2014, there were 81 such bridges and tunnels.China has the world's largest market for automobiles, having surpassed the United States in both auto sales and production. + Sales of passenger cars in 2016 exceeded 24 million. + A side-effect of the rapid growth of China's road network has been a significant rise in traffic accidents, with poorly enforced traffic laws cited as a possible cause—in 2011 alone, around 62,000 Chinese died in road accidents. + However, the Chinese government has taken a lot of steps to address this problem and has reduced the number of fatalities in traffic accidents by 20% from 2007 to 2017. + In urban areas, bicycles remain a common mode of transport, despite the increasing prevalence of automobiles – as of 2012, there are approximately 470 million bicycles in China. +China's railways, which are state-owned, are among the busiest in the world, handling a quarter of the world's rail traffic volume on only 6 percent of the world's tracks in 2006. + as of 2017, the country had 127,000 km (78,914 mi) of railways, the second longest network in the world. + The railways strain to meet enormous demand particularly during the Chinese New Year holiday, when the world's largest annual human migration takes place. + In 2013, Chinese railways delivered 2.106 billion passenger trips, generating 1,059.56 billion passenger-kilometers and carried 3.967 billion tons of freight, generating 2,917.4 billion cargo tons-kilometers. +China's high-speed rail (HSR) system started construction in the early 2000s. + By the end of 2019, high speed rail in China had over 35,000 kilometers (21,748 miles) of dedicated lines alone, a length that exceeds rest of the world's high-speed rail tracks combined twice, making it the longest HSR network in the world. + With an annual ridership of over 1.1 billion passengers in 2015 it is the world's busiest. + The network includes the Beijing–Guangzhou–Shenzhen High-Speed Railway, the single longest HSR line in the world, and the Beijing–Shanghai High-Speed Railway, which has three of longest railroad bridges in the world. + The HSR track network is set to reach approximately 30,000 km (19,000 mi) by the end of 2019. + The Shanghai Maglev Train, which reaches 431 km/h (268 mph), is the fastest commercial train service in the world. + In May 2019, China released a prototype for a maglev high-speed train that would reach a speed of 600 km/hr (375 mph); and it is expected to go into commercial production by 2021. + +Since 2000, the growth of rapid transit systems in Chinese cities has accelerated. + As of January 2016, 26 Chinese cities have urban mass transit systems in operation and 39 more have metro systems approved with a dozen more to join them by 2020. + The Shanghai Metro, Beijing Subway, Guangzhou Metro, Hong Kong MTR and Shenzhen Metro are among the longest and busiest in the world. +There were approximately 229 airports in 2017, with around 240 planned by 2020. + More than two-thirds of the airports under construction worldwide in 2013 were in China, and Boeing expects that China's fleet of active commercial aircraft in China will grow from 1,910 in 2011 to 5,980 in 2031. + In just five years—from 2013 to 2018—China bought 1000 planes from Boeing. + With rapid expansion in civil aviation, the largest airports in China have also joined the ranks of the busiest in the world. + In 2018, Beijing's Capital Airport ranked second in the world by passenger traffic (it was 26th in 2002). + Since 2010, the Hong Kong International Airport and Shanghai Pudong International Airport have ranked first and third in air cargo tonnage. +Some 80% of China's airspace remains restricted for military use, and Chinese airlines made up eight of the 10 worst-performing Asian airlines in terms of delays.China has over 2,000 river and seaports, about 130 of which are open to foreign shipping. + In 2017, the Ports of Shanghai, Hong Kong, Shenzhen, Ningbo-Zhoushan, Guangzhou, Qingdao and Tianjin ranked in the Top 10 in the world in container traffic and cargo tonnage. +Water supply and sanitation infrastructure in China is facing challenges such as rapid urbanization, as well as water scarcity, contamination, and pollution. + According to data presented by the Joint Monitoring Program for Water Supply and Sanitation of WHO and UNICEF in 2015, about 36% of the rural population in China still did not have access to improved sanitation. + In June 2010, there were 1,519 sewage treatment plants in China and 18 plants were added each week. + The ongoing South–North Water Transfer Project intends to abate water shortage in the north. +The national census of 2010 recorded the population of the People's Republic of China as approximately 1,370,536,875. + About 16.60% of the population were 14 years old or younger, 70.14% were between 15 and 59 years old, and 13.26% were over 60 years old. + The population growth rate for 2013 is estimated to be 0.46%. + China used to make up much of the world's poor; now it makes up much of the world's middle class. + Although a middle-income country by Western standards, China's rapid growth has pulled hundreds of millions—800 million, to be more precise—of its people out of poverty since 1978. + By 2013, less than 2% of the Chinese population lived below the international poverty line of US$1.9 per day, down from 88% in 1981. + China's own standards for poverty are higher and still the country is on its way to eradicate national poverty completely by 2019. + From 2009–2018, the unemployment rate in China has averaged about 4%. +Given concerns about population growth, China implemented a two-child limit during the 1970s, and, in 1979, began to advocate for an even stricter limit of one child per family. + Beginning in the mid 1980s, however, given the unpopularity of the strict limits, China began to allow some major exemptions, particularly in rural areas, resulting in what was actually a "1.5"-child policy from the mid-1980s to 2015 (ethnic minorities were also exempt from one child limits). + The next major loosening of the policy was enacted in December 2013, allowing families to have two children if one parent is an only child. + In 2016, the one-child policy was replaced in favor of a two-child policy. + Data from the 2010 census implies that the total fertility rate may be around 1.4, although due to underreporting of births it may be closer to 1.5–1.6. +According to one group of scholars, one-child limits had little effect on population growth or the size of the total population. + However, these scholars have been challenged. + Their own counterfactual model of fertility decline without such restrictions implies that China averted more than 500 million births between 1970 and 2015, a number which may reach one billion by 2060 given all the lost descendants of births averted during the era of fertility restrictions, with one-child restrictions accounting for the great bulk of that reduction. +The policy, along with traditional preference for boys, may have contributed to an imbalance in the sex ratio at birth. + According to the 2010 census, the sex ratio at birth was 118.06 boys for every 100 girls, which is beyond the normal range of around 105 boys for every 100 girls. + The 2010 census found that males accounted for 51.27 percent of the total population. + However, China's sex ratio is more balanced than it was in 1953, when males accounted for 51.82 percent of the total population. +China legally recognizes 56 distinct ethnic groups, who altogether comprise the . + The largest of these nationalities are the ethnic Chinese or "Han", who constitute more than 80% of the totalpopulation. + The Han Chinese – the world's largest single ethnic group – outnumber other ethnic groups in every provincial-level division except Tibet and Xinjiang. + Ethnic minorities account for about less than 25% of the population of China, according tothe 2010 census. + Compared with the 2000 population census, the Han population increased by 66,537,177 persons, or 5.74%, while the population of the 55 national minorities combined increased by 7,362,627 persons, or 6.92%. + The 2010 census recorded a total of 593,832 foreign nationals living in China. + The largest such groups were from South Korea (120,750), theUnited States (71,493) and Japan (66,159). +There are as many as 292 living languages in China. + The languages most commonly spoken belong to the Sinitic branch of the Sino-Tibetan language family, which contains Mandarin (spoken by 70% of the population), and other varieties of Chinese language: Yue (including Cantonese and Taishanese), Wu (including Shanghainese and Suzhounese), Min (including Fuzhounese, Hokkien and Teochew), Xiang, Gan and Hakka. + Languages of the Tibeto-Burman branch, including Tibetan, Qiang, Naxi and Yi, are spoken across the Tibetan and Yunnan–Guizhou Plateau. + Other ethnic minority languages in southwest China include Zhuang, Thai, Dong and Sui of the Tai-Kadai family, Miao and Yao of the Hmong–Mien family, and Wa of the Austroasiatic family. + Across northeastern and northwestern China, local ethnic groups speak Altaic languages including Manchu, Mongolian and several Turkic languages: Uyghur, Kazakh, Kyrgyz, Salar and Western Yugur. + Korean is spoken natively along the border with North Korea. + Sarikoli, the language of Tajiks in western Xinjiang, is an Indo-European language. + Taiwanese aborigines, including a small population on the mainland, speak Austronesian languages. +Standard Mandarin, a variety of Mandarin based on the Beijing dialect, is the official national language of China and is used as a lingua franca in the country between people of different linguistic backgrounds. + Mongolian, Uyghur, Tibetan, Zhuang and various other languages are also regionally recognized throughout the country. +Chinese characters have been used as the written script for the Sinitic languages for thousands of years. + They allow speakers of mutually unintelligible Chinese varieties to communicate with each other through writing. + In 1956, the government introduced simplified characters, which have supplanted the older traditional characters in mainland China. + Chinese characters are romanized using the Pinyin system. + Tibetan uses an alphabet based on an Indic script. + Uyghur is most commonly written in Persian alphabet based Uyghur Arabic alphabet. + The Mongolian script used in China and the Manchu script are both derived from the Old Uyghur alphabet. + Zhuang uses both an official Latin alphabet script and a traditional Chinese character script. +China has urbanized significantly in recent decades. + The percent of the country's population living in urban areas increased from 20% in 1980 to over 55% in 2016. + It is estimated that China's urban population will reach one billion by 2030, potentially equivalent to one-eighth of the world population. + As of 2012, there are more than 262 million migrant workers in China, mostly rural migrants seeking work in cities. +China has over 160 cities with a population of over one million, including the seven megacities (cities with a population of over 10 million) of Chongqing, Shanghai, Beijing, Guangzhou, Tianjin, Shenzhen, and Wuhan. + Shanghai is China's most populous urban area while Chongqing is its largest city proper. + By 2025, it is estimated that the country will be home to 221 cities with over a million inhabitants. + The figures in the table below are from the 2010 census, and are only estimates of the urban populations within administrative city limits; a different ranking exists when considering the total municipal populations (which includes suburban and rural populations). + The large "floating populations" of migrant workers make conducting censuses in urban areas difficult; the figures below include only long-term residents. +Since 1986, compulsory education in China comprises primary and junior secondary school, which together last for nine years. + In 2010, about 82.5 percent of students continued their education at a three-year senior secondary school. + The Gaokao, China's national university entrance exam, is a prerequisite for entrance into most higher education institutions. + In 2010, 27 percent of secondary school graduates are enrolled in higher education. + This number increased significantly over the last years, reaching a tertiary school enrollment of 50 percent in 2018. + Vocational education is available to students at the secondary and tertiary level. +In February 2006, the government pledged to provide completely free nine-year education, including textbooks and fees. + Annual education investment went from less than US$50 billion in 2003 to more than US$250 billion in 2011. + However, there remains an inequality in education spending. + In 2010, the annual education expenditure per secondary school student in Beijing totalled ¥20,023, while in Guizhou, one of the poorest provinces in China, only totalled ¥3,204. + Free compulsory education in China consists of primary school and junior secondary school between the ages of 6 and 15. + In 2011, around 81.4% of Chinese have received secondary education. + By 2007, there were 396,567 primary schools, 94,116 secondary schools, and 2,236 higher education institutions in China. +As of 2018, 96% of the population over age 15 are literate. + In 1949, only 20% of the population could read, compared to 65.5% thirty years later. + In 2009, Chinese students from Shanghai achieved the world's best results in mathematics, science and literacy, as tested by the Programme for International Student Assessment (PISA), a worldwide evaluation of 15-year-old school pupils' scholastic performance. + Despite the high results, Chinese education has also faced both native and international criticism for its emphasis on rote memorization and its gap in quality from rural to urban areas. +There is an increasing number of top-ranked international universities seeking influence in China in recent years. + Washington University in St. + Louis established an EMBA program with Fudan University in 2002 which has since been constantly ranked as one of the best in the world. + Columbia Global Centers Beijing opened in 2009, and Harvard Institute Shanghai opened in 2010. + Two years later, Stanford University established an academic center in Peking University. + The year after, in 2013, Duke Kunshan University was put into operation. + Subsequently, the Yale Center Beijing, and NYU Shanghai were both established in 2014. +The National Health and Family Planning Commission, together with its counterparts in the local commissions, oversees the health needs of the Chinese population. + An emphasis on public health and preventive medicine has characterized Chinese health policy since the early 1950s. + At that time, the Communist Party started the Patriotic Health Campaign, which was aimed at improving sanitation and hygiene, as well as treating and preventing several diseases. + Diseases such as cholera, typhoid and scarlet fever, which were previously rife in China, were nearly eradicated by the campaign. + After Deng Xiaoping began instituting economic reforms in 1978, the health of the Chinese public improved rapidly because of better nutrition, although many of the free public health services provided in the countryside disappeared along with the People's Communes. + Healthcare in China became mostly privatized, and experienced a significant rise in quality. + In 2009, the government began a 3-year large-scale healthcare provision initiative worth US$124 billion. + By 2011, the campaign resulted in 95% of China's population having basic health insurance coverage. + In 2011, China was estimated to be the world's third-largest supplier of pharmaceuticals, but its population has suffered from the development and distribution of counterfeit medications. +As of 2017, the average life expectancy at birth in China is 76 years, and the infant mortality rate is 7 per thousand. + Both have improved significantly since the 1950s. + Rates of stunting, a condition caused by malnutrition, have declined from 33.1% in 1990 to 9.9% in 2010. + Despite significant improvements in health and the construction of advanced medical facilities, China has several emerging public health problems, such as respiratory illnesses caused by widespread air pollution, hundreds of millions of cigarette smokers, and an increase in obesity among urban youths. + China's large population and densely populated cities have led to serious disease outbreaks in recent years, such as the 2003 outbreak of SARS, although this has since been largely contained. + In 2010, air pollution caused 1.2 million premature deaths in China. +The COVID-19 pandemic was first identified in Wuhan in December 2019. + The Chinese government has been criticized for its handling of the epidemic and accused of concealing the extent of the outbreak before it became an international pandemic. +The government of the People's Republic of China officially espouses state atheism, and has conducted antireligious campaigns to this end. + Religious affairs and issues in the country are overseen by the State Administration for Religious Affairs. + Freedom of religion is guaranteed by China's constitution, although religious organizations that lack official approval can be subject to state persecution. +Over the millennia, Chinese civilization has been influenced by various religious movements. + The "three teachings", including Confucianism, Taoism, and Buddhism (Chinese Buddhism), historically have a significant role in shaping Chinese culture, enriching a theological and spiritual framework which harkens back to the early Shang and Zhou dynasty. + Chinese popular or folk religion, which is framed by the three teachings and other traditions, consists in allegiance to the (神), a character that signifies the "energies of generation", who can be deities of the environment or ancestral principles of human groups, concepts of civility, culture heroes, many of whom feature in Chinese mythology and history. + Among the most popular cults are those of Mazu (goddess of the seas), Huangdi (one of the two divine patriarchs of the Chinese race), Guandi (god of war and business), Caishen (god of prosperity and richness), Pangu and many others. + China is home to many of the world's tallest religious statues, including the tallest of all, the Spring Temple Buddha in Henan. +Clear data on religious affiliation in China is difficult to gather due to varying definitions of "religion" and the unorganized, diffusive nature of Chinese religious traditions. + Scholars note that in China there is no clear boundary between three teachings religions and local folk religious practice. + A 2015 poll conducted by Gallup International found that 61% of Chinese people self-identified as "convinced atheist", though it is worthwhile to note that Chinese religions or some of their strands are definable as non-theistic and humanistic religions, since they do not believe that divine creativity is completely transcendent, but it is inherent in the world and in particular in the human being. + According to a 2014 study, approximately 74% are either non-religious or practise Chinese folk belief, 16% are Buddhists, 2% are Christians, 1% are Muslims, and 8% adhere to other religions including Taoists and folk salvationism. + In addition to Han people's local religious practices, there are also various ethnic minority groups in China who maintain their traditional autochthone religions. + The various folk religions today comprise 2–3% of the population, while Confucianism as a religious self-identification is common within the intellectual class. + Significant faiths specifically connected to certain ethnic groups include Tibetan Buddhism and the Islamic religion of the Hui, Uyghur, Kazakh, Kyrgyz and other peoples in Northwest China. +Since ancient times, Chinese culture has been heavily influenced by Confucianism. + For much of the country's dynastic era, opportunities for social advancement could be provided by high performance in the prestigious imperial examinations, which have their origins in the Han dynasty. + The literary emphasis of the exams affected the general perception of cultural refinement in China, such as the belief that calligraphy, poetry and painting were higher forms of art than dancing or drama. + Chinese culture has long emphasized a sense of deep history and a largely inward-looking national perspective. + Examinations and a culture of merit remain greatly valued in China today. +The first leaders of the People's Republic of China were born into the traditional imperial order, but were influenced by the May Fourth Movement and reformist ideals. + They sought to change some traditional aspects of Chinese culture, such as rural land tenure, sexism, and the Confucian system of education, while preserving others, such as the family structure and culture of obedience to the state. + Some observers see the period following the establishment of the PRC in 1949 as a continuation of traditional Chinese dynastic history, while others claim that the Communist Party's rule has damaged the foundations of Chinese culture, especially through political movements such as the Cultural Revolution of the 1960s, where many aspects of traditional culture were destroyed, having been denounced as "regressive and harmful" or "vestiges of feudalism". + Many important aspects of traditional Chinese morals and culture, such as Confucianism, art, literature, and performing arts like Peking opera, were altered to conform to government policies and propaganda at the time. + Access to foreign media remains heavily restricted. +Today, the Chinese government has accepted numerous elements of traditional Chinese culture as being integral to Chinese society. + With the rise of Chinese nationalism and the end of the Cultural Revolution, various forms of traditional Chinese art, literature, music, film, fashion and architecture have seen a vigorous revival, and folk and variety art in particular have sparked interest nationally and even worldwide. + China is now the third-most-visited country in the world, with 55.7 million inbound international visitors in 2010. + It also experiences an enormous volume of domestic tourism; an estimated 740 million Chinese holidaymakers travelled within the country in October 2012. +Chinese literature is based on the literature of the Zhou dynasty. + Concepts covered within the Chinese classic texts present a wide range of thoughts and subjects including calendar, military, astrology, herbology, geography and many others. + Some of the most important early texts include the and the within the Four Books and Five Classics which served as the Confucian authoritative books for the state-sponsored curriculum in dynastic era. + Inherited from the , classical Chinese poetry developed to its floruit during the Tang dynasty. + Li Bai and Du Fu opened the forking ways for the poetic circles through romanticism and realism respectively. + Chinese historiography began with the , the overall scope of the historiographical tradition in China is termed the Twenty-Four Histories, which set a vast stage for Chinese fictions along with Chinese mythology and folklore. + Pushed by a burgeoning citizen class in the Ming dynasty, Chinese classical fiction rose to a boom of the historical, town and gods and demons fictions as represented by the Four Great Classical Novels which include , , and . + Along with the wuxia fictions of Jin Yong and Liang Yusheng, it remains an enduring source of popular culture in the East Asian cultural sphere. +In the wake of the New Culture Movement after the end of the Qing dynasty, Chinese literature embarked on a new era with written vernacular Chinese for ordinary citizens. + Hu Shih and Lu Xun were pioneers in modern literature. + Various literary genres, such as misty poetry, scar literature, young adult fiction and the xungen literature, which is influenced by magic realism, emerged following the Cultural Revolution. + Mo Yan, a xungen literature author, was awarded the Nobel Prize in Literature in 2012. +Chinese cuisine is highly diverse, drawing on several millennia of culinary history and geographical variety, in which the most influential are known as the "Eight Major Cuisines", including Sichuan, Cantonese, Jiangsu, Shandong, Fujian, Hunan, Anhui, and Zhejiang cuisines. + All of them are featured by the precise skills of shaping, heating, colorway and flavoring. + Chinese cuisine is also known for its width of cooking methods and ingredients, as well as food therapy that is emphasized by traditional Chinese medicine. + Generally, China's staple food is rice in the south, wheat based breads and noodles in the north. + The diet of the common people in pre-modern times was largely grain and simple vegetables, with meat reserved for special occasions. + And the bean products, such as tofu and soy milk, remain as a popular source of protein. + Pork is now the most popular meat in China, accounting for about three-fourths of the country's total meat consumption. + While pork dominates the meat market, there is also the vegetarian Buddhist cuisine and the pork-free Chinese Islamic cuisine. + Southern cuisine, due to the area's proximity to the ocean and milder climate, has a wide variety of seafood and vegetables; it differs in many respects from the wheat-based diets across dry northern China. + Numerous offshoots of Chinese food, such as Hong Kong cuisine and American Chinese food, have emerged in the nations that play host to the Chinese diaspora. +China has become a prime sports destination worldwide. + The country gained the hosting rights for several major global sports tournaments including the 2008 Summer Olympics, the 2015 World Championships in Athletics, the 2019 FIBA Basketball World Cup and the upcoming 2022 Winter Olympics. +China has one of the oldest sporting cultures in the world. + There is evidence that archery () was practiced during the Western Zhou dynasty. + Swordplay () and cuju, a sport loosely related to association football date back to China's early dynasties as well. +Physical fitness is widely emphasized in Chinese culture, with morning exercises such as qigong and t'ai chi ch'uan widely practiced, and commercial gyms and private fitness clubs are gaining popularity across the country. + Basketball is currently the most popular spectator sport in China. + The Chinese Basketball Association and the American National Basketball Association have a huge following among the people, with native or ethnic Chinese players such as Yao Ming and Yi Jianlian held in high esteem. + China's professional football league, now known as Chinese Super League, was established in 1994, it is the largest football market in Asia. + Other popular sports in the country include martial arts, table tennis, badminton, swimming and snooker. + Board games such as go (known as in Chinese), xiangqi, mahjong, and more recently chess, are also played at a professional level. + In addition, China is home to a huge number of cyclists, with an estimated 470 million bicycles as of 2012. + Many more traditional sports, such as dragon boat racing, Mongolian-style wrestling and horse racing are also popular. +China has participated in the Olympic Games since 1932, although it has only participated as the PRC since 1952. + China hosted the 2008 Summer Olympics in Beijing, where its athletes received 51 gold medals – the highest number of gold medals of any participating nation that year. + China also won the most medals of any nation at the 2012 Summer Paralympics, with 231 overall, including 95 gold medals. + In 2011, Shenzhen in Guangdong, China hosted the 2011 Summer Universiade. + China hosted the 2013 East Asian Games in Tianjin and the 2014 Summer Youth Olympics in Nanjing; the first country to host both regular and Youth Olympics. + Beijing and its nearby city Zhangjiakou of Hebei province will also collaboratively host the 2022 Olympic Winter Games, which will make Beijing the first city in the world to hold both the Summer Olympics and the Winter Olympics. diff --git a/tools/settings.py b/tools/settings.py index c68b5709..f916e9e8 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -36,7 +36,8 @@ from .. import globs from ..tools.register import register_wrap -from ..googletrans import Translator +# from ..googletrans import Translator # Todo Remove this +from ..extern_tools.google_trans_new.google_trans_new import google_translator from . import translate as Translate from ..translations import t @@ -94,7 +95,7 @@ class DebugTranslations(bpy.types.Operator): def execute(self, context): bpy.context.scene.debug_translations = True - translator = Translator() + translator = google_translator() try: translator.translate('猫') except: diff --git a/tools/translate.py b/tools/translate.py index 2492f5dc..29f49a35 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -40,14 +40,15 @@ from . import common as Common from .register import register_wrap from .. import globs -from ..googletrans import Translator +# from ..googletrans import Translator # TODO Remove this +from ..extern_tools.google_trans_new.google_trans_new import google_translator from ..translations import t if platform.system() != "Linux" or bpy.app.version < (2, 90): from mmd_tools_local import translations as mmd_translations -dictionary = None -dictionary_google = None +dictionary = {} +dictionary_google = {} main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve() resources_dir = os.path.join(str(main_dir), "resources") @@ -211,52 +212,52 @@ def execute(self, context): return {'FINISHED'} -@register_wrap -class TranslateTexturesButton(bpy.types.Operator): - bl_idname = 'cats_translate.textures' - bl_label = t('TranslateTexturesButton.label') - bl_description = t('TranslateTexturesButton.desc') - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} - - def execute(self, context): - # It currently seems to do nothing. This should probably only added when the folder textures really get translated. Currently only the materials are important - self.report({'INFO'}, t('TranslateTexturesButton.success_alt')) - return {'FINISHED'} - - translator = Translator() - - to_translate = [] - for ob in Common.get_objects(): - if ob.type == 'MESH': - for matslot in ob.material_slots: - for texslot in bpy.data.materials[matslot.name].texture_slots: - if texslot: - print(texslot.name) - to_translate.append(texslot.name) - - translated = [] - try: - translations = translator.translate(to_translate) - except SSLError: - self.report({'ERROR'}, t('TranslateTexturesButton.error.noInternet')) - return {'FINISHED'} - - for translation in translations: - translated.append(translation.text) - - i = 0 - for ob in Common.get_objects(): - if ob.type == 'MESH': - for matslot in ob.material_slots: - for texslot in bpy.data.materials[matslot.name].texture_slots: - if texslot: - bpy.data.textures[texslot.name].name = translated[i] - i += 1 - - Common.unselect_all() - - self.report({'INFO'}, t('TranslateTexturesButton.success', number=str(i))) - return {'FINISHED'} +# @register_wrap +# class TranslateTexturesButton(bpy.types.Operator): +# bl_idname = 'cats_translate.textures' +# bl_label = t('TranslateTexturesButton.label') +# bl_description = t('TranslateTexturesButton.desc') +# bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +# +# def execute(self, context): +# # It currently seems to do nothing. This should probably only added when the folder textures really get translated. Currently only the materials are important +# self.report({'INFO'}, t('TranslateTexturesButton.success_alt')) +# return {'FINISHED'} +# +# translator = google_translator() +# +# to_translate = [] +# for ob in Common.get_objects(): +# if ob.type == 'MESH': +# for matslot in ob.material_slots: +# for texslot in bpy.data.materials[matslot.name].texture_slots: +# if texslot: +# print(texslot.name) +# to_translate.append(texslot.name) +# +# translated = [] +# try: +# translations = translator.translate(to_translate, lang_tgt='en') +# except SSLError: +# self.report({'ERROR'}, t('TranslateTexturesButton.error.noInternet')) +# return {'FINISHED'} +# +# for translation in translations: +# translated.append(translation) +# +# i = 0 +# for ob in Common.get_objects(): +# if ob.type == 'MESH': +# for matslot in ob.material_slots: +# for texslot in bpy.data.materials[matslot.name].texture_slots: +# if texslot: +# bpy.data.textures[texslot.name].name = translated[i] +# i += 1 +# +# Common.unselect_all() +# +# self.report({'INFO'}, t('TranslateTexturesButton.success', number=str(i))) +# return {'FINISHED'} @register_wrap @@ -431,11 +432,11 @@ def update_dictionary(to_translate_list, translating_shapes=False, self=None): # Translate the rest with google translate print('GOOGLE DICT UPDATE!') - translator = Translator() + translator = google_translator(url_suffix='com') token_tries = 0 while True: try: - translations = translator.translate(google_input) + translations = [translator.translate(text, lang_src='ja', lang_tgt='en').strip() for text in google_input] break except requests.exceptions.ConnectionError: print('CONNECTION TO GOOGLE FAILED!') @@ -463,13 +464,13 @@ def update_dictionary(to_translate_list, translating_shapes=False, self=None): self.report({'ERROR'}, t('update_dictionary.error.errorMsg') + t('update_dictionary.error.catsTranslated') + '\n' + '\nGoogle: ' + error) print('', 'You got an error message from Google:', error, '') return - except AttributeError as e: + except AttributeError: # If the translator wasn't able to create a stable connection to Google, just retry it again # This is an issue with Google since Nov 2020: https://github.com/ssut/py-googletrans/issues/234 token_tries += 1 - if token_tries < 20: + if token_tries < 10: print('RETRY', token_tries) - translator = Translator() + translator = google_translator(url_suffix='com') continue # If if didn't work after 20 tries, just quit @@ -485,13 +486,17 @@ def update_dictionary(to_translate_list, translating_shapes=False, self=None): name = google_input[i] if use_google_only: - dictionary_google['translations_full'][name] = translation.text + dictionary_google['translations_full'][name] = translation else: - translated_name = translation.text.capitalize() - dictionary[name] = translated_name - dictionary_google['translations'][name] = translated_name + # Capitalize words + translation_words = translation.split(' ') + translation_words = [word.capitalize() for word in translation_words] + translation = ' '.join(translation_words) + + dictionary[name] = translation + dictionary_google['translations'][name] = translation - print(google_input[i], translation.text.capitalize()) + print(google_input[i], '->', translation) # Sort dictionary temp_dict = copy.deepcopy(dictionary) @@ -549,7 +554,8 @@ def translate(to_translate, add_space=False, translating_shapes=False): to_translate = to_translate.replace('.L', '_L').replace('.R', '_R').replace(' ', ' ').replace('し', '').replace('っ', '').strip() - # print(to_translate) + # print('"' + pre_translation + '"') + # print('"' + to_translate + '"') return to_translate, pre_translation != to_translate From 23aa6a1daea1fd0b4b66fd4ac15467da82e502f2 Mon Sep 17 00:00:00 2001 From: Hotox Date: Sun, 6 Dec 2020 00:58:26 +0100 Subject: [PATCH 54/64] Fix mmd_tools not unloading correctly when hiding mmd_tools tabs --- __init__.py | 4 ++++ extentions.py | 2 +- tools/common.py | 18 ++++++++++++++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index b8425a83..6b6baa96 100644 --- a/__init__.py +++ b/__init__.py @@ -352,6 +352,10 @@ def unregister(): # Unregister updater updater.unregister() + # Register unloaded mmd_tools tabs if they are hidden to avoid issues when unloading mmd_tools + if not bpy.context.scene.show_mmd_tabs: + tools.common.toggle_mmd_tabs(shutdown_plugin=True) + # Unload mmd_tools try: mmd_tools_local.unregister() diff --git a/extentions.py b/extentions.py index a0a02e34..3e365911 100644 --- a/extentions.py +++ b/extentions.py @@ -646,7 +646,7 @@ def register(): name=t('Scene.show_mmd_tabs.label'), description=t('Scene.show_mmd_tabs.desc'), default=True, - update=Common.toggle_mmd_tabs + update=Common.toggle_mmd_tabs_update ) Scene.embed_textures = BoolProperty( name=t('Scene.embed_textures.label'), diff --git a/tools/common.py b/tools/common.py index 5441cda5..ed87bb99 100644 --- a/tools/common.py +++ b/tools/common.py @@ -2099,7 +2099,11 @@ def fix_twist_bone_names(armature): bone_twist.name = 'z' + bone_twist.name -def toggle_mmd_tabs(self, context): +def toggle_mmd_tabs_update(self, context): + toggle_mmd_tabs() + + +def toggle_mmd_tabs(shutdown_plugin=False): mmd_cls = [ mmd_tool.MMDToolsObjectPanel, mmd_tool.MMDDisplayItemsPanel, @@ -2108,13 +2112,18 @@ def toggle_mmd_tabs(self, context): mmd_tool.MMDJointSelectorPanel, mmd_util_tools.MMDMaterialSorter, mmd_util_tools.MMDMeshSorter, + ] + mmd_cls_shading = [ mmd_view_prop.MMDViewPanel, mmd_view_prop.MMDSDEFPanel, ] - print('Toggling mmd tabs') + if not version_2_79_or_older(): + mmd_cls = mmd_cls + mmd_cls_shading + + # If the plugin is shutting down, load the mmd_tools tabs before that, to avoid issues when unregistering mmd_tools if platform.system() != "Linux" or bpy.app.version < (2, 90): - if context.scene.show_mmd_tabs: + if bpy.context.scene.show_mmd_tabs or shutdown_plugin: for cls in mmd_cls: try: bpy.utils.register_class(cls) @@ -2127,7 +2136,8 @@ def toggle_mmd_tabs(self, context): except: pass - Settings.update_settings(None, None) + if not shutdown_plugin: + Settings.update_settings(None, None) From 1abe490f6d02fdc594780d0ba2df4608281fd31f Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 9 Dec 2020 16:18:09 +0100 Subject: [PATCH 55/64] Updated mmd_tools, removed old googletrans, updated translations --- .../google_trans_new/google_trans_new.py | 4 +- extern_tools/mmd_tools_local/__init__.py | 20 +- extern_tools/mmd_tools_local/core/material.py | 2 +- extern_tools/mmd_tools_local/core/model.py | 2 +- extern_tools/mmd_tools_local/core/morph.py | 18 +- .../mmd_tools_local/core/pmd/importer.py | 12 +- .../mmd_tools_local/core/pmx/__init__.py | 2 + extern_tools/mmd_tools_local/core/sdef.py | 40 ++- extern_tools/mmd_tools_local/core/shader.py | 55 +++- .../mmd_tools_local/core/vmd/__init__.py | 2 + .../mmd_tools_local/core/vmd/exporter.py | 15 +- .../mmd_tools_local/core/vmd/importer.py | 94 ++++-- .../mmd_tools_local/operators/fileio.py | 3 + .../mmd_tools_local/panels/prop_object.py | 14 +- .../mmd_tools_local/panels/util_tools.py | 222 ++++++++++++++ .../mmd_tools_local/panels/view_prop.py | 2 +- .../mmd_tools_local/properties/bone.py | 1 + .../mmd_tools_local/properties/morph.py | 2 +- googletrans/__init__.py | 7 - googletrans/client.py | 232 -------------- googletrans/compat.py | 11 - googletrans/constants.py | 114 ------- googletrans/gtoken.py | 283 ------------------ googletrans/models.py | 40 --- googletrans/urls.py | 6 - googletrans/utils.py | 74 ----- tools/translate.py | 2 +- translations/__init__.py | 4 +- translations/en_US.py | 98 +++--- translations/ja_JP.py | 127 ++++++++ 30 files changed, 588 insertions(+), 920 deletions(-) delete mode 100644 googletrans/__init__.py delete mode 100644 googletrans/client.py delete mode 100644 googletrans/compat.py delete mode 100644 googletrans/constants.py delete mode 100644 googletrans/gtoken.py delete mode 100644 googletrans/models.py delete mode 100644 googletrans/urls.py delete mode 100644 googletrans/utils.py diff --git a/extern_tools/google_trans_new/google_trans_new.py b/extern_tools/google_trans_new/google_trans_new.py index 7950ff40..61ca1d8f 100644 --- a/extern_tools/google_trans_new/google_trans_new.py +++ b/extern_tools/google_trans_new/google_trans_new.py @@ -3,14 +3,14 @@ # version : 1.1.9 import json, requests, random, re from urllib.parse import quote -# import urllib3 +from requests.packages import urllib3 import logging from .constant import LANGUAGES, DEFAULT_SERVICE_URLS log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) -# urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) URLS_SUFFIX = [re.search('translate.google.(.*)', url.strip()).group(1) for url in DEFAULT_SERVICE_URLS] URL_SUFFIX_DEFAULT = 'cn' diff --git a/extern_tools/mmd_tools_local/__init__.py b/extern_tools/mmd_tools_local/__init__.py index c77c6139..39f908f8 100644 --- a/extern_tools/mmd_tools_local/__init__.py +++ b/extern_tools/mmd_tools_local/__init__.py @@ -3,7 +3,7 @@ bl_info = { "name": "mmd_tools", "author": "sugiany", - "version": (0, 7, 0), + "version": (0, 7, 1), "blender": (2, 80, 0), "location": "View3D > Tool Shelf > MMD Tools Panel", "description": "Utility tools for MMD model editing. (powroupi's forked version)", @@ -43,8 +43,6 @@ def register_wrap(cls): else: import bpy import logging - from bpy.types import AddonPreferences - from bpy.props import StringProperty from bpy.app.handlers import persistent __make_annotations = (bpy.app.version >= (2, 80, 0)) @@ -59,34 +57,42 @@ def register_wrap(cls): @register_wrap -class MMDToolsAddonPreferences(AddonPreferences): +class MMDToolsAddonPreferences(bpy.types.AddonPreferences): # this must match the addon name, use '__package__' # when defining this in a submodule of a python package. bl_idname = __name__ - shared_toon_folder = StringProperty( + shared_toon_folder = bpy.props.StringProperty( name="Shared Toon Texture Folder", description=('Directory path to toon textures. This is normally the ' + '"Data" directory within of your MikuMikuDance directory'), subtype='DIR_PATH', ) - base_texture_folder = StringProperty( + base_texture_folder = bpy.props.StringProperty( name='Base Texture Folder', description='Path for textures shared between models', subtype='DIR_PATH', ) - dictionary_folder = StringProperty( + dictionary_folder = bpy.props.StringProperty( name='Dictionary Folder', description='Path for searching csv dictionaries', subtype='DIR_PATH', default=__file__[:-11], ) + non_collision_threshold = bpy.props.FloatProperty( + name='Non-Collision Threshold', + description='The distance threshold for creating extra non-collision constraints while building physics', + min=0, + soft_max=10, + default=1.5, + ) def draw(self, context): layout = self.layout layout.prop(self, "shared_toon_folder") layout.prop(self, "base_texture_folder") layout.prop(self, "dictionary_folder") + layout.prop(self, "non_collision_threshold") def menu_func_import(self, context): diff --git a/extern_tools/mmd_tools_local/core/material.py b/extern_tools/mmd_tools_local/core/material.py index 089e3bdb..741c865c 100644 --- a/extern_tools/mmd_tools_local/core/material.py +++ b/extern_tools/mmd_tools_local/core/material.py @@ -450,7 +450,7 @@ def update_sphere_texture_type(self, obj=None): self.__update_shader_input('Sphere Tex', (0, 0, 0, 1) if is_sph_add else (1, 1, 1, 1)) texture = self.__get_texture_node('mmd_sphere_tex') - if texture: + if texture and (not texture.inputs['Vector'].is_linked or texture.inputs['Vector'].links[0].from_node.name == 'mmd_tex_uv'): if hasattr(texture, 'color_space'): texture.color_space = 'NONE' if is_sph_add else 'COLOR' elif hasattr(texture.image, 'colorspace_settings'): diff --git a/extern_tools/mmd_tools_local/core/model.py b/extern_tools/mmd_tools_local/core/model.py index b004e953..ba502380 100644 --- a/extern_tools/mmd_tools_local/core/model.py +++ b/extern_tools/mmd_tools_local/core/model.py @@ -545,7 +545,7 @@ def build(self): logging.info('****************************************') start_time = time.time() self.__preBuild() - self.buildRigids() + self.buildRigids(bpyutils.addon_preferences('non_collision_threshold', 1.5)) self.buildJoints() self.__postBuild() logging.info(' Finished building in %f seconds.', time.time() - start_time) diff --git a/extern_tools/mmd_tools_local/core/morph.py b/extern_tools/mmd_tools_local/core/morph.py index 9b163046..1c48b9b4 100644 --- a/extern_tools/mmd_tools_local/core/morph.py +++ b/extern_tools/mmd_tools_local/core/morph.py @@ -303,15 +303,13 @@ def __cleanup(self, names_in_use=None): for m in mesh.modifiers: # uv morph if m.name.startswith('mmd_bind') and m.name not in names_in_use: mesh.modifiers.remove(m) - for m in mesh.data.materials: - if m and m.node_tree: - for n in m.node_tree.nodes: - if n.name.startswith('mmd_bind'): - m.node_tree.nodes.remove(n) - if 'mmd_shader' in m.node_tree.nodes: - m.mmd_material.is_double_sided = m.mmd_material.is_double_sided # update mmd shader - else: - m.update_tag() + + from mmd_tools_local.core.shader import _MaterialMorph + for m in rig.materials(): + if m and m.node_tree: + for n in sorted((x for x in m.node_tree.nodes if x.name.startswith('mmd_bind')), key=lambda x: -x.location[0]): + _MaterialMorph.reset_morph_links(n) + m.node_tree.nodes.remove(n) attributes = set(TransformConstraintOp.min_max_attributes('LOCATION', 'to')) attributes |= set(TransformConstraintOp.min_max_attributes('ROTATION', 'to')) @@ -504,7 +502,7 @@ def __config_bone_morph(constraints, map_type, attributes, val, val_str): for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values(): b = arm.pose.bones[bname] b.location = data.location - b.rotation_quaternion = data.rotation + b.rotation_quaternion = data.rotation.__class__(*data.rotation.to_axis_angle()) # Fix for consistency b.is_mmd_shadow_bone = True b.mmd_shadow_bone_type = 'BIND' pb = armObj.pose.bones[data.bone] diff --git a/extern_tools/mmd_tools_local/core/pmd/importer.py b/extern_tools/mmd_tools_local/core/pmd/importer.py index 099ef674..f5caf4b4 100644 --- a/extern_tools/mmd_tools_local/core/pmd/importer.py +++ b/extern_tools/mmd_tools_local/core/pmd/importer.py @@ -113,6 +113,7 @@ def import_pmd_to_pmx(filepath): elif bone.type == 4: pmx_bone.isMovable = False elif bone.type == 5: + pmx_bone.transform_order = 2 pmx_bone.isMovable = False pmx_bone.hasAdditionalRotate = True pmx_bone.additionalTransform = (bone.ik_bone, 1.0) @@ -124,7 +125,7 @@ def import_pmd_to_pmx(filepath): pmx_bone.isMovable = False elif bone.type == 8: pmx_bone.isMovable = False - tail_loc=mathutils.Vector(pmd_model.bones[bone.tail_bone].position) + tail_loc = mathutils.Vector(pmd_model.bones[bone.tail_bone].position) loc = mathutils.Vector(bone.position) vec = tail_loc - loc vec.normalize() @@ -135,17 +136,14 @@ def import_pmd_to_pmx(filepath): pmx_bone.hasAdditionalRotate = True pmx_bone.additionalTransform = (bone.tail_bone, float(bone.ik_bone)/100.0) - #if bone.type >= 4: - # pmx_bone.transform_order = 2 - pmx_model.bones.append(pmx_bone) if re.search(u'ひざ$', pmx_bone.name): knee_bones.append(i) - #for i in pmx_model.bones: - # if i.parent != -1 and pmd_model.bones[i.parent].type == 2: - # i.transform_order = 1 + for i in pmx_model.bones: + if 0 <= i.parent < len(pmx_model.bones) and i.transform_order < pmx_model.bones[i.parent].transform_order: + i.transform_order = pmx_model.bones[i.parent].transform_order logging.info('----- Converted %d boness', len(pmx_model.bones)) logging.info('') diff --git a/extern_tools/mmd_tools_local/core/pmx/__init__.py b/extern_tools/mmd_tools_local/core/pmx/__init__.py index 19260394..16349d98 100644 --- a/extern_tools/mmd_tools_local/core/pmx/__init__.py +++ b/extern_tools/mmd_tools_local/core/pmx/__init__.py @@ -1265,6 +1265,8 @@ def load(self, fs): self.index = fs.readBoneIndex() self.location_offset = fs.readVector(3) self.rotation_offset = fs.readVector(4) + if not any(self.rotation_offset): + self.rotation_offset = (0, 0, 0, 1) def save(self, fs): fs.writeBoneIndex(self.index) diff --git a/extern_tools/mmd_tools_local/core/sdef.py b/extern_tools/mmd_tools_local/core/sdef.py index 9ed0c235..cba856e3 100644 --- a/extern_tools/mmd_tools_local/core/sdef.py +++ b/extern_tools/mmd_tools_local/core/sdef.py @@ -6,6 +6,16 @@ from mmd_tools_local.bpyutils import matmul + +def _hash(v): + if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)): + return hash(type(v).__name__ + v.name) + elif isinstance(v, bpy.types.Pose): + return hash(type(v).__name__ + v.id_data.name) + else: + raise NotImplementedError('hash') + + class FnSDEF(): g_verts = {} # global cache g_shapekey_data = {} @@ -19,10 +29,10 @@ def __init__(self): @classmethod def __init_cache(cls, obj, shapekey): - key = hash(obj) + key = _hash(obj) obj = getattr(obj, 'original', obj) mod = obj.modifiers.get('mmd_bone_order_override') - key_armature = hash(mod.object.pose) if mod and mod.type == 'ARMATURE' and mod.object else None + key_armature = _hash(mod.object.pose) if mod and mod.type == 'ARMATURE' and mod.object else None if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature: cls.g_verts[key] = cls.__find_vertices(obj) cls.g_bone_check[key] = {} @@ -36,8 +46,8 @@ def __init_cache(cls, obj, shapekey): @classmethod def __check_bone_update(cls, obj, bone0, bone1): - check = cls.g_bone_check[hash(obj)] - key = (hash(bone0), hash(bone1)) + check = cls.g_bone_check[_hash(obj)] + key = (_hash(bone0), _hash(bone1)) if key not in check or (bone0.matrix, bone1.matrix) != check[key]: check[key] = (bone0.matrix.copy(), bone1.matrix.copy()) return True @@ -56,16 +66,16 @@ def mute_sdef_set(cls, obj, mute): @classmethod def __sdef_muted(cls, obj, shapekey): mute = shapekey.mute - if mute != cls.g_bone_check[hash(obj)].get('sdef_mute'): + if mute != cls.g_bone_check[_hash(obj)].get('sdef_mute'): mod = obj.modifiers.get('mmd_bone_order_override') if mod and mod.type == 'ARMATURE': if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != 'EDIT': - mask = tuple(i for v in cls.g_verts[hash(obj)].values() for i in v[3]) + mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3]) obj.vertex_groups.new(name=cls.MASK_NAME).add(mask, 1, 'REPLACE') mod.vertex_group = '' if mute else cls.MASK_NAME mod.invert_vertex_group = True shapekey.vertex_group = cls.MASK_NAME - cls.g_bone_check[hash(obj)]['sdef_mute'] = mute + cls.g_bone_check[_hash(obj)]['sdef_mute'] = mute return mute @staticmethod @@ -136,7 +146,7 @@ def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): shapekey_data = shapekey.data if use_scale: # with scale - for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue @@ -153,7 +163,7 @@ def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): shapekey_data[vid].co = matmul(mat_rot, pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 else: # default - for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue @@ -168,10 +178,10 @@ def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): mat_rot = (rot0*w0 + rot1*w1).normalized().to_matrix() shapekey_data[vid].co = matmul(mat_rot, pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 else: # bulk update - shapekey_data = cls.g_shapekey_data[hash(obj)] + shapekey_data = cls.g_shapekey_data[_hash(obj)] if use_scale: # scale & bulk update - for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue @@ -188,7 +198,7 @@ def scale(mat_rot, w0, w1): shapekey_data[vids] = [matmul(scale((rot0*w0 + rot1*w1).normalized().to_matrix(), w0, w1), pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] else: # bulk update - for bone0, bone1, sdef_data, vids in cls.g_verts[hash(obj)].values(): + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue @@ -257,7 +267,7 @@ def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False): use_skip = False mod = obj.modifiers.get('mmd_bone_order_override') variables = f.driver.variables - for name in set(data[i].name for data in cls.g_verts[hash(obj)].values() for i in range(2)): # add required bones for dependency graph + for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph var = variables.new() var.type = 'TRANSFORMS' var.targets[0].id = mod.object @@ -289,7 +299,7 @@ def unbind(cls, obj): @classmethod def clear_cache(cls, obj=None, unused_only=False): if unused_only: - valid_keys = set(hash(i) for i in bpy.data.objects if i.type == 'MESH' and i != obj) + valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == 'MESH' and i != obj) for key in (cls.g_verts.keys()-valid_keys): del cls.g_verts[key] for key in (cls.g_shapekey_data.keys()-cls.g_verts.keys()): @@ -297,7 +307,7 @@ def clear_cache(cls, obj=None, unused_only=False): for key in (cls.g_bone_check.keys()-cls.g_verts.keys()): del cls.g_bone_check[key] elif obj: - key = hash(obj) + key = _hash(obj) if key in cls.g_verts: del cls.g_verts[key] if key in cls.g_shapekey_data: diff --git a/extern_tools/mmd_tools_local/core/shader.py b/extern_tools/mmd_tools_local/core/shader.py index a8233902..a98e3ce6 100644 --- a/extern_tools/mmd_tools_local/core/shader.py +++ b/extern_tools/mmd_tools_local/core/shader.py @@ -122,6 +122,43 @@ def setup_morph_nodes(cls, material, morphs): node = n return nodes + @classmethod + def reset_morph_links(cls, node): + cls.__update_morph_links(node, reset=True) + + @classmethod + def __update_morph_links(cls, node, reset=False): + nodes, links = node.id_data.nodes, node.id_data.links + if reset: + if any(l.from_node.name.startswith('mmd_bind') for i in node.inputs for l in i.links): + return + def __init_link(socket_morph, socket_shader): + if socket_shader and socket_morph.is_linked: + links.new(socket_morph.links[0].from_socket, socket_shader) + else: + def __init_link(socket_morph, socket_shader): + if socket_shader: + if socket_shader.is_linked: + links.new(socket_shader.links[0].from_socket, socket_morph) + if socket_morph.type == 'VALUE': + socket_morph.default_value = socket_shader.default_value + else: + socket_morph.default_value[:3] = socket_shader.default_value[:3] + shader = nodes.get('mmd_shader', None) + if shader: + __init_link(node.inputs['Ambient1'], shader.inputs.get('Ambient Color')) + __init_link(node.inputs['Diffuse1'], shader.inputs.get('Diffuse Color')) + __init_link(node.inputs['Specular1'], shader.inputs.get('Specular Color')) + __init_link(node.inputs['Reflect1'], shader.inputs.get('Reflect')) + __init_link(node.inputs['Alpha1'], shader.inputs.get('Alpha')) + __init_link(node.inputs['Base1 RGB'], shader.inputs.get('Base Tex')) + __init_link(node.inputs['Toon1 RGB'], shader.inputs.get('Toon Tex')) #FIXME toon only affect shadow color + __init_link(node.inputs['Sphere1 RGB'], shader.inputs.get('Sphere Tex')) + elif 'mmd_edge_preview' in nodes: + shader = nodes['mmd_edge_preview'] + __init_link(node.inputs['Edge1 RGB'], shader.inputs['Color']) + __init_link(node.inputs['Edge1 A'], shader.inputs['Alpha']) + @classmethod def __update_node_inputs(cls, node, morph): node.inputs['Ambient2'].default_value[:3] = morph.ambient_color[:3] @@ -147,6 +184,7 @@ def __morph_node_add(cls, material, morph, prev_node): shader = nodes.get('mmd_shader', None) if morph: node = nodes.new('ShaderNodeGroup') + node.parent = getattr(shader, 'parent', None) node.location = (-250, 0) node.node_tree = cls.__get_shader('Add' if morph.offset_type == 'ADD' else 'Mul') cls.__update_node_inputs(node, morph) @@ -157,24 +195,11 @@ def __morph_node_add(cls, material, morph, prev_node): links.new(prev_node.outputs[id_name+' RGB'], node.inputs[id_name+'1 RGB']) links.new(prev_node.outputs[id_name+' A'], node.inputs[id_name+'1 A']) else: # initial first node - mmd_mat = material.mmd_material - node.inputs['Ambient1'].default_value[:3] = mmd_mat.ambient_color[:3] - node.inputs['Diffuse1'].default_value[:3] = mmd_mat.diffuse_color[:3] - node.inputs['Specular1'].default_value[:3] = mmd_mat.specular_color[:3] - node.inputs['Reflect1'].default_value = mmd_mat.shininess - node.inputs['Alpha1'].default_value = mmd_mat.alpha - node.inputs['Edge1 RGB'].default_value[:3] = mmd_mat.edge_color[:3] - node.inputs['Edge1 A'].default_value = mmd_mat.edge_color[3] if node.node_tree.name.endswith('Add'): node.inputs['Base1 A'].default_value = 1 node.inputs['Toon1 A'].default_value = 1 node.inputs['Sphere1 A'].default_value = 1 - for id_name in ('Base', 'Toon', 'Sphere'): #FIXME toon only affect shadow color - mmd_tex = nodes.get('mmd_%s_tex'%id_name.lower(), None) - if mmd_tex: - links.new(mmd_tex.outputs['Color'], node.inputs['%s1 RGB'%id_name]) - elif shader and id_name+' Tex' in shader.inputs: - node.inputs['%s1 RGB'%id_name].default_value[:3] = shader.inputs[id_name+' Tex'].default_value[:3] + cls.__update_morph_links(node) return node # connect last node to shader if shader: @@ -186,8 +211,6 @@ def __soft_link(socket_out, socket_in): __soft_link(prev_node.outputs['Specular'], shader.inputs.get('Specular Color')) __soft_link(prev_node.outputs['Reflect'], shader.inputs.get('Reflect')) __soft_link(prev_node.outputs['Alpha'], shader.inputs.get('Alpha')) - #__soft_link(prev_node.outputs['Edge RGB'], shader.inputs.get('Edge Color')) - #__soft_link(prev_node.outputs['Edge A'], shader.inputs.get('Edge Alpha')) __soft_link(prev_node.outputs['Base Tex'], shader.inputs.get('Base Tex')) __soft_link(prev_node.outputs['Toon Tex'], shader.inputs.get('Toon Tex')) if int(material.mmd_material.sphere_texture_type) != 2: # shader.inputs['Sphere Mul/Add'].default_value < 0.5 diff --git a/extern_tools/mmd_tools_local/core/vmd/__init__.py b/extern_tools/mmd_tools_local/core/vmd/__init__.py index 7034d53f..f20ee716 100644 --- a/extern_tools/mmd_tools_local/core/vmd/__init__.py +++ b/extern_tools/mmd_tools_local/core/vmd/__init__.py @@ -44,6 +44,8 @@ def load(self, fin): self.frame_number, = struct.unpack(' p2.x: # F-Curve correction + if cls.__BLENDER_2_91_OR_NEWER: # the F-Curve can become near-vertical + if p1.x > p3.x: + t = (p3.x - p0.x) / (p1.x - p0.x) + p1 = (1-t)*p0 + p1*t + if p0.x > p2.x: + t = (p3.x - p0.x) / (p3.x - p2.x) + p2 = (1-t)*p3 + p2*t + elif p1.x > p2.x: # legacy F-Curve correction t = (p3.x - p0.x) / (p1.x - p0.x + p3.x - p2.x) p1 = (1-t)*p0 + p1*t p2 = (1-t)*p3 + p2*t @@ -259,16 +269,11 @@ def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, @staticmethod def __minRotationDiff(prev_q, curr_q): - pq, q = prev_q, curr_q - nq = q.copy() - nq.negate() - t1 = (pq.w-q.w)**2+(pq.x-q.x)**2+(pq.y-q.y)**2+(pq.z-q.z)**2 - t2 = (pq.w-nq.w)**2+(pq.x-nq.x)**2+(pq.y-nq.y)**2+(pq.z-nq.z)**2 - #t1 = pq.rotation_difference(q).angle - #t2 = pq.rotation_difference(nq).angle - if t2 < t1: - return nq - return q + t1 = (prev_q.w - curr_q.w)**2 + (prev_q.x - curr_q.x)**2 + (prev_q.y - curr_q.y)**2 + (prev_q.z - curr_q.z)**2 + t2 = (prev_q.w + curr_q.w)**2 + (prev_q.x + curr_q.x)**2 + (prev_q.y + curr_q.y)**2 + (prev_q.z + curr_q.z)**2 + #t1 = prev_q.rotation_difference(curr_q).angle + #t2 = prev_q.rotation_difference(-curr_q).angle + return -curr_q if t2 < t1 else curr_q @staticmethod def __setInterpolation(bezier, kp0, kp1): @@ -291,6 +296,36 @@ def __fixFcurveHandles(fcurve): kp.handle_right_type = 'FREE' kp.handle_right = kp.co + Vector((1, 0)) + def __getBoneConverter(self, bone): + converter = self.__bone_util_cls(bone, self.__scale) + mode = bone.rotation_mode + compatible_quaternion = self.__minRotationDiff + class _ConverterWrap: + convert_location = converter.convert_location + convert_interpolation = converter.convert_interpolation + if mode == 'QUATERNION': + convert_rotation = converter.convert_rotation + compatible_rotation = compatible_quaternion + elif mode == 'AXIS_ANGLE': + @staticmethod + def convert_rotation(rot): + (x, y, z), angle = converter.convert_rotation(rot).to_axis_angle() + return (angle, x, y, z) + @staticmethod + def compatible_rotation(prev, curr): + angle, x, y, z = curr + if prev[1]*x + prev[2]*y + prev[3]*z < 0: + angle, x, y, z = -angle, -x, -y, -z + angle_diff = prev[0] - angle + if abs(angle_diff) > math.pi: + pi_2 = math.pi * 2 + bias = -0.5 if angle_diff < 0 else 0.5 + angle += int(bias + angle_diff/pi_2) * pi_2 + return (angle, x, y, z) + else: + convert_rotation = lambda rot: converter.convert_rotation(rot).to_euler(mode) + compatible_rotation = lambda prev, curr: curr.make_compatible(prev) or curr + return _ConverterWrap def __assignToArmature(self, armObj, action_name=None): boneAnim = self.__vmdFile.boneAnimation @@ -313,6 +348,10 @@ def __assignToArmature(self, armObj, action_name=None): pose_bones = _MirrorMapper(pose_bones) _loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation + class _Dummy: pass + dummy_keyframe_points = iter(lambda: _Dummy, None) + prop_rot_map = {'QUATERNION':'rotation_quaternion', 'AXIS_ANGLE':'rotation_axis_angle'} + bone_name_table = {} for name, keyFrames in boneAnim.items(): num_frame = len(keyFrames) @@ -326,16 +365,19 @@ def __assignToArmature(self, armObj, action_name=None): assert(bone_name_table.get(bone.name, name) == name) bone_name_table[bone.name] = name - fcurves = [None]*7 # x, y, z, rw, rx, ry, rz - default_values = list(bone.location) + list(bone.rotation_quaternion) + fcurves = [dummy_keyframe_points]*7 # x, y, z, r0, r1, r2, (r3) + data_path_rot = prop_rot_map.get(bone.rotation_mode, 'rotation_euler') + bone_rotation = getattr(bone, data_path_rot) + default_values = list(bone.location) + list(bone_rotation) data_path = 'pose.bones["%s"].location'%bone.name for axis_i in range(3): fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) - data_path = 'pose.bones["%s"].rotation_quaternion'%bone.name - for axis_i in range(4): + data_path = 'pose.bones["%s"].%s'%(bone.name, data_path_rot) + for axis_i in range(len(bone_rotation)): fcurves[3+axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) - for i, c in enumerate(fcurves): + for i in range(len(default_values)): + c = fcurves[i] c.keyframe_points.add(extra_frame+num_frame) kp_iter = iter(c.keyframe_points) if extra_frame: @@ -344,16 +386,16 @@ def __assignToArmature(self, armObj, action_name=None): kp.interpolation = 'LINEAR' fcurves[i] = kp_iter - converter = self.__bone_util_cls(bone, self.__scale) - prev_rot = bone.rotation_quaternion if extra_frame else None - prev_kps, indices = None, tuple(converter.convert_interpolation((0, 16, 32)))+(48,)*4 + converter = self.__getBoneConverter(bone) + prev_rot = bone_rotation if extra_frame else None + prev_kps, indices = None, tuple(converter.convert_interpolation((0, 16, 32)))+(48,)*len(bone_rotation) keyFrames.sort(key=lambda x:x.frame_number) - for k, x, y, z, rw, rx, ry, rz in zip(keyFrames, *fcurves): + for k, x, y, z, r0, r1, r2, r3 in zip(keyFrames, *fcurves): frame = k.frame_number + self.__frame_margin loc = converter.convert_location(_loc(k.location)) curr_rot = converter.convert_rotation(_rot(k.rotation)) if prev_rot is not None: - curr_rot = self.__minRotationDiff(prev_rot, curr_rot) + curr_rot = converter.compatible_rotation(prev_rot, curr_rot) #FIXME the rotation interpolation has slightly different result # Blender: rot(x) = prev_rot*(1 - bezier(t)) + curr_rot*bezier(t) # MMD: rot(x) = prev_rot.slerp(curr_rot, factor=bezier(t)) @@ -362,12 +404,12 @@ def __assignToArmature(self, armObj, action_name=None): x.co = (frame, loc[0]) y.co = (frame, loc[1]) z.co = (frame, loc[2]) - rw.co = (frame, curr_rot[0]) - rx.co = (frame, curr_rot[1]) - ry.co = (frame, curr_rot[2]) - rz.co = (frame, curr_rot[3]) + r0.co = (frame, curr_rot[0]) + r1.co = (frame, curr_rot[1]) + r2.co = (frame, curr_rot[2]) + r3.co = (frame, curr_rot[-1]) - curr_kps = (x, y, z, rw, rx, ry, rz) + curr_kps = (x, y, z, r0, r1, r2, r3) if prev_kps is not None: interp = k.interp for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps): diff --git a/extern_tools/mmd_tools_local/operators/fileio.py b/extern_tools/mmd_tools_local/operators/fileio.py index 7489e6f2..3d7e36aa 100644 --- a/extern_tools/mmd_tools_local/operators/fileio.py +++ b/extern_tools/mmd_tools_local/operators/fileio.py @@ -517,6 +517,9 @@ def _do_execute(self, context, root): rig = mmd_model.Model(root) arm = rig.armature() + if arm is None: + self.report({'ERROR'}, '[Skipped] The armature object of MMD model "%s" can\'t be found'%root.name) + return {'CANCELLED'} orig_pose_position = None if not root.mmd_root.is_built: # use 'REST' pose when the model is not built orig_pose_position = arm.data.pose_position diff --git a/extern_tools/mmd_tools_local/panels/prop_object.py b/extern_tools/mmd_tools_local/panels/prop_object.py index 838ced41..0c5375ad 100644 --- a/extern_tools/mmd_tools_local/panels/prop_object.py +++ b/extern_tools/mmd_tools_local/panels/prop_object.py @@ -43,6 +43,12 @@ class MMDRigidPanel(_PanelBase, Panel): bl_idname = 'RIGID_PT_mmd_tools_bone' bl_label = 'MMD Rigid Body' + __RIGID_SIZE_MAP = { + 'SPHERE':('Radius',), + 'BOX':('X', 'Y', 'Z'), + 'CAPSULE':('Radius', 'Height'), + } + @classmethod def poll(cls, context): obj = context.active_object @@ -73,7 +79,10 @@ def draw(self, context): c = layout.column(align=True) c.enabled = obj.mode == 'OBJECT' c.row(align=True).prop(obj.mmd_rigid, 'shape', expand=True) - c.column(align=True).prop(obj.mmd_rigid, 'size', text='') + #c.column(align=True).prop(obj.mmd_rigid, 'size', text='') + col = c.column(align=True) + for i, name in enumerate(self.__RIGID_SIZE_MAP[obj.mmd_rigid.shape]): + col.prop(obj.mmd_rigid, 'size', text=name, index=i) row = layout.row() if obj.rigid_body is None: @@ -108,9 +117,6 @@ def draw(self, context): class MMDJointPanel(_PanelBase, Panel): bl_idname = 'JOINT_PT_mmd_tools_bone' bl_label = 'MMD Joint' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'object' @classmethod def poll(cls, context): diff --git a/extern_tools/mmd_tools_local/panels/util_tools.py b/extern_tools/mmd_tools_local/panels/util_tools.py index 239d4679..78728817 100644 --- a/extern_tools/mmd_tools_local/panels/util_tools.py +++ b/extern_tools/mmd_tools_local/panels/util_tools.py @@ -1,13 +1,21 @@ # -*- coding: utf-8 -*- +import bpy from bpy.types import Panel, UIList from mmd_tools_local import register_wrap from mmd_tools_local.bpyutils import SceneOp from mmd_tools_local.core.model import Model from mmd_tools_local.panels.tool import TRIA_UP_BAR, TRIA_DOWN_BAR +from mmd_tools_local.panels.tool import _layout_split from mmd_tools_local.panels.tool import _PanelBase + +ICON_APPEND_MOVE, ICON_APPEND_ROT, ICON_APPEND_MOVE_ROT = 'IPO_LINEAR', 'IPO_EXPO', 'IPO_QUAD' +if bpy.app.version < (2, 71, 0): + ICON_APPEND_MOVE, ICON_APPEND_ROT, ICON_APPEND_MOVE_ROT = 'NDOF_TRANS', 'NDOF_TURN', 'FORCE_MAGNETIC' + + @register_wrap class MMD_TOOLS_UL_Materials(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -116,3 +124,217 @@ def draw(self, context): tb1.operator('mmd_tools.object_move', text='', icon='TRIA_DOWN').type = 'DOWN' tb1.operator('mmd_tools.object_move', text='', icon=TRIA_DOWN_BAR).type = 'BOTTOM' +class _DummyVertexGroup: + index = None + def __init__(self, index): + self.index = index + +@register_wrap +class MMD_TOOLS_UL_ModelBones(UIList): + _IK_MAP = {} + _IK_BONES = {} + _DUMMY_VERTEX_GROUPS = {} + + @classmethod + def __wrap_pose_bones(cls, pose_bones): + for i, b in enumerate(pose_bones): + cls._DUMMY_VERTEX_GROUPS[b.name] = _DummyVertexGroup(i) + yield b + + @classmethod + def update_bone_tables(cls, armature, bone_order_object): + cls._IK_MAP.clear() + cls._IK_BONES.clear() + cls._DUMMY_VERTEX_GROUPS.clear() + + ik_target_override = {} + ik_target_custom = {} + ik_target_fin = {} + pose_bones = armature.pose.bones + bone_count = len(pose_bones) + pose_bone_list = pose_bones if bone_order_object else cls.__wrap_pose_bones(pose_bones) + + for b in pose_bone_list: + if b.is_mmd_shadow_bone: + bone_count -= 1 + continue + for c in b.constraints: + if c.type == 'IK' and c.subtarget in pose_bones and c.subtarget not in cls._IK_BONES: + if not c.use_tail: + cls._IK_MAP.setdefault(hash(b), []).append(c.subtarget) + cls._IK_BONES[c.subtarget] = ik_target_fin[c.subtarget] = hash(b) + bone_chain = b.parent_recursive + else: + cls._IK_BONES[c.subtarget] = b.name + bone_chain = [b] + b.parent_recursive + for l in bone_chain[:c.chain_count]: + cls._IK_MAP.setdefault(hash(l), []).append(c.subtarget) + if 'mmd_ik_target_custom' == c.name: + ik_target_custom[getattr(c, 'subtarget', '')] = hash(b) + elif 'mmd_ik_target_override' == c.name and b.parent: + if b.parent.name == getattr(c, 'subtarget', ''): + for c in b.parent.constraints: + if c.type == 'IK' and c.subtarget in pose_bones and c.subtarget not in ik_target_override and c.subtarget not in ik_target_custom: + ik_target_override[c.subtarget] = hash(b) + + for k, v in ik_target_custom.items(): + if k not in ik_target_fin and k in cls._IK_BONES: + cls._IK_BONES[k] = v + cls._IK_MAP.setdefault(v, []).append(k) + if k in ik_target_override: + del ik_target_override[k] + + for k, v in ik_target_override.items(): + if k not in ik_target_fin and k in cls._IK_BONES: + cls._IK_BONES[k] = v + cls._IK_MAP.setdefault(v, []).append(k) + + for k, v in tuple(cls._IK_BONES.items()): + if isinstance(v, str): + b = cls.__get_ik_target_bone(pose_bones[v]) + if b: + cls._IK_BONES[k] = hash(b) + cls._IK_MAP.setdefault(hash(b), []).append(k) + else: + del cls._IK_BONES[k] + return bone_count + + @staticmethod + def __get_ik_target_bone(target_bone): + r = None + min_length = None + for c in (c for c in target_bone.children if not c.is_mmd_shadow_bone): + if c.bone.use_connect: + return c + length = (c.head - target_bone.tail).length + if min_length is None or length < min_length: + min_length = length + r = c + return r + + @classmethod + def _draw_bone_item(cls, layout, bone_name, pose_bones, vertex_groups, index): + bone = pose_bones.get(bone_name, None) + if not bone or bone.is_mmd_shadow_bone: + layout.active = False + layout.label(text=bone_name, translate=False, icon='GROUP_BONE' if bone else 'MESH_DATA') + r = layout.row() + r.alignment = 'RIGHT' + r.label(text=str(index)) + else: + row = _layout_split(layout, factor=0.45, align=False) + r0 = row.row() + r0.label(text=bone_name, translate=False, icon='POSE_HLT' if bone_name in cls._IK_BONES else 'BONE_DATA') + r = r0.row() + r.alignment = 'RIGHT' + r.label(text=str(index)) + + row_sub = _layout_split(row, factor=0.67, align=False) + + mmd_bone = bone.mmd_bone + count = len(pose_bones) + bone_transform_rank = index + mmd_bone.transform_order*count + + r = row_sub.row() + bone_parent = bone.parent + if bone_parent: + bone_parent = bone_parent.name + idx = vertex_groups.get(bone_parent, _DummyVertexGroup).index + if idx is None or bone_transform_rank < (idx + pose_bones[bone_parent].mmd_bone.transform_order*count): + r.label(text=str(idx), icon='ERROR') + else: + r.label(text=str(idx), icon='INFO' if index < idx else 'FILE_PARENT') + else: + r.label() + + r = r.row() + if mmd_bone.has_additional_rotation: + append_bone = mmd_bone.additional_transform_bone + idx = vertex_groups.get(append_bone, _DummyVertexGroup).index + if idx is None or bone_transform_rank < (idx + pose_bones[append_bone].mmd_bone.transform_order*count): + if append_bone: + r.label(text=str(idx), icon='ERROR') + else: + r.label(text=str(idx), icon=ICON_APPEND_MOVE_ROT if mmd_bone.has_additional_location else ICON_APPEND_ROT) + elif mmd_bone.has_additional_location: + append_bone = mmd_bone.additional_transform_bone + idx = vertex_groups.get(append_bone, _DummyVertexGroup).index + if idx is None or bone_transform_rank < (idx + pose_bones[append_bone].mmd_bone.transform_order*count): + if append_bone: + r.label(text=str(idx), icon='ERROR') + else: + r.label(text=str(idx), icon=ICON_APPEND_MOVE) + + for idx, b in sorted(((vertex_groups.get(b, _DummyVertexGroup).index, b) for b in cls._IK_MAP.get(hash(bone), ())), key=lambda i: i[0] or 0): + ik_bone = pose_bones[b] + is_ik_chain = (hash(bone) != cls._IK_BONES.get(b)) + if idx is None or (is_ik_chain and bone_transform_rank > (idx + ik_bone.mmd_bone.transform_order*count)): + r.prop(ik_bone, 'mmd_ik_toggle', text=str(idx), toggle=True, icon='ERROR') + elif b not in cls._IK_BONES: + r.prop(ik_bone, 'mmd_ik_toggle', text=str(idx), toggle=True, icon='QUESTION') + else: + r.prop(ik_bone, 'mmd_ik_toggle', text=str(idx), toggle=True, icon='LINKED' if is_ik_chain else 'HOOK') + + row = row_sub.row(align=True) + if mmd_bone.transform_after_dynamics: + row.prop(mmd_bone, 'transform_after_dynamics', text='', toggle=True, icon='PHYSICS') + else: + row.prop(mmd_bone, 'transform_after_dynamics', text='', toggle=True) + row.prop(mmd_bone, 'transform_order', text='', slider=bool(mmd_bone.transform_order)) + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {'DEFAULT'}: + if self._DUMMY_VERTEX_GROUPS: + self._draw_bone_item(layout, item.name, data.bones, self._DUMMY_VERTEX_GROUPS, index) + else: + self._draw_bone_item(layout, item.name, data.parent.pose.bones, data.vertex_groups, index) + elif self.layout_type in {'COMPACT'}: + pass + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) + +@register_wrap +class MMDBoneOrder(_PanelBase, Panel): + bl_idname = 'OBJECT_PT_mmd_tools_bone_order' + bl_label = 'Bone Order' + bl_context = '' + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + active_obj = context.active_object + root = Model.findRoot(active_obj) + if root is None: + layout.label(text='Select a MMD Model') + return + + armature = Model(root).armature() + if armature is None: + layout.label(text='The armature object of active MMD model can\'t be found', icon='ERROR') + return + + bone_order_object = next((i for i in armature.children if 'mmd_bone_order_override' in i.modifiers), None) #TODO consistency issue + bone_count = MMD_TOOLS_UL_ModelBones.update_bone_tables(armature, bone_order_object) + + col = layout.column(align=True) + row = col.row() + if bone_order_object is None: + row.template_list("MMD_TOOLS_UL_ModelBones", "", + armature.pose, 'bones', + root.vertex_groups, 'active_index') + col.label(text='(%d) %s'%(bone_count, armature.name), icon='OUTLINER_OB_ARMATURE') + col.label(text='No mesh object with "mmd_bone_order_override" modifier', icon='ERROR') + else: + row.template_list("MMD_TOOLS_UL_ModelBones", "", + bone_order_object, 'vertex_groups', + bone_order_object.vertex_groups, 'active_index') + row = col.row() + row.label(text='(%d) %s'%(bone_count, armature.name), icon='OUTLINER_OB_ARMATURE') + row.label(icon='BACK') + row.label(text=bone_order_object.name, icon='OBJECT_DATA') + if bone_order_object == active_obj: + row = row.row(align=True) + row.operator('object.vertex_group_move', text='', icon='TRIA_UP').direction = 'UP' + row.operator('object.vertex_group_move', text='', icon='TRIA_DOWN').direction = 'DOWN' + diff --git a/extern_tools/mmd_tools_local/panels/view_prop.py b/extern_tools/mmd_tools_local/panels/view_prop.py index 7bc9a233..19d733bb 100644 --- a/extern_tools/mmd_tools_local/panels/view_prop.py +++ b/extern_tools/mmd_tools_local/panels/view_prop.py @@ -59,7 +59,7 @@ def __draw_IK_toggle(self, armature): if any(all(x) for x in zip(ik.bone.layers, armature.data.layers)): px, py, pz = -ik.bone.head_local/base bx, by, bz = -b.head_local/base*0.15 - groups.setdefault((int(pz), int(bz), -cnt), set()).add(((px, -py, bx), ik)) # (px, pz, -py, bx, bz, -by) + groups.setdefault((int(pz), int(bz), int(px**2), -cnt), set()).add(((px, -py, bx), ik)) # (px, pz, -py, bx, bz, -by) layout = self.layout.box().column() for _, group in sorted(groups.items()): row = layout.row() diff --git a/extern_tools/mmd_tools_local/properties/bone.py b/extern_tools/mmd_tools_local/properties/bone.py index 14aaab9a..f67e8599 100644 --- a/extern_tools/mmd_tools_local/properties/bone.py +++ b/extern_tools/mmd_tools_local/properties/bone.py @@ -65,6 +65,7 @@ class MMDBone(PropertyGroup): description='Deformation tier', min=0, max=100, + soft_max=7, ) is_controllable = BoolProperty( diff --git a/extern_tools/mmd_tools_local/properties/morph.py b/extern_tools/mmd_tools_local/properties/morph.py index 8931a0cc..9d82e5e7 100644 --- a/extern_tools/mmd_tools_local/properties/morph.py +++ b/extern_tools/mmd_tools_local/properties/morph.py @@ -133,7 +133,7 @@ def _update_bone_morph_data(prop, context): bone = arm.pose.bones.get(prop.name, None) if bone: bone.location = prop.location - bone.rotation_quaternion = prop.rotation + bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency @register_wrap class BoneMorphData(PropertyGroup): diff --git a/googletrans/__init__.py b/googletrans/__init__.py deleted file mode 100644 index 51f5a3eb..00000000 --- a/googletrans/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Free Google Translate API for Python. Translates totally free of charge.""" -__all__ = 'Translator', -__version__ = '2.2.0' - - -from .client import Translator -from .constants import LANGCODES, LANGUAGES diff --git a/googletrans/client.py b/googletrans/client.py deleted file mode 100644 index 466bd3ce..00000000 --- a/googletrans/client.py +++ /dev/null @@ -1,232 +0,0 @@ -# -*- coding: utf-8 -*- -""" -A Translation module. - -You can translate text using this module. -""" -import requests -import random -import bpy - -from . import urls, utils -from .compat import PY3 -from .gtoken import TokenAcquirer -from .constants import DEFAULT_USER_AGENT, LANGCODES, LANGUAGES, SPECIAL_CASES -from .models import Translated, Detected - - -EXCLUDES = ('en', 'ca', 'fr') - - -class Translator(object): - """Google Translate ajax API implementation class - - You have to create an instance of Translator to use this API - - :param service_urls: google translate url list. URLs will be used randomly. - For example ``['translate.google.com', 'translate.google.co.kr']`` - :type service_urls: a sequence of strings - - :param user_agent: the User-Agent header to send when making requests. - :type user_agent: :class:`str` - """ - - def __init__(self, service_urls=None, user_agent=DEFAULT_USER_AGENT): - self.session = requests.Session() - self.session.headers.update({ - 'User-Agent': user_agent, - }) - self.service_urls = service_urls or ['translate.google.com'] - self.token_acquirer = TokenAcquirer(session=self.session, host=self.service_urls[0]) - - # # Use HTTP2 Adapter if hyper is installed - # try: # pragma: nocover - # from hyper.contrib import HTTP20Adapter - # self.session.mount(urls.BASE, HTTP20Adapter()) - # print('USING HYPER!!!') - # except ImportError: # pragma: nocover - # print('NOT USING HYPER!!!') - # pass - - def _pick_service_url(self): - if len(self.service_urls) == 1: - return self.service_urls[0] - return random.choice(self.service_urls) - - def _translate(self, text, dest, src): - if not PY3 and isinstance(text, str): # pragma: nocover - text = text.decode('utf-8') - - token = self.token_acquirer.do(text) - params = utils.build_params(query=text, src=src, dest=dest, - token=token) - url = urls.TRANSLATE.format(host=self._pick_service_url()) - r = self.session.get(url, params=params) - - # print('JSON:', r.text) - - data = utils.format_json(r.text) - return data - - def translate(self, text, dest='en', src='auto'): - """Translate text from source language to destination language - - :param text: The source text(s) to be translated. Batch translation is supported via sequence input. - :type text: UTF-8 :class:`str`; :class:`unicode`; string sequence (list, tuple, iterator, generator) - - :param dest: The language to translate the source text into. - The value should be one of the language codes listed in :const:`googletrans.LANGUAGES` - or one of the language names listed in :const:`googletrans.LANGCODES`. - :param dest: :class:`str`; :class:`unicode` - - :param src: The language of the source text. - The value should be one of the language codes listed in :const:`googletrans.LANGUAGES` - or one of the language names listed in :const:`googletrans.LANGCODES`. - If a language is not specified, - the system will attempt to identify the source language automatically. - :param src: :class:`str`; :class:`unicode` - - :rtype: Translated - :rtype: :class:`list` (when a list is passed) - - Basic usage: - >>> from googletrans import Translator - >>> translator = Translator() - >>> translator.translate('안녕하세요.') - - >>> translator.translate('안녕하세요.', dest='ja') - - >>> translator.translate('veritas lux mea', src='la') - - - Advanced usage: - >>> translations = translator.translate(['The quick brown fox', 'jumps over', 'the lazy dog'], dest='ko') - >>> for translation in translations: - ... print(translation.origin, ' -> ', translation.text) - The quick brown fox -> 빠른 갈색 여우 - jumps over -> 이상 점프 - the lazy dog -> 게으른 개 - """ - dest = dest.lower().split('_', 1)[0] - src = src.lower().split('_', 1)[0] - - if src != 'auto' and src not in LANGUAGES: - if src in SPECIAL_CASES: - src = SPECIAL_CASES[src] - elif src in LANGCODES: - src = LANGCODES[src] - else: - raise ValueError('invalid source language') - - if dest not in LANGUAGES: - if dest in SPECIAL_CASES: - dest = SPECIAL_CASES[dest] - elif dest in LANGCODES: - dest = LANGCODES[dest] - else: - raise ValueError('invalid destination language') - - if isinstance(text, list): - wm = bpy.context.window_manager - current_step = 0 - wm.progress_begin(current_step, len(text)) - - result = [] - for item in text: - translated = self.translate(item, dest=dest, src=src) - result.append(translated) - current_step += 1 - wm.progress_update(current_step) - wm.progress_end() - return result - - origin = text - data = self._translate(text, dest, src) - - # this code will be updated when the format is changed. - translated = ''.join([d[0] if d[0] else '' for d in data[0]]) - - # actual source language that will be recognized by Google Translator when the - # src passed is equal to auto. - try: - src = data[2] - except Exception: # pragma: nocover - pass - - pron = origin - try: - pron = data[0][1][-2] - except Exception: # pragma: nocover - pass - if not PY3 and isinstance(pron, unicode) and isinstance(origin, str): # pragma: nocover - origin = origin.decode('utf-8') - if dest in EXCLUDES and pron == origin: - pron = translated - - # for python 2.x compatbillity - if not PY3: # pragma: nocover - if isinstance(src, str): - src = src.decode('utf-8') - if isinstance(dest, str): - dest = dest.decode('utf-8') - if isinstance(translated, str): - translated = translated.decode('utf-8') - - # put final values into a new Translated object - result = Translated(src=src, dest=dest, origin=origin, - text=translated, pronunciation=pron) - - return result - - def detect(self, text): - """Detect language of the input text - - :param text: The source text(s) whose language you want to identify. - Batch detection is supported via sequence input. - :type text: UTF-8 :class:`str`; :class:`unicode`; string sequence (list, tuple, iterator, generator) - - :rtype: Detected - :rtype: :class:`list` (when a list is passed) - - Basic usage: - >>> from googletrans import Translator - >>> translator = Translator() - >>> translator.detect('이 문장은 한글로 쓰여졌습니다.') - - >>> translator.detect('この文章は日本語で書かれました。') - - >>> translator.detect('This sentence is written in English.') - - >>> translator.detect('Tiu frazo estas skribita en Esperanto.') - - - Advanced usage: - >>> langs = translator.detect(['한국어', '日本語', 'English', 'le français']) - >>> for lang in langs: - ... print(lang.lang, lang.confidence) - ko 1 - ja 0.92929292 - en 0.96954316 - fr 0.043500196 - """ - if isinstance(text, list): - result = [] - for item in text: - lang = self.detect(item) - result.append(lang) - return result - - data = self._translate(text, dest='en', src='auto') - - # actual source language that will be recognized by Google Translator when the - # src passed is equal to auto. - src = '' - confidence = 0.0 - try: - src = ''.join(data[8][0]) - confidence = data[8][-2][0] - except Exception: # pragma: nocover - pass - result = Detected(lang=src, confidence=confidence) - - return result diff --git a/googletrans/compat.py b/googletrans/compat.py deleted file mode 100644 index c11e55fe..00000000 --- a/googletrans/compat.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -try: # pragma: nocover - from urllib.parse import quote -except: # pragma: nocover - from urllib import quote - - -PY3 = sys.version_info > (3, ) - -unicode = str if PY3 else unicode diff --git a/googletrans/constants.py b/googletrans/constants.py deleted file mode 100644 index c5099e78..00000000 --- a/googletrans/constants.py +++ /dev/null @@ -1,114 +0,0 @@ -DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' - -SPECIAL_CASES = { - 'ee': 'et', -} - -LANGUAGES = { - 'af': 'afrikaans', - 'sq': 'albanian', - 'am': 'amharic', - 'ar': 'arabic', - 'hy': 'armenian', - 'az': 'azerbaijani', - 'eu': 'basque', - 'be': 'belarusian', - 'bn': 'bengali', - 'bs': 'bosnian', - 'bg': 'bulgarian', - 'ca': 'catalan', - 'ceb': 'cebuano', - 'ny': 'chichewa', - 'zh-cn': 'chinese (simplified)', - 'zh-tw': 'chinese (traditional)', - 'co': 'corsican', - 'hr': 'croatian', - 'cs': 'czech', - 'da': 'danish', - 'nl': 'dutch', - 'en': 'english', - 'eo': 'esperanto', - 'et': 'estonian', - 'tl': 'filipino', - 'fi': 'finnish', - 'fr': 'french', - 'fy': 'frisian', - 'gl': 'galician', - 'ka': 'georgian', - 'de': 'german', - 'el': 'greek', - 'gu': 'gujarati', - 'ht': 'haitian creole', - 'ha': 'hausa', - 'haw': 'hawaiian', - 'iw': 'hebrew', - 'hi': 'hindi', - 'hmn': 'hmong', - 'hu': 'hungarian', - 'is': 'icelandic', - 'ig': 'igbo', - 'id': 'indonesian', - 'ga': 'irish', - 'it': 'italian', - 'ja': 'japanese', - 'jw': 'javanese', - 'kn': 'kannada', - 'kk': 'kazakh', - 'km': 'khmer', - 'ko': 'korean', - 'ku': 'kurdish (kurmanji)', - 'ky': 'kyrgyz', - 'lo': 'lao', - 'la': 'latin', - 'lv': 'latvian', - 'lt': 'lithuanian', - 'lb': 'luxembourgish', - 'mk': 'macedonian', - 'mg': 'malagasy', - 'ms': 'malay', - 'ml': 'malayalam', - 'mt': 'maltese', - 'mi': 'maori', - 'mr': 'marathi', - 'mn': 'mongolian', - 'my': 'myanmar (burmese)', - 'ne': 'nepali', - 'no': 'norwegian', - 'ps': 'pashto', - 'fa': 'persian', - 'pl': 'polish', - 'pt': 'portuguese', - 'pa': 'punjabi', - 'ro': 'romanian', - 'ru': 'russian', - 'sm': 'samoan', - 'gd': 'scots gaelic', - 'sr': 'serbian', - 'st': 'sesotho', - 'sn': 'shona', - 'sd': 'sindhi', - 'si': 'sinhala', - 'sk': 'slovak', - 'sl': 'slovenian', - 'so': 'somali', - 'es': 'spanish', - 'su': 'sundanese', - 'sw': 'swahili', - 'sv': 'swedish', - 'tg': 'tajik', - 'ta': 'tamil', - 'te': 'telugu', - 'th': 'thai', - 'tr': 'turkish', - 'uk': 'ukrainian', - 'ur': 'urdu', - 'uz': 'uzbek', - 'vi': 'vietnamese', - 'cy': 'welsh', - 'xh': 'xhosa', - 'yi': 'yiddish', - 'yo': 'yoruba', - 'zu': 'zulu' -} - -LANGCODES = dict(map(reversed, LANGUAGES.items())) diff --git a/googletrans/gtoken.py b/googletrans/gtoken.py deleted file mode 100644 index 3ca18640..00000000 --- a/googletrans/gtoken.py +++ /dev/null @@ -1,283 +0,0 @@ -# -*- coding: utf-8 -*- -import re -import os -import ast -import bpy -import math -import time -import pathlib -import requests -from .compat import PY3 -from .utils import rshift -from .compat import unicode -from html.parser import HTMLParser -from html.entities import name2codepoint - - -class TokenAcquirer(object): - """Google Translate API token generator - - translate.google.com uses a token to authorize the requests. If you are - not Google, you do have this token and will have to pay for use. - This class is the result of reverse engineering on the obfuscated and - minified code used by Google to generate such token. - - The token is based on a seed which is updated once per hour and on the - text that will be translated. - Both are combined - by some strange math - in order to generate a final - token (e.g. 744915.856682) which is used by the API to validate the - request. - - This operation will cause an additional request to get an initial - token from translate.google.com. - - Example usage: - >>> from googletrans.gtoken import TokenAcquirer - >>> acquirer = TokenAcquirer() - >>> text = 'test' - >>> tk = acquirer.do(text) - >>> tk - 950629.577246 - """ - - RE_TKK = re.compile(r'TKK=eval\(\'\(\(function\(\)\{(.+?)\}\)\(\)\)\'\);', re.DOTALL) - RE_TKK2 = re.compile(r'tkk:eval\(\'\(\(function\(\)\{(.+?)\}\)\(\)\)\'\);', re.DOTALL) - RE_RAWTKK = re.compile(r'TKK=\'([^\']*)\';', re.DOTALL) - RE_NEWTKK = re.compile(r'tkk:\'([^\']*)\',', re.DOTALL) - - def __init__(self, tkk='0', session=None, host='translate.google.com'): - self.session = session or requests.Session() - self.tkk = tkk - self.host = host if 'http' in host else 'https://' + host - - def _update(self): - """update tkk - """ - # we don't need to update the base TKK value when it is still valid - now = math.floor(int(time.time() * 1000) / 3600000.0) - if self.tkk and int(self.tkk.split('.')[0]) == now: - return - - r = self.session.get(self.host, verify=False) - - # This prints the google response if the button in the cats settings is pressed - # Prints the response from google into a text file inside cats/resources/google-response.txt - print_response(r.text) - - rawtkk = self.RE_RAWTKK.search(r.text) - if rawtkk: - self.tkk = rawtkk.group(1) - return - - newtkk = self.RE_NEWTKK.search(r.text) - if newtkk: - self.tkk = newtkk.group(1) - return - - # this will be the same as python code after stripping out a reserved word 'var' - try: - if self.RE_TKK.search(r.text): - code = unicode(self.RE_TKK.search(r.text).group(1)).replace('var ', '') - else: - code = unicode(self.RE_TKK2.search(r.text).group(1)).replace('var ', '') - except AttributeError as e: - print_response(r.text, force_print=True) - raise AttributeError(e) - - # unescape special ascii characters such like a \x3d(=) - if PY3: # pragma: no cover - code = code.encode().decode('unicode-escape') - else: # pragma: no cover - code = code.decode('string_escape') - - if code: - tree = ast.parse(code) - visit_return = False - operator = '+' - n, keys = 0, dict(a=0, b=0) - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - name = node.targets[0].id - if name in keys: - if isinstance(node.value, ast.Num): - keys[name] = node.value.n - # the value can sometimes be negative - elif isinstance(node.value, ast.UnaryOp) and \ - isinstance(node.value.op, ast.USub): # pragma: nocover - keys[name] = -node.value.operand.n - elif isinstance(node, ast.Return): - # parameters should be set after this point - visit_return = True - elif visit_return and isinstance(node, ast.Num): - n = node.n - elif visit_return and n > 0: - # the default operator is '+' but implement some more for - # all possible scenarios - if isinstance(node, ast.Add): # pragma: nocover - pass - elif isinstance(node, ast.Sub): # pragma: nocover - operator = '-' - elif isinstance(node, ast.Mult): # pragma: nocover - operator = '*' - elif isinstance(node, ast.Pow): # pragma: nocover - operator = '**' - elif isinstance(node, ast.BitXor): # pragma: nocover - operator = '^' - # a safety way to avoid Exceptions - clause = compile('{1}{0}{2}'.format( - operator, keys['a'], keys['b']), '', 'eval') - value = eval(clause, dict(__builtin__={})) - result = '{}.{}'.format(n, value) - - self.tkk = result - - def _lazy(self, value): - """like lazy evalution, this method returns a lambda function that - returns value given. - We won't be needing this because this seems to have been built for - code obfuscation. - - the original code of this method is as follows: - - ... code-block: javascript - - var ek = function(a) { - return function() { - return a; - }; - } - """ - return lambda: value - - def _xr(self, a, b): - size_b = len(b) - c = 0 - while c < size_b - 2: - d = b[c + 2] - d = ord(d[0]) - 87 if 'a' <= d else int(d) - d = rshift(a, d) if '+' == b[c + 1] else a << d - a = a + d & 4294967295 if '+' == b[c] else a ^ d - - c += 3 - return a - - def acquire(self, text): - b = self.tkk if self.tkk != '0' else '' - d = b.split('.') - b = int(d[0]) if len(d) > 1 else 0 - - # assume e means char code array - e = [] - g = 0 - size = len(text) - for i, char in enumerate(text): - l = ord(char) - # just append if l is less than 128(ascii: DEL) - if l < 128: - e.append(l) - # append calculated value if l is less than 2048 - else: - if l < 2048: - e.append(l >> 6 | 192) - else: - # append calculated value if l matches special condition - if (l & 64512) == 55296 and g + 1 < size and \ - ord(text[g + 1]) & 64512 == 56320: - g += 1 - l = 65536 + ((l & 1023) << 10) + ord(text[g]) & 1023 - e.append(l >> 18 | 240) - e.append(l >> 12 & 63 | 128) - else: - e.append(l >> 12 | 224) - e.append(l >> 6 & 63 | 128) - e.append(l & 63 | 128) - a = b - for i, value in enumerate(e): - a += value - a = self._xr(a, '+-a^+6') - a = self._xr(a, '+-3^+b+-f') - a ^= int(d[1]) if len(d) > 1 else 0 - if a < 0: # pragma: nocover - a = (a & 2147483647) + 2147483648 - a %= 1000000 # int(1E6) - - return '{}.{}'.format(a, a ^ b) - - def do(self, text): - self._update() - tk = self.acquire(text) - return tk - - -def print_response(text, force_print=False): - if not force_print and not bpy.context.scene.debug_translations: - return - # Prints the response from google into a textfile inside cats/resources/google-response.txt - main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve() - resources_dir = os.path.join(str(main_dir), "resources") - output_file = os.path.join(resources_dir, "google-response.txt") - output_file_readable = os.path.join(resources_dir, "google-response-readable.txt") - with open(output_file, 'w', encoding="utf8") as outfile: - outfile.write(text) - with open(output_file_readable, 'w', encoding="utf8") as outfile: - outfile.write(html_to_text(text)) - - -""" -HTML <-> text conversions. -http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python -""" - - -class _HTMLToText(HTMLParser): - def __init__(self): - HTMLParser.__init__(self) - self._buf = [] - self.hide_output = False - - def handle_starttag(self, tag, attrs): - if tag in ('p', 'br') and not self.hide_output: - self._buf.append('\n') - elif tag in ('script', 'style'): - self.hide_output = True - - def handle_startendtag(self, tag, attrs): - if tag == 'br': - self._buf.append('\n') - - def handle_endtag(self, tag): - if tag == 'p': - self._buf.append('\n') - elif tag in ('script', 'style'): - self.hide_output = False - - def handle_data(self, text): - if text and not self.hide_output: - self._buf.append(re.sub(r'\s+', ' ', text)) - - def handle_entityref(self, name): - if name in name2codepoint and not self.hide_output: - c = chr(name2codepoint[name]) - self._buf.append(c) - - def handle_charref(self, name): - if not self.hide_output: - n = int(name[1:], 16) if name.startswith('x') else int(name) - self._buf.append(chr(n)) - - def get_text(self): - return re.sub(r' +', ' ', ''.join(self._buf)) - - -def html_to_text(html): - """ - Given a piece of HTML, return the plain text it contains. - This handles entities and char refs, but not javascript and stylesheets. - """ - parser = _HTMLToText() - try: - parser.feed(html) - parser.close() - except: # HTMLParseError: No good replacement? - pass - return parser.get_text() \ No newline at end of file diff --git a/googletrans/models.py b/googletrans/models.py deleted file mode 100644 index 043af280..00000000 --- a/googletrans/models.py +++ /dev/null @@ -1,40 +0,0 @@ -class Translated(object): - """Translate result object - - :param src: source langauge (default: auto) - :param dest: destination language (default: en) - :param origin: original text - :param text: translated text - :param pronunciation: pronunciation - """ - def __init__(self, src, dest, origin, text, pronunciation): - self.src = src - self.dest = dest - self.origin = origin - self.text = text - self.pronunciation = pronunciation - - def __str__(self): # pragma: nocover - return self.__unicode__() - - def __unicode__(self): # pragma: nocover - return u'Translated(src={src}, dest={dest}, text={text}, pronunciation={pronunciation})'.format( - src=self.src, dest=self.dest, text=self.text, pronunciation=self.pronunciation) - - -class Detected(object): - """Language detection result object - - :param lang: detected language - :param confidence: the confidence of detection result (0.00 to 1.00) - """ - def __init__(self, lang, confidence): - self.lang = lang - self.confidence = confidence - - def __str__(self): # pragma: nocover - return self.__unicode__() - - def __unicode__(self): # pragma: nocover - return u'Detected(lang={lang}, confidence={confidence})'.format( - lang=self.lang, confidence=self.confidence) \ No newline at end of file diff --git a/googletrans/urls.py b/googletrans/urls.py deleted file mode 100644 index 0bf8933f..00000000 --- a/googletrans/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Predefined URLs used to make google translate requests. -""" -BASE = 'https://translate.google.com' -TRANSLATE = 'https://{host}/translate_a/single' diff --git a/googletrans/utils.py b/googletrans/utils.py deleted file mode 100644 index 0075adb3..00000000 --- a/googletrans/utils.py +++ /dev/null @@ -1,74 +0,0 @@ -"""A conversion module for googletrans""" -from __future__ import print_function -import re -import json - - -def build_params(query, src, dest, token): - params = { - 'client': 't', - 'sl': src, - 'tl': dest, - 'hl': dest, - 'dt': ['at', 'bd', 'ex', 'ld', 'md', 'qca', 'rw', 'rm', 'ss', 't'], - 'ie': 'UTF-8', - 'oe': 'UTF-8', - 'otf': 1, - 'ssel': 0, - 'tsel': 0, - 'tk': token, - 'q': query, - } - return params - - -def legacy_format_json(original): - # save state - states = [] - text = original - - # save position for double-quoted texts - for i, pos in enumerate(re.finditer('"', text)): - # pos.start() is a double-quote - p = pos.start() + 1 - if i % 2 == 0: - nxt = text.find('"', p) - states.append((p, text[p:nxt])) - - # replace all weired characters in text - while text.find(',,') > -1: - text = text.replace(',,', ',null,') - while text.find('[,') > -1: - text = text.replace('[,', '[null,') - - # recover state - for i, pos in enumerate(re.finditer('"', text)): - p = pos.start() + 1 - if i % 2 == 0: - j = int(i / 2) - nxt = text.find('"', p) - # replacing a portion of a string - # use slicing to extract those parts of the original string to be kept - text = text[:p] + states[j][1] + text[nxt:] - - try: - converted = json.loads(text) - except json.JSONDecodeError: - raise RuntimeError(text) - - return converted - - -def format_json(original): - #original = 'Error 403 (Forbidden)!!1

403. That’s an error.

Your client does not have permission to get URL /translate_a/single?client=t&sl=auto&tl=en&hl=en&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&ie=UTF-8&oe=UTF-8&otf=1&ssel=0&tsel=0&tk=684737.684737&q=test from this server. That’s all we know.

' - try: - converted = json.loads(original) - except ValueError: - converted = legacy_format_json(original) - return converted - - -def rshift(val, n): - """python port for '>>>'(right shift with padding) - """ - return (val % 0x100000000) >> n diff --git a/tools/translate.py b/tools/translate.py index 29f49a35..1b818dec 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -468,7 +468,7 @@ def update_dictionary(to_translate_list, translating_shapes=False, self=None): # If the translator wasn't able to create a stable connection to Google, just retry it again # This is an issue with Google since Nov 2020: https://github.com/ssut/py-googletrans/issues/234 token_tries += 1 - if token_tries < 10: + if token_tries < 3: print('RETRY', token_tries) translator = google_translator(url_suffix='com') continue diff --git a/translations/__init__.py b/translations/__init__.py index 8280c887..84bb2fe7 100644 --- a/translations/__init__.py +++ b/translations/__init__.py @@ -41,8 +41,8 @@ def load_translations(): importlib.reload(lang_en_module) lang_en_module = importlib.import_module(package_name + '.translations.en_US') dictionary_en = lang_en_module.dictionary - print('LOADED TRANSLATIONS') - print(dictionary_en.get('CreditsPanel.descContributors2')) + # print('LOADED TRANSLATIONS') + # print(dictionary_en.get('CreditsPanel.descContributors2')) def t(phrase: str, *args, **kwargs): diff --git a/translations/en_US.py b/translations/en_US.py index 86d60135..8e96ca88 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -1001,9 +1001,9 @@ 'Scene.decimation_remove_doubles.label': 'Remove Doubles', 'Scene.decimation_remove_doubles.desc': 'Uncheck this if you got issues with with this checked', 'Scene.decimation_animation_weighting.label': "Animation weighting", - 'Scene.decimation_animation_weighting.desc': "Weight decimation based on shape keys and vertex group overlap\n" \ - "Results in better animating topology by trading off overall shape accuracy\n" \ - "Use if your elbows/joints end up with bad topology", + 'Scene.decimation_animation_weighting.desc': "Weight decimation based on shape keys and vertex group overlap\n" + "Results in better animating topology by trading off overall shape accuracy\n" + "Use if your elbows/joints end up with bad topology", 'Scene.decimation_animation_weighting_factor.label': "Factor", 'Scene.decimation_animation_weighting_factor.desc': "How much influence the animation weighting has on the overall shape", @@ -1135,51 +1135,51 @@ # Bake 'Scene.bake_resolution.label': "Resolution", - 'Scene.bake_resolution.desc': "Output resolution for the textures.\n" \ - "- 2048 is typical for desktop use.\n" \ - "- 1024 is reccomended for the Quest", + 'Scene.bake_resolution.desc': "Output resolution for the textures.\n" + "- 2048 is typical for desktop use.\n" + "- 1024 is reccomended for the Quest", 'Scene.bake_use_decimation.label': 'Decimate', 'Scene.bake_use_decimation.desc': 'Reduce polycount before baking, then use Normal maps to restore detail', 'Scene.bake_generate_uvmap.label': 'Generate UVMap', - 'Scene.bake_generate_uvmap.desc': "Re-pack islands for your mesh to a new non-overlapping UVMap.\n" \ - "Only disable if your UVs are non-overlapping already.\n" \ - "This will leave any map named \"Detail Map\" alone.\n" \ - "Uses UVPackMaster where available for more efficient UVs, make sure the window is showing", + 'Scene.bake_generate_uvmap.desc': "Re-pack islands for your mesh to a new non-overlapping UVMap.\n" + "Only disable if your UVs are non-overlapping already.\n" + "This will leave any map named \"Detail Map\" alone.\n" + "Uses UVPackMaster where available for more efficient UVs, make sure the window is showing", 'Scene.bake_uv_overlap_correction.label': "Overlap correction", 'Scene.bake_uv_overlap_correction.desc': "Method used to prevent overlaps in UVMap", - 'Scene.bake_prioritize_face.label':'Prioritize Head/Eyes', + 'Scene.bake_prioritize_face.label': 'Prioritize Head/Eyes', 'Scene.bake_prioritize_face.desc': 'Scale any UV islands attached to the head/eyes by a given factor.', 'Scene.bake_face_scale.label': "Head/Eyes Scale", 'Scene.bake_face_scale.desc': "How much to scale up the face/eyes portion of the textures.", 'Scene.bake_quick_compare.label': 'Quick compare', 'Scene.bake_quick_compare.desc': 'Move output avatar next to existing one to quickly compare', 'Scene.bake_illuminate_eyes.label': 'Set eyes to full brightness', - 'Scene.bake_illuminate_eyes.desc': 'Relight LeftEye and RightEye to be full brightness.\n' \ - "Without this, the eyes will have the shadow of the surrounding socket baked in,\n" - "which doesn't animate well", + 'Scene.bake_illuminate_eyes.desc': 'Relight LeftEye and RightEye to be full brightness.\n' + "Without this, the eyes will have the shadow of the surrounding socket baked in,\n" + "which doesn't animate well", 'Scene.bake_pass_smoothness.label': 'Smoothness', - 'Scene.bake_pass_smoothness.desc': 'Bakes Roughness and then inverts the values.\n' \ - 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.\n' \ - 'Not neccesary if your mesh has a global roughness value', + 'Scene.bake_pass_smoothness.desc': 'Bakes Roughness and then inverts the values.\n' + 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.\n' + 'Not neccesary if your mesh has a global roughness value', 'Scene.bake_pass_diffuse.label': 'Diffuse (Color)', - 'Scene.bake_pass_diffuse.desc': 'Bakes diffuse, un-lighted color. Usually you will want this.\n' \ - 'While baking, this temporarily links "Metallic" to "Anisotropic Rotation" as metallic can cause issues.', + 'Scene.bake_pass_diffuse.desc': 'Bakes diffuse, un-lighted color. Usually you will want this.\n' + 'While baking, this temporarily links "Metallic" to "Anisotropic Rotation" as metallic can cause issues.', 'Scene.bake_preserve_seams.label': "Preserve seams", - 'Scene.bake_preserve_seams.desc': 'Forces the Decimate operation to preserve vertices making up seams, preventing hard edges along seams.\n' \ - 'May result in less ideal geometry.\n' \ - "Use if you notice ugly edges along your texture seams.", + 'Scene.bake_preserve_seams.desc': 'Forces the Decimate operation to preserve vertices making up seams, preventing hard edges along seams.\n' + 'May result in less ideal geometry.\n' + "Use if you notice ugly edges along your texture seams.", 'Scene.bake_pass_normal.label': 'Normal (Bump)', - 'Scene.bake_pass_normal.desc': "Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" \ - "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" \ - "that makes the low res output look like the high res input.\n" \ - "Will not work well if you have self-intersecting islands", + 'Scene.bake_pass_normal.desc': "Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" + "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" + "that makes the low res output look like the high res input.\n" + "Will not work well if you have self-intersecting islands", 'Scene.bake_normal_apply_trans.label': 'Apply transforms', - 'Scene.bake_normal_apply_trans.desc': "Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps\n" \ - "Turn this off if applying location causes problems with your model", + 'Scene.bake_normal_apply_trans.desc': "Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps\n" + "Turn this off if applying location causes problems with your model", 'Scene.bake_pass_ao.label': 'Ambient Occlusion', - 'Scene.bake_pass_ao.desc': 'Bakes Ambient Occlusion, non-projected shadows. Adds a significant amount of detail to your model.\n' \ - 'Reccomended for non-toon style avatars.\n' \ - 'Takes a fairly long time to bake', + 'Scene.bake_pass_ao.desc': 'Bakes Ambient Occlusion, non-projected shadows. Adds a significant amount of detail to your model.\n' + 'Reccomended for non-toon style avatars.\n' + 'Takes a fairly long time to bake', 'Scene.bake_pass_questdiffuse.label': 'Quest Diffuse (Color+AO)', 'Scene.bake_pass_questdiffuse.desc': 'Blends the result of the Diffuse and AO bakes to make Quest-compatible shading.', 'Scene.bake_pass_emit.label': 'Emit', @@ -1189,17 +1189,17 @@ 'Scene.bake_metallic_alpha_pack.label': "Metallic Alpha Channel", 'Scene.bake_metallic_alpha_pack.desc': "What to pack to the Metallic Alpha channel", 'Scene.bake_pass_alpha.label': 'Transparency', - 'Scene.bake_pass_alpha.desc': 'Bakes transparency by connecting the last Principled BSDF Alpha input\n' \ - 'to the Base Color input and baking Diffuse.\n' \ - 'Not a native pass in Blender, results may vary\n' \ - 'Unused if you are baking to Quest', + 'Scene.bake_pass_alpha.desc': 'Bakes transparency by connecting the last Principled BSDF Alpha input\n' + 'to the Base Color input and baking Diffuse.\n' + 'Not a native pass in Blender, results may vary\n' + 'Unused if you are baking to Quest', 'Scene.bake_pass_metallic.label': 'Metallic', - 'Scene.bake_pass_metallic.desc': 'Bakes metallic by connecting the last Principled BSDF Metallic input\n' \ - 'to the Base Color input and baking Diffuse.\n' \ - 'Not a native pass in Blender, results may vary', + 'Scene.bake_pass_metallic.desc': 'Bakes metallic by connecting the last Principled BSDF Metallic input\n' + 'to the Base Color input and baking Diffuse.\n' + 'Not a native pass in Blender, results may vary', 'Scene.bake_questdiffuse_opacity.label': "AO Opacity", - 'Scene.bake_questdiffuse_opacity.desc': "The opacity of the shadows to blend onto the Diffuse map.\n" \ - "This should match the unity slider for AO on the Desktop version.", + 'Scene.bake_questdiffuse_opacity.desc': "The opacity of the shadows to blend onto the Diffuse map.\n" + "This should match the unity slider for AO on the Desktop version.", "Scene.bake_uv_overlap_correction.none.label": "None", "Scene.bake_uv_overlap_correction.none.desc": "Leave islands as they are. Use if islands don't self-intersect at all", @@ -1222,17 +1222,17 @@ "cats_bake.warn_missing_nodes": "A material in use isn't using Nodes, fix this in the Shading tab.", "cats_bake.preset_desktop.label": "Desktop", - "cats_bake.preset_desktop.desc": "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" \ - "This will try to automatically detect which bake passes are relevant to your model", + "cats_bake.preset_desktop.desc": "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" + "This will try to automatically detect which bake passes are relevant to your model", "cats_bake.preset_quest.label": 'Quest', - "cats_bake.preset_quest.desc": "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" \ - "This will try to automatically detect which bake passes are relevant to your model", + "cats_bake.preset_quest.desc": "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" + "This will try to automatically detect which bake passes are relevant to your model", 'cats_bake.bake.label': 'Copy and Bake (SLOW!)', - 'cats_bake.bake.desc': "Perform the bake. Warning, this performs an actual render!\n" \ - "This will create a copy of your avatar to leave the original alone.\n" \ - "Depending on your machine and model, this could take an hour or more.\n" \ - "For each pass, any Value node in your materials labeled bake_ will be\n" \ - "set to 1.0, for more granular customization.", + 'cats_bake.bake.desc': "Perform the bake. Warning, this performs an actual render!\n" + "This will create a copy of your avatar to leave the original alone.\n" + "Depending on your machine and model, this could take an hour or more.\n" + "For each pass, any Value node in your materials labeled bake_ will be\n" + "set to 1.0, for more granular customization.", 'cats_bake.error.no_meshes': "No meshes found!", 'cats_bake.error.render_engine': "You need to set your render engine to Cycles first!", 'cats_bake.error.render_disabled': "One or more of your armature's meshes have rendering disabled!", diff --git a/translations/ja_JP.py b/translations/ja_JP.py index 3229f2e3..c9dca062 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -113,6 +113,7 @@ 'DecimationPanel.preset.quest.label': 'Quest', 'DecimationPanel.preset.quest.description': 'Questアバターの推奨トリス数.\n' '将来的には、これをはるかに超えることのない厳しい制限が設定されます。', + 'DecimationPanel.warn.notIfBaking': "Not reccomended if baking!", # UI Eye tracking 'EyeTrackingPanel.label': 'アイトラッキング', @@ -156,6 +157,17 @@ 'CopyProtectionPanel.desc2': 'この保護は100%安全ではありません!', 'CopyProtectionPanel.desc3': '使用前: ドキュメントを読んでください!', + # UI Bake + 'BakePanel.autodetectlabel': 'Autodetect:', + 'BakePanel.generaloptionslabel': "General options:", + 'BakePanel.noheadfound': "No \"Head\" bone found!", + 'BakePanel.overlapfixlabel': "Overlap fix:", + 'BakePanel.bakepasseslabel': "Bake passes:", + 'BakePanel.alphalabel': "Alpha:", + 'BakePanel.transparencywarning': "Transparency isn't currently selected!", + 'BakePanel.smoothnesswarning': "Smoothness isn't currently selected!", + 'BakePanel.doublepackwarning': "Smoothness packed in two places!", + # UI Settings & Updates 'UpdaterPanel.label': '設定と更新', 'UpdaterPanel.name': '設定:', @@ -175,6 +187,7 @@ 'CreditsPanel.desc2': 'HotoxとGiveMeAllYourCatsによって作成', 'CreditsPanel.desc3': '素晴らしいVRChatコミュニティのために <3', 'CreditsPanel.desc4': '特別な感謝: ShotariyaとNeitri', + 'CreditsPanel.descContributors': 'Feilen、Jordo、Ruubick、ShotariyaとNeitri', 'CreditsPanel.desc5': '助けが必要ですか、バグを見つけましたか?', # Tools Armature @@ -209,9 +222,12 @@ 'StartPoseMode.label': 'ポーズモードを開始', 'StartPoseMode.desc': 'Starts the pose mode.\n' 'This lets you test how your model will move', + 'StartPoseModeNoReset.desc': 'Starts the pose mode without resetting the pose.\n' + 'This lets you test how your model will move', 'StopPoseMode.label': 'ポーズモードを停止する', 'StopPoseMode.desc': 'Stops the pose mode and resets the pose to normal', + 'StopPoseModeNoReset.desc': 'Stops the pose mode and keeps the current pose', 'PoseToShape.label': 'シェイプキーへのポーズ', 'PoseToShape.desc': 'This saves your current pose as a new shape key.' @@ -983,6 +999,12 @@ 'Scene.decimation_remove_doubles.label': '重複を削除', 'Scene.decimation_remove_doubles.desc': 'Uncheck this if you got issues with with this checked', + 'Scene.decimation_animation_weighting.label': "Animation weighting", + 'Scene.decimation_animation_weighting.desc': "Weight decimation based on shape keys and vertex group overlap\n" + "Results in better animating topology by trading off overall shape accuracy\n" + "Use if your elbows/joints end up with bad topology", + 'Scene.decimation_animation_weighting_factor.label': "Factor", + 'Scene.decimation_animation_weighting_factor.desc': "How much influence the animation weighting has on the overall shape", 'Scene.max_tris.label': 'トリス', 'Scene.max_tris.desc': 'The target amount of tris after decimation', @@ -1110,6 +1132,111 @@ 'Scene.debug_translations.label': 'Google翻訳をデバッグ', 'Scene.debug_translations.desc': 'Tests the Google Translations and prints the Google response in case of error', + # Bake + 'Scene.bake_resolution.label': "Resolution", + 'Scene.bake_resolution.desc': "Output resolution for the textures.\n" + "- 2048 is typical for desktop use.\n" + "- 1024 is reccomended for the Quest", + 'Scene.bake_use_decimation.label': 'Decimate', + 'Scene.bake_use_decimation.desc': 'Reduce polycount before baking, then use Normal maps to restore detail', + 'Scene.bake_generate_uvmap.label': 'Generate UVMap', + 'Scene.bake_generate_uvmap.desc': "Re-pack islands for your mesh to a new non-overlapping UVMap.\n" + "Only disable if your UVs are non-overlapping already.\n" + "This will leave any map named \"Detail Map\" alone.\n" + "Uses UVPackMaster where available for more efficient UVs, make sure the window is showing", + 'Scene.bake_uv_overlap_correction.label': "Overlap correction", + 'Scene.bake_uv_overlap_correction.desc': "Method used to prevent overlaps in UVMap", + 'Scene.bake_prioritize_face.label': 'Prioritize Head/Eyes', + 'Scene.bake_prioritize_face.desc': 'Scale any UV islands attached to the head/eyes by a given factor.', + 'Scene.bake_face_scale.label': "Head/Eyes Scale", + 'Scene.bake_face_scale.desc': "How much to scale up the face/eyes portion of the textures.", + 'Scene.bake_quick_compare.label': 'Quick compare', + 'Scene.bake_quick_compare.desc': 'Move output avatar next to existing one to quickly compare', + 'Scene.bake_illuminate_eyes.label': 'Set eyes to full brightness', + 'Scene.bake_illuminate_eyes.desc': 'Relight LeftEye and RightEye to be full brightness.\n' + "Without this, the eyes will have the shadow of the surrounding socket baked in,\n" + "which doesn't animate well", + 'Scene.bake_pass_smoothness.label': 'Smoothness', + 'Scene.bake_pass_smoothness.desc': 'Bakes Roughness and then inverts the values.\n' + 'To use this, it needs to be packed to the Alpha channel of either Diffuse or Metallic.\n' + 'Not neccesary if your mesh has a global roughness value', + 'Scene.bake_pass_diffuse.label': 'Diffuse (Color)', + 'Scene.bake_pass_diffuse.desc': 'Bakes diffuse, un-lighted color. Usually you will want this.\n' + 'While baking, this temporarily links "Metallic" to "Anisotropic Rotation" as metallic can cause issues.', + 'Scene.bake_preserve_seams.label': "Preserve seams", + 'Scene.bake_preserve_seams.desc': 'Forces the Decimate operation to preserve vertices making up seams, preventing hard edges along seams.\n' + 'May result in less ideal geometry.\n' + "Use if you notice ugly edges along your texture seams.", + 'Scene.bake_pass_normal.label': 'Normal (Bump)', + 'Scene.bake_pass_normal.desc': "Bakes a normal (bump) map. Allows you to keep the shading of a complex object with\n" + "the geometry of a simple object. If you have selected 'Decimate', it will create a map\n" + "that makes the low res output look like the high res input.\n" + "Will not work well if you have self-intersecting islands", + 'Scene.bake_normal_apply_trans.label': 'Apply transforms', + 'Scene.bake_normal_apply_trans.desc': "Applies offsets while baking normals. Neccesary if your model has many materials with different normal maps\n" + "Turn this off if applying location causes problems with your model", + 'Scene.bake_pass_ao.label': 'Ambient Occlusion', + 'Scene.bake_pass_ao.desc': 'Bakes Ambient Occlusion, non-projected shadows. Adds a significant amount of detail to your model.\n' + 'Reccomended for non-toon style avatars.\n' + 'Takes a fairly long time to bake', + 'Scene.bake_pass_questdiffuse.label': 'Quest Diffuse (Color+AO)', + 'Scene.bake_pass_questdiffuse.desc': 'Blends the result of the Diffuse and AO bakes to make Quest-compatible shading.', + 'Scene.bake_pass_emit.label': 'Emit', + 'Scene.bake_pass_emit.desc': 'Bakes Emit, glowyness', + 'Scene.bake_diffuse_alpha_pack.label': "Alpha Channel", + 'Scene.bake_diffuse_alpha_pack.desc': "What to pack to the Diffuse Alpha channel", + 'Scene.bake_metallic_alpha_pack.label': "Metallic Alpha Channel", + 'Scene.bake_metallic_alpha_pack.desc': "What to pack to the Metallic Alpha channel", + 'Scene.bake_pass_alpha.label': 'Transparency', + 'Scene.bake_pass_alpha.desc': 'Bakes transparency by connecting the last Principled BSDF Alpha input\n' + 'to the Base Color input and baking Diffuse.\n' + 'Not a native pass in Blender, results may vary\n' + 'Unused if you are baking to Quest', + 'Scene.bake_pass_metallic.label': 'Metallic', + 'Scene.bake_pass_metallic.desc': 'Bakes metallic by connecting the last Principled BSDF Metallic input\n' + 'to the Base Color input and baking Diffuse.\n' + 'Not a native pass in Blender, results may vary', + 'Scene.bake_questdiffuse_opacity.label': "AO Opacity", + 'Scene.bake_questdiffuse_opacity.desc': "The opacity of the shadows to blend onto the Diffuse map.\n" + "This should match the unity slider for AO on the Desktop version.", + + "Scene.bake_uv_overlap_correction.none.label": "None", + "Scene.bake_uv_overlap_correction.none.desc": "Leave islands as they are. Use if islands don't self-intersect at all", + "Scene.bake_uv_overlap_correction.unmirror.label": "Unmirror", + "Scene.bake_uv_overlap_correction.unmirror.desc": "Move all face islands with positive X values over one to un-pin mirrored UVs. Solves most UV pinning issues.", + "Scene.bake_uv_overlap_correction.reproject.label": "Reproject", + "Scene.bake_uv_overlap_correction.reproject.desc": "Use blender's Smart UV Project to come up with an entirely new island layout. Tends to reduce quality.", + + "Scene.bake_diffuse_alpha_pack.none.label": "None", + "Scene.bake_diffuse_alpha_pack.none.desc": "No alpha channel", + "Scene.bake_diffuse_alpha_pack.transparency.label": "Transparency", + "Scene.bake_diffuse_alpha_pack.transparency.desc": "Pack Transparency", + "Scene.bake_diffuse_alpha_pack.smoothness.label": "Smoothness", + "Scene.bake_diffuse_alpha_pack.smoothness.desc": "Pack Smoothness. Most efficient if you don't have transparency or metallic textures.", + + "Scene.bake_metallic_alpha_pack.none.label": "None", + "Scene.bake_metallic_alpha_pack.none.desc": "No alpha channel", + "Scene.bake_metallic_alpha_pack.smoothness.label": "Smoothness", + "Scene.bake_metallic_alpha_pack.smoothness.desc": "Pack Smoothness. Use this if your Diffuse alpha channel is already populated with Transparency", + + "cats_bake.warn_missing_nodes": "A material in use isn't using Nodes, fix this in the Shading tab.", + "cats_bake.preset_desktop.label": "Desktop", + "cats_bake.preset_desktop.desc": "Preset for producing an Excellent-rated Desktop avatar, not accounting for bones.\n" + "This will try to automatically detect which bake passes are relevant to your model", + "cats_bake.preset_quest.label": 'Quest', + "cats_bake.preset_quest.desc": "Preset for producing an Excellent-rated Quest avatar, not accounting for bones.\n" + "This will try to automatically detect which bake passes are relevant to your model", + 'cats_bake.bake.label': 'Copy and Bake (SLOW!)', + 'cats_bake.bake.desc': "Perform the bake. Warning, this performs an actual render!\n" + "This will create a copy of your avatar to leave the original alone.\n" + "Depending on your machine and model, this could take an hour or more.\n" + "For each pass, any Value node in your materials labeled bake_ will be\n" + "set to 1.0, for more granular customization.", + 'cats_bake.error.no_meshes': "No meshes found!", + 'cats_bake.error.render_engine': "You need to set your render engine to Cycles first!", + 'cats_bake.error.render_disabled': "One or more of your armature's meshes have rendering disabled!", + 'cats_bake.info.success': "Success! Textures and model saved to \'CATS Bake\' folder next to your .blend file.", + # Updater 'CheckForUpdateButton.label': '今すぐアップデートを確認', 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', From 8a5ca587e447ee6972172623938d9ea9069860ff Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 9 Dec 2020 16:43:20 +0100 Subject: [PATCH 56/64] Added tutorial button to bake panel --- tools/bake.py | 17 ++++++++++++++++- translations/en_US.py | 10 +++++++--- translations/ja_JP.py | 7 ++++++- ui/bake.py | 9 +++++---- ui/custom.py | 2 +- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index aef8ed33..6f9e2d2d 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -22,14 +22,29 @@ # Code author: Feilen -import bpy import os +import bpy +import webbrowser from . import common as Common from .register import register_wrap from ..translations import t +@register_wrap +class BakeTutorialButton(bpy.types.Operator): + bl_idname = 'cats_bake.tutorial' + bl_label = t('cats_bake.tutorial_button.label') + bl_description = t('cats_bake.tutorial_button.desc') + bl_options = {'INTERNAL'} + + def execute(self, context): + webbrowser.open(t('cats_bake.tutorial_button.URL')) + + self.report({'INFO'}, t('cats_bake.tutorial_button.success')) + return {'FINISHED'} + + def autodetect_passes(self, context, tricount, is_desktop): context.scene.bake_max_tris = tricount context.scene.bake_resolution = 2048 if is_desktop else 1024 diff --git a/translations/en_US.py b/translations/en_US.py index 8e96ca88..4d510b1e 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -78,7 +78,6 @@ # UI Custom 'CustomPanel.label': 'Custom Model Creation', - 'CustomPanel.CustomModelTutorialButton': 'How to use', 'CustomPanel.mergeArmatures': 'Merge Armatures:', 'CustomPanel.warn.twoArmatures': 'Two armatures are required!', 'CustomPanel.mergeInto': 'Base', @@ -409,9 +408,9 @@ '\nE.g.: A jacket won\'t work, because it requires multiple bones', 'AttachMesh.success': 'Mesh successfully attached to armature.', - 'CustomModelTutorialButton.label': 'Go to Documentation', + 'CustomModelTutorialButton.label': 'How to use', 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) - 'CustomModelTutorialButton.success': 'Documentation', + 'CustomModelTutorialButton.success': 'Documentation opened.', 'merge_armatures.error.transformReset': ['If you want to rotate the new part, only modify the mesh instead of the armature,', 'or select "Apply Transforms"!', @@ -1238,6 +1237,11 @@ 'cats_bake.error.render_disabled': "One or more of your armature's meshes have rendering disabled!", 'cats_bake.info.success': "Success! Textures and model saved to \'CATS Bake\' folder next to your .blend file.", + 'cats_bake.tutorial_button.label': "How to use", + 'cats_bake.tutorial_button.desc': "This will open the Cats wiki page for the Bake panel", + 'cats_bake.tutorial_button.URL': "https://github.com/GiveMeAllYourCats/cats-blender-plugin/wiki/Bake", + 'cats_bake.tutorial_button.success': "Bake Tutorial opened.", + # Updater 'CheckForUpdateButton.label': 'Check now for Update', 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', diff --git a/translations/ja_JP.py b/translations/ja_JP.py index c9dca062..da18950e 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -409,7 +409,7 @@ '\nE.g.: A jacket won\'t work, because it requires multiple bones', 'AttachMesh.success': 'Mesh successfully attached to armature.', - 'CustomModelTutorialButton.label': 'ドキュメントに移動', + 'CustomModelTutorialButton.label': '使用方法', 'CustomModelTutorialButton.URL': 'https://github.com/michaeldegroot/cats-blender-plugin#custom-model-creation', # BOOM, now we can point at the Japanese link now ;) 'CustomModelTutorialButton.success': 'Documentation', @@ -1237,6 +1237,11 @@ 'cats_bake.error.render_disabled': "One or more of your armature's meshes have rendering disabled!", 'cats_bake.info.success': "Success! Textures and model saved to \'CATS Bake\' folder next to your .blend file.", + 'cats_bake.tutorial_button.label': "How to use", + 'cats_bake.tutorial_button.desc': "This will open the Cats wiki page for the Bake panel", + 'cats_bake.tutorial_button.URL': "https://github.com/GiveMeAllYourCats/cats-blender-plugin/wiki/Bake", + 'cats_bake.tutorial_button.success': "Bake Tutorial opened.", + # Updater 'CheckForUpdateButton.label': '今すぐアップデートを確認', 'CheckForUpdateButton.desc': 'Checks if a new update is available for CATS', diff --git a/ui/bake.py b/ui/bake.py index c8fba79f..32ef8304 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -1,15 +1,12 @@ import bpy -from .. import globs from .main import ToolPanel -from .main import layout_split from ..tools import common as Common -from ..tools import decimation as Decimation from ..tools import bake as Bake -from ..tools import armature_manual as Armature_manual from ..tools.register import register_wrap from ..translations import t + @register_wrap class BakePanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_catsbake' @@ -21,6 +18,10 @@ def draw(self, context): box = layout.box() col = box.column(align=True) + row = col.row(align=True) + row.operator(Bake.BakeTutorialButton.bl_idname, icon='FORWARD') + col.separator() + col.label(text=t('BakePanel.autodetectlabel')) row = col.row(align=True) row.operator(Bake.BakePresetDesktop.bl_idname, icon="ANTIALIASED") diff --git a/ui/custom.py b/ui/custom.py index ccc6c814..9fff9dd3 100644 --- a/ui/custom.py +++ b/ui/custom.py @@ -22,7 +22,7 @@ def draw(self, context): col = box.column(align=True) row = col.row(align=True) - row.operator(Armature_custom.CustomModelTutorialButton.bl_idname, text=t('CustomPanel.CustomModelTutorialButton'), icon='FORWARD') + row.operator(Armature_custom.CustomModelTutorialButton.bl_idname, icon='FORWARD') col.separator() row = col.row(align=True) From b85b7fc9df9ec15f0a485171cc4f6082ac866101 Mon Sep 17 00:00:00 2001 From: Hotox Date: Wed, 9 Dec 2020 17:18:35 +0100 Subject: [PATCH 57/64] Made Sims 4 models compatible --- tools/armature.py | 3 +++ tools/common.py | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/armature.py b/tools/armature.py index 016ea2c3..11595851 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -506,12 +506,15 @@ def execute(self, context): ('Def_', ''), ('DEF_', ''), ('Chr_', ''), + ('Chr_', ''), + ('B_', ''), ] # List of chars to replace if they are at the end of a bone name ends_with = [ ('_Bone', ''), ('_Le', '_L'), ('_Ri', '_R'), + ('_', ''), ] # List of chars to replace replaces = [ diff --git a/tools/common.py b/tools/common.py index ed87bb99..baf58d65 100644 --- a/tools/common.py +++ b/tools/common.py @@ -221,9 +221,8 @@ def is_selected(obj): def hide(obj, val=True): - if version_2_79_or_older(): - obj.hide = val - else: + obj.hide = val + if not version_2_79_or_older(): obj.hide_set(val) From 111db3e63faba95b7e1c5a075b0f73781821d1e5 Mon Sep 17 00:00:00 2001 From: Hotox Date: Fri, 11 Dec 2020 00:33:15 +0100 Subject: [PATCH 58/64] Made UE3 models compatible --- tools/armature.py | 1 + tools/armature_bones.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/tools/armature.py b/tools/armature.py index 11595851..bf0578b5 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -491,6 +491,7 @@ def execute(self, context): ('Bip1_', 'Bip_'), ('Bip01_', 'Bip_'), ('Bip001_', 'Bip_'), + ('Bip01', ''), ('Character1_', ''), ('HLP_', ''), ('JD_', ''), diff --git a/tools/armature_bones.py b/tools/armature_bones.py index a8d470a9..255f4bcb 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -645,6 +645,7 @@ 'J_Clavicle_\L', '\L_Clav', 'Clav_\L', + '\LClavicle', ] bone_rename['\Left arm'] = [ '\Left_Arm', @@ -693,6 +694,7 @@ 'Arm_Stretch_\L', 'J_Ude_A_\L', 'J_Shoulder_\L', + '\LUpperArm', ] bone_rename['Left arm'] = [ '+_Leisure_Elder_Supplement', @@ -886,6 +888,7 @@ 'Leg_Stretch_\L', 'J_Asi_B_\L', 'J_Knee_\L', + '\LCalf', ] bone_rename['\Left ankle'] = [ '\Left_Ankle', @@ -967,6 +970,7 @@ 'J_Asi_E_\L', 'J_Ball_\L', '\LToeBase', + '\LToe0', ] bone_rename['Eye_\L'] = [ '\Left_Eye', @@ -1754,6 +1758,7 @@ 'J_Oya_A_\L', 'J_Thumb_\L_1', '\LHandThumb', + '\LFinger0', ] bone_rename_fingers['Thumb1_\L'] = [ # 'Arm_\Left_Finger_1b', @@ -1788,6 +1793,7 @@ 'J_Oya_B_\L', 'J_Thumb_\L_2', '\LHandThumb1', + '\LFinger01', ] bone_rename_fingers['Thumb2_\L'] = [ # 'Arm_\Left_Finger_1c', @@ -1821,6 +1827,7 @@ 'J_Oya_C_\L', 'J_Thumb_\L_3', '\LHandThumb2', + '\LFinger02', ] bone_rename_fingers['IndexFinger1_\L'] = [ 'Fore1_\L', @@ -1857,6 +1864,7 @@ 'J_Hito_A_\L', 'J_Index_\L_1', '\LHandIndex', + '\LFinger1', ] bone_rename_fingers['IndexFinger2_\L'] = [ 'Fore2_\L', @@ -1893,6 +1901,7 @@ 'J_Hito_B_\L', 'J_Index_\L_2', '\LHandIndex1', + '\LFinger11', ] bone_rename_fingers['IndexFinger3_\L'] = [ 'Fore3_\L', @@ -1930,6 +1939,7 @@ 'J_Hito_C_\L', 'J_Index_\L_3', '\LHandIndex2', + '\LFinger12', ] bone_rename_fingers['MiddleFinger1_\L'] = [ 'Middle1_\L', @@ -1967,6 +1977,7 @@ 'J_Naka_A_\L', 'J_Mid_\L_1', '\LHandMiddle', + '\LFinger2', ] bone_rename_fingers['MiddleFinger2_\L'] = [ 'Middle2_\L', @@ -2003,6 +2014,7 @@ 'J_Naka_B_\L', 'J_Mid_\L_2', '\LHandMiddle1', + '\LFinger21', ] bone_rename_fingers['MiddleFinger3_\L'] = [ 'Middle3_\L', @@ -2039,6 +2051,7 @@ 'J_Naka_C_\L', 'J_Mid_\L_3', '\LHandMiddle2', + '\LFinger22', ] bone_rename_fingers['RingFinger1_\L'] = [ 'Third1_\L', @@ -2076,6 +2089,7 @@ 'J_Kusu_A_\L', 'J_Ring_\L_1', '\LHandRing', + '\LFinger3', ] bone_rename_fingers['RingFinger2_\L'] = [ 'Third2_\L', @@ -2113,6 +2127,7 @@ 'J_Kusu_B_\L', 'J_Ring_\L_2', '\LHandRing1', + '\LFinger31', ] bone_rename_fingers['RingFinger3_\L'] = [ 'Third3_\L', @@ -2150,6 +2165,7 @@ 'J_Kusu_C_\L', 'J_Ring_\L_3', '\LHandRing2', + '\LFinger32', ] bone_rename_fingers['LittleFinger1_\L'] = [ 'Little1_\L', @@ -2188,6 +2204,7 @@ 'J_Ko_A_\L', 'J_Pinky_\L_1', '\LHandPinky', + '\LFinger4', ] bone_rename_fingers['LittleFinger2_\L'] = [ 'Little2_\L', @@ -2226,6 +2243,7 @@ 'J_Ko_B_\L', 'J_Pinky_\L_2', '\LHandPinky1', + '\LFinger41', ] bone_rename_fingers['LittleFinger3_\L'] = [ 'Little3_\L', @@ -2264,4 +2282,5 @@ 'J_Ko_C_\L', 'J_Pinky_\L_3', '\LHandPinky2', + '\LFinger42', ] From 268e544aeccfc02a07c6b4e91ccce13d14ad4673 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Wed, 9 Dec 2020 19:33:02 -0800 Subject: [PATCH 59/64] A couple more fixes for Linux and 2.91, support NeosVR materials - Changed 'emit' to 'emission' so the neos importer can see it better. --- __init__.py | 5 +++++ tools/bake.py | 8 ++++---- tools/common.py | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 6b6baa96..ff12fa61 100644 --- a/__init__.py +++ b/__init__.py @@ -289,6 +289,8 @@ def register(): # Load mmd_tools try: mmd_tools_local.register() + except NameError: + print('Could not register local mmd_tools') except AttributeError: print('Could not register local mmd_tools') except ValueError: @@ -359,6 +361,9 @@ def unregister(): # Unload mmd_tools try: mmd_tools_local.unregister() + except NameError: + print('mmd_tools was not registered') + pass except AttributeError: print('Could not unregister local mmd_tools') pass diff --git a/tools/bake.py b/tools/bake.py index 6f9e2d2d..40d3e873 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -74,7 +74,7 @@ def autodetect_passes(self, context, tricount, is_desktop): # Smoothness: similar to diffuse context.scene.bake_pass_smoothness = (any([node.inputs["Roughness"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Roughness"].default_value for node in bsdf_nodes])) > 1) - + # Emit: similar to diffuse context.scene.bake_pass_emit = (any([node.inputs["Emission"].is_linked for node in bsdf_nodes]) or len(set([node.inputs["Emission"].default_value for node in bsdf_nodes])) > 1) @@ -546,7 +546,7 @@ def first_bsdf(objs): # bake emit if pass_emit: - self.bake_pass(context, "emit", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], + self.bake_pass(context, "emission", "EMIT", set(), [obj for obj in collection.all_objects if obj.type == "MESH"], (resolution, resolution), 32, 0, [0, 0, 0, 1.0], True, int(margin * resolution / 2)) # advanced: bake alpha from bsdf output @@ -819,7 +819,7 @@ def first_bsdf(objs): mat.blend_method = 'CLIP' if pass_emit: emittexnode = tree.nodes.new("ShaderNodeTexImage") - emittexnode.image = bpy.data.images["SCRIPT_emit.png"] + emittexnode.image = bpy.data.images["SCRIPT_emission.png"] emittexnode.location.x -= 800 emittexnode.location.y -= 150 tree.links.new(bsdfnode.inputs["Emission"], emittexnode.outputs["Color"]) @@ -862,7 +862,7 @@ def first_bsdf(objs): if pass_diffuse and pass_ao and pass_questdiffuse: bpy.data.images["SCRIPT_questdiffuse.png"].save() if pass_emit: - bpy.data.images["SCRIPT_emit.png"].save() + bpy.data.images["SCRIPT_emission.png"].save() if pass_alpha and (diffuse_alpha_pack != "TRANSPARENCY"): bpy.data.images["SCRIPT_alpha.png"].save() if pass_metallic: diff --git a/tools/common.py b/tools/common.py index baf58d65..8d914858 100644 --- a/tools/common.py +++ b/tools/common.py @@ -221,9 +221,10 @@ def is_selected(obj): def hide(obj, val=True): - obj.hide = val if not version_2_79_or_older(): obj.hide_set(val) + else: + obj.hide = val def is_hidden(obj): From f0089a5a2dc35b0339f96b876e283b25d661ce18 Mon Sep 17 00:00:00 2001 From: Chelsea Jaggi Date: Fri, 11 Dec 2020 13:04:36 -0800 Subject: [PATCH 60/64] Fix for smart project on 2.91 --- tools/bake.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/bake.py b/tools/bake.py index 40d3e873..8fddeed7 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -443,8 +443,7 @@ def first_bsdf(objs): bpy.ops.object.editmode_toggle() bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.select_all(action='SELECT') - bpy.ops.uv.smart_project(angle_limit=66.0, island_margin=0.01, user_area_weight=0.0, - use_aspect=True, stretch_to_bounds=True) + bpy.ops.uv.smart_project(angle_limit=66.0, island_margin=0.01) bpy.ops.object.editmode_toggle() child.data.uv_layers.active_index = idx elif uv_overlap_correction == "UNMIRROR": From f87979b39bba8eb0de271e934a923d6a075cbd32 Mon Sep 17 00:00:00 2001 From: Hotox Date: Fri, 11 Dec 2020 22:12:07 +0100 Subject: [PATCH 61/64] Small fixes --- tools/armature.py | 1 + tools/bake.py | 18 ++++++++++++------ tools/common.py | 4 ++-- translations/en_US.py | 1 + translations/ja_JP.py | 1 + ui/bake.py | 4 ++++ 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tools/armature.py b/tools/armature.py index bf0578b5..f6e46efe 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -513,6 +513,7 @@ def execute(self, context): # List of chars to replace if they are at the end of a bone name ends_with = [ ('_Bone', ''), + ('_Bn', ''), ('_Le', '_L'), ('_Ri', '_R'), ('_', ''), diff --git a/tools/bake.py b/tools/bake.py index 8fddeed7..4378fbeb 100644 --- a/tools/bake.py +++ b/tools/bake.py @@ -325,18 +325,21 @@ def recurse(ob, parent, depth): return recurse(ob, ob.parent, 0) def execute(self, context): - meshes = Common.get_meshes_objects() - if not meshes or len(meshes) == 0: + if not Common.get_meshes_objects(): self.report({'ERROR'}, t('cats_bake.error.no_meshes')) return {'FINISHED'} - if context.scene.render.engine != 'CYCLES': - self.report({'ERROR'}, t('cats_bake.error.render_engine')) - return {'FINISHED'} + # if context.scene.render.engine != 'CYCLES': + # self.report({'ERROR'}, t('cats_bake.error.render_engine')) + # return {'FINISHED'} if any([obj.hide_render for obj in Common.get_armature().children]): self.report({'ERROR'}, t('cats_bake.error.render_disabled')) return {'FINISHED'} # TODO: Check if any UV islands are self-overlapping, emit a warning + # Change render engine to cycles and save the current one + render_engine_tmp = context.scene.render.engine + context.scene.render.engine = 'CYCLES' + # Change decimate settings, run bake, change them back decimation_mode = context.scene.decimation_mode max_tris = context.scene.max_tris @@ -360,6 +363,9 @@ def execute(self, context): context.scene.decimation_animation_weighting = decimation_animation_weighting context.scene.decimation_animation_weighting_factor = decimation_animation_weighting_factor + # Change render engine back to original + context.scene.render.engine = render_engine_tmp + return {'FINISHED'} def perform_bake(self, context): @@ -401,7 +407,7 @@ def perform_bake(self, context): context.scene.collection.children.link(collection) # Tree-copy all meshes - armature = Common.get_armature() + armature = Common.set_default_stage() arm_copy = self.tree_copy(armature, None, collection) # Make sure all armature modifiers target the new armature diff --git a/tools/common.py b/tools/common.py index 8d914858..f4d74708 100644 --- a/tools/common.py +++ b/tools/common.py @@ -221,10 +221,10 @@ def is_selected(obj): def hide(obj, val=True): + if hasattr(obj, 'hide'): + obj.hide = val if not version_2_79_or_older(): obj.hide_set(val) - else: - obj.hide = val def is_hidden(obj): diff --git a/translations/en_US.py b/translations/en_US.py index 4d510b1e..e6b55eb3 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -157,6 +157,7 @@ 'CopyProtectionPanel.desc3': 'Before use: Read the documentation!', # UI Bake + 'BakePanel.versionTooOld': 'Only for Blender 2.80+', 'BakePanel.autodetectlabel': 'Autodetect:', 'BakePanel.generaloptionslabel': "General options:", 'BakePanel.noheadfound': "No \"Head\" bone found!", diff --git a/translations/ja_JP.py b/translations/ja_JP.py index da18950e..848948c1 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -158,6 +158,7 @@ 'CopyProtectionPanel.desc3': '使用前: ドキュメントを読んでください!', # UI Bake + 'BakePanel.versionTooOld': 'Only for Blender 2.80+', 'BakePanel.autodetectlabel': 'Autodetect:', 'BakePanel.generaloptionslabel': "General options:", 'BakePanel.noheadfound': "No \"Head\" bone found!", diff --git a/ui/bake.py b/ui/bake.py index 32ef8304..c99d6f73 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -18,6 +18,10 @@ def draw(self, context): box = layout.box() col = box.column(align=True) + if Common.version_2_79_or_older(): + col.label(text=t('BakePanel.versionTooOld'), icon='ERROR') + return + row = col.row(align=True) row.operator(Bake.BakeTutorialButton.bl_idname, icon='FORWARD') col.separator() From 65bbffcc796ef34460d9882c6671425b751e64cf Mon Sep 17 00:00:00 2001 From: Hotox Date: Fri, 18 Dec 2020 02:33:20 +0100 Subject: [PATCH 62/64] Removed fix for mmd_tools crash on Linux, fixed potential UnicodeDecodingError (#234) --- README.md | 2 ++ __init__.py | 4 +--- tools/armature.py | 6 ++---- tools/armature_bones.py | 3 +++ tools/common.py | 40 +++++++++++++++++++--------------------- tools/importer.py | 11 +++++------ tools/translate.py | 10 ++++------ translations/en_US.py | 1 + translations/ja_JP.py | 1 + ui/bake.py | 10 +++++----- 10 files changed, 43 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 69aa387c..ce1d1627 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,8 @@ It checks for a new version automatically once every day. - Fixed Google Translations no longer working - Fixed bug in "Apply as Rest Pose" and "Pose to Shape Key" in Blender 2.90 - More fixes for Blender 2.90 + - NOTE: Using Cats in Blender 2.90+ on Ubuntu might cause Blender to crash on load (caused by mmd_tools) + - To fix this use a Blender version prior to 2.90 or try updating your drivers #### 0.17.0 - **Cats is now fully compatible with Blender 2.83!** diff --git a/__init__.py b/__init__.py index ff12fa61..8807ba17 100644 --- a/__init__.py +++ b/__init__.py @@ -60,9 +60,7 @@ else: is_reloading = True -# Only load mmd_tools if it's not on linux and 2.90 or higher since it causes Blender to crash -if platform.system() != "Linux" or bpy.app.version < (2, 90): - import mmd_tools_local +import mmd_tools_local # Load or reload all cats modules if not is_reloading: diff --git a/tools/armature.py b/tools/armature.py index f6e46efe..91c98383 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -40,10 +40,8 @@ from ..translations import t # Only load mmd_tools if it's not on linux and 2.90 or higher since it causes Blender to crash -mmd_tools_installed = False -if platform.system() != "Linux" or bpy.app.version < (2, 90): - from mmd_tools_local.operators import morph as Morph - mmd_tools_installed = True +from mmd_tools_local.operators import morph as Morph +mmd_tools_installed = True @register_wrap diff --git a/tools/armature_bones.py b/tools/armature_bones.py index 255f4bcb..9e51d24a 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -746,6 +746,7 @@ 'Forearm_Stretch_\L', 'J_Ude_B_\L', 'J_Elbow_\L', + '\L_LowerArm', ] bone_rename['\Left wrist'] = [ '\Left_Wrist', @@ -841,6 +842,7 @@ 'J_Asi_A_\L', 'J_Hip_\L', '\LUpLeg', + '\L_UpperLeg', ] bone_rename['\Left knee'] = [ '\Left_Knee', @@ -889,6 +891,7 @@ 'J_Asi_B_\L', 'J_Knee_\L', '\LCalf', + '\L_LowerLeg', ] bone_rename['\Left ankle'] = [ '\Left_Ankle', diff --git a/tools/common.py b/tools/common.py index f4d74708..51446d72 100644 --- a/tools/common.py +++ b/tools/common.py @@ -45,11 +45,10 @@ from .register import register_wrap from ..translations import t -if platform.system() != "Linux" or bpy.app.version < (2, 90): - from mmd_tools_local import utils - from mmd_tools_local.panels import tool as mmd_tool - from mmd_tools_local.panels import util_tools as mmd_util_tools - from mmd_tools_local.panels import view_prop as mmd_view_prop +from mmd_tools_local import utils +from mmd_tools_local.panels import tool as mmd_tool +from mmd_tools_local.panels import util_tools as mmd_util_tools +from mmd_tools_local.panels import view_prop as mmd_view_prop # TODO: # - Add check if hips bone really needs to be rotated @@ -2075,19 +2074,19 @@ def fix_twist_bones(mesh, bones_to_delete): mix_weights(mesh, vg_twist.name, vg_twist.name, mix_strength=0.2, mix_mode='SUB', delete_old_vg=False) if vg_twist1: + bones_to_delete.append(vg_twist1.name) mix_weights(mesh, vg_twist1.name, vg_twist.name, mix_strength=0.25, delete_old_vg=False) mix_weights(mesh, vg_twist1.name, vg_parent.name, mix_strength=0.75) - bones_to_delete.append(vg_twist1.name) if vg_twist2: + bones_to_delete.append(vg_twist2.name) mix_weights(mesh, vg_twist2.name, vg_twist.name, mix_strength=0.5, delete_old_vg=False) mix_weights(mesh, vg_twist2.name, vg_parent.name, mix_strength=0.5) - bones_to_delete.append(vg_twist2.name) if vg_twist3: + bones_to_delete.append(vg_twist3.name) mix_weights(mesh, vg_twist3.name, vg_twist.name, mix_strength=0.75, delete_old_vg=False) mix_weights(mesh, vg_twist3.name, vg_parent.name, mix_strength=0.25) - bones_to_delete.append(vg_twist3.name) def fix_twist_bone_names(armature): @@ -2122,19 +2121,18 @@ def toggle_mmd_tabs(shutdown_plugin=False): mmd_cls = mmd_cls + mmd_cls_shading # If the plugin is shutting down, load the mmd_tools tabs before that, to avoid issues when unregistering mmd_tools - if platform.system() != "Linux" or bpy.app.version < (2, 90): - if bpy.context.scene.show_mmd_tabs or shutdown_plugin: - for cls in mmd_cls: - try: - bpy.utils.register_class(cls) - except: - pass - else: - for cls in reversed(mmd_cls): - try: - bpy.utils.unregister_class(cls) - except: - pass + if bpy.context.scene.show_mmd_tabs or shutdown_plugin: + for cls in mmd_cls: + try: + bpy.utils.register_class(cls) + except: + pass + else: + for cls in reversed(mmd_cls): + try: + bpy.utils.unregister_class(cls) + except: + pass if not shutdown_plugin: Settings.update_settings(None, None) diff --git a/tools/importer.py b/tools/importer.py index 6dca5fa1..f1e8869b 100644 --- a/tools/importer.py +++ b/tools/importer.py @@ -42,12 +42,11 @@ from ..translations import t mmd_tools_installed = False -if platform.system() != "Linux" or bpy.app.version < (2, 90): - try: - import mmd_tools_local - mmd_tools_installed = True - except: - pass +try: + import mmd_tools_local + mmd_tools_installed = True +except: + pass current_blender_version = str(bpy.app.version[:2])[1:-1].replace(', ', '.') diff --git a/tools/translate.py b/tools/translate.py index 1b818dec..17f13d7c 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -44,8 +44,7 @@ from ..extern_tools.google_trans_new.google_trans_new import google_translator from ..translations import t -if platform.system() != "Linux" or bpy.app.version < (2, 90): - from mmd_tools_local import translations as mmd_translations +from mmd_tools_local import translations as mmd_translations dictionary = {} dictionary_google = {} @@ -561,10 +560,9 @@ def translate(to_translate, add_space=False, translating_shapes=False): def fix_jp_chars(name): - if platform.system() != "Linux" or bpy.app.version < (2, 90): - for values in mmd_translations.jp_half_to_full_tuples: - if values[0] in name: - name = name.replace(values[0], values[1]) + for values in mmd_translations.jp_half_to_full_tuples: + if values[0] in name: + name = name.replace(values[0], values[1]) return name diff --git a/translations/en_US.py b/translations/en_US.py index e6b55eb3..29817de1 100644 --- a/translations/en_US.py +++ b/translations/en_US.py @@ -157,6 +157,7 @@ 'CopyProtectionPanel.desc3': 'Before use: Read the documentation!', # UI Bake + 'BakePanel.label': 'Bake', 'BakePanel.versionTooOld': 'Only for Blender 2.80+', 'BakePanel.autodetectlabel': 'Autodetect:', 'BakePanel.generaloptionslabel': "General options:", diff --git a/translations/ja_JP.py b/translations/ja_JP.py index 848948c1..982174cf 100644 --- a/translations/ja_JP.py +++ b/translations/ja_JP.py @@ -158,6 +158,7 @@ 'CopyProtectionPanel.desc3': '使用前: ドキュメントを読んでください!', # UI Bake + 'BakePanel.label': '焼く', 'BakePanel.versionTooOld': 'Only for Blender 2.80+', 'BakePanel.autodetectlabel': 'Autodetect:', 'BakePanel.generaloptionslabel': "General options:", diff --git a/ui/bake.py b/ui/bake.py index c99d6f73..c39541f8 100644 --- a/ui/bake.py +++ b/ui/bake.py @@ -10,7 +10,7 @@ @register_wrap class BakePanel(ToolPanel, bpy.types.Panel): bl_idname = 'VIEW3D_PT_catsbake' - bl_label = 'Bake' + bl_label = t('BakePanel.label') bl_options = {'DEFAULT_CLOSED'} def draw(self, context): @@ -18,14 +18,14 @@ def draw(self, context): box = layout.box() col = box.column(align=True) - if Common.version_2_79_or_older(): - col.label(text=t('BakePanel.versionTooOld'), icon='ERROR') - return - row = col.row(align=True) row.operator(Bake.BakeTutorialButton.bl_idname, icon='FORWARD') col.separator() + if Common.version_2_79_or_older(): + col.label(text=t('BakePanel.versionTooOld'), icon='ERROR') + return + col.label(text=t('BakePanel.autodetectlabel')) row = col.row(align=True) row.operator(Bake.BakePresetDesktop.bl_idname, icon="ANTIALIASED") From d38ed3253fba31de89f6954345ad6f19238aa2ad Mon Sep 17 00:00:00 2001 From: Hotox Date: Fri, 18 Dec 2020 21:17:36 +0100 Subject: [PATCH 63/64] Ready for version 0.18.0 --- __init__.py | 4 ++-- tools/armature.py | 1 + tools/armature_bones.py | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 8807ba17..cd676ed5 100644 --- a/__init__.py +++ b/__init__.py @@ -30,13 +30,13 @@ 'author': 'GiveMeAllYourCats & Hotox', 'location': 'View 3D > Tool Shelf > CATS', 'description': 'A tool designed to shorten steps needed to import and optimize models into VRChat', - 'version': (0, 17, 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! + 'version': (0, 18, 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': (2, 80, 0), 'wiki_url': 'https://github.com/michaeldegroot/cats-blender-plugin', 'tracker_url': 'https://github.com/michaeldegroot/cats-blender-plugin/issues', 'warning': '', } -dev_branch = True +dev_branch = False import os import sys diff --git a/tools/armature.py b/tools/armature.py index 91c98383..b63e4b5b 100644 --- a/tools/armature.py +++ b/tools/armature.py @@ -490,6 +490,7 @@ def execute(self, context): ('Bip01_', 'Bip_'), ('Bip001_', 'Bip_'), ('Bip01', ''), + ('Bip02_', 'Bip_'), ('Character1_', ''), ('HLP_', ''), ('JD_', ''), diff --git a/tools/armature_bones.py b/tools/armature_bones.py index 9e51d24a..1105c1e6 100644 --- a/tools/armature_bones.py +++ b/tools/armature_bones.py @@ -646,6 +646,7 @@ '\L_Clav', 'Clav_\L', '\LClavicle', + 'Bip_\L_Arm', ] bone_rename['\Left arm'] = [ '\Left_Arm', @@ -695,6 +696,7 @@ 'J_Ude_A_\L', 'J_Shoulder_\L', '\LUpperArm', + 'Bip_\L_Arm1', ] bone_rename['Left arm'] = [ '+_Leisure_Elder_Supplement', @@ -747,6 +749,7 @@ 'J_Ude_B_\L', 'J_Elbow_\L', '\L_LowerArm', + 'Bip_\L_Arm2', ] bone_rename['\Left wrist'] = [ '\Left_Wrist', @@ -843,6 +846,7 @@ 'J_Hip_\L', '\LUpLeg', '\L_UpperLeg', + 'Bip_\L_Leg', ] bone_rename['\Left knee'] = [ '\Left_Knee', @@ -892,6 +896,7 @@ 'J_Knee_\L', '\LCalf', '\L_LowerLeg', + 'Bip_\L_Leg1', ] bone_rename['\Left ankle'] = [ '\Left_Ankle', From 01986c0c21c654e0392849df0762a9573655c9f8 Mon Sep 17 00:00:00 2001 From: Hotox Date: Fri, 18 Dec 2020 21:42:12 +0100 Subject: [PATCH 64/64] Small readme update --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ce1d1627..84a8504e 100644 --- a/README.md +++ b/README.md @@ -316,17 +316,18 @@ It checks for a new version automatically once every day. ## Changelog #### 0.18.0 +- **Added Bake Panel!** + - This is a non-destructive way to produce an optimized variant of (almost) any avatar! + - Full credit goes to **feilen**! Thanks so much for this awesome feature <3 + - Check out the wiki for more information: https://github.com/GiveMeAllYourCats/cats-blender-plugin/wiki/Bake +- **Added Smart Decimation!** + - This lets you decimate without loosing any shapekeys! + - Full credit goes to **feilen**! Tons of thanks for this awesome feature as well <3 - **Added Japanese translation!** - Cats is now almost fully translated into Japanese - To use it simply change your Blender language to Japanese and then restart Blender - Full credit goes to **Jordo** and **Ruuubick**! Thank you so much <3 - If you want to help translating Cats into any language, please us know! -- **Added Smart Decimation!** - - This lets you decimate without loosing any shapekeys! - - Full credit goes to **feilen**! Tons of thanks for this awesome feature <3 -- **Added Bake Panel!** - - This is a non-destructive way to produce an optimized variant of (almost) any avatar! - - Full credit goes to **feilen**! Thanks so much for this awesome feature as well <3 - **General:** - Cats is now fully compatible with Blender 2.90 and 2.91 - Added "Show mmd_tools tabs" option to Settings