commit 5d9e09e9c3f231760d59c6dce403baf5281a3b88
Author: 小煜 <2082529121@qq.com>
Date: Sat Dec 6 15:26:19 2025 +0800
feat: Add KoikatsuBlenderPipeline addon with import/export functionality
- Add core addon files (__init__.py, KKPanel.py, preferences.py, common.py)
- Add import pipeline with armature, mesh, and material modification modules
- Add export pipeline with material baking and FBX preparation utilities
- Add material combiner tool for texture atlas generation and optimization
- Add extras utilities for animation, rigging, and asset management
- Add bone orientation data from better_fbx for accurate skeletal structure
- Add comprehensive documentation and wiki with multi-language support (EN, JP, ZH)
- Add animation library retargeting lists for ARP and Rokoko motion capture
- Add Rigify integration scripts for advanced rigging workflows
- Add shader file (KK Shader V8.0.blend) for material rendering
- Add manifest and license files for addon distribution
- Add changelog documenting version history and improvements
- Initialize complete Blender addon project for Koikatsu character import/export
diff --git a/BONE_ORIENTATION_UPDATE.md b/BONE_ORIENTATION_UPDATE.md
new file mode 100644
index 0000000..eea3997
--- /dev/null
+++ b/BONE_ORIENTATION_UPDATE.md
@@ -0,0 +1,123 @@
+# KKBP 骨骼方向更新说明
+
+## 修改内容
+
+已将 `importing/modifyarmature.py` 修改为使用 better_fbx 的真实骨骼方向数据,让导入的模型骨骼看起来更自然。
+
+## 修改的文件
+
+- `importing/modifyarmature.py` - 主要修改文件
+
+## 主要变化
+
+### 1. 添加了完整的骨骼数据字典(第 47-140 行)
+
+包含了所有主要骨骼的真实方向数据:
+- 核心骨骼(根、臀部、腰部)
+- 脊柱和头部
+- 左右腿部(大腿、小腿、脚、脚趾)
+- 左右肩膀和手臂
+- 左右手的所有手指(食指、中指、无名指、小指、拇指)
+
+共约 90+ 个关键骨骼的方向数据。
+
+### 2. 修改了 reorient 函数(第 318-325 行)
+
+**之前:**
+```python
+height_adjust = Vector((0,0,0.1))
+def reorient(bone):
+ armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head + height_adjust
+```
+
+**之后:**
+```python
+def reorient(bone):
+ if bone in better_fbx_bone_tails:
+ # 使用 better_fbx 的真实骨骼方向数据
+ offset = Vector(better_fbx_bone_tails[bone])
+ armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head + offset
+ else:
+ # 如果骨骼不在字典中,使用原来的默认行为(向上)
+ height_adjust = Vector((0,0,0.1))
+ armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head + height_adjust
+```
+
+## 效果对比
+
+### 修改前
+- 腿部、脚部、手臂、肩膀、手指骨骼都是向上的小线段 (0, 0, 0.1)
+- 骨架看起来不直观,难以理解骨骼结构
+- 所有骨骼都像"竖起的小棍子"
+
+### 修改后
+- **腿部**:大腿向下延伸(-0.26 单位),小腿向下延伸(-0.22 单位)
+- **脚部**:向前延伸(-0.107 单位)
+- **手臂**:肩膀和手臂水平向外延伸(0.09-0.15 单位)
+- **手指**:所有手指沿着手指方向延伸(0.02-0.03 单位)
+- **脊柱**:向上延伸(0.04-0.05 单位)
+- **头部**:向上延伸(0.025 单位)
+- 骨架看起来自然,完全类似 better_fbx 导入的效果
+
+## 测试步骤
+
+1. **备份原文件**(如果还没有)
+ ```
+ cp importing/modifyarmature.py importing/modifyarmature.py.backup
+ ```
+
+2. **在 Koikatsu 中导出一个角色**
+ - 使用 KKBP Exporter 导出
+
+3. **在 Blender 中导入**
+ - 使用 KKBP Importer 导入
+ - 选择 "Use KKBP Armature" 选项
+
+4. **检查骨骼方向**
+ - 进入编辑模式(Tab 键)
+ - 查看腿部、脚部、手臂骨骼
+ - 它们应该沿着肢体方向延伸,而不是都朝上
+
+5. **验证功能**
+ - 切换到姿态模式
+ - 测试 IK 是否正常工作
+ - 测试骨骼旋转是否正常
+
+## 注意事项
+
+1. **手指骨骼未修改**
+ - 手指骨骼保持原有的特殊处理逻辑
+ - 因为它们需要特殊的旋转操作
+
+2. **向后兼容**
+ - 如果某个骨骼不在 `better_fbx_bone_tails` 字典中
+ - 会自动使用原来的默认行为(向上 0.1 单位)
+
+3. **Roll 值未修改**
+ - 这次只修改了骨骼的尾部位置
+ - Roll 值仍然使用 `set_bone_roll_data()` 函数中的数据
+
+## 如果需要回滚
+
+如果遇到问题,可以恢复原文件:
+```
+cp importing/modifyarmature.py.backup importing/modifyarmature.py
+```
+
+或者手动删除添加的代码:
+1. 删除第 47-59 行的 `better_fbx_bone_tails` 字典
+2. 将第 318-325 行的 `reorient` 函数改回原样
+
+## 扩展
+
+如果你想添加更多骨骼的方向数据,可以:
+1. 打开 `better_fbx_complete_bone_data.py`
+2. 找到对应骨骼的 `tail_offset` 值
+3. 添加到 `better_fbx_bone_tails` 字典中
+4. 将骨骼名称添加到 `reorient_list` 列表中
+
+## 相关文件
+
+- `better_fbx_bone_data.py` - 简化版骨骼数据(只有尾部偏移)
+- `better_fbx_complete_bone_data.py` - 完整骨骼数据(包含头部、尾部、Roll、父子关系)
+- `importing/modifyarmature.py` - 已修改的文件
diff --git a/Changelog.md b/Changelog.md
new file mode 100644
index 0000000..f6d002e
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,461 @@
+### Changes for V8.1.0
+* Major import improvements by **Sonogami-Rinne**!
+ * The importer now saturates images in several parallel threads, greatly reducing peak memory usage and speeding up import times!
+ * More headmods should import now! (including ones with missing tongue materials, gag eyes, and rigged tongues!)
+ * The importer now saturates images with 32 bit precision to reduce memory usage
+ * The importer now stores the character json data after first load, so it's not constantly reading in the same json files over and over again
+ * The default settings should work for most computers, but if it doesn't you can try lowering the "Max threads" and "Max parallel images" values in Edit > Preferences > Add-ons > KKBP menu
+* Exporter improvements by **Sonogami-Rinne**!
+ * Accessories that don't have DynamicBone data will now be skipped instead of crashing the game
+* Updated Chinese translation by **Sonogami-Rinne**!
+* The "Show bones for current outfit" button in the Extras section of the KKBP panel works again (KKBP armature and Rigify armature only)
+* Cards with forbidden ascii characters in their name will now export instead of throwing an error
+
+### Changes for V8.0.3
+* Fixed atlas save error issue because of new filename
+* fixed re-finalizing the atlas on windows
+
+### Changes for V8.0.2
+* Allow folder names without underscores
+* Allow eye controller to work with atlased model
+* Fix baking normal map with glasses material
+* Fix armature linking when using the material combiner setup button
+* Add back object names to atlas texture filename
+
+### Changes for V8.0.1
+* Compatibility fix for "Send to Unreal" plugin by **AnalogKnight**
+* Fixed import when the default collection was missing
+* Fixed normal map baking
+* Fixed baking with multiple outfits
+
+### Changes for V8.0.0
+* Fixed a longstanding issue related to white clothes / overlays. Everything loads correctly out of the box now
+ * KKBP now has an 80% success rate out of the box! [Check this page for more details.](https://flailingfog.github.io/material_breakdown)
+* You can change the colors straight from the materials tab now, so you don't have to touch the material nodes if you don't want to
+* Hair materials are no longer linked. Click on a hair material, modify it and then click the "Update hair materials" button in the material tab to update the rest of the hair materials with the same settings
+* Almost every material node setup was recreated from scratch, so it is now very easy to follow what the material nodes are doing
+ * including automatic generation of detail colors, so those will be different too
+* A lot of the material related code in the addon was rewritten from scratch, so it is now much easier to follow what the python script is loading in to each material slot
+* Glasses materials are now transparent instead of being completely white (only the glass part, not the frame)
+* Character name is used in collection now
+* Character name is used in all objects now to ensure they are unique
+* Character name is used in all materials now to ensure they are unique
+* Character name is used in all node groups now to ensure they are unique
+* Outfits, hair and alts get their own collections now
+* Materials that are supposed to be semi transparent are now automatically set to blended to prevent a grainy look
+* Added a new cycles shading option (now you can choose between cycles toon or cycles principled BSDF)
+* Remove the exporter plugin from the importer zip
+* Removed raw shading light linking
+* Removed raw shading hue passthrough
+* Combined raw shading normal map parsing into one group
+* Removed all rims
+* Shadowcast and bonelyfans are now deleted instead of hidden
+* Removed KK Fangs template
+* Added a switch to use eyeline main textures with color
+* Added a switch to use eyebrow main textures with color
+* Removed the shader to rgb nodes on KK General's color inputs
+* Merged Eyewhite materials L and R into one
+* Removed unused LUT images
+* Removed baked image slot on all materials. Baked materials are now tracked through custom attributes
+* Some very specific colors are now hard coded because the ones being loaded in were not great.
+ * You can still change them, but they won't react to any changes you make in game.
+* Dark main textures now keep their alpha channel when generated
+* Node groups related to the KKBP shaders will no longer show up in the material node search (they have a "." in front of them now)
+
+### Changes for V7.2.2
+* New Unreal Engine export option by **AnalogKnight**!
+
+### Changes for V7.2.1
+* fixed an issue with the armature not being reparented after baking
+
+### Changes for V7.2.0
+* Fixed crashes when creating a material atlas!
+ * This feature will now use atlas generation scripts borrowed from the [material-combiner-addon](https://github.com/Grim-es/material-combiner-addon)
+ * You will have to install pillow by clicking on the button in the KKBP panel (this uses pip, so it requires an internet connection)
+* More headmods will now import thanks to **VickyFrenzy**!
+ * The plugin expected all body materials to be present, but some headmods either remove or rename these materials, causing errors. Now it will skip some materials if they don't exist, allowing the headmod to at least import so you can fix it manually
+* Chinese translation fixes and updates by **AnalogKnight**!
+* Backported the plugin to work in Blender 3.6
+ * Only the import features work, so no exporting from 3.6
+* Added a button to revert all of your finalized materials back to the -ORG / "heavyweight" version of the material
+* Foot IKs will finally work correctly on taller characters
+* The "prep for target application" button will now allow you to simplify the KK and PMX armatures
+* Add back linux / mac support (again)
+
+### Changes for V7.1.0
+* New image saturation method
+* ~~Add back linux / mac support~~
+
+### Changes for V7.0.0
+* Blender 4.2 LTS support!
+ * You need the new version of [mmd_tools](https://extensions.blender.org/add-ons/mmd-tools/)
+* Koikatsu Sunshine exporter bugfix by **Guerra24**!
+ * This fixes colors not showing up on KKS exports that had their material set to "Koikano"
+* Replaced Generated Face Normal smoothing with a new geometry nodes setup from **MoriMorinya**! (original node group by **aVersionOfReality**)
+* Added a button to import a single ripped animation from the game onto your model!
+ * This feature requires Rokoko studio live plugin to be installed
+ * This supports Mixamo animations too thanks to **hsxfunc**
+* Import KeyError fixes by **justturniphead**!
+* Chinese translation updates by **AnalogKnight**!
+* Normal maps are now loaded into the optimized material thanks to **FrankV724**!
+* The KKBP plugin can now generate a material atlas by itself!
+ * The plugin also automatically generates a copy of your model that uses the atlas
+ * This feature does not have a pixel limit, so if the atlas is too large, you can split up your objects and re-generate it
+* Streamlined the KKBP main panel!
+ * There's now only one button for importing, one for optimizing materials and one for exporting
+ * If a button in the panel cannot be used, it will now be grayed out. Some features (like the animation import feature) require you to use the Rigify armature, and other features (like the prep for export button) require you to use the KKBP armature
+* During very long sequences, like when finalizing materials or bone simplification, the plugin will now attempt to give you progress information
+* Generating dark colors and textures can now be skipped to slightly speed up import times
+* Blue pixels on textures should now be fixed
+ * This appeared to be an issue with certain blender versions. You can now manually download any version of blender.exe between 2.80 and 3.6 and make the KKBP importer use the version you downloaded for generating the textures, so if the built in downloads for 2.90 or 3.6.9 are not working you can experiment with different blender versions until you find one that is working.
+* Updated the colors in the KK Shader to look good in the default "Blender dark" theme
+* Updated the plugin to be a Blender 4.2 extension (it is no longer a legacy addon)
+* Renamed the folders inside of the exporter zip to be clearer
+* Added decompiled source code for the KKBP_Exporter.dll to the exporter zip (thanks to **Guerra24**)
+* Moved wiki and usage instructions to a github pages site: https://flailingfog.github.io
+
+### Changes for V6.6.3
+* Blender export bug fix by **AnalogKnight**!
+* Removed LBS support
+
+### Changes for V6.6.2
+* KKBP Exporter improvements by **MediaMoots**!
+ * The exporter can now export with pushups enabled
+ * The exporter can now export with the current pose applied
+ * The exporter can now export with the current face expression applied
+ * All sub meshes now have unique material names
+ * All bones now have unique names
+ * See https://github.com/FlailingFog/KK-Blender-Porter-Pack/pull/398 for details
+* Fixed a bug that prevented baking
+* Hairs with a maintex can now be imported
+* Hairs with multiple colors can now be imported
+* Hair detail will now show above hair fade
+* The Blender import scripts in /importing/ were refactored
+* Manual categorization was removed (because hairs can have maintexes now)
+* The Blender plugin will now show a lot more progress information in the console and time every function
+* Added a basic wiki
+
+### Changes for V6.5.0
+* Blender 3.5 support
+* The Blender Pose Asset Library button will now try to continue where it left off if you interrupt it
+
+### Changes for V6.4.2
+* KKBP Exporter improvements by **MediaMoots**!
+ * [The exporter is up to 400% faster now!](https://github.com/FlailingFog/KK-Blender-Porter-Pack/pull/362)
+ * Skirt bone structures will now be correct if you're exporting multiple outfits with differently sized skirts
+ * [MeshFilter accessories will now export from the game](https://github.com/FlailingFog/KK-Blender-Porter-Pack/pull/344)
+ * Models can now be exported without the physics deformations applied, allowing you to apply your own physics to accessory / skirt bones later on
+ * ShapeInfo values are now exported to KK_CharacterInfoData.json
+* Added a button for creating a Blender Pose Asset Library from exported Koikatsu animation files
+* Added a button for creating a Blender Map Asset Library from exported Koikatsu map files
+ * This will only work properly if the [Better FBX Importer](https://www.blendermarket.com/products/better-fbx-importer--exporter) is installed and enabled
+ * If this addon isn't installed, maps and objects will still import, but their orientations and locations may be incorrect
+* Added a button to "Finalize" a material
+ * Finalizing materials will improve viewport performance during animation playback
+ * This will replace the heavy KKBP node groups with a simple texture + toon shader
+ * You need to bake light and dark versions of the model to a folder, then use the "Switch baked templates" button for the Light and the Dark selection before attempting to use the "Finalize materials" button
+ * The original materials are saved as "material_name-ORG" if you need to go back to edit or bake them again
+* Updated Rigify scripts (January 24th)
+ * These contain a bugfix related to headmods
+* The KKBP and Rigify armatures now have slight knee deformation drivers to smooth out the knee in kneeling poses
+* On Windows, the console will now show during long operations (importing characters, baking, importing animations, etc)
+ * This will help the user more easily identify errors. If an error is encountered, the console will remain open after the operation is complete
+* Clothes without a shadow color will now generate a dark texture with a default shadow color (instead of not generating and appearing completely white)
+* Using both the Rigify and Cycles options on the panel will no longer result in a rotated body mesh
+* Dark textures are now created when the Import Studio Object button is used
+* Only new textures will be saturated when the Import Studio Object button is used
+* Baking materials should work on Linux and Mac now
+
+### Changes for V6.3.0
+* Blender 3.4 support
+ * Models will no longer get a [KeyError 'color'] error during import
+ * Baking with KKBP 6.3.0 will only work on Blender 3.4+ due to a change with the Mix node
+* Added a script to generate a pose asset library from ripped Koikatsu animation data
+ * Open the .py file in KKBP / extras / animationlibrary for usage instructions
+ * See [this video](https://user-images.githubusercontent.com/65811931/211217974-66e3a961-8e40-4244-b7a4-11f6d4bbcb14.mp4) for a finished example
+* Importing is slightly faster for cards that have a lot of alphamasks or maintexes
+
+### Changes for V6.2.0
+* Dark colors and textures for clothes and accessories are now much closer to the in-game look
+ * This uses the darkening code from [Xukumi KKShadersPlus](https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Item/)
+ * Dark colors are automatically set in the dark section of the shader
+ * Dark versions of all main textures are created and loaded automatically (files that end in _MT_CT.png will be used to generate a file that ends in _MT_DT.png)
+ * Dark colors for skin are no longer hard-coded
+ * Dark hair and dark skin colors have a different darkening process. These processes aren't used, instead they get the same darkening process as the clothes
+* KKBP Exporter updates by **MediaMoots**!
+ * Animation curve data is now exported to KK_DynamicBoneData.json
+ * SMR dataname fixes
+ * More headmods will now successfully export
+ * Eyes that have different overlays on different outfits will now be exported (Only the first outfit's eye overlays will be loaded into blender automatically)
+ * Simplified Chinese translation by **castbohea**!
+* Cards missing certain body materials will now import without an error
+* If KK shapekey creation for a headmod fails, the original shapekeys for the headmod will be preserved instead of being deleted
+* The files for clothes and hair should correctly load in on Linux and Mac now (tested on SteamOS 3.3.2, Blender 3.3.1 flatpak)
+* Transparency should work now when using Lightning Boy Shader
+* Initial placement of the Lightning Boy Shader nodes is a little cleaner than before
+
+### Changes for V6.1.0
+* Rigify armature updates by an anonymous contributor! (Changelog copy-pasted below)
+ * 'Rigified' the new rigged tongue;
+ * Moved the eyes and rigged tongue bones to their own layers to reduce clutter in the face layers;
+ * Fixed the skirt bones alignment when converted, and also added the sixth bone (cf_j_sk_##_05) to the chains, just in case;
+ * Characters without skirt bones are now supported;
+ * Connected hair/accessory bones in the same way the CATS plugin does, and changed their widgets from 'circle' to 'limb' type, so the connections with their MCH parents are more apparent;
+ * Enabled a small new Rigify adjustment to the toe bones for Blender 3.2+ (this one: https://www.youtube.com/watch?v=H80AjLWgECY );
+ * Enabled a small new Rigify feature to the limb bones for Blender 3.3+, which allows you to scale the arms and legs uniformly by scaling the gear control bone at their base;
+ * Now the script that is run after the Rigify conversion automatically changes parents and armature modifier targets to the generated rig for all mesh objects in the scene that are related to its metarig; it does it without relying on object names so it should be safe even with multiple characters in the same scene (it detects its metarig from a common bone with a random alphanumeric string in its name). In other words, there's no more need to parent objects with empty groups.
+* Multiple bugfixes by **MediaMoots**!
+ * See #266, #257, #281
+* Basic Cycles shader support!
+ * This will make the KKBP materials work in Cycles
+* Basic Lightning Boy Shader support!
+ * This will attach the KKBP materials to LBS nodes
+ * The LBS addon must be installed for this to work
+ * Tested on LBS 2.1.3
+* Baking changes
+ * Bugfixes to the new baking system
+ * The old baking system can now be accessed with the 'old baker' toggle on the KKBP panel
+ * If Blender crashes during the baking process, the baking process will resume where it left off instead of trying to bake everything again.
+
+### Changes for V6.0.0
+Huge feature and usability updates by **MediaMoots**!
+* The KKBP exporter now works in Koikatsu Sunshine!
+* The KKBP Blender plugin now works in Blender 3.1+!
+* All outfits are now exported!
+ * These are automatically exported from Koikatsu
+ * These are available as hidden objects after the model is imported into Blender. They are parented to the armature
+ * If you don't want to use these, you can shorten your import time by deleting the "Outfit ##" folder from the export folder
+* Alternate clothing states (shift / hang state) can now be exported!
+ * Export these by checking the "Export Variations" box in Koikatsu
+ * These are available as hidden objects after the model is imported into Blender. They are parented to the outfit object
+* Hitboxes can now be exported!
+ * Export these by checking the "Export Hit Meshes" box in Koikatsu
+ * These are placed in their own collection when the model is imported into Blender
+* The tears object is now exported and available as new shapekeys on the body object!
+ * These are parented to the body
+ * The tears material also has settings to allow minor color edits
+* The eye gag object for heart eyes, firey eyes, etc is now exported and available as new shapekeys on the body object!
+ * These are parented to the body
+ * The shapekeys will automatically hide the eyes and eyeline materials when active
+ * The swirly eye rotation speed, heart eye pulse speed and cry/fire eye animation speed can be changed in the Eye Gag materials
+* The animated tongue is now exported!
+ * This is parented to the body object and hidden by default
+ * The rigged tongue doesn't use shapekeys like the rest of the face does
+* Shapekeys are more accurate than before!
+* The heart and sparkle Eye overlays are now exported
+* Eyewhite shapekeys are fixed on the exporter-side now!
+ * This means Blender is less likely to crash when importing the model
+* Small fangs are now exported!
+* Accessories are now automatically linked to the correct limb bone!
+* Eye and overlay textures are now scaled automatically!
+* Hair shine, eyeshadow, nipple and underhair UVmaps are now exported!
+ * Thanks to that, these items no longer need to be set and scaled manually for each character
+* Converted Normal Maps for use in Unreal and Unity are now exported
+* Separated objects can now be exported with the "Export separated meshes" button
+* A lot of character info is now exported
+ * Check the .json files in the export folder for info on materials, accessories, objects, renderers and bones
+* Exported character heights are 4% more accurate
+* Texture suffixes are shortened
+ * Image names over 64 characters long would cause blender to cufoff the filename, so this means long texture names are less likely to cause issues during import
+* The Koikatsu / BepInEx console will now be print out each mesh being exported. If the exporter is not working for a specific character, accessory or clothing item, you can use this to track down what is causing the exporter to fail.
+ * These messages are prefixed with [Info : Console]
+
+Rigify armature updates by **an anonymous contributor**!
+* Better Penetration bones will now be placed in the "Torso (Tweak)" layer when converting to the Rigify armature
+ * You need to use the Better Penetration armature type in Koikatsu for these bones to appear
+
+Unity normal blending and mirrored blush scaling by **poisenbery**!
+* Unity normal blending is an alternative normal map detail blending method that can be accessed in the "Raw shading" group
+* Mirrored blush scaling is an easier way to scale the blush and eyeshadow if the texture is symmetrical. This can be accessed in the "Blush positioning" group on the Face material
+
+Better face normals using shader nodes!
+* The face now uses a bastardized version of the Generated Face Normals setup [described in this post by **aVersionOfReality**](https://www.aversionofreality.com/blog/2021/9/5/clean-toon-shading)
+* This setup is disabled by default for performance reasons. Enable it by going to the face material > swap the "Raw Shading" node group to "Raw Shading (face)"
+* This setup only works in Blender
+* The GFN Empty position and scale can be edited by unhiding it. It's parented to the armature
+* The GFN options can be edited in the node group called "Generated Face Normals" inside of the the "Raw shading (face)" group
+
+Plus some misc changes to the Blender plugin:
+* Added a one-click option for importing models!
+ * Hair is now separated from the model automatically, so the entire import process has been reduced to a single button
+ * The behaviour from V5.0 (where you can separate objects as you please) can still be accessed by changing the "Don't pause to categorize" option on the upper right to "Pause to categorize", then pressing the "Finish categorization" button when you're done.
+* The main plugin UI can now be fully translated!
+ * Current languages: English, Japanese (日本語)
+* Material baking is now done [through the use of Geometry Nodes!](https://blender.stackexchange.com/questions/231662/)
+ * This works with multiple UV maps, so you no longer need to create a new mesh for hair highlights or anything that uses a separate uv map
+ * You can also speed up the baking process by skipping the dark and normal bakes if you don't want them
+* Exporting can now be done without installing the CATS addon
+ * Material Combiner is still required if you want a material atlas
+* Added a simplification choices menu to the export prep button
+* Removed the vanilla armature type toggle in favor of a menu
+ * There's four options to choose from. Check the tooltip for a brief description of each option
+ * The ability to switch between armature types after import was removed
+* Shapekeys on clothes and hair objects are now deleted
+ * Shapekeys only affect the face, so these weren't needed anyway
+* The Eye, Eyebrow, Eyewhite, Eyeline and Nose materials are now marked as freestyle faces by default (for freestyle exclusion)
+* Lipstick and Flush textures are now loaded into the face material
+* Added a safe for work mode toggle that probably works
+* The permalight/permadark texture has been merged into one file
+* Python errors are now copied to the KK Log in the Scripting tab on the top
+* The import directory string listed in the KK Log is now censored if it detects your Windows username
+* The plugin now uses an HDRI from polyhaven
+* All KKBP panel options are now visible by default
+* The KKBP panel will now gray out some buttons after a model is imported
+
+And many bugfixes
+
+### Changes for V5.1.0
+* Added per-character light linking to the KK shader
+ * If you have multiple characters in a scene, this allows you to "link" a light to a character, so you can achieve ideal lighting for each character and not have to fiddle with a global lighting setup that affects all characters at once. This works for up to three characters / light sources. It's enabled by default and can be found inside of the Raw Shading group.
+ * Usage: Make sure the "Light linking options (open me)" group framed in pink has it's slider set to 1, and any red lights will light your character. Import a second character, return to the ""Light linking options (open me)" group, and open it. Change the output of the pink "Match RGB output and sunlight color for light linking" frame from R to G. Any green lights will light the second character, but not the first character. Using a yellow light will light both R and G characters. Using a white light will light all characters.
+* Added reaction to colored light to the KK shader
+ * When you use a light that isn't pure white, the colors on the model will become affected by the color of the light. This is disabled by default and can be found inside of the Raw Shading group. Colored lights and light linking can't be used at the same time.
+ * Usage: Make sure the "Light linking options (open me)" group framed in pink has it's slider set to 0, and any colored lights will add additional color to your character. Any white lights will light the model like normal.
+* Updated Rigify scripts to the latest version (Feb 24th)
+* Overlay masks for Body and Face shaders now retain their original color.
+ * The "Overlay color" inputs on the body/face shader that used to set the overlay color have been changed to "Overlay color multiplier" inputs. Keep this as a white color to preserve the overlay mask's original color. If the overlay is pure white itself, the color on the multiplier will just set the color of the overlay.
+* Added an image slot for the "plain" version of the maintex for clothing items.
+ * This can be accessed by going into the clothing shader group and setting the "Use colored maintex?" slider to zero
+ * If there's no plain version available, you'll get a pink placeholder texture as a warning
+* The permanant light and dark image masks in the raw shading group have been moved to the green texture group
+ * This removes the need to create a unique raw shading group for every material.
+* Bugfixes ([#94](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/94), [#90](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/90), [#105](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/105), [#114](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/114))
+
+### Changes for V5.0.1
+* Bugfixes (see [#106](https://github.com/FlailingFog/KK-Blender-Porter-Pack/pull/106), [#95](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/95))
+
+### Changes for V5.0.0
+* New export plugin, image saturation functions, and automated color setting by **MediaMoots**!
+ * These make the import process more than twice as fast as the previous version!
+ * Color data from the game is now extracted and applied to the shaders automatically during the import process
+ * Textures are now automatically saturated to replicate the look of the in-game saturation filter
+ * More textures are extracted than before
+ * The new export plugin fixes common bugs involving broken charamaker functionality, shapekeys and accessories
+* Rigify compatible armature conversion by an anonymous contributor!
+ * Requires Rigify and the "auto-execute scripts" setting to be enabled
+ * Adds IK toggles for fingers, skirts, and accessories
+ * Improves the eye controls with extra features like single eye manipulation and eye target tracking
+ * Adds FK <-> IK switches to limb bones
+ * Makes more armature controls accessible
+* Better joint correction bones!
+ * Taken from **johnbbob_la_petite** on the Koikatsu Discord
+* Better viewport performance using preview quality normal nodes!
+ * Taken from **pcanback** [on blenderartists](https://blenderartists.org/t/way-faster-normal-map-node-for-realtime-animation-playback-with-tangent-space-normals/1175379)
+* Extracted in-game animation files can now be imported automatically!
+* New base armature!
+ * Bones are now organized by type through armature layers
+ * Toggle for keeping the stock KK armature
+ * Accessory bones show up automatically now
+* Upgrades to the export process!
+ * Added a button for exporting prep
+ * Smarter baking script
+ * Rebaking materials at higher resolutions is easier now
+ * Support for baking and atlasing normal maps
+* KK shader.blend is now integrated into the plugin!
+* Easy slots for forcing light and dark colors!
+* Easy slots for makeup and body paint!
+* Easy slots for patterns!
+* Support for colorized normal maps!
+* Automatic separation of Eyes and Eyebrow materials!
+ * This allows you to make the Eyes and Eyebrows show through the hair using Cryptomatte and other compositor features
+* Shader-to-RGB nodes added to clothing color inputs!
+ * This allows you to plug metal and other types of materials into clothing colormask inputs
+
+### Changes for V4.3.1:
+* Renamed the spine bones during the "1) Run right after importing" script
+ * CATS was not detecting the Spine, Chest and Upper Chest bones correctly. This resulted in the spine and chest bones being merged into one bone with awkward spine bends. Renaming the bones lets CATS detect the three bones correctly.
+
+### Changes for V4.3.0:
+* Added a new button to the KK Panel: **Import Studio Object**
+ * This script speeds up the import process for an accessory, studio object, or studio map that has been exported as an fbx file with SB3U
+ * Example usage video: https://www.youtube.com/watch?v=PeryYTsAN6E
+ * The shader applied to the object can be left as the default, changed to an emission shader, changed to the KK Shader, or changed to a custom user-defined node group
+* Added a new button to the KK Panel: **Link Shapekeys**
+ * If the Eyebrows material is separated from the body object, this script will allow you to link the shapekeys values on the Body object to the Eyebrows object
+ * Example usage video: https://www.youtube.com/watch?v=sqEBau1enWE
+ * (click this button instead of running the script in the editor as shown)
+ * This can be used for the Eyes and Eyeline materials as well
+ * Script source: https://blender.stackexchange.com/questions/86757/
+* Added a Center bone back to the armature
+ * This bone lets you move all the bones on the armature without moving the armature's object location
+* Added a Pelvis bone back to the armature
+ * This bone lets you move the pelvis and legs without moving the upper spine
+* Edited the armature bone tree a little
+* Changed the bone widget shapes in the KK shader to be slightly more rigify-like
+* Fixed the orientation of the top skirt bones
+* Gave the face material a separate outline
+* The "Select unused bones" button now unparents the Hips bone from the new Center bone
+ * Leaving the Hips bone parented caused import issues in Unity
+
+### Changes for V4.2.3:
+* Made eyewhite operations for the shapekeys optional through a toggle
+* Fixed the Basis shapekey being deleted when not set to English
+
+### Changes for V4.2.2:
+* The plugin now works in Blender 2.93
+* The plugin should now work when Blender's interface is set to any language
+* Added lazy crash prevention to the shapekeys button
+
+### Changes for V4.2.1:
+* Bake button wasn't assigned correctly in the new panel layout
+
+### Changes for V4.2:
+* The eyewhites shapekeys are now used when generating the KK shapekeys
+ * This will prevent gaps between the eyewhite and the lower eyelash for certain expressions
+* The plugin will now recognize the *KK Better Penetration* body type
+ * Toes are placed on armature layer 2
+ * BP bones are placed on armature layer 3
+* Materials without an image texture should properly bake now
+ * This will ensure fully transparent materials and materials that are a solid color (but still use the KK Shader) are baked
+* The Separate Body button will now attempt to separate the shadowcast and bonelyfans meshes automatically, then shove them into a collection
+* The Import Textures button will now check if the user has actually selected the Textures folder and will abort if they chose the wrong folder
+ * This was done to prevent texture loading issues
+ * This check can be disabled with a toggle
+* Adjusted panel layout
+* Added better joint correction for the hip bone
+ * In order to use them, the "Enable hip drivers" toggle needs to be enabled before pressing the "5 Add bone drivers" button
+* Better descriptions of what each toggle does
+* Made using multiple outlines optional through a toggle
+* The hair object can now be named either "hair" or "Hair"
+
+### Changes for V4.1.2:
+* Added an easier way to scale and move UVs with the "Scale Vector" node group.
+ * This replaces the mapping node setup used for the eyes, blush, etc
+* Materials that have a maintex or alphamask will now get their own outline material
+ * This should prevent transparency issues with the outline
+* Added a heel controller to the armature
+ * This is for rotating the foot bone while keeping the toes in place; a "tip toe" bone.
+* Added HSV sliders for the maintex of general/clothes materials
+* Fixed the nsfw texture parser node group in the body shader
+* The Bone Widgets collection is now hidden automatically
+* Fix bake script to account for world lighting
+* Fixed the foot IK
+* Moved the "MouthCavity" bone to the secondary layer
+* Moved supporting bones to their own armature layers instead of hiding them(layers 17, 18, and 19)
+* Relocated bone scaling and armature layer script operations to different buttons
+* Moved debug mode toggles to the shapekeys/drivers buttons
+
+### Changes for V4.0.6:
+* Fix male body material not being recognized by the scripts
+* Added sliders to the body shader to control optional genital textures
+ * The genital texture can be loaded into the "NSFW Textures" node group in the body template
+ * The file location of the genital texture (cf_body_00_t) depends on what body mod you're using
+* Added an example for materials that are partially metal to the KK Shader.blend
+
+### Changes for V4.0.5:
+* Quick fix for an issue with the Import Textures button
+ * If the body alpha mask did not exist in the "Textures" folder, the script would not handle it correctly
+
+### Changes for V4.0.4:
+* Put all the scripts into a blender plugin so the user can press a button instead of having to copy paste scripts
+* Added a button that applies the material templates to the model automatically
+ * These templates can be edited in the KK Shader.blend
+* Added a button that applies the textures to the model automatically
+ * *Grey's Mesh Exporter plugin for Koikatsu* is required in order for this to work
+ * Colors still need to be set manually
+ * This works 90% of the time, so a handful of textures might need to be loaded in manually
+* Added custom shapes for the armature to make the important bones more obvious
+ * These shapes can be edited in the KK Shader.blend
+ * These can be disabled in the armature tab (Armature tab > Viewport Display > Shapes)
+* Added second bone layer on the armature for skirt, face and eyebrow bones
+ * Multiple bone layers can be made visible at once by shift selecting the layer in the Armature > Skeleton section
diff --git a/KK Shader V8.0.blend b/KK Shader V8.0.blend
new file mode 100644
index 0000000..6cd9e56
Binary files /dev/null and b/KK Shader V8.0.blend differ
diff --git a/KKPanel.py b/KKPanel.py
new file mode 100644
index 0000000..bdceb55
--- /dev/null
+++ b/KKPanel.py
@@ -0,0 +1,428 @@
+import bpy, os, textwrap
+from bpy.types import PropertyGroup
+from bpy.props import (
+ IntProperty,
+ FloatProperty,
+ EnumProperty,
+ BoolProperty,
+ StringProperty
+)
+
+from .interface.dictionary_en import t
+from .exporting.material_combiner import globs
+
+class PlaceholderProperties(PropertyGroup):
+ #this will let the plugin know where to look for texture / json data
+ import_dir: StringProperty(default='')
+
+ #this will let the plugin know where the user is in the import / export process
+ plugin_state:StringProperty(default='')
+
+ #This will let the plugin track what objects belong to what character
+ character_name: StringProperty(default='')
+
+ #this lets the plugin time various actions
+ total_timer : FloatProperty(default=0)
+ timer : FloatProperty(default=0)
+
+ bake_mult: IntProperty(
+ min=1, max = 6,
+ default=1,
+ description=t('bake_mult_tt'))
+
+ sfw_mode : BoolProperty(
+ description=t('sfw_mode_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.sfw_mode)
+
+ fix_seams : BoolProperty(
+ description=t('seams_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.fix_seams)
+
+ use_single_outline : BoolProperty(
+ description= t('outline_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.use_single_outline)
+
+ use_material_fake_user : BoolProperty(
+ description=t('keep_templates_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.use_material_fake_user)
+
+ old_bake_bool : BoolProperty(
+ description=t('old_bake_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.old_bake_bool)
+
+ armature_dropdown : EnumProperty(
+ items=(
+ ("A", t('arm_drop_A'), t('arm_drop_A_tt')),
+ ("B", t('arm_drop_B'), t('arm_drop_B_tt')),
+ ("C", t('arm_drop_C'), t('arm_drop_C_tt')),
+ ("D", t('arm_drop_D'), t('arm_drop_D_tt')),
+ ), name="", default=bpy.context.preferences.addons[__package__].preferences.armature_dropdown, description=t('arm_drop'))
+
+ categorize_dropdown : EnumProperty(
+ items=(
+ ("A", t('cat_drop_A'), t('cat_drop_A_tt')),
+ ("B", t('cat_drop_B'), t('cat_drop_B_tt')),
+ ), name="", default=bpy.context.preferences.addons[__package__].preferences.categorize_dropdown, description=t('cat_drop'))
+
+ colors_dropdown : BoolProperty(
+ description = t('dark'),
+ default=bpy.context.preferences.addons[__package__].preferences.colors_dropdown)
+
+ prep_dropdown : EnumProperty(
+ items=(
+ ("A", t('prep_drop_A'), t('prep_drop_A_tt')),
+ #("C", "MikuMikuDance - PMX compatible", " "),
+ ("D", t('prep_drop_D'), t('prep_drop_D_tt')),
+ ("E", t('prep_drop_E'), t('prep_drop_E_tt')),
+ ("B", t('prep_drop_B'), t('prep_drop_B_tt')),
+ ), name="", default=bpy.context.preferences.addons[__package__].preferences.prep_dropdown, description=t('prep_drop'))
+
+ simp_dropdown : EnumProperty(
+ items=(
+ ("A", t('simp_drop_A'), t('simp_drop_A_tt')),
+ ("B", t('simp_drop_B'), t('simp_drop_B_tt')),
+ ("C", t('simp_drop_C'), t('simp_drop_C_tt')),
+ ), name="", default=bpy.context.preferences.addons[__package__].preferences.simp_dropdown, description=t('simp_drop'))
+
+ bake_light_bool : BoolProperty(
+ description=t('bake_light_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.bake_light_bool)
+
+ bake_dark_bool : BoolProperty(
+ description=t('bake_dark_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.bake_dark_bool)
+
+ bake_norm_bool : BoolProperty(
+ description=t('bake_norm_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.bake_norm_bool)
+
+ delete_cache : BoolProperty(
+ description=t('delete_cache_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.delete_cache)
+
+ use_atlas : BoolProperty(
+ description=t('use_atlas_tt'),
+ default = bpy.context.preferences.addons[__package__].preferences.use_atlas)
+
+ animation_library_scale : BoolProperty(
+ description=t('animation_library_scale_tt'),
+ default = True)
+
+ shapekeys_dropdown : EnumProperty(
+ items=(
+ ("A", t('shape_A'), t('shape_A_tt')),
+ ("B", t('shape_B'), t('shape_B_tt')),
+ ("C", t('shape_C'), t('shape_C_tt')),
+ ), name="", default=bpy.context.preferences.addons[__package__].preferences.shapekeys_dropdown, description="")
+
+ shader_dropdown : EnumProperty(
+ items=(
+ ("A", t('shader_A'), ''),
+ ("B", t('shader_B'), ''),
+ ("D", t('shader_D'), ''),
+ ("C", t('shader_C'), t('shader_C_tt')),
+ ), name="", default=bpy.context.preferences.addons[__package__].preferences.shader_dropdown, description="Shader")
+
+ atlas_dropdown : EnumProperty(
+ items=(
+ ("A", t('bake_light'), ""),
+ ("B", t('bake_dark'), ""),
+ ("C", t('bake_norm'), ""),
+ ), name="", default="A", description='')
+
+ dropdown_box : EnumProperty(
+ items=(
+ ("A", "Principled BSDF", "Default shading"),
+ ("B", "Emission", "Flat shading"),
+ ("C", "KK Shader", "Anime cel shading"),
+ ("D", "Custom", "Custom shading")
+ ), name="", default="A", description="Shader type")
+
+ shadows_dropdown : EnumProperty(
+ items=(
+ ("A", "None", "No shadows"),
+ ("B", "Opaque", ""),
+ ("C", "Alpha Clip", ""),
+ ("D", "Alpha Hashed", "")
+ ), name="", default="A", description="Shadow Mode")
+
+ blend_dropdown : EnumProperty(
+ items=(
+ ("A", "Opaque", ""),
+ ("B", "Alpha Clip", ""),
+ ("C", "Alpha Hashed", ""),
+ ("D", "Alpha Blend", ""),
+ ), name="", default="B", description="Blend Mode")
+
+ studio_lut_bool : BoolProperty(
+ name="Enable or Disable",
+ description=t('convert_texture_tt'),
+ default = True)
+
+ rokoko_bool : BoolProperty(
+ name="Enable or Disable",
+ description="""Enable this if you don't want KKBP to process the fbx animation, and instead want to use the rokoko plugin to transfer the fbx animation to your character.
+ Stock / unmodified armatures only!""",
+ default = False)
+
+ animation_import_type : BoolProperty(
+ name="Enable or Disable",
+ description=t('animation_type_tt'),
+ default = False)
+
+ image_dropdown : EnumProperty(
+ items=(
+ ("A", t('dark_C'), "Use Day LUT to saturate image"),
+ ("B", t('dark_A'), "Use Night LUT to saturate image"),
+ ("C", t('dark_B'), "Use Sunset LUT to saturate image")
+ ), name="", default="A", description="LUT Choice")
+
+#The main panel
+class IMPORTINGHEADER_PT_panel(bpy.types.Panel):
+ bl_label = t('import_export')
+ bl_category = "KKBP"
+ bl_space_type = "VIEW_3D"
+ bl_region_type = "UI"
+ def draw(self,context):
+ layout = self.layout
+
+class IMPORTING_PT_panel(bpy.types.Panel):
+ bl_parent_id = "IMPORTINGHEADER_PT_panel"
+ bl_label = t('import_export')
+ bl_category = "KKBP"
+ bl_space_type = "VIEW_3D"
+ bl_region_type = "UI"
+ bl_options = {'HIDE_HEADER'}
+
+ def draw(self,context):
+ scene = context.scene.kkbp
+ layout = self.layout
+ splitfac = 0.5
+ box = layout.box()
+ col = box.column(align=True)
+
+ # row = col.row(align=True)
+ # row.operator('kkbp.debug', text = 'Debug', icon='FILE_FOLDER')
+
+ row = col.row(align=True)
+ row.operator('kkbp.kkbpimport', text = t('import_model'), icon='FILE_FOLDER')
+ row.enabled = scene.plugin_state not in ['imported', 'prepped']
+
+ row = col.row(align = True)
+ box = row.box()
+ col = box.column(align=True)
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(context.scene.kkbp, "categorize_dropdown")
+ split.prop(context.scene.kkbp, "armature_dropdown")
+ split.enabled = scene.plugin_state not in ['imported', 'prepped']
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(context.scene.kkbp, "shapekeys_dropdown")
+ split.prop(context.scene.kkbp, "shader_dropdown")
+ row.enabled = scene.plugin_state not in ['imported', 'prepped']
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(context.scene.kkbp, "colors_dropdown", toggle=True, text = t('dark_F') if scene.colors_dropdown else t('dark_C'))
+ split.prop(context.scene.kkbp, "delete_cache", toggle=True, text = t('delete_cache'))
+ row.enabled = scene.plugin_state not in ['imported', 'prepped']
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(context.scene.kkbp, "fix_seams", toggle=True, text = t('seams'))
+ split.prop(context.scene.kkbp, "use_material_fake_user", toggle=True, text = t('keep_templates'))
+ row.enabled = scene.plugin_state not in ['imported', 'prepped']
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(context.scene.kkbp, "use_single_outline", toggle=True, text = t('outline'))
+ split.prop(context.scene.kkbp, "sfw_mode", toggle=True, text = t('sfw_mode'))
+ row.enabled = scene.plugin_state not in ['imported', 'prepped']
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ row.operator('kkbp.bakematerials', text = t('bake'), icon='OUTPUT')
+ row.enabled = scene.plugin_state in ['imported', 'prepped']
+ row = col.row(align=True)
+ split = row.split(align=True, factor=0.33)
+ split.prop(context.scene.kkbp, "bake_light_bool", toggle=True, text = t('bake_light'))
+ split.prop(context.scene.kkbp, "bake_dark_bool", toggle=True, text = t('bake_dark'))
+ split.prop(context.scene.kkbp, "bake_norm_bool", toggle=True, text = t('bake_norm'))
+ row.enabled = scene.plugin_state in ['imported', 'prepped']
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.prop(context.scene.kkbp, 'old_bake_bool', toggle=True, text = t('old_bake'))
+ split.prop(context.scene.kkbp, "bake_mult", text = t('bake_mult'))
+ row.enabled = scene.plugin_state in ['imported', 'prepped']
+ row = col.row(align = True)
+ split = row.split(align=True, factor=splitfac)
+ if globs.pil_exist == 'no':
+ split.operator('kkbp.get_pillow', text = t('pillow'), icon='FILE_REFRESH')
+ split.operator('kkbp.resetmaterials', text = t('reset_mats'), icon='RECOVER_LAST')
+ elif globs.pil_exist == 'restart':
+ col = col.box().column()
+ col.label(text='Installation complete')
+ col.label(text='Please restart Blender')
+ else:
+ split.prop(context.scene.kkbp, "use_atlas", toggle=True, text = t('use_atlas') if scene.use_atlas else t('dont_use_atlas'))
+ split.operator('kkbp.resetmaterials', text = t('reset_mats'), icon='RECOVER_LAST')
+
+ row.enabled = scene.plugin_state in ['imported', 'prepped']
+
+class EXPORTING_PT_panel(bpy.types.Panel):
+ bl_parent_id = "IMPORTING_PT_panel"
+ bl_label = 'Exporting'
+ bl_options = {'HIDE_HEADER'}
+ bl_category = "KKBP"
+ bl_space_type = "VIEW_3D"
+ bl_region_type = "UI"
+
+ def draw(self,context):
+ scene = context.scene.kkbp
+ layout = self.layout
+ splitfac = 0.5
+
+ box = layout.box()
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ row.operator('kkbp.exportprep', text = t('prep'), icon = 'MODIFIER')
+ row.enabled = scene.plugin_state in ['imported'] and bpy.context.scene.kkbp.armature_dropdown in ['A', 'C', 'D']
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.prop(context.scene.kkbp, "simp_dropdown")
+ split.prop(context.scene.kkbp, "prep_dropdown")
+ row.enabled = scene.plugin_state in ['imported'] and bpy.context.scene.kkbp.armature_dropdown in ['A', 'C', 'D']
+
+class EXTRAS_PT_panel(bpy.types.Panel):
+ bl_label = t('extras')
+ bl_category = "KKBP"
+ bl_space_type = "VIEW_3D"
+ bl_region_type = "UI"
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw(self,context):
+ layout = self.layout
+ scene = context.scene.kkbp
+ splitfac = 0.6
+
+ box = layout.box()
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('studio_object'))
+ split.operator('kkbp.importstudio', text = '', icon = 'PACKAGE')
+ row = col.row(align=True)
+ split = row.split(align=True)
+ split.label(text="Shader")
+ split.label(text="Shadow Mode")
+ split.label(text="Blend Mode")
+ split.label(text="")
+ row = col.row(align=True)
+ split = row.split(align=True)
+ split.prop(context.scene.kkbp, "dropdown_box")
+ split.prop(context.scene.kkbp, "shadows_dropdown")
+ split.prop(context.scene.kkbp, "blend_dropdown")
+ split.prop(context.scene.kkbp, "studio_lut_bool", toggle=True, text = t('convert_texture'))
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text = t('single_animation'))
+ split.operator('kkbp.importanimation', text = '', icon = 'ARMATURE_DATA')
+ row.enabled = scene.plugin_state in ['imported'] and bpy.context.scene.kkbp.armature_dropdown == 'B'
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text="")
+ split.prop(context.scene.kkbp, "animation_import_type", toggle=True, text = t('animation_mix') if scene.animation_import_type else t('animation_koi'))
+ row.enabled = scene.plugin_state in ['imported'] and bpy.context.scene.kkbp.armature_dropdown == 'B'
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('animation_library'))
+ split.operator('kkbp.createanimassetlib', text = '', icon = 'ARMATURE_DATA')
+ row.enabled = scene.plugin_state in ['imported'] and bpy.context.scene.kkbp.armature_dropdown == 'B'
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text="")
+ split.prop(context.scene.kkbp, "animation_library_scale", toggle=True, text = t('animation_library_scale'))
+ row.enabled = scene.plugin_state in ['imported'] and bpy.context.scene.kkbp.armature_dropdown == 'B'
+
+ # col = box.column(align=True)
+ # row = col.row(align=True)
+ # split = row.split(align=True, factor=splitfac)
+ # split.label(text=t('map_library'))
+ # split.operator('kkbp.createmapassetlib', text = '', icon = 'WORLD')
+
+ box = layout.box()
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('sep_eye'))
+ split.operator('kkbp.linkshapekeys', text = '', icon='HIDE_OFF')
+
+ box = layout.box()
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('bone_visibility'))
+ split.operator('kkbp.updatebones', text = '', icon = 'GROUP_BONE')
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('rigify_convert'))
+ split.operator('kkbp.rigifyconvert', text = '', icon='MOD_ARMATURE')
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('matcomb'))
+ split.operator('kkbp.matcombsetup', text = '', icon='NODETREE')
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.label(text=t('mat_comb_switch'))
+ split.operator('kkbp.matcombswitch', text = '', icon='FILE_REFRESH')
+
+ #check https://ui.blender.org/icons/ for all icon names
+
+#Add a button to the materials tab that lets you update all hair material settings at once
+class HAIR_PT_panel(bpy.types.Panel):
+ #bl_parent_id = "EEVEE_MATERIAL_PT_surface"
+ bl_label = "kkbp_hair"
+ bl_space_type = 'PROPERTIES'
+ bl_region_type = 'WINDOW'
+ bl_context = "material"
+ bl_options = {'HIDE_HEADER'}
+ COMPAT_ENGINES = {'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'}
+
+ def draw(self, context):
+ layout = self.layout
+ mat = context.material
+ if mat:
+ if mat.get('hair'):
+ layout.operator('kkbp.linkhair', text = t('link_hair'), icon='NODETREE')
+
+def register():
+ bpy.utils.register_class(PlaceholderProperties)
+ bpy.utils.register_class(IMPORTINGHEADER_PT_panel)
+ bpy.utils.register_class(IMPORTING_PT_panel)
+ bpy.utils.register_class(EXPORTING_PT_panel)
+ bpy.utils.register_class(EXTRAS_PT_panel)
+ bpy.utils.register_class(HAIR_PT_panel)
+
+def unregister():
+ bpy.utils.unregister_class(HAIR_PT_panel)
+ bpy.utils.unregister_class(EXTRAS_PT_panel)
+ bpy.utils.unregister_class(EXPORTING_PT_panel)
+ bpy.utils.unregister_class(IMPORTING_PT_panel)
+ bpy.utils.unregister_class(IMPORTINGHEADER_PT_panel)
+ bpy.utils.unregister_class(PlaceholderProperties)
+
+if __name__ == "__main__":
+ #unregister()
+ register()
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..32d89fa
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,26 @@
+# License
+### Program
+The majority of KKBP's Python source code is developed and distributed under the MIT License. Additional third party licenses may also exist in the code base, see the source code for complete and up to date details.
+
+
+### The MIT License (MIT)
+
+Copyright © 2024 KKBP developers
+
+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/README.md b/README.md
new file mode 100644
index 0000000..2366a2b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+# KK Blender Porter Pack
+
+
+
+Plugin pack for exporting and cleaning up Koikatsu characters in Blender.
+
+The ```KKBP exporter for Koikatsu``` exports the character's mesh, armature and color data from the game. The ```KKBP importer for Blender``` processes that data to setup the character in Blender. Once characters are setup in Blender, they can be saved as FBX files for use in other programs.
+
+* **Download:** https://github.com/FlailingFog/KK-Blender-Porter-Pack/releases
+* **How to use the plugins:** https://kkbpwiki.github.io/.
+* **Changelog:** https://github.com/FlailingFog/KK-Blender-Shader-Pack/blob/master/Changelog.md
+* **Alternate download (this is a live snapshot of the repo that might not work!):** https://github.com/FlailingFog/KK-Blender-Porter-Pack/archive/refs/heads/master.zip
+
+## Video walkthrough of the plugins
+
+[(Click for playlist!)
+](https://www.youtube.com/watch?v=hib8NWBvgvA&list=PLhiuav2SCuveMgQUA2YqqbSE7BtOrkZ-Q&index=1)
+
+## Help
+
+[Check the wiki for FAQ and basic info.](https://kkbpwiki.github.io/)
+If you're still having trouble please [create a new issue](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues).
+
+## Contributing
+
+Any contributions are welcome! Please check out the links below:
+* [The issues page](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues)
+* [Make a pull request](https://github.com/FlailingFog/KK-Blender-Porter-Pack/pulls)
+* [Edit the wiki site](https://kkbpwiki.github.io/) (click the edit buttons on the top right of any page, then make a pull request)
+* [Translate the KKBP interface](https://github.com/FlailingFog/KK-Blender-Porter-Pack/tree/master/interface)
+
+This project does not accept donations.
+
+## Similar Projects
+
+* [KKBP Exporter](https://github.com/FlailingFog/KKBP_Exporter)
+* [SKLX-creator](https://www.patreon.com/posts/sklx-lite-118039975)
+* [KKPMX](https://github.com/CazzoPMX/KKPMX)
+* [Koikatsu Pmx Exporter (Reverse Engineered & updated)](https://github.com/Snittern/KoikatsuPmxExporterReverseEngineered)
+* [Grey's mesh exporter for Koikatsu](https://github.com/FlailingFog/KK-Blender-Porter-Pack/tree/9fcef4127ba56b4e8e8718fb546945fc00eaaad9/GME)
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..9f8f517
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,122 @@
+#The init file for the plugin
+bl_info = {
+ "name" : "KKBP (Koikatsu Blender Porter)",
+ "author" : "a blendlet and some blenderchads",
+ "location" : "View 3D > Tool Shelf > KKBP and Image Editor > Tool Shelf > KKBP",
+ "description" : "Scripts to automate cleanup of a Koikatsu export",
+ "version": (8, 1, 0),
+ "blender" : (3, 6, 0),
+ "category" : "3D View",
+ "tracker_url" : "https://github.com/FlailingFog/KK-Blender-Porter-Pack/",
+ "doc_url": "https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/wiki/Wiki%20top.md",
+}
+
+from .exporting.material_combiner.extend_types import register_smc_types, unregister_smc_types
+from bpy.utils import register_class, unregister_class
+from bpy.types import Scene
+from bpy.props import PointerProperty
+
+def reg_unreg(register_bool):
+ from .preferences import KKBPPreferences
+ if register_bool:
+ register_class(KKBPPreferences)
+ else:
+ unregister_class(KKBPPreferences)
+
+ from .importing.importbuttons import kkbp_import
+ from .importing.modifymesh import modify_mesh
+ from .importing.modifyarmature import modify_armature
+ from .importing.modifymaterial import modify_material
+ from .importing.postoperations import post_operations
+
+ from .exporting.bakematerials import bake_materials
+ from .exporting.exportprep import export_prep
+ from .exporting.material_combiner.combiner import Combiner
+ from .exporting.material_combiner.combine_list import RefreshObData, CombineSwitch
+ from .exporting.material_combiner.extend_types import CombineList
+ from .exporting.material_combiner.get_pillow import InstallPIL
+
+ from .extras.importstudio import import_studio
+ from .extras.createmapassetlibrary import map_asset_lib
+ from .extras.createanimationlibrary import anim_asset_lib
+ from .extras.linkshapekeys import link_shapekeys
+ from .extras.updatebones import update_bones
+ from .extras.imageconvert import image_convert
+ from .extras.imageconvert import image_dark_convert
+ from .extras.rigifywrapper import rigify_convert
+ from .extras.rigifyscripts.rigify_before import rigify_before
+ from .extras.rigifyscripts.rigify_after import rigify_after
+ from .extras.catsscripts.armature_manual import MergeWeights
+ from .extras.importanimation import anim_import
+ from .extras.matcombsetup import mat_comb_setup
+ from .extras.matcombswitch import mat_comb_switch
+ from .extras.resetmaterials import reset_materials
+ from .extras.linkhair import link_hair
+
+ from . KKPanel import PlaceholderProperties
+ from . KKPanel import (
+ IMPORTINGHEADER_PT_panel,
+ IMPORTING_PT_panel,
+ EXPORTING_PT_panel,
+ EXTRAS_PT_panel,
+ HAIR_PT_panel,
+ )
+
+ classes = (
+ bake_materials,
+ export_prep,
+ image_convert,
+ image_dark_convert,
+
+ import_studio,
+ map_asset_lib,
+ anim_asset_lib,
+ link_shapekeys,
+ update_bones,
+ rigify_convert,
+ rigify_before,
+ rigify_after,
+ MergeWeights,
+ anim_import,
+ Combiner,
+ RefreshObData,
+ CombineSwitch,
+ CombineList,
+ mat_comb_setup,
+ mat_comb_switch,
+ InstallPIL,
+ reset_materials,
+ link_hair,
+
+ kkbp_import,
+ modify_mesh,
+ modify_armature,
+ modify_material,
+ post_operations,
+
+ PlaceholderProperties,
+ IMPORTINGHEADER_PT_panel,
+ IMPORTING_PT_panel,
+ EXPORTING_PT_panel,
+ EXTRAS_PT_panel,
+ HAIR_PT_panel,
+ )
+
+ for cls in classes:
+ register_class(cls) if register_bool else unregister_class(cls)
+
+ if register_bool:
+ Scene.kkbp = PointerProperty(type=PlaceholderProperties)
+ else:
+ del Scene.kkbp
+
+def register():
+ reg_unreg(True)
+ register_smc_types()
+
+def unregister():
+ reg_unreg(False)
+ unregister_smc_types()
+
+if __name__ == "__main__":
+ register()
diff --git a/__pycache__/KKPanel.cpython-311.pyc b/__pycache__/KKPanel.cpython-311.pyc
new file mode 100644
index 0000000..b031dc8
Binary files /dev/null and b/__pycache__/KKPanel.cpython-311.pyc differ
diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..6a42444
Binary files /dev/null and b/__pycache__/__init__.cpython-311.pyc differ
diff --git a/__pycache__/common.cpython-311.pyc b/__pycache__/common.cpython-311.pyc
new file mode 100644
index 0000000..a87d3b3
Binary files /dev/null and b/__pycache__/common.cpython-311.pyc differ
diff --git a/__pycache__/preferences.cpython-311.pyc b/__pycache__/preferences.cpython-311.pyc
new file mode 100644
index 0000000..9c5960d
Binary files /dev/null and b/__pycache__/preferences.cpython-311.pyc differ
diff --git a/better_fbx_bone_data.py b/better_fbx_bone_data.py
new file mode 100644
index 0000000..0d735c2
--- /dev/null
+++ b/better_fbx_bone_data.py
@@ -0,0 +1,202 @@
+# Better FBX 导入的骨骼尾部位置数据
+# 这些数据可以用来替换 KKBP 中的 reorient 逻辑
+# 从 better_fbx 导入的 Koikatsu 模型骨架中提取
+
+better_fbx_bone_tails = {
+ 'p_cf_body_00': (0.0000000000, 0.0000000000, 0.5717499852),
+ 'cf_o_root': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'n_body': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'o_body_a': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'cf_j_root': (0.0000000000, 0.0000000000, 0.0099999998),
+ 'cf_n_height': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'cf_j_hips': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'cf_j_waist01': (0.0000000050, 0.0082969246, -0.0676312447),
+ 'cf_j_waist02': (0.0000061035, 0.0022072941, -0.0269574523),
+ 'cf_d_ana': (0.0000000011, -0.0037057437, 0.0183483958),
+ 'cf_j_ana': (0.0000000000, 0.0004356094, -0.0009342432),
+ 'cf_s_ana': (0.0000000000, 0.0004356094, -0.0009342432),
+ 'cf_d_kokan': (0.0000000003, -0.0000340615, 0.0193923712),
+ 'cf_j_kokan': (0.0000000003, -0.0000340852, 0.0193923712),
+ 'cf_d_siri_L': (0.0006995648, 0.0001653936, 0.0200324059),
+ 'cf_d_siri01_L': (-0.0000022426, 0.0499999970, 0.0000028610),
+ 'cf_j_siri_L': (0.0006995425, 0.0006653816, 0.0200324059),
+ 'cf_s_siri_L': (0.0006995425, 0.0006653816, 0.0200324059),
+ 'cf_d_siri_R': (-0.0007000044, 0.0001655202, 0.0200324059),
+ 'cf_d_siri01_R': (0.0000000000, 0.0499999858, 0.0000020266),
+ 'cf_j_siri_R': (-0.0007000044, 0.0006655231, 0.0200324059),
+ 'cf_s_siri_R': (-0.0007000044, 0.0006655082, 0.0200324059),
+ 'cf_j_thigh00_L': (0.0000000000, 0.0045367014, -0.2599085569),
+ 'cf_d_thigh01_L': (0.0008304268, -0.0000085849, 0.0197808743),
+ 'cf_s_thigh01_L': (0.0008304268, -0.0000085849, 0.0197808743),
+ 'cf_d_thigh02_L': (0.0008304268, 0.0000311676, 0.0175034404),
+ 'cf_s_thigh02_L': (0.0008304268, 0.0000311676, 0.0175034404),
+ 'cf_d_thigh03_L': (0.0008304268, 0.0000528507, 0.0162612200),
+ 'cf_s_thigh03_L': (0.0008304268, 0.0000528507, 0.0162612200),
+ 'cf_j_leg01_L': (-0.0000001565, -0.0001882389, -0.2238392234),
+ 'cf_d_kneeF_L': (-0.0000010356, -0.0399756283, -0.0013961792),
+ 'cf_d_leg02_L': (0.0008304194, -0.0000160560, 0.0126936734),
+ 'cf_s_leg02_L': (0.0004141256, 0.0006808266, -0.0072855055),
+ 'cf_d_leg03_L': (0.0008304194, 0.0000490360, 0.0108296350),
+ 'cf_s_leg03_L': (0.0008304194, 0.0000490360, 0.0108296350),
+ 'cf_j_leg03_L': (0.0000000000, 0.0117738210, -0.0496065579),
+ 'cf_j_foot_L': (-0.0000000075, -0.1070634499, -0.0073633678),
+ 'cf_j_toes_L': (-0.0000000075, -0.1070634536, -0.0073633678),
+ 'cf_s_leg01_L': (0.0000015497, 0.0599634517, 0.0020940304),
+ 'cf_s_kneeB_L': (0.0000015497, 0.0599634424, 0.0020940304),
+ 'cf_j_thigh00_R': (0.0000000149, 0.0045371708, -0.2599344850),
+ 'cf_d_thigh01_R': (-0.0008299947, -0.0000085682, 0.0197798610),
+ 'cf_s_thigh01_R': (-0.0008299947, -0.0000085682, 0.0197798610),
+ 'cf_d_thigh02_R': (-0.0008299947, 0.0000311676, 0.0175034404),
+ 'cf_s_thigh02_R': (-0.0008299947, 0.0000311676, 0.0175034404),
+ 'cf_d_thigh03_R': (-0.0008299947, 0.0000528507, 0.0162612200),
+ 'cf_s_thigh03_R': (-0.0008299947, 0.0000528507, 0.0162612200),
+ 'cf_j_leg01_R': (0.0000000000, -0.0001882352, -0.2238395214),
+ 'cf_d_kneeF_R': (-0.0000000894, -0.0399756199, -0.0013961196),
+ 'cf_d_leg02_R': (-0.0008300021, -0.0000160560, 0.0126936436),
+ 'cf_s_leg02_R': (-0.0008300021, -0.0000160560, 0.0126936436),
+ 'cf_d_leg03_R': (-0.0008300021, 0.0000490360, 0.0108296126),
+ 'cf_s_leg03_R': (-0.0008300021, 0.0000490360, 0.0108296126),
+ 'cf_j_leg03_R': (0.0000000745, 0.0117738321, -0.0495891199),
+ 'cf_j_foot_R': (0.0000000447, -0.1070637517, -0.0073810909),
+ 'cf_j_toes_R': (0.0000000447, -0.1070637591, -0.0073810909),
+ 'cf_s_leg01_R': (0.0000001490, 0.0599634480, 0.0020939708),
+ 'cf_s_kneeB_R': (0.0000001490, 0.0599634498, 0.0020939708),
+ 'cf_s_waist02': (0.0000041698, 0.0000045616, 0.0999919176),
+ 'cf_s_leg_L': (0.1000121087, 0.0000034906, 0.0999964476),
+ 'cf_s_leg_R': (-0.1000037491, 0.0000056308, 0.0999873877),
+ 'cf_s_waist01': (0.0000000001, 0.0000000000, 0.0213849545),
+ 'cf_j_spine01': (0.0000000000, 0.0032969250, 0.0450000763),
+ 'cf_s_spine01': (0.0000000000, 0.0000000000, 0.0214849710),
+ 'cf_j_spine02': (0.0000000000, 0.0050841947, 0.0449999571),
+ 'cf_s_spine02': (0.0000000000, 0.0000659386, 0.0223850012),
+ 'cf_j_spine03': (0.0000064438, 0.0028000008, 0.0566434860),
+ 'cf_j_neck': (0.0000000000, 0.0000000000, 0.0325000286),
+ 'cf_j_head': (0.0000000000, 0.0001676232, 0.0251541138),
+ 'cf_s_head': (0.0000000000, 0.0001676232, 0.0251541138),
+ 'cf_s_neck': (0.0000000000, 0.0001676232, 0.0245041847),
+ 'cf_s_spine03': (0.0000000000, 0.0001676232, 0.0232850313),
+ 'cf_d_shoulder_L': (0.0001563374, 0.0002376232, 0.0241320133),
+ 'cf_j_shoulder_L': (0.0940132719, -0.0000001993, 0.0000000000),
+ 'cf_d_shoulder02_L': (0.0010964721, 0.0002376176, 0.0241320133),
+ 'cf_s_shoulder02_L': (0.0010964721, 0.0002376176, 0.0241320133),
+ 'cf_j_arm00_L': (0.1489044800, 0.0066715311, 0.0000000000),
+ 'cf_d_arm01_L': (0.0012971163, 0.0002268590, 0.0241320133),
+ 'cf_s_arm01_L': (0.0012968481, 0.0002330560, 0.0241320133),
+ 'cf_d_arm02_L': (0.0022885203, 0.0002723690, 0.0241320133),
+ 'cf_s_arm02_L': (0.0022885203, 0.0002723690, 0.0241320133),
+ 'cf_d_arm03_L': (0.0031349957, 0.0003089495, 0.0241320133),
+ 'cf_s_arm03_L': (0.0031350553, 0.0003072731, 0.0241320133),
+ 'cf_j_forearm01_L': (0.1090970039, 0.0050626770, 0.0000000000),
+ 'cf_d_forearm02_L': (0.0046593249, 0.0003493950, 0.0241320133),
+ 'cf_s_forearm02_L': (0.0046591461, 0.0003536306, 0.0241320133),
+ 'cf_d_wrist_L': (0.0055440068, 0.0003493391, 0.0241320133),
+ 'cf_s_wrist_L': (0.0250180364, -0.0004227310, 0.0000002384),
+ 'cf_d_hand_L': (0.0250180364, -0.0004227310, 0.0000002384),
+ 'cf_j_hand_L': (0.0061240196, 0.0003494397, 0.0241320133),
+ 'cf_s_hand_L': (0.0434948206, -0.0036587194, -0.0018885136),
+ 'cf_j_index01_L': (0.0308244824, -0.0027071098, -0.0026963949),
+ 'cf_j_index02_L': (0.0205492973, -0.0018050103, -0.0018019676),
+ 'cf_j_index03_L': (0.0205492973, -0.0018050103, -0.0018019676),
+ 'cf_j_little01_L': (0.0280875564, 0.0039625615, -0.0024572611),
+ 'cf_j_little02_L': (0.0152931809, 0.0021575317, -0.0013381243),
+ 'cf_j_little03_L': (0.0152931809, 0.0021575317, -0.0013381243),
+ 'cf_j_middle01_L': (0.0340363383, 0.0000000112, -0.0029777288),
+ 'cf_j_middle02_L': (0.0226911902, 0.0000002403, -0.0019830465),
+ 'cf_j_middle03_L': (0.0226911902, 0.0000002403, -0.0019830465),
+ 'cf_j_ring01_L': (0.0324101448, 0.0022749901, -0.0028355122),
+ 'cf_j_ring02_L': (0.0216068029, 0.0015166700, -0.0018904209),
+ 'cf_j_ring03_L': (0.0216068029, 0.0015166700, -0.0018904209),
+ 'cf_j_thumb01_L': (0.0274280310, -0.0195015902, -0.0048363209),
+ 'cf_j_thumb02_L': (0.0201677084, -0.0143394098, -0.0035561323),
+ 'cf_j_thumb03_L': (0.0201677084, -0.0143394079, -0.0035561323),
+ 'cf_s_elbo_L': (-0.0000313818, 0.0248819850, 0.0000001192),
+ 'cf_s_forearm01_L': (0.0004531145, -0.0307315015, 0.0000000000),
+ 'cf_s_elboback_L': (0.0004531145, -0.0307315006, 0.0000000000),
+ 'cf_d_bust00': (0.0000000000, 0.0001676232, 0.0232039690),
+ 'cf_s_bust00_R': (-0.0575999990, -0.0454922598, 0.0026999712),
+ 'cf_d_bust01_R': (-0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_j_bust01_R': (-0.0051772669, -0.0134591460, 0.0015155077),
+ 'cf_d_bust02_R': (-0.0003042668, 0.0004191138, 0.0232065916),
+ 'cf_j_bust02_R': (-0.0046416745, -0.0120668225, 0.0013588667),
+ 'cf_d_bust03_R': (-0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_j_bust03_R': (-0.0017852709, -0.0046411008, 0.0005227327),
+ 'cf_d_bnip01_R': (-0.0006545857, -0.0017017275, 0.0001918077),
+ 'cf_j_bnip02root_R': (-0.0048966482, -0.0061617792, 0.0016967058),
+ 'cf_s_bnip02_R': (-0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_j_bnip02_R': (-0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_s_bnip025_R': (-0.0083650872, -0.0086106881, 0.0029753447),
+ 'cf_s_bnip01_R': (-0.0004328042, 0.0000849515, 0.0232441425),
+ 'cf_s_bnip015_R': (-0.0008926094, -0.0023205206, 0.0002615452),
+ 'cf_s_bust03_R': (-0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_s_bust02_R': (-0.0003042668, 0.0004191138, 0.0232064724),
+ 'cf_s_bust01_R': (-0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_s_bust00_L': (0.0575999990, -0.0454922598, 0.0026999712),
+ 'cf_d_bust01_L': (0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_j_bust01_L': (0.0051772669, -0.0134591460, 0.0015155077),
+ 'cf_d_bust02_L': (0.0003042668, 0.0004191138, 0.0232065916),
+ 'cf_j_bust02_L': (0.0046416819, -0.0120668299, 0.0013588667),
+ 'cf_d_bust03_L': (0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_j_bust03_L': (0.0017852709, -0.0046411008, 0.0005227327),
+ 'cf_d_bnip01_L': (0.0006546006, -0.0017017350, 0.0001916885),
+ 'cf_j_bnip02root_L': (0.0049032941, -0.0061767399, 0.0016988516),
+ 'cf_s_bnip02_L': (0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_j_bnip02_L': (0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_s_bnip025_L': (0.0083783790, -0.0086406097, 0.0029795170),
+ 'cf_s_bnip01_L': (0.0004328042, 0.0000849515, 0.0232441425),
+ 'cf_s_bnip015_L': (0.0008926317, -0.0023205578, 0.0002613068),
+ 'cf_s_bust03_L': (0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_s_bust02_L': (0.0003042668, 0.0004191138, 0.0232064724),
+ 'cf_s_bust01_L': (0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_d_shoulder_R': (-0.0001560142, 0.0002376232, 0.0241320133),
+ 'cf_j_shoulder_R': (-0.0939531336, -0.0000126194, 0.0000000000),
+ 'cf_d_shoulder02_R': (-0.0010954589, 0.0002373699, 0.0241320133),
+ 'cf_s_shoulder02_R': (-0.0010954589, 0.0002373699, 0.0241320133),
+ 'cf_j_arm00_R': (-0.1495516151, 0.0065441933, 0.0000001192),
+ 'cf_d_arm01_R': (-0.0012957752, 0.0002367441, 0.0241320133),
+ 'cf_s_arm01_R': (-0.0012955368, 0.0002426561, 0.0241320133),
+ 'cf_d_arm02_R': (-0.0022943318, 0.0002824105, 0.0241320133),
+ 'cf_s_arm02_R': (-0.0022938997, 0.0002823994, 0.0241320133),
+ 'cf_d_arm03_R': (-0.0031512678, 0.0003276281, 0.0241320133),
+ 'cf_s_arm03_R': (-0.0031512082, 0.0003276169, 0.0241320133),
+ 'cf_j_forearm01_R': (-0.1111739874, 0.0051015355, -0.0000002384),
+ 'cf_s_elbo_R': (0.0000000298, 0.0251077451, 0.0000021458),
+ 'cf_s_forearm01_R': (-0.0000276864, -0.0304288915, -0.0000027418),
+ 'cf_s_elboback_R': (-0.0000276864, -0.0304288925, -0.0000027418),
+ 'cf_d_forearm02_R': (-0.0047625899, 0.0003447086, 0.0241320133),
+ 'cf_s_forearm02_R': (-0.0047624111, 0.0003491975, 0.0241320133),
+ 'cf_d_wrist_R': (-0.0055437088, 0.0003451817, 0.0241320133),
+ 'cf_s_wrist_R': (-0.0250178576, -0.0004224181, 0.0000003576),
+ 'cf_d_hand_R': (-0.0250178576, -0.0004224181, 0.0000003576),
+ 'cf_j_hand_R': (-0.0061238408, 0.0003451258, 0.0241320133),
+ 'cf_s_hand_R': (-0.0434947014, -0.0036587510, -0.0018894672),
+ 'cf_j_little01_R': (-0.0281152725, 0.0039664730, -0.0024597645),
+ 'cf_j_little02_R': (-0.0152906775, 0.0021577775, -0.0013365746),
+ 'cf_j_little03_R': (-0.0152906775, 0.0021577775, -0.0013365746),
+ 'cf_j_middle01_R': (-0.0340698957, -0.0000000205, -0.0029808283),
+ 'cf_j_middle02_R': (-0.0227140188, 0.0000003595, -0.0019767284),
+ 'cf_j_middle03_R': (-0.0227140188, 0.0000003595, -0.0019767284),
+ 'cf_j_ring01_R': (-0.0323968530, 0.0022740699, -0.0028343201),
+ 'cf_j_ring02_R': (-0.0215651989, 0.0015144981, -0.0018799305),
+ 'cf_j_ring03_R': (-0.0215651989, 0.0015144981, -0.0018799305),
+ 'cf_j_thumb01_R': (-0.0274280310, -0.0195016302, -0.0048363209),
+ 'cf_j_thumb02_R': (-0.0201677680, -0.0143393399, -0.0035561323),
+ 'cf_j_thumb03_R': (-0.0201677680, -0.0143393390, -0.0035561323),
+ 'cf_j_index01_R': (-0.0308635831, -0.0027105501, -0.0027000904),
+ 'cf_j_index02_R': (-0.0205416679, -0.0018021995, -0.0018149614),
+ 'cf_j_index03_R': (-0.0205416679, -0.0018021995, -0.0018149614),
+}
+
+# 使用方法:
+# 在 modifyarmature.py 的 modify_finger_bone_orientations 函数中
+# 将 reorient 函数替换为:
+#
+# from mathutils import Vector
+#
+# def reorient(bone):
+# if bone in better_fbx_bone_tails:
+# offset = Vector(better_fbx_bone_tails[bone])
+# armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head + offset
+# else:
+# # 如果骨骼不在字典中,使用原来的默认行为
+# height_adjust = Vector((0,0,0.1))
+# armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head + height_adjust
diff --git a/better_fbx_complete_bone_data.py b/better_fbx_complete_bone_data.py
new file mode 100644
index 0000000..c150e2b
--- /dev/null
+++ b/better_fbx_complete_bone_data.py
@@ -0,0 +1,1128 @@
+# Better FBX 完整骨骼数据
+# 包含头部位置、尾部偏移、Roll值和父子关系
+# 可以直接用于在 Blender 中重建完整的 181 个骨骼的骨架
+# 从 better_fbx 导入的 Koikatsu 模型骨架中提取
+
+from mathutils import Vector
+
+better_fbx_complete_bone_data = {
+ 'p_cf_body_00': {
+ 'head': (0.0000000000, 0.0000000000, 0.0000000000),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.5717499852),
+ 'roll': 0.0000000000,
+ 'parent': None
+ },
+ 'cf_o_root': {
+ 'head': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'roll': 0.0000000000,
+ 'parent': 'p_cf_body_00'
+ },
+ 'n_body': {
+ 'head': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_o_root'
+ },
+ 'o_body_a': {
+ 'head': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'roll': 0.0000000000,
+ 'parent': 'n_body'
+ },
+ 'cf_j_root': {
+ 'head': (0.0000000000, 0.0000000000, 0.0000000000),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0099999998),
+ 'roll': 0.0000000000,
+ 'parent': 'p_cf_body_00'
+ },
+ 'cf_n_height': {
+ 'head': (0.0000000000, 0.0000000000, 0.0000000000),
+ 'tail_offset': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_root'
+ },
+ 'cf_j_hips': {
+ 'head': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_n_height'
+ },
+ 'cf_j_waist01': {
+ 'head': (0.0000000000, 0.0000000000, 1.1384999752),
+ 'tail_offset': (0.0000000050, 0.0082969246, -0.0676312447),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_hips'
+ },
+ 'cf_j_waist02': {
+ 'head': (0.0000000000, 0.0165938493, 1.0032373667),
+ 'tail_offset': (0.0000061035, 0.0022072941, -0.0269574523),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_waist01'
+ },
+ 'cf_d_ana': {
+ 'head': (0.0000000100, 0.0520442016, 0.9285330772),
+ 'tail_offset': (0.0000000011, -0.0037057437, 0.0183483958),
+ 'roll': 0.0000005385,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_j_ana': {
+ 'head': (0.0000000100, 0.0520441495, 0.9285331368),
+ 'tail_offset': (0.0000000000, 0.0004356094, -0.0009342432),
+ 'roll': 0.0000004141,
+ 'parent': 'cf_d_ana'
+ },
+ 'cf_s_ana': {
+ 'head': (0.0000000100, 0.0524797589, 0.9275988936),
+ 'tail_offset': (0.0000000000, 0.0004356094, -0.0009342432),
+ 'roll': 0.0000004141,
+ 'parent': 'cf_j_ana'
+ },
+ 'cf_d_kokan': {
+ 'head': (0.0000000300, -0.0034061500, 0.9392373562),
+ 'tail_offset': (0.0000000003, -0.0000340615, 0.0193923712),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_j_kokan': {
+ 'head': (0.0000000300, -0.0034062001, 0.9392373562),
+ 'tail_offset': (0.0000000003, -0.0000340852, 0.0193923712),
+ 'roll': 0.0000007500,
+ 'parent': 'cf_d_kokan'
+ },
+ 'cf_d_siri_L': {
+ 'head': (0.0700005591, 0.0165946409, 1.0032392740),
+ 'tail_offset': (0.0006995648, 0.0001653936, 0.0200324059),
+ 'roll': -0.0000000013,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_d_siri01_L': {
+ 'head': (0.0700005591, 0.0165946409, 1.0032392740),
+ 'tail_offset': (-0.0000022426, 0.0499999970, 0.0000028610),
+ 'roll': 0.0000004936,
+ 'parent': 'cf_d_siri_L'
+ },
+ 'cf_j_siri_L': {
+ 'head': (0.0699983165, 0.0665946379, 1.0032421350),
+ 'tail_offset': (0.0006995425, 0.0006653816, 0.0200324059),
+ 'roll': 0.0000004936,
+ 'parent': 'cf_d_siri01_L'
+ },
+ 'cf_s_siri_L': {
+ 'head': (0.0699983090, 0.0665946379, 1.0032421350),
+ 'tail_offset': (0.0006995425, 0.0006653816, 0.0200324059),
+ 'roll': 0.0000004936,
+ 'parent': 'cf_j_siri_L'
+ },
+ 'cf_d_siri_R': {
+ 'head': (-0.0700001270, 0.0165937506, 1.0032376051),
+ 'tail_offset': (-0.0007000044, 0.0001655202, 0.0200324059),
+ 'roll': -0.0000000600,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_d_siri01_R': {
+ 'head': (-0.0700001270, 0.0165937506, 1.0032376051),
+ 'tail_offset': (0.0000000000, 0.0499999858, 0.0000020266),
+ 'roll': -0.0000000600,
+ 'parent': 'cf_d_siri_R'
+ },
+ 'cf_j_siri_R': {
+ 'head': (-0.0700001270, 0.0665937364, 1.0032396317),
+ 'tail_offset': (-0.0007000044, 0.0006655231, 0.0200324059),
+ 'roll': -0.0000000600,
+ 'parent': 'cf_d_siri01_R'
+ },
+ 'cf_s_siri_R': {
+ 'head': (-0.0700001270, 0.0665937364, 1.0032396317),
+ 'tail_offset': (-0.0007000044, 0.0006655082, 0.0200324059),
+ 'roll': 0.0000001400,
+ 'parent': 'cf_j_siri_R'
+ },
+ 'cf_j_thigh00_L': {
+ 'head': (0.0830422416, 0.0165938493, 0.9782373309),
+ 'tail_offset': (0.0000000000, 0.0045367014, -0.2599085569),
+ 'roll': -0.0000257950,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_d_thigh01_L': {
+ 'head': (0.0830422416, 0.0165938493, 0.9782373309),
+ 'tail_offset': (0.0008304268, -0.0000085849, 0.0197808743),
+ 'roll': 0.0000004869,
+ 'parent': 'cf_j_thigh00_L'
+ },
+ 'cf_s_thigh01_L': {
+ 'head': (0.0830422416, 0.0165938493, 0.9782374501),
+ 'tail_offset': (0.0008304268, -0.0000085849, 0.0197808743),
+ 'roll': 0.0000004869,
+ 'parent': 'cf_d_thigh01_L'
+ },
+ 'cf_d_thigh02_L': {
+ 'head': (0.0830421895, 0.0205690805, 0.7504963279),
+ 'tail_offset': (0.0008304268, 0.0000311676, 0.0175034404),
+ 'roll': 0.0000007420,
+ 'parent': 'cf_j_thigh00_L'
+ },
+ 'cf_s_thigh02_L': {
+ 'head': (0.0830421820, 0.0205690805, 0.7504962683),
+ 'tail_offset': (0.0008304268, 0.0000311676, 0.0175034404),
+ 'roll': 0.0000007420,
+ 'parent': 'cf_d_thigh02_L'
+ },
+ 'cf_d_thigh03_L': {
+ 'head': (0.0830421671, 0.0227373894, 0.6262739897),
+ 'tail_offset': (0.0008304268, 0.0000528507, 0.0162612200),
+ 'roll': 0.0000009920,
+ 'parent': 'cf_j_thigh00_L'
+ },
+ 'cf_s_thigh03_L': {
+ 'head': (0.0830421597, 0.0227373894, 0.6262739897),
+ 'tail_offset': (0.0008304268, 0.0000528507, 0.0162612200),
+ 'roll': 0.0000009920,
+ 'parent': 'cf_d_thigh03_L'
+ },
+ 'cf_j_leg01_L': {
+ 'head': (0.0830423534, 0.0246218797, 0.5183074474),
+ 'tail_offset': (-0.0000001565, -0.0001882389, -0.2238392234),
+ 'roll': -0.0000262570,
+ 'parent': 'cf_j_thigh00_L'
+ },
+ 'cf_d_kneeF_L': {
+ 'head': (0.0830413178, -0.0153537504, 0.5169112682),
+ 'tail_offset': (-0.0000010356, -0.0399756283, -0.0013961792),
+ 'roll': -0.0000262570,
+ 'parent': 'cf_j_leg01_L'
+ },
+ 'cf_d_leg02_L': {
+ 'head': (0.0830424130, 0.0332937911, 0.2699765563),
+ 'tail_offset': (0.0008304194, -0.0000160560, 0.0126936734),
+ 'roll': -0.0000132481,
+ 'parent': 'cf_j_leg01_L'
+ },
+ 'cf_s_leg02_L': {
+ 'head': (0.0830423832, 0.0332937911, 0.2699761987),
+ 'tail_offset': (0.0004141256, 0.0006808266, -0.0072855055),
+ 'roll': -3.1033129692,
+ 'parent': 'cf_d_leg02_L'
+ },
+ 'cf_d_leg03_L': {
+ 'head': (0.0830424502, 0.0398031399, 0.0835729465),
+ 'tail_offset': (0.0008304194, 0.0000490360, 0.0108296350),
+ 'roll': -0.0000002392,
+ 'parent': 'cf_j_leg01_L'
+ },
+ 'cf_s_leg03_L': {
+ 'head': (0.0830424428, 0.0398032106, 0.0835727602),
+ 'tail_offset': (0.0008304194, 0.0000490360, 0.0108296350),
+ 'roll': -0.0000002392,
+ 'parent': 'cf_d_leg03_L'
+ },
+ 'cf_j_leg03_L': {
+ 'head': (0.0830424502, 0.0398031399, 0.0835729465),
+ 'tail_offset': (0.0000000000, 0.0117738210, -0.0496065579),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_leg01_L'
+ },
+ 'cf_j_foot_L': {
+ 'head': (0.0830424502, 0.0515769608, 0.0339663886),
+ 'tail_offset': (-0.0000000075, -0.1070634499, -0.0073633678),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_leg03_L'
+ },
+ 'cf_j_toes_L': {
+ 'head': (0.0830424428, -0.0554864891, 0.0266030207),
+ 'tail_offset': (-0.0000000075, -0.1070634536, -0.0073633678),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_foot_L'
+ },
+ 'cf_s_leg01_L': {
+ 'head': (0.0830423608, 0.0246218797, 0.5183073282),
+ 'tail_offset': (0.0000015497, 0.0599634517, 0.0020940304),
+ 'roll': -0.0000262570,
+ 'parent': 'cf_j_leg01_L'
+ },
+ 'cf_s_kneeB_L': {
+ 'head': (0.0830439106, 0.0845853314, 0.5204013586),
+ 'tail_offset': (0.0000015497, 0.0599634424, 0.0020940304),
+ 'roll': -0.0000262570,
+ 'parent': 'cf_s_leg01_L'
+ },
+ 'cf_j_thigh00_R': {
+ 'head': (-0.0829999968, 0.0165938493, 0.9782373309),
+ 'tail_offset': (0.0000000149, 0.0045371708, -0.2599344850),
+ 'roll': -0.0000023800,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_d_thigh01_R': {
+ 'head': (-0.0829999968, 0.0165955909, 0.9781373739),
+ 'tail_offset': (-0.0008299947, -0.0000085682, 0.0197798610),
+ 'roll': 0.0000005021,
+ 'parent': 'cf_j_thigh00_R'
+ },
+ 'cf_s_thigh01_R': {
+ 'head': (-0.0829999894, 0.0165956002, 0.9781373739),
+ 'tail_offset': (-0.0008299947, -0.0000085682, 0.0197798610),
+ 'roll': 0.0000004819,
+ 'parent': 'cf_d_thigh01_R'
+ },
+ 'cf_d_thigh02_R': {
+ 'head': (-0.0829999968, 0.0205690991, 0.7504955530),
+ 'tail_offset': (-0.0008299947, 0.0000311676, 0.0175034404),
+ 'roll': 0.0000007572,
+ 'parent': 'cf_j_thigh00_R'
+ },
+ 'cf_s_thigh02_R': {
+ 'head': (-0.0829999894, 0.0205690991, 0.7504956126),
+ 'tail_offset': (-0.0008299947, 0.0000311676, 0.0175034404),
+ 'roll': 0.0000007370,
+ 'parent': 'cf_d_thigh02_R'
+ },
+ 'cf_d_thigh03_R': {
+ 'head': (-0.0829999968, 0.0227374099, 0.6262727380),
+ 'tail_offset': (-0.0008299947, 0.0000528507, 0.0162612200),
+ 'roll': 0.0000010122,
+ 'parent': 'cf_j_thigh00_R'
+ },
+ 'cf_s_thigh03_R': {
+ 'head': (-0.0829999894, 0.0227374099, 0.6262726784),
+ 'tail_offset': (-0.0008299947, 0.0000528507, 0.0162612200),
+ 'roll': 0.0000009920,
+ 'parent': 'cf_d_thigh03_R'
+ },
+ 'cf_j_leg01_R': {
+ 'head': (-0.0829999372, 0.0246219803, 0.5183057785),
+ 'tail_offset': (0.0000000000, -0.0001882352, -0.2238395214),
+ 'roll': -0.0000025280,
+ 'parent': 'cf_j_thigh00_R'
+ },
+ 'cf_d_kneeF_R': {
+ 'head': (-0.0830000266, -0.0153536396, 0.5169096589),
+ 'tail_offset': (-0.0000000894, -0.0399756199, -0.0013961196),
+ 'roll': -0.0000025484,
+ 'parent': 'cf_j_leg01_R'
+ },
+ 'cf_d_leg02_R': {
+ 'head': (-0.0829999074, 0.0332938693, 0.2699745595),
+ 'tail_offset': (-0.0008300021, -0.0000160560, 0.0126936436),
+ 'roll': -0.0000016877,
+ 'parent': 'cf_j_leg01_R'
+ },
+ 'cf_s_leg02_R': {
+ 'head': (-0.0829998925, 0.0332938805, 0.2699744105),
+ 'tail_offset': (-0.0008300021, -0.0000160560, 0.0126936436),
+ 'roll': -0.0000017081,
+ 'parent': 'cf_d_leg02_R'
+ },
+ 'cf_d_leg03_R': {
+ 'head': (-0.0829998925, 0.0398032591, 0.0835707411),
+ 'tail_offset': (-0.0008300021, 0.0000490360, 0.0108296126),
+ 'roll': -0.0000025680,
+ 'parent': 'cf_j_leg01_R'
+ },
+ 'cf_s_leg03_R': {
+ 'head': (-0.0829998776, 0.0398032703, 0.0835706517),
+ 'tail_offset': (-0.0008300021, 0.0000490360, 0.0108296126),
+ 'roll': -0.0000025884,
+ 'parent': 'cf_d_leg03_R'
+ },
+ 'cf_j_leg03_R': {
+ 'head': (-0.0829998776, 0.0398032591, 0.0835707411),
+ 'tail_offset': (0.0000000745, 0.0117738321, -0.0495891199),
+ 'roll': 0.0000023126,
+ 'parent': 'cf_j_leg01_R'
+ },
+ 'cf_j_foot_R': {
+ 'head': (-0.0829998031, 0.0515770912, 0.0339816213),
+ 'tail_offset': (0.0000000447, -0.1070637517, -0.0073810909),
+ 'roll': 0.0000023200,
+ 'parent': 'cf_j_leg03_R'
+ },
+ 'cf_j_toes_R': {
+ 'head': (-0.0829997584, -0.0554866605, 0.0266005304),
+ 'tail_offset': (0.0000000447, -0.1070637591, -0.0073810909),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_foot_R'
+ },
+ 'cf_s_leg01_R': {
+ 'head': (-0.0829999372, 0.0246219803, 0.5183055997),
+ 'tail_offset': (0.0000001490, 0.0599634480, 0.0020939708),
+ 'roll': -0.0000025484,
+ 'parent': 'cf_j_leg01_R'
+ },
+ 'cf_s_kneeB_R': {
+ 'head': (-0.0829997882, 0.0845854282, 0.5203995705),
+ 'tail_offset': (0.0000001490, 0.0599634498, 0.0020939708),
+ 'roll': -0.0000025484,
+ 'parent': 'cf_s_leg01_R'
+ },
+ 'cf_s_waist02': {
+ 'head': (0.0000000100, 0.0165938493, 1.0032373667),
+ 'tail_offset': (0.0000041698, 0.0000045616, 0.0999919176),
+ 'roll': 0.0000000600,
+ 'parent': 'cf_j_waist02'
+ },
+ 'cf_s_leg_L': {
+ 'head': (0.1000121087, 0.0165973399, 1.1032338142),
+ 'tail_offset': (0.1000121087, 0.0000034906, 0.0999964476),
+ 'roll': 0.0000959927,
+ 'parent': 'cf_s_waist02'
+ },
+ 'cf_s_leg_R': {
+ 'head': (-0.1000037491, 0.0165994801, 1.1032247543),
+ 'tail_offset': (-0.1000037491, 0.0000056308, 0.0999873877),
+ 'roll': -0.0000147290,
+ 'parent': 'cf_s_waist02'
+ },
+ 'cf_s_waist01': {
+ 'head': (0.0000000100, 0.0000000000, 1.1384999752),
+ 'tail_offset': (0.0000000001, 0.0000000000, 0.0213849545),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_waist01'
+ },
+ 'cf_j_spine01': {
+ 'head': (0.0000000000, 0.0000000000, 1.1484999657),
+ 'tail_offset': (0.0000000000, 0.0032969250, 0.0450000763),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_hips'
+ },
+ 'cf_s_spine01': {
+ 'head': (0.0000000000, 0.0000000000, 1.1484999657),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0214849710),
+ 'roll': 0.0000002100,
+ 'parent': 'cf_j_spine01'
+ },
+ 'cf_j_spine02': {
+ 'head': (0.0000000000, 0.0065938500, 1.2384999990),
+ 'tail_offset': (0.0000000000, 0.0050841947, 0.0449999571),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine01'
+ },
+ 'cf_s_spine02': {
+ 'head': (0.0000000000, 0.0065938500, 1.2384999990),
+ 'tail_offset': (0.0000000000, 0.0000659386, 0.0223850012),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine02'
+ },
+ 'cf_j_spine03': {
+ 'head': (0.0000000000, 0.0167622399, 1.3284999132),
+ 'tail_offset': (0.0000064438, 0.0028000008, 0.0566434860),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine02'
+ },
+ 'cf_j_neck': {
+ 'head': (0.0000000000, 0.0167622399, 1.4504170418),
+ 'tail_offset': (0.0000000000, 0.0000000000, 0.0325000286),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine03'
+ },
+ 'cf_j_head': {
+ 'head': (0.0000000000, 0.0167622399, 1.5154169798),
+ 'tail_offset': (0.0000000000, 0.0001676232, 0.0251541138),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_neck'
+ },
+ 'cf_s_head': {
+ 'head': (0.0000000000, 0.0167622399, 1.5154169798),
+ 'tail_offset': (0.0000000000, 0.0001676232, 0.0251541138),
+ 'roll': 0.0000002100,
+ 'parent': 'cf_j_head'
+ },
+ 'cf_s_neck': {
+ 'head': (0.0000000000, 0.0167622399, 1.4504170418),
+ 'tail_offset': (0.0000000000, 0.0001676232, 0.0245041847),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_neck'
+ },
+ 'cf_s_spine03': {
+ 'head': (0.0000000000, 0.0167622399, 1.3284999132),
+ 'tail_offset': (0.0000000000, 0.0001676232, 0.0232850313),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine03'
+ },
+ 'cf_d_shoulder_L': {
+ 'head': (0.0156336892, 0.0237622391, 1.4131999016),
+ 'tail_offset': (0.0001563374, 0.0002376232, 0.0241320133),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine03'
+ },
+ 'cf_j_shoulder_L': {
+ 'head': (0.0156336892, 0.0237622391, 1.4131999016),
+ 'tail_offset': (0.0940132719, -0.0000001993, 0.0000000000),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_d_shoulder_L'
+ },
+ 'cf_d_shoulder02_L': {
+ 'head': (0.1096468568, 0.0237618405, 1.4131999016),
+ 'tail_offset': (0.0010964721, 0.0002376176, 0.0241320133),
+ 'roll': -0.0000057600,
+ 'parent': 'cf_j_shoulder_L'
+ },
+ 'cf_s_shoulder02_L': {
+ 'head': (0.1096468568, 0.0237618405, 1.4132000208),
+ 'tail_offset': (0.0010964721, 0.0002376176, 0.0241320133),
+ 'roll': -0.0000057600,
+ 'parent': 'cf_d_shoulder02_L'
+ },
+ 'cf_j_arm00_L': {
+ 'head': (0.1096470580, 0.0237622391, 1.4131999016),
+ 'tail_offset': (0.1489044800, 0.0066715311, 0.0000000000),
+ 'roll': 0.0439909510,
+ 'parent': 'cf_j_shoulder_L'
+ },
+ 'cf_d_arm01_L': {
+ 'head': (0.1296277046, 0.0246417802, 1.4131999016),
+ 'tail_offset': (0.0012971163, 0.0002268590, 0.0241320133),
+ 'roll': 0.0428693146,
+ 'parent': 'cf_j_arm00_L'
+ },
+ 'cf_s_arm01_L': {
+ 'head': (0.1296011955, 0.0252614897, 1.4132013321),
+ 'tail_offset': (0.0012968481, 0.0002330560, 0.0241320133),
+ 'roll': 0.0532971583,
+ 'parent': 'cf_d_arm01_L'
+ },
+ 'cf_d_arm02_L': {
+ 'head': (0.2287678421, 0.0292154495, 1.4131999016),
+ 'tail_offset': (0.0022885203, 0.0002723690, 0.0241320133),
+ 'roll': 0.0428703651,
+ 'parent': 'cf_j_arm00_L'
+ },
+ 'cf_s_arm02_L': {
+ 'head': (0.2287678570, 0.0292154495, 1.4131997824),
+ 'tail_offset': (0.0022885203, 0.0002723690, 0.0241320133),
+ 'roll': 0.0480841845,
+ 'parent': 'cf_d_arm02_L'
+ },
+ 'cf_d_arm03_L': {
+ 'head': (0.3134112954, 0.0329414085, 1.4131999016),
+ 'tail_offset': (0.0031349957, 0.0003089495, 0.0241320133),
+ 'roll': 0.0428732038,
+ 'parent': 'cf_j_arm00_L'
+ },
+ 'cf_s_arm03_L': {
+ 'head': (0.3134184778, 0.0327739306, 1.4131997824),
+ 'tail_offset': (0.0031350553, 0.0003072731, 0.0241320133),
+ 'roll': 0.0428689606,
+ 'parent': 'cf_d_arm03_L'
+ },
+ 'cf_j_forearm01_L': {
+ 'head': (0.3623993397, 0.0349364392, 1.4131999016),
+ 'tail_offset': (0.1090970039, 0.0050626770, 0.0000000000),
+ 'roll': 0.0000298800,
+ 'parent': 'cf_j_arm00_L'
+ },
+ 'cf_d_forearm02_L': {
+ 'head': (0.4659337103, 0.0349395312, 1.4131999016),
+ 'tail_offset': (0.0046593249, 0.0003493950, 0.0241320133),
+ 'roll': 0.0000298800,
+ 'parent': 'cf_j_forearm01_L'
+ },
+ 'cf_s_forearm02_L': {
+ 'head': (0.4659157991, 0.0353630111, 1.4132000208),
+ 'tail_offset': (0.0046591461, 0.0003536306, 0.0241320133),
+ 'roll': 0.0000298900,
+ 'parent': 'cf_d_forearm02_L'
+ },
+ 'cf_d_wrist_L': {
+ 'head': (0.5543993115, 0.0349338204, 1.4131999016),
+ 'tail_offset': (0.0055440068, 0.0003493391, 0.0241320133),
+ 'roll': 0.0000298800,
+ 'parent': 'cf_j_forearm01_L'
+ },
+ 'cf_s_wrist_L': {
+ 'head': (0.5543813109, 0.0353573002, 1.4131997824),
+ 'tail_offset': (0.0250180364, -0.0004227310, 0.0000002384),
+ 'roll': 0.0000298800,
+ 'parent': 'cf_d_wrist_L'
+ },
+ 'cf_d_hand_L': {
+ 'head': (0.5793993473, 0.0349345692, 1.4132000208),
+ 'tail_offset': (0.0250180364, -0.0004227310, 0.0000002384),
+ 'roll': 0.0000298800,
+ 'parent': 'cf_s_wrist_L'
+ },
+ 'cf_j_hand_L': {
+ 'head': (0.6123993397, 0.0349439085, 1.4131999016),
+ 'tail_offset': (0.0061240196, 0.0003494397, 0.0241320133),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_forearm01_L'
+ },
+ 'cf_s_hand_L': {
+ 'head': (0.6123992801, 0.0349439085, 1.4132000208),
+ 'tail_offset': (0.0434948206, -0.0036587194, -0.0018885136),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_hand_L'
+ },
+ 'cf_j_index01_L': {
+ 'head': (0.6684081554, 0.0140276598, 1.4142888784),
+ 'tail_offset': (0.0308244824, -0.0027071098, -0.0026963949),
+ 'roll': -0.0063064583,
+ 'parent': 'cf_s_hand_L'
+ },
+ 'cf_j_index02_L': {
+ 'head': (0.6992326379, 0.0113205500, 1.4115924835),
+ 'tail_offset': (0.0205492973, -0.0018050103, -0.0018019676),
+ 'roll': -0.0061317827,
+ 'parent': 'cf_j_index01_L'
+ },
+ 'cf_j_index03_L': {
+ 'head': (0.7197819352, 0.0095155397, 1.4097905159),
+ 'tail_offset': (0.0205492973, -0.0018050103, -0.0018019676),
+ 'roll': -0.0060734837,
+ 'parent': 'cf_j_index02_L'
+ },
+ 'cf_j_little01_L': {
+ 'head': (0.6591357589, 0.0573692396, 1.4077364206),
+ 'tail_offset': (0.0280875564, 0.0039625615, -0.0024572611),
+ 'roll': 0.1925563514,
+ 'parent': 'cf_s_hand_L'
+ },
+ 'cf_j_little02_L': {
+ 'head': (0.6872233152, 0.0613318011, 1.4052791595),
+ 'tail_offset': (0.0152931809, 0.0021575317, -0.0013381243),
+ 'roll': 0.1925694495,
+ 'parent': 'cf_j_little01_L'
+ },
+ 'cf_j_little03_L': {
+ 'head': (0.7025164962, 0.0634893328, 1.4039410353),
+ 'tail_offset': (0.0152931809, 0.0021575317, -0.0013381243),
+ 'roll': 0.1925694495,
+ 'parent': 'cf_j_little02_L'
+ },
+ 'cf_j_middle01_L': {
+ 'head': (0.6709137559, 0.0295578092, 1.4168773890),
+ 'tail_offset': (0.0340363383, 0.0000000112, -0.0029777288),
+ 'roll': 0.0872664526,
+ 'parent': 'cf_s_hand_L'
+ },
+ 'cf_j_middle02_L': {
+ 'head': (0.7049500942, 0.0295578204, 1.4138996601),
+ 'tail_offset': (0.0226911902, 0.0000002403, -0.0019830465),
+ 'roll': 0.0871735811,
+ 'parent': 'cf_j_middle01_L'
+ },
+ 'cf_j_middle03_L': {
+ 'head': (0.7276412845, 0.0295580607, 1.4119166136),
+ 'tail_offset': (0.0226911902, 0.0000002403, -0.0019830465),
+ 'roll': 0.0871735811,
+ 'parent': 'cf_j_middle02_L'
+ },
+ 'cf_j_ring01_L': {
+ 'head': (0.6663091183, 0.0442365594, 1.4139549732),
+ 'tail_offset': (0.0324101448, 0.0022749901, -0.0028355122),
+ 'roll': 0.1479601413,
+ 'parent': 'cf_s_hand_L'
+ },
+ 'cf_j_ring02_L': {
+ 'head': (0.6987192631, 0.0465115495, 1.4111194611),
+ 'tail_offset': (0.0216068029, 0.0015166700, -0.0018904209),
+ 'roll': 0.1479601413,
+ 'parent': 'cf_j_ring01_L'
+ },
+ 'cf_j_ring03_L': {
+ 'head': (0.7203260660, 0.0480282195, 1.4092290401),
+ 'tail_offset': (0.0216068029, 0.0015166700, -0.0018904209),
+ 'roll': 0.1479601413,
+ 'parent': 'cf_j_ring02_L'
+ },
+ 'cf_j_thumb01_L': {
+ 'head': (0.6147037148, 0.0112346699, 1.4036999941),
+ 'tail_offset': (0.0274280310, -0.0195015902, -0.0048363209),
+ 'roll': -2.9670588970,
+ 'parent': 'cf_s_hand_L'
+ },
+ 'cf_j_thumb02_L': {
+ 'head': (0.6421317458, -0.0082669202, 1.3988636732),
+ 'tail_offset': (0.0201677084, -0.0143394098, -0.0035561323),
+ 'roll': -2.9670588970,
+ 'parent': 'cf_j_thumb01_L'
+ },
+ 'cf_j_thumb03_L': {
+ 'head': (0.6622994542, -0.0226063300, 1.3953075409),
+ 'tail_offset': (0.0201677084, -0.0143394079, -0.0035561323),
+ 'roll': -2.9670588970,
+ 'parent': 'cf_j_thumb02_L'
+ },
+ 'cf_s_elbo_L': {
+ 'head': (0.3623679578, 0.0598184206, 1.4132000208),
+ 'tail_offset': (-0.0000313818, 0.0248819850, 0.0000001192),
+ 'roll': 0.0008395802,
+ 'parent': 'cf_j_forearm01_L'
+ },
+ 'cf_s_forearm01_L': {
+ 'head': (0.3623813689, 0.0353599116, 1.4132000208),
+ 'tail_offset': (0.0004531145, -0.0307315015, 0.0000000000),
+ 'roll': 0.0000298900,
+ 'parent': 'cf_j_forearm01_L'
+ },
+ 'cf_s_elboback_L': {
+ 'head': (0.3628344834, 0.0046284101, 1.4132000208),
+ 'tail_offset': (0.0004531145, -0.0307315006, 0.0000000000),
+ 'roll': 0.0008395802,
+ 'parent': 'cf_s_forearm01_L'
+ },
+ 'cf_d_bust00': {
+ 'head': (0.0000000000, 0.0167622399, 1.3203999996),
+ 'tail_offset': (0.0000000000, 0.0001676232, 0.0232039690),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine03'
+ },
+ 'cf_s_bust00_R': {
+ 'head': (0.0000000000, 0.0167622399, 1.3203999996),
+ 'tail_offset': (-0.0575999990, -0.0454922598, 0.0026999712),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_d_bust00'
+ },
+ 'cf_d_bust01_R': {
+ 'head': (-0.0575999990, -0.0287300199, 1.3230999708),
+ 'tail_offset': (-0.0002007224, 0.0006882958, 0.0231761932),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_s_bust00_R'
+ },
+ 'cf_j_bust01_R': {
+ 'head': (-0.0575999990, -0.0287300199, 1.3230999708),
+ 'tail_offset': (-0.0051772669, -0.0134591460, 0.0015155077),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_d_bust01_R'
+ },
+ 'cf_d_bust02_R': {
+ 'head': (-0.0679545328, -0.0556483082, 1.3261312246),
+ 'tail_offset': (-0.0003042668, 0.0004191138, 0.0232065916),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_j_bust01_R'
+ },
+ 'cf_j_bust02_R': {
+ 'head': (-0.0679545328, -0.0556483082, 1.3261312246),
+ 'tail_offset': (-0.0046416745, -0.0120668225, 0.0013588667),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_d_bust02_R'
+ },
+ 'cf_d_bust03_R': {
+ 'head': (-0.0772378966, -0.0797819495, 1.3288489580),
+ 'tail_offset': (-0.0003971010, 0.0001777783, 0.0232336521),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_j_bust02_R'
+ },
+ 'cf_j_bust03_R': {
+ 'head': (-0.0772378966, -0.0797819495, 1.3288489580),
+ 'tail_offset': (-0.0017852709, -0.0046411008, 0.0005227327),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_d_bust03_R'
+ },
+ 'cf_d_bnip01_R': {
+ 'head': (-0.0808084309, -0.0890641212, 1.3298943043),
+ 'tail_offset': (-0.0006545857, -0.0017017275, 0.0001918077),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_j_bust03_R'
+ },
+ 'cf_j_bnip02root_R': {
+ 'head': (-0.0818795711, -0.0918487534, 1.3302081823),
+ 'tail_offset': (-0.0048966482, -0.0061617792, 0.0016967058),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_d_bnip01_R'
+ },
+ 'cf_s_bnip02_R': {
+ 'head': (-0.0833077803, -0.0955616236, 1.3306262493),
+ 'tail_offset': (-0.0004578009, 0.0000199825, 0.0232515335),
+ 'roll': -0.3313451409,
+ 'parent': 'cf_j_bnip02root_R'
+ },
+ 'cf_j_bnip02_R': {
+ 'head': (-0.0833077803, -0.0955616236, 1.3306261301),
+ 'tail_offset': (-0.0004578009, 0.0000199825, 0.0232515335),
+ 'roll': -0.3313450813,
+ 'parent': 'cf_s_bnip02_R'
+ },
+ 'cf_s_bnip025_R': {
+ 'head': (-0.0902446583, -0.1004594415, 1.3331835270),
+ 'tail_offset': (-0.0083650872, -0.0086106881, 0.0029753447),
+ 'roll': -0.3000984490,
+ 'parent': 'cf_j_bnip02root_R'
+ },
+ 'cf_s_bnip01_R': {
+ 'head': (-0.0808084309, -0.0890641287, 1.3298943043),
+ 'tail_offset': (-0.0004328042, 0.0000849515, 0.0232441425),
+ 'roll': -0.3313450813,
+ 'parent': 'cf_d_bnip01_R'
+ },
+ 'cf_s_bnip015_R': {
+ 'head': (-0.0817010403, -0.0913846418, 1.3301558495),
+ 'tail_offset': (-0.0008926094, -0.0023205206, 0.0002615452),
+ 'roll': -0.3313450813,
+ 'parent': 'cf_d_bnip01_R'
+ },
+ 'cf_s_bust03_R': {
+ 'head': (-0.0772379115, -0.0797819793, 1.3288490772),
+ 'tail_offset': (-0.0003971010, 0.0001777783, 0.0232336521),
+ 'roll': -0.3313450813,
+ 'parent': 'cf_j_bust03_R'
+ },
+ 'cf_s_bust02_R': {
+ 'head': (-0.0679545179, -0.0556483082, 1.3261313438),
+ 'tail_offset': (-0.0003042668, 0.0004191138, 0.0232064724),
+ 'roll': -0.3313450813,
+ 'parent': 'cf_j_bust02_R'
+ },
+ 'cf_s_bust01_R': {
+ 'head': (-0.0575999990, -0.0287300199, 1.3230998516),
+ 'tail_offset': (-0.0002007224, 0.0006882958, 0.0231761932),
+ 'roll': -0.3313450813,
+ 'parent': 'cf_j_bust01_R'
+ },
+ 'cf_s_bust00_L': {
+ 'head': (0.0000000000, 0.0167622399, 1.3203999996),
+ 'tail_offset': (0.0575999990, -0.0454922598, 0.0026999712),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_d_bust00'
+ },
+ 'cf_d_bust01_L': {
+ 'head': (0.0575999990, -0.0287300199, 1.3230999708),
+ 'tail_offset': (0.0002007224, 0.0006882958, 0.0231761932),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_s_bust00_L'
+ },
+ 'cf_j_bust01_L': {
+ 'head': (0.0575999990, -0.0287300199, 1.3230999708),
+ 'tail_offset': (0.0051772669, -0.0134591460, 0.0015155077),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_d_bust01_L'
+ },
+ 'cf_d_bust02_L': {
+ 'head': (0.0679545328, -0.0556483082, 1.3261312246),
+ 'tail_offset': (0.0003042668, 0.0004191138, 0.0232065916),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_j_bust01_L'
+ },
+ 'cf_j_bust02_L': {
+ 'head': (0.0679545328, -0.0556483082, 1.3261312246),
+ 'tail_offset': (0.0046416819, -0.0120668299, 0.0013588667),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_d_bust02_L'
+ },
+ 'cf_d_bust03_L': {
+ 'head': (0.0772378966, -0.0797819495, 1.3288489580),
+ 'tail_offset': (0.0003971010, 0.0001777783, 0.0232336521),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_j_bust02_L'
+ },
+ 'cf_j_bust03_L': {
+ 'head': (0.0772378966, -0.0797819495, 1.3288489580),
+ 'tail_offset': (0.0017852709, -0.0046411008, 0.0005227327),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_d_bust03_L'
+ },
+ 'cf_d_bnip01_L': {
+ 'head': (0.0808084309, -0.0890641212, 1.3298943043),
+ 'tail_offset': (0.0006546006, -0.0017017350, 0.0001916885),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_j_bust03_L'
+ },
+ 'cf_j_bnip02root_L': {
+ 'head': (0.0818795934, -0.0918487683, 1.3302078247),
+ 'tail_offset': (0.0049032941, -0.0061767399, 0.0016988516),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_d_bnip01_L'
+ },
+ 'cf_s_bnip02_L': {
+ 'head': (0.0833078027, -0.0955616385, 1.3306260109),
+ 'tail_offset': (0.0004578009, 0.0000199825, 0.0232515335),
+ 'roll': 0.3313451409,
+ 'parent': 'cf_j_bnip02root_L'
+ },
+ 'cf_j_bnip02_L': {
+ 'head': (0.0833078027, -0.0955616534, 1.3306258917),
+ 'tail_offset': (0.0004578009, 0.0000199825, 0.0232515335),
+ 'roll': 0.3313450813,
+ 'parent': 'cf_s_bnip02_L'
+ },
+ 'cf_s_bnip025_L': {
+ 'head': (0.0902579725, -0.1004893780, 1.3331873417),
+ 'tail_offset': (0.0083783790, -0.0086406097, 0.0029795170),
+ 'roll': 0.3000984490,
+ 'parent': 'cf_j_bnip02root_L'
+ },
+ 'cf_s_bnip01_L': {
+ 'head': (0.0808084309, -0.0890641287, 1.3298943043),
+ 'tail_offset': (0.0004328042, 0.0000849515, 0.0232441425),
+ 'roll': 0.3313450813,
+ 'parent': 'cf_d_bnip01_L'
+ },
+ 'cf_s_bnip015_L': {
+ 'head': (0.0817010626, -0.0913846791, 1.3301556110),
+ 'tail_offset': (0.0008926317, -0.0023205578, 0.0002613068),
+ 'roll': 0.3313450813,
+ 'parent': 'cf_d_bnip01_L'
+ },
+ 'cf_s_bust03_L': {
+ 'head': (0.0772379115, -0.0797819793, 1.3288490772),
+ 'tail_offset': (0.0003971010, 0.0001777783, 0.0232336521),
+ 'roll': 0.3313450813,
+ 'parent': 'cf_j_bust03_L'
+ },
+ 'cf_s_bust02_L': {
+ 'head': (0.0679545328, -0.0556483194, 1.3261313438),
+ 'tail_offset': (0.0003042668, 0.0004191138, 0.0232064724),
+ 'roll': 0.3313450813,
+ 'parent': 'cf_j_bust02_L'
+ },
+ 'cf_s_bust01_L': {
+ 'head': (0.0575999990, -0.0287300199, 1.3230998516),
+ 'tail_offset': (0.0002007224, 0.0006882958, 0.0231761932),
+ 'roll': 0.3313450813,
+ 'parent': 'cf_j_bust01_L'
+ },
+ 'cf_d_shoulder_R': {
+ 'head': (-0.0156014701, 0.0237622391, 1.4131999016),
+ 'tail_offset': (-0.0001560142, 0.0002376232, 0.0241320133),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_spine03'
+ },
+ 'cf_j_shoulder_R': {
+ 'head': (-0.0156014701, 0.0237622391, 1.4131999016),
+ 'tail_offset': (-0.0939531336, -0.0000126194, 0.0000000000),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_d_shoulder_R'
+ },
+ 'cf_d_shoulder02_R': {
+ 'head': (-0.1095482782, 0.0237370003, 1.4132000208),
+ 'tail_offset': (-0.0010954589, 0.0002373699, 0.0241320133),
+ 'roll': 0.0003692400,
+ 'parent': 'cf_j_shoulder_R'
+ },
+ 'cf_s_shoulder02_R': {
+ 'head': (-0.1095482782, 0.0237370003, 1.4132000208),
+ 'tail_offset': (-0.0010954589, 0.0002373699, 0.0241320133),
+ 'roll': 0.0003692400,
+ 'parent': 'cf_d_shoulder02_R'
+ },
+ 'cf_j_arm00_R': {
+ 'head': (-0.1095609218, 0.0237622391, 1.4131999016),
+ 'tail_offset': (-0.1495516151, 0.0065441933, 0.0000001192),
+ 'roll': -0.0423438102,
+ 'parent': 'cf_j_shoulder_R'
+ },
+ 'cf_d_arm01_R': {
+ 'head': (-0.1295429915, 0.0246088598, 1.4131999016),
+ 'tail_offset': (-0.0012957752, 0.0002367441, 0.0241320133),
+ 'roll': -0.0414048992,
+ 'parent': 'cf_j_arm00_R'
+ },
+ 'cf_s_arm01_R': {
+ 'head': (-0.1295185238, 0.0252000708, 1.4132006168),
+ 'tail_offset': (-0.0012955368, 0.0002426561, 0.0241320133),
+ 'roll': -0.0414048955,
+ 'parent': 'cf_d_arm01_R'
+ },
+ 'cf_d_arm02_R': {
+ 'head': (-0.2294044346, 0.0290195905, 1.4131999016),
+ 'tail_offset': (-0.0022943318, 0.0002824105, 0.0241320133),
+ 'roll': -0.0413984470,
+ 'parent': 'cf_j_arm00_R'
+ },
+ 'cf_s_arm02_R': {
+ 'head': (-0.2293606550, 0.0290177800, 1.4131999016),
+ 'tail_offset': (-0.0022938997, 0.0002823994, 0.0241320133),
+ 'roll': -0.0466109850,
+ 'parent': 'cf_d_arm02_R'
+ },
+ 'cf_d_arm03_R': {
+ 'head': (-0.3151178360, 0.0330731496, 1.4131999016),
+ 'tail_offset': (-0.0031512678, 0.0003276281, 0.0241320133),
+ 'roll': -0.0413790382,
+ 'parent': 'cf_j_arm00_R'
+ },
+ 'cf_s_arm03_R': {
+ 'head': (-0.3151178062, 0.0330731496, 1.4132000208),
+ 'tail_offset': (-0.0031512082, 0.0003276169, 0.0241320133),
+ 'roll': -0.0413735993,
+ 'parent': 'cf_d_arm03_R'
+ },
+ 'cf_j_forearm01_R': {
+ 'head': (-0.3623849750, 0.0345241316, 1.4132004976),
+ 'tail_offset': (-0.1111739874, 0.0051015355, -0.0000002384),
+ 'roll': 0.0000210945,
+ 'parent': 'cf_j_arm00_R'
+ },
+ 'cf_s_elbo_R': {
+ 'head': (-0.3623849452, 0.0596318804, 1.4132026434),
+ 'tail_offset': (0.0000000298, 0.0251077451, 0.0000021458),
+ 'roll': 0.0000008745,
+ 'parent': 'cf_j_forearm01_R'
+ },
+ 'cf_s_forearm01_R': {
+ 'head': (-0.3623671234, 0.0349453315, 1.4132003784),
+ 'tail_offset': (-0.0000276864, -0.0304288915, -0.0000027418),
+ 'roll': 0.0000211045,
+ 'parent': 'cf_j_forearm01_R'
+ },
+ 'cf_s_elboback_R': {
+ 'head': (-0.3623948097, 0.0045164400, 1.4131976366),
+ 'tail_offset': (-0.0000276864, -0.0304288925, -0.0000027418),
+ 'roll': 0.0000008745,
+ 'parent': 'cf_s_forearm01_R'
+ },
+ 'cf_d_forearm02_R': {
+ 'head': (-0.4762727916, 0.0345205218, 1.4131991863),
+ 'tail_offset': (-0.0047625899, 0.0003447086, 0.0241320133),
+ 'roll': 0.0000707803,
+ 'parent': 'cf_j_forearm01_R'
+ },
+ 'cf_s_forearm02_R': {
+ 'head': (-0.4762549698, 0.0349417105, 1.4131994247),
+ 'tail_offset': (-0.0047624111, 0.0003491975, 0.0241320133),
+ 'roll': 0.0000707901,
+ 'parent': 'cf_d_forearm02_R'
+ },
+ 'cf_d_wrist_R': {
+ 'head': (-0.5543849468, 0.0345180407, 1.4131983519),
+ 'tail_offset': (-0.0055437088, 0.0003451817, 0.0241320133),
+ 'roll': 0.0000707800,
+ 'parent': 'cf_j_forearm01_R'
+ },
+ 'cf_s_wrist_R': {
+ 'head': (-0.5543671250, 0.0349389985, 1.4131983519),
+ 'tail_offset': (-0.0250178576, -0.0004224181, 0.0000003576),
+ 'roll': 0.0000707900,
+ 'parent': 'cf_d_wrist_R'
+ },
+ 'cf_d_hand_R': {
+ 'head': (-0.5793849826, 0.0345165804, 1.4131987095),
+ 'tail_offset': (-0.0250178576, -0.0004224181, 0.0000003576),
+ 'roll': 0.0000707900,
+ 'parent': 'cf_s_wrist_R'
+ },
+ 'cf_j_hand_R': {
+ 'head': (-0.6123849154, 0.0345125683, 1.4132008553),
+ 'tail_offset': (-0.0061238408, 0.0003451258, 0.0241320133),
+ 'roll': -0.0000000100,
+ 'parent': 'cf_j_forearm01_R'
+ },
+ 'cf_s_hand_R': {
+ 'head': (-0.6123849750, 0.0345125683, 1.4132009745),
+ 'tail_offset': (-0.0434947014, -0.0036587510, -0.0018894672),
+ 'roll': 0.0000000000,
+ 'parent': 'cf_j_hand_R'
+ },
+ 'cf_j_little01_R': {
+ 'head': (-0.6591213346, 0.0569378883, 1.4077343941),
+ 'tail_offset': (-0.0281152725, 0.0039664730, -0.0024597645),
+ 'roll': 0.0977955684,
+ 'parent': 'cf_s_hand_R'
+ },
+ 'cf_j_little02_R': {
+ 'head': (-0.6872366071, 0.0609043613, 1.4052746296),
+ 'tail_offset': (-0.0152906775, 0.0021577775, -0.0013365746),
+ 'roll': 0.0979485959,
+ 'parent': 'cf_j_little01_R'
+ },
+ 'cf_j_little03_R': {
+ 'head': (-0.7025272846, 0.0630621389, 1.4039380550),
+ 'tail_offset': (-0.0152906775, 0.0021577775, -0.0013365746),
+ 'roll': 0.0981021821,
+ 'parent': 'cf_j_little02_R'
+ },
+ 'cf_j_middle01_R': {
+ 'head': (-0.6708994508, 0.0291264802, 1.4168765545),
+ 'tail_offset': (-0.0340698957, -0.0000000205, -0.0029808283),
+ 'roll': -0.0872664824,
+ 'parent': 'cf_s_hand_R'
+ },
+ 'cf_j_middle02_R': {
+ 'head': (-0.7049693465, 0.0291264597, 1.4138957262),
+ 'tail_offset': (-0.0227140188, 0.0000003595, -0.0019767284),
+ 'roll': -0.0867964178,
+ 'parent': 'cf_j_middle01_R'
+ },
+ 'cf_j_middle03_R': {
+ 'head': (-0.7276833653, 0.0291268192, 1.4119189978),
+ 'tail_offset': (-0.0227140188, 0.0000003595, -0.0019767284),
+ 'roll': -0.0873360857,
+ 'parent': 'cf_j_middle02_R'
+ },
+ 'cf_j_ring01_R': {
+ 'head': (-0.6662942171, 0.0438052602, 1.4139567614),
+ 'tail_offset': (-0.0323968530, 0.0022740699, -0.0028343201),
+ 'roll': -0.0069661369,
+ 'parent': 'cf_s_hand_R'
+ },
+ 'cf_j_ring02_R': {
+ 'head': (-0.6986910701, 0.0460793301, 1.4111224413),
+ 'tail_offset': (-0.0215651989, 0.0015144981, -0.0018799305),
+ 'roll': -0.0066119703,
+ 'parent': 'cf_j_ring01_R'
+ },
+ 'cf_j_ring03_R': {
+ 'head': (-0.7202562690, 0.0475938283, 1.4092425108),
+ 'tail_offset': (-0.0215651989, 0.0015144981, -0.0018799305),
+ 'roll': -0.0069324221,
+ 'parent': 'cf_j_ring02_R'
+ },
+ 'cf_j_thumb01_R': {
+ 'head': (-0.6146893501, 0.0108031603, 1.4037010670),
+ 'tail_offset': (-0.0274280310, -0.0195016302, -0.0048363209),
+ 'roll': -0.1745328754,
+ 'parent': 'cf_s_hand_R'
+ },
+ 'cf_j_thumb02_R': {
+ 'head': (-0.6421173811, -0.0086984700, 1.3988647461),
+ 'tail_offset': (-0.0201677680, -0.0143393399, -0.0035561323),
+ 'roll': -0.1745328754,
+ 'parent': 'cf_j_thumb01_R'
+ },
+ 'cf_j_thumb03_R': {
+ 'head': (-0.6622851491, -0.0230378099, 1.3953086138),
+ 'tail_offset': (-0.0201677680, -0.0143393390, -0.0035561323),
+ 'roll': -0.1745328754,
+ 'parent': 'cf_j_thumb02_R'
+ },
+ 'cf_j_index01_R': {
+ 'head': (-0.6683939695, 0.0135963000, 1.4142879248),
+ 'tail_offset': (-0.0308635831, -0.0027105501, -0.0027000904),
+ 'roll': -0.1686505526,
+ 'parent': 'cf_s_hand_R'
+ },
+ 'cf_j_index02_R': {
+ 'head': (-0.6992575526, 0.0108857499, 1.4115878344),
+ 'tail_offset': (-0.0205416679, -0.0018021995, -0.0018149614),
+ 'roll': -0.1694059968,
+ 'parent': 'cf_j_index01_R'
+ },
+ 'cf_j_index03_R': {
+ 'head': (-0.7197992206, 0.0090835504, 1.4097728729),
+ 'tail_offset': (-0.0205416679, -0.0018021995, -0.0018149614),
+ 'roll': -0.1691437811,
+ 'parent': 'cf_j_index02_R'
+ },
+}
+
+# ========== 使用示例 ==========
+# 在 Blender 脚本编辑器中运行以下代码来重建骨架:
+
+# import bpy
+# from mathutils import Vector
+#
+# # 创建骨架
+# bpy.ops.object.armature_add(enter_editmode=True, location=(0, 0, 0))
+# armature = bpy.context.active_object
+# armature.name = 'Reconstructed_Armature'
+#
+# # 删除默认骨骼
+# bpy.ops.armature.select_all(action='SELECT')
+# bpy.ops.armature.delete()
+#
+# # 创建所有骨骼
+# for bone_name, bone_info in better_fbx_complete_bone_data.items():
+# bone = armature.data.edit_bones.new(bone_name)
+# bone.head = Vector(bone_info['head'])
+# bone.tail = bone.head + Vector(bone_info['tail_offset'])
+# bone.roll = bone_info['roll']
+#
+# # 设置父子关系
+# for bone_name, bone_info in better_fbx_complete_bone_data.items():
+# if bone_info['parent'] is not None:
+# parent_name = bone_info['parent']
+# if parent_name in armature.data.edit_bones:
+# armature.data.edit_bones[bone_name].parent = armature.data.edit_bones[parent_name]
+#
+# # 切换到对象模式
+# bpy.ops.object.mode_set(mode='OBJECT')
+# print(f'骨架重建完成!共 {len(better_fbx_complete_bone_data)} 个骨骼')
\ No newline at end of file
diff --git a/blender_manifest.toml b/blender_manifest.toml
new file mode 100644
index 0000000..3f60fa7
--- /dev/null
+++ b/blender_manifest.toml
@@ -0,0 +1,18 @@
+schema_version = "1.0.0"
+# https://docs.blender.org/manual/en/latest/advanced/extensions/getting_started.html
+
+id = "kkbp"
+version = "8.1.0"
+name = "KKBP (Koikatsu Blender Porter)"
+tagline = "Imports Koikatsu models into Blender"
+maintainer = "FlailingFog"
+type = "add-on"
+website = "https://github.com/FlailingFog/KK-Blender-Porter-Pack"
+tags = ["Import-Export"]
+blender_version_min = "3.6.0"
+license = ["SPDX:MIT"]
+platforms = ["windows-x64", "macos-arm64", "linux-x64"]
+
+[permissions]
+files = "Imports Koikatsu .pmx models, exports .fbx and texture files"
+network = "Installs pillow package from pypi"
\ No newline at end of file
diff --git a/common.py b/common.py
new file mode 100644
index 0000000..73a1b14
--- /dev/null
+++ b/common.py
@@ -0,0 +1,436 @@
+import bpy, json, datetime, traceback
+from pathlib import Path
+
+
+class JsonFileManager:
+ '''
+ Manages json files to avoid loading files repeatedly. The instance is declared at the bottom of the file
+ '''
+
+ def __init__(self):
+ self.json_files = {}
+ self.smr_materials_data = {}
+ self.materials_data = {}
+
+ def clear(self):
+ self.json_files.clear()
+ self.smr_materials_data.clear()
+
+ def init(self):
+ '''
+ Recording json filenames in the import directory, but only load MaterialDataComplete.json
+ Also record material information for MaterialDataComplete.json
+ '''
+ self.clear()
+ for file in Path(bpy.context.scene.kkbp.import_dir).glob('*.json'):
+ self.json_files[file.name] = str(file)
+
+ raw_data = self.get_json_file('KK_MaterialDataComplete.json')
+
+ for item in raw_data:
+ if material_infos := item['MaterialInformation']:
+ if (smr_data := self.smr_materials_data.get(item['SMRName'])) is None:
+ smr_data = []
+ self.smr_materials_data[item['SMRName']] = smr_data
+ smr_data.append(item)
+ for material_info in material_infos:
+ if (materials := self.materials_data.get(material_info['MaterialName'])) is None:
+ materials = []
+ self.materials_data[material_info['MaterialName']] = materials
+ materials.append(material_info)
+
+ def get_json_file(self, filename: str) -> json:
+ '''
+ Returns the json data by filename. Include the .json in the filename argument
+ '''
+ if json_data := self.json_files.get(filename): # Returns None if the file is not found (will likely cause an error)
+ if isinstance(json_data, str):
+ json_data = self.json_files[filename] = json.load(open(json_data))
+ return json_data
+
+ def get_material_info_by_smr(self, smr_name: str) -> list[dict]:
+ return self.smr_materials_data.get(smr_name)
+
+ def get_materials_info(self) -> dict[str, list[dict]]:
+ return self.smr_materials_data
+
+ def get_color(self, material_name: str, color: str) -> dict[str: float]:
+ '''Find the material material_name and return an RGBA dict list of the desired color ranging from 0-1'''
+ if material_name := bpy.data.materials[material_name].get('id'):
+ material_colors = self.materials_data.get(material_name)
+ for material_color in material_colors:
+ color_dict = zip(material_color['ShaderPropNames'], material_color['ShaderPropColorValues'])
+ for pair in color_dict:
+ if color in pair[0]:
+ return pair[1]
+ kklog(f"Couldn't find {color} for {material_name}", 'warn')
+ return {'r': 1, 'g': 1, 'b': 1, 'a': 1}
+
+ def get_shadow_color(self, material_name: str) -> dict[str, float]:
+ '''Find the material material_name and return an RGBA float list ranging from 0-1'''
+ # get original name
+ if material_name := bpy.data.materials[material_name].get('id'):
+ material_colors = self.materials_data.get(material_name)
+ for material_color in material_colors:
+ color_dict = zip(material_color['ShaderPropNames'], material_color['ShaderPropColorValues'])
+ for pair in color_dict:
+ if '_shadowcolor' in pair[0].lower():
+ return pair[1]
+ # return a default color if not found
+ kklog(f'Couldn\'t find shadow color for {material_name}', 'warn')
+ return {'r': 0.764, 'g': 0.880, 'b': 1}
+
+
+
+def toggle_console():
+ '''toggle the console. will do nothing on Linux or Mac'''
+ try:
+ bpy.ops.wm.console_toggle()
+ except:
+ return # only available on windows so it might error out for other platforms
+
+
+def kklog(log_text: str, type=''):
+ '''Log to the KKBP Log text in the scripting tab. Also prints to console. type can be error or warn'''
+ if not bpy.data.texts.get('KKBP Log'):
+ bpy.data.texts.new(name='KKBP Log')
+ if bpy.data.screens.get('Scripting'):
+ for area in bpy.data.screens['Scripting'].areas:
+ if area.type == 'TEXT_EDITOR':
+ area.spaces[0].text = bpy.data.texts['KKBP Log']
+ bpy.data.texts['KKBP Log'].write('==== KKBP Log ====\n')
+ if type == 'error':
+ log_text = '\nError: ' + str(log_text)
+ elif type == 'warn':
+ log_text = 'Warning: ' + str(log_text)
+ bpy.data.texts['KKBP Log'].write(str(log_text) + '\n')
+ print(str(log_text))
+
+
+def set_viewport_shading(type='MATERIAL'):
+ '''set the viewport shading in the layout tab. Accepts 'WIREFRAME' 'SOLID' 'MATERIAL' or 'RENDERED' '''
+ for area in bpy.context.workspace.screens[0].areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.shading.type = type
+
+
+def get_hairs() -> list[bpy.types.Object]:
+ '''Returns a list of all the hair objects for this import'''
+ hairs = [o for o in bpy.data.objects if
+ o.type == 'MESH' and o.get('hair') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return hairs
+
+
+def get_outfits() -> list[bpy.types.Object]:
+ '''Returns a list of all the outfit objects for this import'''
+ outfits = [o for o in bpy.data.objects if
+ o.type == 'MESH' and o.get('outfit') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return outfits
+
+
+def get_alts() -> list[bpy.types.Object]:
+ '''Returns a list of all the alternate outfit objects for this import'''
+ alts = [o for o in bpy.data.objects if
+ o.type == 'MESH' and o.get('alt') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return alts
+
+
+def get_hitboxes() -> list[bpy.types.Object]:
+ '''Returns a list of all the hitbox objects for this import'''
+ hits = [o for o in bpy.data.objects if
+ o.type == 'MESH' and o.get('hitbox') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return hits
+
+
+def get_body() -> bpy.types.Object:
+ '''Returns the body object for this import'''
+ bodies = [o for o in bpy.data.objects if o.get('body') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return bodies[0] if bodies else None
+
+
+def get_armature() -> bpy.types.Object:
+ '''Returns the armature object for this import'''
+ arms = [o for o in bpy.data.objects if o.get('armature') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return arms[0] if arms else None
+
+
+def get_rig() -> bpy.types.Object:
+ '''Returns the rigify armature object for this import'''
+ arms = [o for o in bpy.data.objects if o.get('rig') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return arms[0] if arms else None
+
+
+def get_empties() -> list[bpy.types.Object]:
+ '''Returns a list of all empty objects for this import'''
+ empties = [o for o in bpy.data.objects if
+ o.type == 'EMPTY' and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return empties
+
+
+def get_tears() -> bpy.types.Object:
+ '''Returns the tears object for this import'''
+ tears = [o for o in bpy.data.objects if o.get('tears') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return tears[0] if tears else None
+
+
+def get_gags() -> bpy.types.Object:
+ '''Returns the gag eyes object for this import'''
+ gags = [o for o in bpy.data.objects if o.get('gag') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return gags[0] if gags else None
+
+
+def get_tongue() -> bpy.types.Object:
+ '''Returns the rigged tongue object for this import'''
+ tongues = [o for o in bpy.data.objects if
+ o.get('tongue') and o.get('name') == bpy.context.scene.kkbp.character_name]
+ return tongues[0] if tongues else None
+
+
+def get_all_objects() -> list[bpy.types.Object]:
+ '''Returns all objects associated with this import'''
+ everything = get_outfits()
+ everything.extend(get_alts())
+ everything.append(get_body())
+ everything.extend(get_hairs())
+ everything.append(get_tears())
+ everything.append(get_gags())
+ everything.append(get_tongue())
+ return everything
+
+
+def get_all_bakeable_objects() -> list[bpy.types.Object]:
+ '''Returns all objects associated with this import that can be baked'''
+ everything = get_outfits()
+ everything.extend(get_alts())
+ everything.append(get_body())
+ everything.extend(get_hairs())
+ return everything
+
+
+def get_name() -> str:
+ '''Returns the character name'''
+ return bpy.context.scene.kkbp.character_name
+
+
+def get_import_path() -> str:
+ '''Returns the import path'''
+ return bpy.context.scene.kkbp.import_dir
+
+
+def get_material_names(smr_name: str) -> list[str] | None:
+ '''Returns a list of the material names this smr object is using'''
+ material_data = json_file_manager.get_material_info_by_smr(smr_name)
+ if material_data is None:
+ kklog(f'Could not find smr object: {smr_name}', 'warn')
+ return []
+ materials = [
+ sub_item['MaterialName']
+ for item in material_data
+ for sub_item in item['MaterialInformation']
+ if sub_item.get('MaterialName')
+ ]
+
+ # remove dupes and sort
+ materials = list(set(materials))
+ return sorted(materials)
+
+
+def get_shader_name(material_name: str) -> str:
+ '''Returns the shader name for this material'''
+ material_data = json_file_manager.get_json_file('KK_MaterialDataComplete.json')
+ material_infos = [m['MaterialInformation'] for m in material_data if m.get('MaterialInformation')]
+ shaders = []
+ for material_info in material_infos:
+ shaders.extend([m.get('ShaderName') for m in material_info if m.get('MaterialName') == material_name])
+ return shaders[0] if shaders else None
+
+
+def get_color(material_name: str, color: str) -> dict[float]:
+ '''Find the material material_name and return an RGBA dict list of the specified color ranging from 0-1. If material_name contains a space and the character name, it will be filtered out.'''
+ material_name = bpy.data.materials[material_name].get('id')
+ if material_name:
+ material_data = json_file_manager.get_json_file('KK_MaterialDataComplete.json')
+ material_infos = [m['MaterialInformation'] for m in material_data if m.get('MaterialInformation')]
+ # get all the colors
+ material_colors = []
+ for material_info in material_infos:
+ material_colors.extend([m for m in material_info if m.get('MaterialName') == material_name])
+ for material_color in material_colors:
+ # then zip them and find the shadow color
+ color_dict = zip(material_color['ShaderPropNames'], material_color['ShaderPropColorValues'])
+ # key names are not consistent, so look through all of them
+ for pair in color_dict:
+ if color in pair[0]:
+ return pair[1]
+ kklog(f"Couldn't find {color} for {material_name}", 'warn')
+ return {'r': 1, 'g': 1, 'b': 1, 'a': 1}
+
+
+def get_body_materials() -> list[bpy.types.Material]:
+ '''Returns a list of all the body materials'''
+ materials = [m for m in bpy.data.materials if
+ m.get('body') and m.get('name') == bpy.context.scene.kkbp.character_name]
+ return materials
+
+
+def get_hair_materials() -> list[bpy.types.Material]:
+ '''Returns a list of all the body materials'''
+ materials = [m for m in bpy.data.materials if
+ m.get('hair') and m.get('name') == bpy.context.scene.kkbp.character_name]
+ return materials
+
+
+def get_outfit_materials() -> list[bpy.types.Material]:
+ '''Returns a list of all the outfit materials'''
+ materials = [m for m in bpy.data.materials if
+ m.get('outfit') and m.get('name') == bpy.context.scene.kkbp.character_name]
+ return materials
+
+
+def initialize_timer():
+ bpy.context.scene.kkbp.total_timer = datetime.datetime.now().minute * 60 + datetime.datetime.now().second + datetime.datetime.now().microsecond / 1e6
+ bpy.context.scene.kkbp.timer = datetime.datetime.now().minute * 60 + datetime.datetime.now().second + datetime.datetime.now().microsecond / 1e6
+
+
+def reset_timer():
+ bpy.context.scene.kkbp.timer = datetime.datetime.now().minute * 60 + datetime.datetime.now().second + datetime.datetime.now().microsecond / 1e6
+
+
+def print_timer(operation_name: str):
+ '''Prints the time between now and the last operation that was timed'''
+ kklog('{} operation took {} seconds'.format(
+ operation_name,
+ abs(round(((datetime.datetime.now().minute * 60 + datetime.datetime.now().second + datetime.datetime.now().microsecond / 1e6) - bpy.context.scene.kkbp.timer),
+ 3))))
+ reset_timer()
+
+
+def handle_error(error_causer: bpy.types.Operator, error: Exception):
+ kklog(
+ 'Unknown python error occurred. \n Make sure the default model imports correctly before troubleshooting on this model!\n\n\n',
+ type='error')
+ kklog(traceback.format_exc())
+ error_causer.report({'ERROR'}, traceback.format_exc())
+
+
+def switch(object: bpy.types.Object, mode='OBJECT'):
+ '''Switches blender mode on a blender object. Valid modes are 'object', 'edit' and 'pose' '''
+ bpy.context.view_layer.objects.active = object
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.select_all(action='DESELECT')
+ object.select_set(True)
+ bpy.context.view_layer.objects.active = object
+
+ mode = mode.upper()
+ bpy.ops.object.mode_set(mode=mode)
+
+ if mode == 'POSE':
+ bpy.ops.pose.select_all(action='DESELECT')
+ elif mode == 'EDIT':
+ if object.type == 'MESH':
+ bpy.ops.mesh.select_all(action='DESELECT')
+ else:
+ bpy.ops.armature.select_all(action='DESELECT')
+ elif mode == 'OBJECT':
+ pass
+ else:
+ kklog('INVALID MODE CHOICE', type='error')
+ raise ('INVALID MODE CHOICE')
+
+
+def move_and_hide_collection(objects: bpy.types.Object, new_collection: str, hide=True):
+ '''Move the objects into a new collection called "new_collection" and hide the new collection'''
+ if not objects:
+ return
+ switch(objects[0], 'object')
+ # unlink from old collection, then move to new collection
+ object_collection = bpy.data.collections.new(new_collection)
+ for object in objects:
+ object.users_collection[0].objects.unlink(object)
+ object_collection.objects.link(object)
+ bpy.context.scene.collection.children[get_name()].children.link(object_collection)
+ # then hide the new collection
+ try:
+ bpy.context.scene.view_layers[0].active_layer_collection = \
+ bpy.context.view_layer.layer_collection.children[get_name()].children[new_collection]
+ bpy.context.scene.view_layers[0].active_layer_collection.exclude = hide
+ except:
+ kklog(f'Failed to move and hide collection: {new_collection}', type='error')
+
+
+def get_layer_collection_from_name(base_collection: bpy.types.LayerCollection,
+ search_term: str) -> bpy.types.LayerCollection:
+ '''Returns the view layer collection object by name'''
+ # check if this is it
+ if (base_collection.name == search_term):
+ return base_collection
+ # If not, recursively go through the collection's children for the search term
+ for child in base_collection.children:
+ if child.name == search_term:
+ return child
+ else:
+ recursive_result = get_layer_collection_from_name(child, search_term)
+ if recursive_result:
+ return recursive_result
+
+
+def get_layer_collection_state(collection_name: str) -> bool:
+ '''Gets the exclude state of a view layer collection'''
+ base_collection = bpy.context.view_layer.layer_collection
+ collection = get_layer_collection_from_name(base_collection, collection_name)
+ return collection.exclude
+
+
+def show_layer_collection(collection_name: str, state: bool):
+ '''Sets the exclude state of a view layer collection'''
+ base_collection = bpy.context.view_layer.layer_collection
+ collection = get_layer_collection_from_name(base_collection, collection_name)
+ collection.exclude = state
+
+
+def clean_orphaned_data():
+ '''clean data that is no longer being used'''
+ bpy.ops.object.mode_set(mode='OBJECT')
+ for block in bpy.data.meshes:
+ if block.users == 0 and not block.use_fake_user:
+ bpy.data.meshes.remove(block)
+ for block in bpy.data.cameras:
+ if block.users == 0 and not block.use_fake_user:
+ bpy.data.cameras.remove(block)
+ for block in bpy.data.lights:
+ if block.users == 0 and not block.use_fake_user:
+ bpy.data.lights.remove(block)
+ for block in bpy.data.materials:
+ if block.users == 0 and not block.use_fake_user:
+ bpy.data.materials.remove(block)
+
+
+def import_from_library_file(category, list_of_items, use_fake_user=False):
+ '''Import items from the KKBP library file. The category is 'Armature', 'Brush', 'Collection' etc and
+ the list_of_items is an array with all of the item names that you want to import.
+ This will try to import the material templates from the KK Shader.blend file in the PMX import folder.
+ If there's no KK Shader.blend file in the PMX folder, it will default to the one that comes with the plugin'''
+ fileList = Path(bpy.context.scene.kkbp.import_dir).glob('*.*')
+ files = [file for file in fileList if file.is_file()]
+ blend_file_missing = True
+ for file in files:
+ if '.blend' in str(file) and '.blend1' not in str(file) and 'KK Shader' in str(file):
+ directory = Path(file).resolve()
+ blend_file_missing = False
+ if blend_file_missing:
+ # grab it from the plugin directory
+ directory = Path(__file__)
+ filename = 'KK Shader V8.0.blend/'
+
+ library_path = (Path(directory).parent / filename).resolve()
+ template_list = [{'name': item} for item in list_of_items]
+ bpy.ops.wm.append(
+ filepath=str((library_path / category).resolve()),
+ directory=str(library_path / category) + '/',
+ files=template_list,
+ set_fake=use_fake_user
+ )
+
+
+json_file_manager = JsonFileManager()
diff --git a/exporting/__pycache__/bakematerials.cpython-311.pyc b/exporting/__pycache__/bakematerials.cpython-311.pyc
new file mode 100644
index 0000000..b4500a8
Binary files /dev/null and b/exporting/__pycache__/bakematerials.cpython-311.pyc differ
diff --git a/exporting/__pycache__/exportprep.cpython-311.pyc b/exporting/__pycache__/exportprep.cpython-311.pyc
new file mode 100644
index 0000000..17436b7
Binary files /dev/null and b/exporting/__pycache__/exportprep.cpython-311.pyc differ
diff --git a/exporting/bakematerials.py b/exporting/bakematerials.py
new file mode 100644
index 0000000..daf8366
--- /dev/null
+++ b/exporting/bakematerials.py
@@ -0,0 +1,657 @@
+# BAKE MATERIAL TO TEXTURE SCRIPT
+# Bakes all materials of an object into image textures (to use in other programs)
+# Will only bake a material if an image node is present in the green texture group
+# If no image is present, a low resolution failsafe image will be baked to account for
+# fully opaque or transparent materials that don't rely on a texture file
+# If there are multiple image resolutions, only the highest resolution be baked
+# Materials are baked to an 8-bit PNG with an alpha channel.
+# (optional) Creates a copy of the model and generates a material atlas to the copy
+
+# Notes:
+# - This script deletes all camera objects in the scene
+# - fillerplane driver + shader code taken from https://blenderartists.org/t/scripts-create-camera-image-plane/580839
+# - material combiner code taken from https://github.com/Grim-es/material-combiner-addon/
+
+
+import bpy, os, traceback, time, pathlib, subprocess
+from .. import common as c
+from ..interface.dictionary_en import t
+
+def print_memory_usage(stri):
+ run = subprocess.run('wmic OS get FreePhysicalMemory', capture_output=True)
+ c.kklog((stri, '\n mem usage ', 16000 - int(run.stdout.split(b'\r')[2].split(b'\n')[1])/1000))
+
+#setup and return a camera
+def setup_camera():
+ #Delete all cameras in the scene
+ for obj in bpy.context.scene.objects:
+ if obj.type == 'CAMERA':
+ obj.select_set(True)
+ else:
+ obj.select_set(False)
+ bpy.ops.object.delete()
+ for block in bpy.data.cameras:
+ if block.users == 0:
+ bpy.data.cameras.remove(block)
+
+ #Add a new camera
+ bpy.ops.object.camera_add(enter_editmode=False, align='VIEW', location=(0, 0, 1), rotation=(0, 0, 0))
+ #save it for later
+ camera = bpy.context.active_object
+ #and set it as the active one
+ bpy.context.scene.camera=camera
+ #Set camera to orthographic
+ bpy.data.cameras[camera.name].type='ORTHO'
+ bpy.data.cameras[camera.name].ortho_scale=6
+ bpy.context.scene.render.pixel_aspect_y=1
+ bpy.context.scene.render.pixel_aspect_x=1
+ return camera
+
+def setup_geometry_nodes_and_fillerplane(camera: bpy.types.Object):
+ object_to_bake = bpy.context.active_object
+
+ #create fillerplane
+ bpy.ops.mesh.primitive_plane_add()
+ bpy.ops.object.material_slot_add()
+ fillerplane = bpy.context.active_object
+ fillerplane.data.uv_layers[0].name = 'uv_main'
+ fillerplane.name = "fillerplane"
+ bpy.ops.object.editmode_toggle()
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.transform.resize(value=(0.5,0.5,0.5))
+ bpy.ops.uv.reset()
+ bpy.ops.object.editmode_toggle()
+
+ fillerplane.location = (0,0,-0.0001)
+
+ def setup_driver_variables(driver, camera):
+ cam_ortho_scale = driver.variables.new()
+ cam_ortho_scale.name = 'cOS'
+ cam_ortho_scale.type = 'SINGLE_PROP'
+ cam_ortho_scale.targets[0].id_type = 'CAMERA'
+ cam_ortho_scale.targets[0].id = bpy.data.cameras[camera.name]
+ cam_ortho_scale.targets[0].data_path = 'ortho_scale'
+ resolution_x = driver.variables.new()
+ resolution_x.name = 'r_x'
+ resolution_x.type = 'SINGLE_PROP'
+ resolution_x.targets[0].id_type = 'SCENE'
+ resolution_x.targets[0].id = bpy.context.scene
+ resolution_x.targets[0].data_path = 'render.resolution_x'
+ resolution_y = driver.variables.new()
+ resolution_y.name = 'r_y'
+ resolution_y.type = 'SINGLE_PROP'
+ resolution_y.targets[0].id_type = 'SCENE'
+ resolution_y.targets[0].id = bpy.context.scene
+ resolution_y.targets[0].data_path = 'render.resolution_y'
+
+ #setup X scale for bake object and plane
+ driver = object_to_bake.driver_add('scale',0).driver
+ driver.type = 'SCRIPTED'
+ setup_driver_variables(driver, camera)
+ driver.expression = "((r_x)/(r_y)*(cOS)) if (((r_x)/(r_y)) < 1) else (cOS)"
+
+ driver = fillerplane.driver_add('scale',0).driver
+ driver.type = 'SCRIPTED'
+ setup_driver_variables(driver, camera)
+ driver.expression = "((r_x)/(r_y)*(cOS)) if (((r_x)/(r_y)) < 1) else (cOS)"
+
+ #setup drivers for object's Y scale
+ driver = object_to_bake.driver_add('scale',1).driver
+ driver.type = 'SCRIPTED'
+ setup_driver_variables(driver, camera)
+ driver.expression = "((r_y)/(r_x)*(cOS)) if (((r_y)/(r_x)) < 1) else (cOS)"
+
+ driver = fillerplane.driver_add('scale',1).driver
+ driver.type = 'SCRIPTED'
+ setup_driver_variables(driver, camera)
+ driver.expression = "((r_y)/(r_x)*(cOS)) if (((r_y)/(r_x)) < 1) else (cOS)"
+
+ ###########################
+ #import the premade flattener node to unwrap the mesh into the UV structure
+ c.import_from_library_file('NodeTree', ['.Geometry Nodes'])
+
+ #give the object a geometry node modifier
+ geonodes_mod = object_to_bake.modifiers.new('Flattener', 'NODES')
+ geonodes_mod.node_group = bpy.data.node_groups['.Geometry Nodes']
+ identifier = [str(i) for i in geonodes_mod.keys()][0]
+ geonodes_mod[identifier+'_attribute_name'] = 'uv_main'
+ geonodes_mod[identifier+'_use_attribute'] = True
+
+ #Make the originally selected object active again
+ c.switch(object_to_bake, 'OBJECT')
+
+##############################
+#Changes the material of the image plane to the material of the object,
+# and then puts a render of the image plane into the specified folder
+
+def sanitizeMaterialName(text: str) -> str:
+ '''Mat names need to be sanitized else you can't delete the files with windows explorer'''
+ for ch in ['\\','`','*','<','>','.',':','?','|','/','\"']:
+ if ch in text:
+ text = text.replace(ch,'')
+ return text
+
+def bake_pass(folderpath: str, bake_type: str):
+ '''Folds the body / clothes / hair down to a UV rectangle
+ Places a filler plane right below it to fill in the rest of the image
+ Bakes all materials on this object down to an image using the orthographic camera
+ '''
+ #get the currently selected object as the active object
+ object_to_bake = bpy.context.active_object
+ #remember what order the materials are in for later
+ original_material_order = []
+ for matslot in object_to_bake.material_slots:
+ original_material_order.append(matslot.name)
+
+ #if this is a light or dark pass, make sure the color output is a constant light or dark
+ combine = bpy.data.node_groups['.Combine colors']
+ combine.links.remove(combine.nodes['mix'].inputs[0].links[0])
+ combine.nodes['mix'].inputs[0].default_value = 1 if bake_type == 'light' else 0
+
+ #go through each material slot
+ for index, current_material in enumerate(object_to_bake.data.materials):
+ #Don't bake this material if it doesn't have the bake tag
+ if not current_material.get('bake'):
+ c.kklog(f'Detected material that cannot be finalized. Skipping: {current_material.name}')
+ continue
+
+ nodes = current_material.node_tree.nodes
+ links = current_material.node_tree.links
+
+ #Turn off the normals for the toon_shading shading node group input if this isn't a normal pass
+ if nodes.get('textures') and bake_type != 'normal':
+ toon_shading = nodes.get('textures').node_tree.nodes.get('shade')
+ if toon_shading:
+ original_normal_state = toon_shading.inputs[1].default_value
+ toon_shading.inputs[1].default_value = 0
+
+ #if this is a normal pass, attach the normal passthrough to the output
+ elif nodes.get('textures') and bake_type == 'normal':
+ if len(nodes['textures'].outputs):
+ links.remove(nodes['out'].inputs[0].links[0])
+ links.new(nodes['textures'].outputs[-1], nodes['out'].inputs[0])
+
+ if nodes.get('textures'):
+ #Go through each of the textures loaded into the textures group and get the highest resolution one
+ highest_resolution = [0, 0]
+ for image_node in nodes['textures'].node_tree.nodes:
+ if image_node.type == 'TEX_IMAGE' and image_node.image:
+ image_size = image_node.image.size[0] * image_node.image.size[1]
+ largest_so_far = highest_resolution[0] * highest_resolution[1]
+ if image_size > largest_so_far:
+ highest_resolution = image_node.image.size
+
+ resolution_multiplier = bpy.context.scene.kkbp.bake_mult
+ #Render an image using the highest dimensions
+ if highest_resolution:
+ bpy.context.scene.render.resolution_x=highest_resolution[0] * resolution_multiplier
+ bpy.context.scene.render.resolution_y=highest_resolution[1] * resolution_multiplier
+ else:
+ #if no images were found, render a 64px failsafe image anyway to catch
+ # materials that are a solid color, don't rely on textures, or are completely transparent
+ bpy.context.scene.render.resolution_x=64
+ bpy.context.scene.render.resolution_y=64
+
+ #set every material slot except the current material to be transparent
+ for matslot in object_to_bake.material_slots:
+ if matslot.material != current_material:
+ matslot.material = bpy.data.materials['KK Eyeline kage ' + c.get_name()]
+
+ #set the filler plane to the current material
+ bpy.data.objects['fillerplane'].material_slots[0].material = current_material
+
+ #then render it
+ matname = sanitizeMaterialName(current_material.name)
+ matname = matname[:-4] if matname[-4:] == '-ORG' else matname
+ bpy.context.scene.render.filepath = folderpath + matname + ' ' + bake_type
+ bpy.context.scene.render.image_settings.file_format='PNG'
+ bpy.context.scene.render.image_settings.color_mode='RGBA'
+
+ print('Rendering {} / {}'.format(index+1, len(object_to_bake.data.materials)))
+ bpy.ops.render.render(write_still = True)
+
+ #reset folderpath after render
+ bpy.context.scene.render.filepath = folderpath
+
+ #Restore the value in the toon_shading shading node group for the normals
+ if nodes.get('textures') and bake_type != 'normal':
+ toon_shading = nodes.get('textures').node_tree.nodes.get('shade')
+ if toon_shading:
+ toon_shading.inputs[1].default_value = original_normal_state
+
+ #Restore the links if they were edited for the normal pass
+ elif nodes.get('textures') and bake_type == 'normal':
+ if len(nodes['textures'].outputs):
+ links.remove(nodes['out'].inputs[0].links[0])
+ links.new(nodes['combine'].outputs[0], nodes['out'].inputs[0])
+
+ #reset material slots to their original order
+ for material_index in range(len(original_material_order)):
+ object_to_bake.material_slots[material_index].material = bpy.data.materials[original_material_order[material_index]]
+
+ #reset the color output group link
+ combine = bpy.data.node_groups['.Combine colors']
+ combine.links.new(combine.nodes['input'].outputs[2], combine.nodes['mix'].inputs[0])
+
+def cleanup():
+ # Deselect all objects
+ bpy.ops.object.select_all(action='DESELECT')
+ #Select the camera
+ for camera in [o for o in bpy.data.objects if o.type == 'CAMERA']:
+ bpy.data.objects.remove(camera)
+ #Select fillerplane
+ for fillerplane in [o for o in bpy.data.objects if 'fillerplane' in o.name]:
+ bpy.data.objects.remove(fillerplane)
+ #delete orphan data
+ for block in bpy.data.meshes:
+ if block.users == 0:
+ bpy.data.meshes.remove(block)
+ for block in bpy.data.cameras:
+ if block.users == 0:
+ bpy.data.cameras.remove(block)
+ for ob in [obj for obj in bpy.context.view_layer.objects if obj and obj.type == 'MESH']:
+ #delete the geometry modifier
+ if ob.modifiers.get('Flattener'):
+ ob.modifiers.remove(ob.modifiers['Flattener'])
+ #delete the two scale drivers
+ ob.animation_data.drivers.remove(ob.animation_data.drivers[0])
+ ob.animation_data.drivers.remove(ob.animation_data.drivers[0])
+ ob.scale = (1,1,1)
+ bpy.data.node_groups.remove(bpy.data.node_groups['.Geometry Nodes'])
+
+def replace_all_baked_materials(folderpath: str, bake_object: bpy.types.Object):
+ #load all baked images into blender
+ fileList = pathlib.Path(folderpath).glob('*.png')
+ files = [file for file in fileList if file.is_file()]
+ for file in files:
+ try:
+ image = bpy.data.images.load(filepath=str(file))
+ image.pack()
+ #if there was an older version of this image, get rid of it
+ if image.name[-4:] == '.001':
+ if bpy.data.images.get(image.name[:-4]):
+ bpy.data.images[image.name[:-4]].user_remap(image)
+ bpy.data.images.remove(bpy.data.images[image.name[:-4]])
+ image.name = image.name[:-4]
+ except:
+ c.kklog(f'Could not load in file because the name exceeds 64 characters: {file}')
+
+ #now all needed images are loaded into the file. Match each material to it's image textures
+ for bake_type in ['light', 'dark', 'normal']:
+ for mat in bake_object.material_slots:
+ image = bpy.data.images.get(mat.material.name.replace('-ORG', '') + f' {bake_type}.png', '')
+ if image:
+ #the simplified material already exists and is loaded into the material slot, so just load in the image
+ if mat.material.get('simple'):
+ simple = mat.material
+ textures_group = simple.node_tree.nodes['textures'].node_tree
+ textures_group.nodes[bake_type].image = image
+
+ #the simplified material already exists, but the user swapped it back to the -ORG version to rebake it,
+ # so load the material back into the material slot and load in the image
+ elif mat.material.get('bake') and '-ORG' in mat.material.name and bpy.data.materials.get(mat.material.name.replace('-ORG','')):
+ simple = bpy.data.materials[mat.material.name.replace('-ORG','')]
+ mat.material = simple
+ textures_group = simple.node_tree.nodes['textures'].node_tree
+ print(mat.material.name)
+ textures_group.nodes[bake_type].image = image
+
+ #check if a simplified version of this material exists yet. If it doesn't, create it
+ elif mat.material.get('bake'):
+ #rename the original material to "material_name-ORG" and create the simplified material
+ mat.material.name += '-ORG'
+ try:
+ simple = bpy.data.materials['KK Simple'].copy()
+ except:
+ c.import_from_library_file('Material', ['KK Simple'], use_fake_user = False)
+ simple = bpy.data.materials['KK Simple'].copy()
+ simple.name = mat.material.name.replace('-ORG', '')
+ textures_group = simple.node_tree.nodes['textures'].node_tree.copy()
+ textures_group.name = simple.name
+ simple.node_tree.nodes['textures'].node_tree = textures_group
+ textures_group.nodes[bake_type].image = image
+ # you have the ability to only bake the light textures, but it looks weird if there is no dark texture to go along with it,
+ # put the light image into the dark slot. it will be overwritten if the dark texture exists on the next loop
+ if bake_type == 'light':
+ textures_group.nodes['dark'].image = image
+
+ #and then replace the original material with this new simplified one
+ mat.material.use_fake_user = True
+ def replace_mat():
+ if bpy.app.version[0] > 3:
+ blend_method = mat.material.surface_render_method
+ mat.material = simple
+ mat.material.surface_render_method = blend_method
+ mat.material.use_transparency_overlap = True if ('KK Eyewhites (sirome) ' + c.get_name() in mat.name) else False
+ else:
+ blend_method = mat.material.blend_method
+ mat.material = simple
+ mat.material.blend_method = blend_method
+ mat.material.show_transparent_back = False
+ simple['simple'] = True
+ replace_mat()
+
+ #load the Eevee Mod simple shader if using Eevee Mod
+ if bpy.context.scene.kkbp.shader_dropdown == 'C':
+ try:
+ simple = bpy.data.node_groups['.Simple Shader (Eevee Mod)'].copy()
+ except:
+ c.import_from_library_file('NodeTree', ['.Simple Shader (Eevee Mod)'], use_fake_user = False)
+ simple = bpy.data.node_groups['.Simple Shader (Eevee Mod)'].copy()
+ #and then replace the original material with this new simplified one
+ mat.material.use_fake_user = True
+ replace_mat()
+
+def create_material_atlas(folderpath: str):
+ '''Merges all the finalized material png files into a single atlas file, copies the current model and applies the atlas to the copy'''
+ # https://blender.stackexchange.com/questions/127403/change-active-collection
+ #Recursivly transverse layer_collection for a particular name
+ def recurLayerCollection(layerColl, collName):
+ found = None
+ if (layerColl.name == collName):
+ return layerColl
+ for layer in layerColl.children:
+ found = recurLayerCollection(layer, collName)
+ if found:
+ return found
+
+ def remove_orphan_data():
+ #revert the image back from the atlas file to the baked file
+ for mat in bpy.data.materials:
+ if mat.name[-4:] == '-ORG':
+ simplified_name = mat.name[:-4]
+ if bpy.data.materials.get(simplified_name):
+ simplified_mat = bpy.data.materials[simplified_name]
+ for bake_type in ['light', 'dark', 'normal']:
+ simplified_mat.node_tree.nodes['textures'].node_tree.nodes[bake_type].image = bpy.data.images.get(simplified_name + ' ' + bake_type + '.png')
+ #delete orphan data
+ for cat in [bpy.data.armatures, bpy.data.objects, bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.node_groups]:
+ for block in cat:
+ if block.users == 0:
+ cat.remove(block)
+
+ if bpy.data.collections.get(c.get_name() + ' atlas'):
+ c.kklog(f'deleting previous collection "{c.get_name()} atlas" and regenerating atlas model...')
+ def del_collection(coll):
+ for c in coll.children:
+ del_collection(c)
+ bpy.data.collections.remove(coll,do_unlink=True)
+ del_collection(bpy.data.collections[c.get_name() + ' atlas'])
+ remove_orphan_data()
+ #show the original collection again
+ c.show_layer_collection(c.get_name(), False)
+
+ #Change the Active LayerCollection to the character collection
+ layer_collection = bpy.context.view_layer.layer_collection
+ layerColl = recurLayerCollection(layer_collection, c.get_name())
+ bpy.context.view_layer.active_layer_collection = layerColl
+
+ # https://blender.stackexchange.com/questions/157828/how-to-duplicate-a-certain-collection-using-python
+ from collections import defaultdict
+ def copy_objects(from_col, to_col, linked, dupe_lut):
+ for o in from_col.objects:
+ dupe = o.copy()
+ if not linked and o.data:
+ dupe.data = dupe.data.copy()
+ to_col.objects.link(dupe)
+ dupe_lut[o] = dupe
+ def copy(parent, collection, linked=False):
+ dupe_lut = defaultdict(lambda : None)
+ def _copy(parent, collection, linked=False):
+ cc = bpy.data.collections.new(collection.name)
+ copy_objects(collection, cc, linked, dupe_lut)
+ for c in collection.children:
+ _copy(cc, c, linked)
+ parent.children.link(cc)
+ return cc
+ the_copy = _copy(parent, collection, linked)
+ for o, dupe in tuple(dupe_lut.items()):
+ parent = dupe_lut[o.parent]
+ if parent:
+ dupe.parent = parent
+ return the_copy
+ context = bpy.context
+ scene = context.scene
+ col = context.collection
+ assert(col is not scene.collection)
+ copied_collection = copy(scene.collection, col)
+ copied_collection.name = c.get_name() + ' atlas'
+
+ #setup materials for the combiner script
+ for obj in [o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'MESH']:
+ for mat in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
+ nodes = mat.node_tree.nodes
+ links = mat.node_tree.links
+ emissive_node = nodes.new('ShaderNodeEmission')
+ emissive_node.name = 'Emission'
+ image_node = nodes.new('ShaderNodeTexImage')
+ image_node.name = 'Image Texture'
+ links.new(emissive_node.inputs[0], image_node.outputs[0])
+ image_node.image = nodes['textures'].node_tree.nodes['light'].image
+ context.view_layer.objects.active = obj
+ bpy.ops.object.material_slot_remove_unused()
+
+ #call the material combiner script
+ bpy.ops.kkbp.combiner()
+
+ #replace all images with the atlas in a new atlas material
+ bake_types = []
+ if scene.kkbp.bake_light_bool:
+ bake_types.append('light')
+ if scene.kkbp.bake_dark_bool:
+ bake_types.append('dark')
+ if scene.kkbp.bake_norm_bool:
+ bake_types.append('normal')
+ for index, obj in enumerate([o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'MESH']):
+ #fix modifiers for all objects in this collection
+ for mod in obj.modifiers:
+ if mod.type == 'ARMATURE':
+ #fix the armature modifier to use the copied aramture
+ copied_armature = [o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'ARMATURE'][0]
+ mod.object = copied_armature
+ elif mod.type == 'SOLIDIFY':
+ #disable the outline on the atlased object because I don't feel like fixing it
+ obj.modifiers['Outline Modifier'].show_render = False
+ obj.modifiers['Outline Modifier'].show_viewport = False
+ elif mod.type == 'UV_WARP':
+ #fix the UV warp modifier to use the copied armature
+ copied_armature = [o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'ARMATURE'][0]
+ mod.object_from = copied_armature
+ mod.object_to = copied_armature
+
+ #check if this object had any atlas-able materials to begin with. If not, skip
+ if not [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
+ continue
+
+ for bake_type in bake_types:
+ #check for atlas dupes
+ atlas_image_name = f'{sanitizeMaterialName(obj.name).replace("001","")}_{bake_type}.png'
+ if bpy.data.images.get(atlas_image_name):
+ bpy.data.images.remove(bpy.data.images.get(atlas_image_name))
+ #the atlas image is originally named after the index of the object. Rename it to the object name
+ original_image_path = os.path.join(context.scene.kkbp.import_dir, 'atlas_files', f'{index}_{bake_type}.png')
+ new_image_path = os.path.join(context.scene.kkbp.import_dir, 'atlas_files', atlas_image_name)
+ if os.path.exists(original_image_path):
+ try:
+ os.rename(original_image_path, new_image_path)
+ except:
+ #rename failed because the file already exists. Delete the old one and try again
+ os.remove(new_image_path)
+ os.rename(original_image_path, new_image_path)
+ #then load it into blender
+ atlas_image = bpy.data.images.load(new_image_path)
+ bpy.data.images.remove(bpy.data.images.get(f'{index}_{bake_type}.png'))
+ for material in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
+ image = material.node_tree.nodes['textures'].node_tree.nodes[bake_type].image
+ if image:
+ if image.name == 'Template: Pattern Placeholder':
+ image = None
+ if not image:
+ print(image)
+ continue
+ else:
+ if not bpy.data.materials.get('{} Atlas'.format(material.name)):
+ #remove the emission nodes from earlier
+ if material.node_tree.nodes.get('Emission'):
+ material.node_tree.nodes.remove(material.node_tree.nodes['Image Texture'])
+ material.node_tree.nodes.remove(material.node_tree.nodes['Emission'])
+ atlas_material = material.copy()
+ atlas_material['simple'] = False
+ atlas_material['atlas'] = True
+ atlas_material.name = '{} Atlas'.format(material.name)
+ new_group = atlas_material.node_tree.nodes['textures'].node_tree.copy()
+ new_group.name = '{} Atlas'.format(material.name)
+ else:
+ atlas_material = bpy.data.materials.get('{} Atlas'.format(material.name))
+ new_group = bpy.data.node_groups.get('{} Atlas'.format(material.name))
+ atlas_material.node_tree.nodes['textures'].node_tree = new_group
+ new_group.nodes[bake_type].image = atlas_image
+ #load in the light image to the dark slot to make it look better when only the light colors are baked.
+ # This will be overwritten with the dark image in the next loop if the user baked it
+ if bake_type == 'light':
+ new_group.nodes['dark'].image = atlas_image
+
+ #replace all images with the atlas in a new atlas material
+ for mat_slot in [m for m in obj.material_slots if m.material.get('simple')]:
+ material = mat_slot.material
+ atlas_material = bpy.data.materials.get('{} Atlas'.format(material.name))
+ mat_slot.material = atlas_material
+
+ #setup the new collection for exporting
+ if bpy.app.version[0] > 3:
+ layer_collection = bpy.context.view_layer.layer_collection
+ layerColl = recurLayerCollection(layer_collection, c.get_name() + ' atlas')
+ bpy.context.view_layer.active_layer_collection = layerColl
+ bpy.ops.collection.exporter_add(name="IO_FH_fbx")
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.object_types = {'EMPTY', 'ARMATURE', 'MESH', 'OTHER'}
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.use_mesh_modifiers = False
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.add_leaf_bones = False
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.bake_anim = False
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.apply_scale_options = 'FBX_SCALE_ALL'
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.path_mode = 'COPY'
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.embed_textures = False
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.mesh_smooth_type = 'OFF'
+ bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.filepath = os.path.join(folderpath.replace('baked_files', 'atlas_files'), f'{sanitizeMaterialName(c.get_name())} exported model atlas.fbx')
+
+ #hide the new collection
+ c.show_layer_collection('Bone Widgets', True)
+ c.show_layer_collection('Rigged tongue ' + c.get_name(), True)
+ c.show_layer_collection('Rigged tongue ' + c.get_name() + '.001', True)
+ c.show_layer_collection('Bone Widgets.001', True)
+ c.show_layer_collection(c.get_name() + ' atlas', True)
+ remove_orphan_data()
+
+class bake_materials(bpy.types.Operator):
+ bl_idname = "kkbp.bakematerials"
+ bl_label = "Bake and generate atlased model"
+ bl_description = t('bake_mats_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ try:
+ #just use the pmx folder for the baked files
+ scene = context.scene.kkbp
+ folderpath = os.path.join(context.scene.kkbp.import_dir, 'baked_files', '')
+ last_step = time.time()
+ c.toggle_console()
+ c.reset_timer()
+ c.kklog('Switching to EEVEE for material baking...')
+ bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT' if bpy.app.version[0] > 3 else 'BLENDER_EEVEE'
+ c.switch(c.get_body(), 'OBJECT')
+ c.set_viewport_shading('SOLID')
+
+ #enable transparency
+ bpy.context.scene.render.film_transparent = True
+ bpy.context.scene.render.filter_size = 0.50
+
+ for bake_object in c.get_all_bakeable_objects():
+ #do a quick check to make sure this object has any materials that can be baked
+ worth_baking = [m for m in bake_object.material_slots if m.material.get('bake')]
+ if not worth_baking:
+ c.kklog(f'Not finalizing object because there were no materials worth baking: {bake_object.name}')
+ continue
+
+ #make sure the collection for this object is enabled in the outliner if it is a clothing item
+ if bake_object != c.get_body():
+ original_collection_state = c.get_layer_collection_state(bake_object.users_collection[0].name)
+ c.show_layer_collection(bake_object.users_collection[0].name, False)
+
+ #hide all objects except this one
+ for obj in [o for o in bpy.context.view_layer.objects if o]:
+ obj.hide_render = True
+ #unhide the object to bake (but only if the old baking system is not used)
+ if not bpy.context.scene.kkbp.old_bake_bool:
+ bake_object.hide_render = False
+ camera = setup_camera()
+ c.switch(bake_object)
+ setup_geometry_nodes_and_fillerplane(camera)
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+
+ #perform the baking operation
+ bake_types = []
+ if scene.bake_light_bool:
+ bake_types.append('light')
+ if scene.bake_dark_bool:
+ bake_types.append('dark')
+ if scene.bake_norm_bool:
+ bake_types.append('normal')
+ for bake_type in bake_types:
+ bake_pass(folderpath, bake_type)
+ cleanup()
+
+ #restore the original collection state
+ if bake_object != c.get_body():
+ c.show_layer_collection(bake_object.users_collection[0].name, original_collection_state)
+
+ #disable transparency
+ bpy.context.scene.render.film_transparent = False
+ bpy.context.scene.render.filter_size = 1.5
+ for bake_object in c.get_all_bakeable_objects():
+ replace_all_baked_materials(folderpath, bake_object)
+
+ #show all objects again
+ for obj in bpy.context.view_layer.objects:
+ obj.hide_render = False
+
+ if scene.use_atlas:
+ create_material_atlas(folderpath)
+
+ #setup the original collection for exporting
+ # https://blender.stackexchange.com/questions/127403/change-active-collection
+ #Recursively transverse layer_collection for a particular name
+ def recurLayerCollection(layerColl, collName):
+ found = None
+ if (layerColl.name == collName):
+ return layerColl
+ for layer in layerColl.children:
+ found = recurLayerCollection(layer, collName)
+ if found:
+ return found
+
+ layer_collection = bpy.context.view_layer.layer_collection
+ layerColl = recurLayerCollection(layer_collection, c.get_name())
+ bpy.context.view_layer.active_layer_collection = layerColl
+ if bpy.app.version[0] != 3:
+ if not bpy.data.collections[c.get_name()].exporters:
+ bpy.ops.collection.exporter_add(name="IO_FH_fbx")
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.object_types = {'EMPTY', 'ARMATURE', 'MESH', 'OTHER'}
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.use_mesh_modifiers = False
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.add_leaf_bones = False
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.bake_anim = False
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.apply_scale_options = 'FBX_SCALE_ALL'
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.path_mode = 'COPY'
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.embed_textures = False
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.mesh_smooth_type = 'OFF'
+ bpy.data.collections[c.get_name()].exporters[0].export_properties.filepath = os.path.join(folderpath.replace('baked_files', 'atlas_files'), f'{sanitizeMaterialName(c.get_name())} exported model.fbx')
+ c.toggle_console()
+
+ c.kklog('Finished in ' + str(time.time() - last_step)[0:4] + 's')
+ c.set_viewport_shading('SOLID')
+ return {'FINISHED'}
+ except:
+ c.kklog('Unknown python error occurred', type = 'error')
+ c.kklog(traceback.format_exc())
+ c.set_viewport_shading('SOLID')
+ self.report({'ERROR'}, traceback.format_exc())
+ return {"CANCELLED"}
+
diff --git a/exporting/exportprep.py b/exporting/exportprep.py
new file mode 100644
index 0000000..5e49723
--- /dev/null
+++ b/exporting/exportprep.py
@@ -0,0 +1,338 @@
+#simplfies bone count using the merge weights function in CATS
+
+import bpy, traceback, time
+from .. import common as c
+from ..interface.dictionary_en import t
+
+def main(prep_type, simp_type):
+ try:
+ #always try to use the atlased model first
+ body = bpy.data.objects['Body ' + c.get_name() + '.001']
+ bpy.context.view_layer.objects.active=body
+ body_name = body.name
+ armature_name = 'Armature.001'
+ if not bpy.data.objects[armature_name].data.bones.get('Pelvis'):
+ #the atlased body has already been modified. Skip.
+ c.kklog('Model with atlas has already been prepped. Skipping export prep functions...', type='warn')
+ return False
+ except:
+ #fallback to the non-atlased model if the atlased model collection is not visible
+ body = bpy.data.objects['Body ' + c.get_name()]
+ bpy.context.view_layer.objects.active=body
+ body_name = body.name
+ armature_name = 'Armature'
+
+ armature = bpy.data.objects[armature_name]
+
+ c.kklog('\nPrepping for export...')
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.select_all(action='DESELECT')
+
+ #Assume hidden items are unused and move them to their own collection
+ c.kklog('Moving unused objects to their own collection...')
+ no_move_objects = ['Hitboxes ' + c.get_name(), body_name, armature_name]
+ for object in bpy.context.scene.objects:
+ try:
+ move_this_one = object.name not in no_move_objects and 'Widget' not in object.name and object.hide_get()
+ if move_this_one:
+ object.hide_set(False)
+ object.select_set(True)
+ bpy.context.view_layer.objects.active=object
+ except:
+ c.kklog("During export prep, couldn't move object '{}' for some reason...".format(object), type='error')
+ if bpy.context.selected_objects:
+ bpy.ops.object.move_to_collection(collection_index=0, is_new=True, new_collection_name='Unused clothing items')
+ #hide the new collection
+ try:
+ bpy.context.scene.view_layers[0].active_layer_collection = bpy.context.view_layer.layer_collection.children['Unused clothing items']
+ bpy.context.scene.view_layers[0].active_layer_collection.exclude = True
+ except:
+ try:
+ #maybe the collection is in the default Collection collection
+ bpy.context.scene.view_layers[0].active_layer_collection = bpy.context.view_layer.layer_collection.children['Collection'].children['Unused clothing items']
+ bpy.context.scene.view_layers[0].active_layer_collection.exclude = True
+ except:
+ #maybe the collection is already hidden, or doesn't exist
+ pass
+
+ c.kklog('Removing object outline modifier...')
+ for ob in bpy.data.objects:
+ if ob.modifiers.get('Outline Modifier'):
+ ob.modifiers['Outline Modifier'].show_render = False
+ ob.modifiers['Outline Modifier'].show_viewport = False
+ #remove the outline materials because they won't be baked
+ if ob in [obj for obj in bpy.context.view_layer.objects if obj.type == 'MESH']:
+ ob.select_set(True)
+ bpy.context.view_layer.objects.active=ob
+ bpy.ops.object.material_slot_remove_unused()
+ bpy.ops.object.select_all(action='DESELECT')
+ body = bpy.data.objects[body_name]
+ bpy.context.view_layer.objects.active=body
+ body.select_set(True)
+
+ #Select the armature and make it active
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.select_all(action='DESELECT')
+ bpy.data.objects[armature_name].hide_set(False)
+ bpy.data.objects[armature_name].select_set(True)
+ bpy.context.view_layer.objects.active=bpy.data.objects[armature_name]
+ bpy.ops.object.mode_set(mode='POSE')
+
+ # If exporting for Unreal...
+ if prep_type == 'E':
+ armature = bpy.data.objects[armature_name]
+ bpy.context.view_layer.objects.active = armature
+ bpy.ops.armature.collection_show_all()
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ #Clear IK, it won't work in unreal
+ for bone in armature.pose.bones:
+ for constraint in bone.constraints:
+ bone.constraints.remove(constraint)
+
+ #Rename some bones to make it match Mannequin skeleton
+ #Not necessary, but allows Unreal automatically recognize and match bone names when retargeting
+ ue_rename_dict = {
+ 'Hips': 'pelvis',
+ 'Spine': 'spine_01',
+ 'Chest': 'spine_02',
+ 'Upper Chest': 'spine_03',
+ 'Neck': 'neck',
+ 'Head': 'head',
+ 'Left shoulder': 'clavicle_l',
+ 'Right shoulder': 'clavicle_r',
+ 'Left arm': 'upperarm_l',
+ 'Right arm': 'upperarm_r',
+ 'Left elbow': 'lowerarm_l',
+ 'Right elbow': 'lowerarm_r',
+ 'Left wrist': 'hand_l',
+ 'Right wrist': 'hand_r',
+
+ 'Left leg': 'thigh_l',
+ 'Right leg': 'thigh_r',
+ 'Left knee': 'calf_l',
+ 'Right knee': 'calf_r',
+ 'cf_j_leg03_L': 'foot_l',
+ 'cf_j_leg03_R': 'foot_r',
+ 'Left toe': 'ball_l',
+ 'Right toe': 'ball_r',
+ }
+ for bone in ue_rename_dict:
+ if armature.data.bones.get(bone):
+ armature.data.bones[bone].name = ue_rename_dict[bone]
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ #Make all the bones on the legs face the same direction, otherwise IK won't work in Unreal
+ armature.data.edit_bones["calf_l"].tail.z = armature.data.edit_bones["calf_l"].head.z + 0.1
+ armature.data.edit_bones["calf_l"].head.y += 0.01
+ armature.data.edit_bones["calf_r"].tail.z = armature.data.edit_bones["calf_r"].head.z + 0.1
+ armature.data.edit_bones["calf_r"].head.y += 0.01
+
+ armature.data.edit_bones["ball_l"].tail.z = armature.data.edit_bones["ball_l"].head.z
+ armature.data.edit_bones["ball_l"].tail.y = armature.data.edit_bones["ball_l"].head.y - 0.05
+ armature.data.edit_bones["ball_r"].tail.z = armature.data.edit_bones["ball_r"].head.z
+ armature.data.edit_bones["ball_r"].tail.y = armature.data.edit_bones["ball_r"].head.y - 0.05
+
+ bpy.ops.object.mode_set(mode='POSE')
+
+ #If simplifying the bones...
+ if simp_type in ['A', 'B']:
+ #show all bones on the armature
+ bpy.ops.armature.collection_show_all()
+ bpy.ops.pose.select_all(action='DESELECT')
+
+ #Move pupil bones to layer 1
+ armature = bpy.data.objects[armature_name]
+ if armature.data.bones.get('Left Eye'):
+ armature.data.bones['Left Eye'].collections.clear()
+ armature.data.collections['0'].assign(armature.data.bones.get('Left Eye'))
+ armature.data.bones['Right Eye'].collections.clear()
+ armature.data.collections['0'].assign(armature.data.bones.get('Right Eye'))
+
+ #Select bones on layer 11
+ for bone in armature.data.bones:
+ if bone.collections.get('10'):
+ bone.select = True
+
+ #if very simple selected, also get 3-5,12,17-19
+ if simp_type in ['A']:
+ for bone in armature.data.bones:
+ select_bool = (bone.collections.get('2') or
+ bone.collections.get('3') or
+ bone.collections.get('4') or
+ bone.collections.get('11') or
+ bone.collections.get('12') or
+ bone.collections.get('16') or
+ bone.collections.get('17') or
+ bone.collections.get('18')
+ )
+ if select_bool:
+ bone.select = True
+
+ c.kklog('Using the merge weights function in CATS to simplify bones...')
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.kkbp.cats_merge_weights()
+
+ #If exporting for VRM or VRC...
+ if prep_type in ['A', 'D']:
+ c.kklog('Editing armature for VRM...')
+ bpy.context.view_layer.objects.active=armature
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ #Rearrange bones to match CATS output
+ if armature.data.edit_bones.get('Pelvis'):
+ armature.data.edit_bones['Pelvis'].parent = None
+ armature.data.edit_bones['Spine'].parent = armature.data.edit_bones['Pelvis']
+ armature.data.edit_bones['Hips'].name = 'dont need lol'
+ armature.data.edit_bones['Pelvis'].name = 'Hips'
+ armature.data.edit_bones['Left leg'].parent = armature.data.edit_bones['Hips']
+ armature.data.edit_bones['Right leg'].parent = armature.data.edit_bones['Hips']
+ armature.data.edit_bones['Left ankle'].parent = armature.data.edit_bones['Left knee']
+ armature.data.edit_bones['Right ankle'].parent = armature.data.edit_bones['Right knee']
+ armature.data.edit_bones['Left shoulder'].parent = armature.data.edit_bones['Upper Chest']
+ armature.data.edit_bones['Right shoulder'].parent = armature.data.edit_bones['Upper Chest']
+ armature.data.edit_bones.remove(armature.data.edit_bones['dont need lol'])
+
+ bpy.ops.object.mode_set(mode='POSE')
+ bpy.ops.pose.select_all(action='DESELECT')
+
+ #Merge specific bones for unity rig autodetect
+ armature = bpy.data.objects[armature_name]
+ merge_these = ['cf_j_waist02', 'cf_s_waist01', 'cf_s_hand_L', 'cf_s_hand_R']
+ #Delete the upper chest for VR chat models, since it apparently causes errors with eye tracking
+ if prep_type == 'D':
+ merge_these.append('Upper Chest')
+ for bone in armature.data.bones:
+ if bone.name in merge_these:
+ bone.select = True
+
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.kkbp.cats_merge_weights()
+
+ #If exporting for MMD...
+ if prep_type == 'C':
+ #Create the empty
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=(0, 0, 0))
+ empty = bpy.data.objects['Empty']
+ bpy.ops.object.select_all(action='DESELECT')
+ armature.parent = empty
+ bpy.context.view_layer.objects.active = armature
+
+ #rename bones to stock
+ if armature.data.bones.get('Center'):
+ bpy.ops.kkbp.switcharmature('INVOKE_DEFAULT')
+
+ #then rename bones to japanese
+ pmx_rename_dict = {
+ '全ての親':'cf_n_height',
+ 'センター':'cf_j_hips',
+ '上半身':'cf_j_spine01',
+ '上半身2':'cf_j_spine02',
+ '上半身3':'cf_j_spine03',
+ '首':'cf_j_neck',
+ '頭':'cf_j_head',
+ '両目':'Eyesx',
+ '左目':'cf_J_hitomi_tx_L',
+ '右目':'cf_J_hitomi_tx_R',
+ '左腕':'cf_j_arm00_L',
+ '右腕':'cf_j_arm00_R',
+ '左ひじ':'cf_j_forearm01_L',
+ '右ひじ':'cf_j_forearm01_R',
+ '左肩':'cf_j_shoulder_L',
+ '右肩':'cf_j_shoulder_R',
+ '左手首':'cf_j_hand_L',
+ '右手首':'cf_j_hand_R',
+ '左親指0':'cf_j_thumb01_L',
+ '左親指1':'cf_j_thumb02_L',
+ '左親指2':'cf_j_thumb03_L',
+ '左薬指1':'cf_j_ring01_L',
+ '左薬指2':'cf_j_ring02_L',
+ '左薬指3':'cf_j_ring03_L',
+ '左中指1':'cf_j_middle01_L',
+ '左中指2':'cf_j_middle02_L',
+ '左中指3':'cf_j_middle03_L',
+ '左小指1':'cf_j_little01_L',
+ '左小指2':'cf_j_little02_L',
+ '左小指3':'cf_j_little03_L',
+ '左人指1':'cf_j_index01_L',
+ '左人指2':'cf_j_index02_L',
+ '左人指3':'cf_j_index03_L',
+ '右親指0':'cf_j_thumb01_R',
+ '右親指1':'cf_j_thumb02_R',
+ '右親指2':'cf_j_thumb03_R',
+ '右薬指1':'cf_j_ring01_R',
+ '右薬指2':'cf_j_ring02_R',
+ '右薬指3':'cf_j_ring03_R',
+ '右中指1':'cf_j_middle01_R',
+ '右中指2':'cf_j_middle02_R',
+ '右中指3':'cf_j_middle03_R',
+ '右小指1':'cf_j_little01_R',
+ '右小指2':'cf_j_little02_R',
+ '右小指3':'cf_j_little03_R',
+ '右人指1':'cf_j_index01_R',
+ '右人指2':'cf_j_index02_R',
+ '右人指3':'cf_j_index03_R',
+ '下半身':'cf_j_waist01',
+ '左足':'cf_j_thigh00_L',
+ '右足':'cf_j_thigh00_R',
+ '左ひざ':'cf_j_leg01_L',
+ '右ひざ':'cf_j_leg01_R',
+ '左足首':'cf_j_leg03_L',
+ '右足首':'cf_j_leg03_R',
+ }
+
+ for bone in pmx_rename_dict:
+ armature.data.bones[pmx_rename_dict[bone]].name = bone
+
+ #Rearrange bones to match a random pmx model I found
+ bpy.ops.object.mode_set(mode='EDIT')
+ armature.data.edit_bones['左肩'].parent = armature.data.edit_bones['上半身3']
+ armature.data.edit_bones['右肩'].parent = armature.data.edit_bones['上半身3']
+ armature.data.edit_bones['左足'].parent = armature.data.edit_bones['下半身']
+ armature.data.edit_bones['右足'].parent = armature.data.edit_bones['下半身']
+
+ #refresh the vertex groups? Bones will act as if they're detached if this isn't done
+ body.vertex_groups.active=body.vertex_groups['BodyTop']
+
+ #combine all objects into one
+
+ #create leg IKs?
+
+ c.kklog('Using CATS to simplify more bones for MMD...')
+
+ #use mmd_tools to convert
+ bpy.ops.mmd_tools.convert_to_mmd_model()
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ #only disable the prep button if the non-atlas model has been modified.
+ #This is because the model with atlas can be regenerated with the bake materials button
+ return armature_name == 'Armature'
+
+class export_prep(bpy.types.Operator):
+ bl_idname = "kkbp.exportprep"
+ bl_label = "Prep for target application"
+ bl_description = t('export_prep_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ scene = context.scene.kkbp
+ prep_type = scene.prep_dropdown
+ simp_type = scene.simp_dropdown
+ last_step = time.time()
+ try:
+ c.toggle_console()
+ if main(prep_type, simp_type):
+ scene.plugin_state = 'prepped'
+ c.kklog('Finished in ' + str(time.time() - last_step)[0:4] + 's')
+ c.toggle_console()
+ return {'FINISHED'}
+ except:
+ c.kklog('Unknown python error occurred', type = 'error')
+ c.kklog(traceback.format_exc())
+ self.report({'ERROR'}, traceback.format_exc())
+ return {"CANCELLED"}
+
diff --git a/exporting/material_combiner/LICENSE b/exporting/material_combiner/LICENSE
new file mode 100644
index 0000000..68473e0
--- /dev/null
+++ b/exporting/material_combiner/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 shotariya
+
+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/exporting/material_combiner/__pycache__/combine_list.cpython-311.pyc b/exporting/material_combiner/__pycache__/combine_list.cpython-311.pyc
new file mode 100644
index 0000000..684c70d
Binary files /dev/null and b/exporting/material_combiner/__pycache__/combine_list.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/combiner.cpython-311.pyc b/exporting/material_combiner/__pycache__/combiner.cpython-311.pyc
new file mode 100644
index 0000000..865dd87
Binary files /dev/null and b/exporting/material_combiner/__pycache__/combiner.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/combiner_ops.cpython-311.pyc b/exporting/material_combiner/__pycache__/combiner_ops.cpython-311.pyc
new file mode 100644
index 0000000..8cb1004
Binary files /dev/null and b/exporting/material_combiner/__pycache__/combiner_ops.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/extend_types.cpython-311.pyc b/exporting/material_combiner/__pycache__/extend_types.cpython-311.pyc
new file mode 100644
index 0000000..ec81c3b
Binary files /dev/null and b/exporting/material_combiner/__pycache__/extend_types.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/get_pillow.cpython-311.pyc b/exporting/material_combiner/__pycache__/get_pillow.cpython-311.pyc
new file mode 100644
index 0000000..3a3df94
Binary files /dev/null and b/exporting/material_combiner/__pycache__/get_pillow.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/globs.cpython-311.pyc b/exporting/material_combiner/__pycache__/globs.cpython-311.pyc
new file mode 100644
index 0000000..2e89e86
Binary files /dev/null and b/exporting/material_combiner/__pycache__/globs.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/images.cpython-311.pyc b/exporting/material_combiner/__pycache__/images.cpython-311.pyc
new file mode 100644
index 0000000..4c6c0d7
Binary files /dev/null and b/exporting/material_combiner/__pycache__/images.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/materials.cpython-311.pyc b/exporting/material_combiner/__pycache__/materials.cpython-311.pyc
new file mode 100644
index 0000000..c9f8fd0
Binary files /dev/null and b/exporting/material_combiner/__pycache__/materials.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/objects.cpython-311.pyc b/exporting/material_combiner/__pycache__/objects.cpython-311.pyc
new file mode 100644
index 0000000..e57086c
Binary files /dev/null and b/exporting/material_combiner/__pycache__/objects.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/packer.cpython-311.pyc b/exporting/material_combiner/__pycache__/packer.cpython-311.pyc
new file mode 100644
index 0000000..07629c3
Binary files /dev/null and b/exporting/material_combiner/__pycache__/packer.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/textures.cpython-311.pyc b/exporting/material_combiner/__pycache__/textures.cpython-311.pyc
new file mode 100644
index 0000000..de6a198
Binary files /dev/null and b/exporting/material_combiner/__pycache__/textures.cpython-311.pyc differ
diff --git a/exporting/material_combiner/__pycache__/type_annotations.cpython-311.pyc b/exporting/material_combiner/__pycache__/type_annotations.cpython-311.pyc
new file mode 100644
index 0000000..99bc195
Binary files /dev/null and b/exporting/material_combiner/__pycache__/type_annotations.cpython-311.pyc differ
diff --git a/exporting/material_combiner/combine_list.py b/exporting/material_combiner/combine_list.py
new file mode 100644
index 0000000..c98dada
--- /dev/null
+++ b/exporting/material_combiner/combine_list.py
@@ -0,0 +1,126 @@
+from collections import defaultdict
+from typing import List
+from typing import Set
+from typing import cast
+
+import bpy
+from bpy.props import *
+
+from . import globs
+from .type_annotations import CombineListData
+from .type_annotations import Scene
+from .materials import get_materials
+
+
+class RefreshObData(bpy.types.Operator):
+ bl_idname = 'kkbp.refresh_ob_data'
+ bl_label = 'Combine List'
+ bl_description = 'Updates the material list'
+
+ @staticmethod
+ def execute(self, context: bpy.types.Context) -> Set[str]:
+ scn = context.scene
+ ob_list = [ob for ob in context.visible_objects if
+ ob.type == 'MESH' and ob.data.uv_layers.active and ob.data.materials]
+ combine_list_data = self._cache_previous_values(scn)
+ self._rebuild_items_list(scn, ob_list, combine_list_data)
+ return {'FINISHED'}
+
+ @staticmethod
+ def _cache_previous_values(scn: Scene) -> CombineListData:
+ combine_list_data = cast(CombineListData, defaultdict(lambda: {
+ 'used': True,
+ 'mats': defaultdict(lambda: {
+ 'used': True,
+ 'layer': 1,
+ }),
+ }))
+
+ for item in scn.kkbp_ob_data:
+ if item.type == globs.CL_OBJECT:
+ combine_list_data[item.ob]['used'] = item.used
+ elif item.type == globs.CL_MATERIAL:
+ mat_data = combine_list_data[item.ob]['mats'][item.mat]
+ mat_data.update({'used': item.used, 'layer': item.layer})
+ return combine_list_data
+
+ def _rebuild_items_list(self, scn: Scene, ob_list: Set[bpy.types.Object],
+ combine_list_data: CombineListData) -> None:
+ scn.kkbp_ob_data.clear()
+
+ for ob_id, ob in enumerate(ob_list):
+ ob_data = combine_list_data[ob]
+ ob_used = ob_data['used']
+ self._create_ob_item(scn, ob, ob_id, ob_used)
+
+ for mat in get_materials(ob):
+ if globs.is_blender_3_or_newer and not mat.preview:
+ mat.preview_ensure()
+
+ mat_data = ob_data['mats'][mat]
+ mat_used = ob_used and mat_data['used']
+ mat_layer = mat_data['layer']
+ self._create_mat_item(scn, ob, ob_id, mat, mat_used, mat_layer)
+ self._create_separator_item(scn)
+
+ @staticmethod
+ def _create_ob_item(scn: Scene, ob: bpy.types.Object, ob_id: int, used: bool) -> None:
+ item = scn.kkbp_ob_data.add()
+ item.ob = ob
+ item.ob_id = ob_id
+ item.type = 0
+ item.used = used
+
+ @staticmethod
+ def _create_mat_item(scn: Scene, ob: bpy.types.Object, ob_id: int, mat: bpy.types.Material, used: bool,
+ layer: int) -> None:
+ item = scn.kkbp_ob_data.add()
+ item.ob = ob
+ item.ob_id = ob_id
+ item.mat = mat
+ item.type = 1
+ item.used = used
+ item.layer = layer
+
+ @staticmethod
+ def _create_separator_item(scn: Scene) -> None:
+ item = scn.kkbp_ob_data.add()
+ item.type = 2
+
+
+class CombineSwitch(bpy.types.Operator):
+ bl_idname = 'kkbp.combine_switch'
+ bl_label = 'Add Item'
+ bl_description = 'Selected materials will be combined into one texture atlas'
+
+ list_id = IntProperty(default=0)
+
+ def execute(self, context: bpy.types.Context) -> Set[str]:
+ scn = context.scene
+ data = scn.kkbp_ob_data
+ item = data[self.list_id]
+ if item.type == globs.CL_OBJECT:
+ self._switch_ob_state(data, item)
+ elif item.type == globs.CL_MATERIAL:
+ self._switch_mat_state(data, item)
+ return {'FINISHED'}
+
+ @staticmethod
+ def _switch_ob_state(data: List[bpy.types.PropertyGroup], item: bpy.types.PropertyGroup) -> None:
+ mat_list = [mat for mat in data if mat.ob_id == item.ob_id and mat.type == globs.CL_MATERIAL]
+ if not mat_list:
+ return
+
+ item.used = not item.used
+ for mat in mat_list:
+ mat.used = item.used
+
+ @staticmethod
+ def _switch_mat_state(data: List[bpy.types.PropertyGroup], item: bpy.types.PropertyGroup) -> None:
+ ob = next((ob for ob in data if ob.ob_id == item.ob_id and ob.type == globs.CL_OBJECT), None)
+ if not ob:
+ return
+
+ if not item.used:
+ ob.used = True
+ item.used = not item.used
diff --git a/exporting/material_combiner/combiner.py b/exporting/material_combiner/combiner.py
new file mode 100644
index 0000000..2994f63
--- /dev/null
+++ b/exporting/material_combiner/combiner.py
@@ -0,0 +1,71 @@
+import bpy
+from bpy.props import *
+from .combiner_ops import *
+from .packer import BinPacker
+from ... import common as c
+
+class Combiner(bpy.types.Operator):
+ bl_idname = 'kkbp.combiner'
+ bl_label = 'Create Atlas'
+ bl_description = 'Combine materials'
+ bl_options = {'UNDO', 'INTERNAL'}
+
+ def execute(self, context: bpy.types.Context) -> Set[str]:
+ #from invoke
+ scn = context.scene
+ bpy.ops.kkbp.refresh_ob_data()
+ for index, object in enumerate([o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'MESH' and not o.hide_get()]):
+ #check if this object is worth doing anything with
+ if not [mat_slot.material for mat_slot in object.material_slots if mat_slot.material.get('simple')]:
+ continue
+
+ set_ob_mode(context.view_layer, scn.kkbp_ob_data)
+ self.data = get_data(scn.kkbp_ob_data, object)
+ self.mats_uv = get_mats_uv(scn, self.data)
+ clear_empty_mats(scn, self.data, self.mats_uv)
+ get_duplicates(self.mats_uv)
+ self.structure = get_structure(scn, self.data, self.mats_uv)
+
+ #from execute
+ scn.kkbp_save_path = os.path.join(context.scene.kkbp.import_dir, 'atlas_files')
+ self.structure = BinPacker(get_size(scn, self.structure)).fit()
+
+ size = get_atlas_size(self.structure)
+ atlas_size = calculate_adjusted_size(scn, size)
+
+ if max(atlas_size, default=0) > 20000:
+ text = 'The output image size of {0}x{1}px is too large'.format(*atlas_size)
+ c.kklog(text)
+ self.report({'ERROR'}, text)
+ return {'FINISHED'}
+
+ bake_types = []
+ if scn.kkbp.bake_light_bool:
+ bake_types.append('light')
+ if scn.kkbp.bake_dark_bool:
+ bake_types.append('dark')
+ if scn.kkbp.bake_norm_bool:
+ bake_types.append('normal')
+
+ for type in bake_types:
+ #replace all images
+ for material in [mat_slot.material for mat_slot in object.material_slots if mat_slot.material.get('simple')]:
+ image = material.node_tree.nodes['textures'].node_tree.nodes[type].image
+ if image:
+ if image.name == 'Template: Placeholder':
+ image = None
+ if not image:
+ continue
+ else:
+ material.node_tree.nodes['Image Texture'].image = image
+
+ #then run the atlas creation
+ atlas = get_atlas(scn, self.structure, atlas_size)
+ comb_mats = get_comb_mats(scn, atlas, self.mats_uv, type, index)
+ c.print_timer(f'save atlas for {object.name} {type}')
+
+ align_uvs(scn, self.structure, atlas.size, size)
+ bpy.ops.kkbp.refresh_ob_data()
+
+ return {'FINISHED'}
+
diff --git a/exporting/material_combiner/combiner_ops.py b/exporting/material_combiner/combiner_ops.py
new file mode 100644
index 0000000..c462667
--- /dev/null
+++ b/exporting/material_combiner/combiner_ops.py
@@ -0,0 +1,482 @@
+import io
+import itertools
+import math
+import os
+import random
+import re
+from collections import OrderedDict
+from collections import defaultdict
+from itertools import chain
+from typing import Dict
+from typing import List
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Union
+from typing import cast
+
+import bpy
+import numpy as np
+
+from ... import common as c
+from . import globs
+from .type_annotations import CombMats
+from .type_annotations import Diffuse
+from .type_annotations import MatsUV
+from .type_annotations import ObMats
+from .type_annotations import SMCObData
+from .type_annotations import SMCObDataItem
+from .type_annotations import Scene
+from .type_annotations import Structure
+from .type_annotations import StructureItem
+from .images import get_packed_file
+from .materials import get_diffuse
+from .materials import get_shader_type
+from .materials import shader_image_nodes
+from .materials import sort_materials
+from .objects import align_uv
+from .objects import get_polys
+from .objects import get_uv
+
+try:
+ from PIL import Image
+
+ ImageType = Image.Image
+except ImportError:
+ Image = None
+ ImageType = None
+
+try:
+ from PIL import ImageChops
+except ImportError:
+ ImageChops = None
+
+try:
+ from PIL import ImageFile
+except ImportError:
+ ImageFile = None
+
+if Image:
+ Image.MAX_IMAGE_PIXELS = None
+ try:
+ resampling = Image.LANCZOS
+ except AttributeError:
+ resampling = Image.ANTIALIAS
+
+if ImageFile:
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+
+atlas_prefix = 'atlas_'
+atlas_texture_prefix = 'texture_atlas_'
+atlas_material_prefix = 'material_atlas_'
+
+
+def set_ob_mode(scn: Scene, data: SMCObData) -> None:
+ scn.objects.active = bpy.data.objects['Body ' + c.get_name() + '.001']
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+
+def get_data(data: Sequence[bpy.types.PropertyGroup], object) -> SMCObData:
+ mats = defaultdict(dict)
+ if object.type == 'MESH':
+ for mat in [m for m in object.data.materials if 'Outline ' not in m.name]:
+ mats[object.name][mat] = 1 #layer, just set to always 1
+ return mats
+
+
+def get_mats_uv(scn: Scene, data: SMCObData) -> MatsUV:
+ mats_uv = defaultdict(lambda: defaultdict(list))
+ for ob_n, item in data.items():
+ ob = scn.objects[ob_n]
+ for idx, polys in get_polys(ob).items():
+ mat = ob.data.materials[idx]
+ if mat not in item:
+ continue
+ for poly in polys:
+ mats_uv[ob_n][mat].extend(align_uv(get_uv(ob, poly)))
+ return mats_uv
+
+
+def clear_empty_mats(scn: Scene, data: SMCObData, mats_uv: MatsUV) -> None:
+ for ob_n, item in data.items():
+ ob = scn.objects[ob_n]
+ for mat in item:
+ if mat not in mats_uv[ob_n]:
+ _delete_material(ob, mat.name)
+
+
+def _delete_material(ob: bpy.types.Object, mat_name: str) -> None:
+ ob_mats = ob.data.materials
+ mat_idx = ob_mats.find(mat_name)
+ if mat_idx > -1:
+ ob_mats.pop(index=mat_idx)
+
+
+def get_duplicates(mats_uv: MatsUV) -> None:
+ mat_list = list(chain.from_iterable(mats_uv.values()))
+ sorted_mat_list = sort_materials(mat_list)
+ for mats in sorted_mat_list:
+ kkbp_root_mat = mats[0]
+ for mat in mats[1:]:
+ mat.kkbp_root_mat = kkbp_root_mat
+
+
+def get_structure(scn: Scene, data: SMCObData, mats_uv: MatsUV) -> Structure:
+ structure = defaultdict(lambda: {
+ 'gfx': {
+ 'img_or_color': None,
+ 'size': (),
+ 'uv_size': ()
+ },
+ 'dup': [],
+ 'ob': [],
+ 'uv': []
+ })
+
+ for ob_n, item in data.items():
+ ob = scn.objects[ob_n]
+ for mat in item:
+ if mat.name not in ob.data.materials:
+ continue
+ kkbp_root_mat = mat.kkbp_root_mat or mat
+ if mat.kkbp_root_mat and mat.name not in structure[kkbp_root_mat]['dup']:
+ structure[kkbp_root_mat]['dup'].append(mat.name)
+ if ob.name not in structure[kkbp_root_mat]['ob']:
+ structure[kkbp_root_mat]['ob'].append(ob.name)
+ structure[kkbp_root_mat]['uv'].extend(mats_uv[ob_n][mat])
+ return structure
+
+
+def clear_duplicates(scn: Scene, data: Structure) -> None:
+ for item in data.values():
+ for ob_n in item['ob']:
+ ob = scn.objects[ob_n]
+ for dup_name in item['dup']:
+ _delete_material(ob, dup_name)
+
+
+def get_size(scn: Scene, data: Structure) -> Dict:
+ for mat, item in data.items():
+ img = _get_image(mat)
+ packed_file = get_packed_file(img)
+ max_x, max_y = _get_max_uv_coordinates(item['uv'])
+ item['gfx']['uv_size'] = (np.clip(max_x, 1, 25), np.clip(max_y, 1, 25))
+
+ if not scn.kkbp_crop:
+ item['gfx']['uv_size'] = tuple(math.ceil(x) for x in item['gfx']['uv_size'])
+
+ if packed_file:
+ img_size = _get_image_size(mat, img)
+ item['gfx']['size'] = _calculate_size(img_size, item['gfx']['uv_size'], scn.kkbp_gaps)
+ else:
+ item['gfx']['size'] = (scn.kkbp_diffuse_size + scn.kkbp_gaps,) * 2
+
+ return OrderedDict(sorted(data.items(), key=_size_sorting, reverse=True))
+
+
+def _size_sorting(item: Sequence[StructureItem]) -> Tuple[int, int, int, Union[str, Diffuse, None]]:
+ gfx = item[1]['gfx']
+ size_x, size_y = gfx['size']
+
+ img_or_color = gfx['img_or_color']
+ name_or_color = None
+ if isinstance(img_or_color, tuple):
+ name_or_color = gfx['img_or_color']
+ elif isinstance(img_or_color, bpy.types.PackedFile):
+ name_or_color = img_or_color.id_data.name
+
+ return max(size_x, size_y), size_x * size_y, size_x, name_or_color
+
+
+def _get_image(mat: bpy.types.Material) -> Union[bpy.types.Image, None]:
+ shader = get_shader_type(mat) if mat else None
+ node = mat.node_tree.nodes.get(shader_image_nodes.get(shader, ''))
+ return node.image if node else None
+
+
+def _get_image_size(mat: bpy.types.Material, img: bpy.types.Image) -> Tuple[int, int]:
+ return (
+ (
+ min(mat.kkbp_size_width, img.size[0]),
+ min(mat.kkbp_size_height, img.size[1]),
+ )
+ if mat.kkbp_size
+ else cast(Tuple[int, int], img.size)
+ )
+
+
+def _get_max_uv_coordinates(uv_loops: List[bpy.types.MeshUVLoop]) -> Tuple[float, float]:
+ max_x = 1
+ max_y = 1
+
+ for uv in uv_loops:
+ if not math.isnan(uv.x):
+ max_x = max(max_x, uv.x)
+ if not math.isnan(uv.y):
+ max_y = max(max_y, uv.y)
+
+ return max_x, max_y
+
+
+def _calculate_size(img_size: Tuple[int, int], uv_size: Tuple[int, int], gaps: int) -> Tuple[int, int]:
+ return cast(Tuple[int, int], tuple(s * uv_s + gaps for s, uv_s in zip(img_size, uv_size)))
+
+
+def get_atlas_size(structure: Structure) -> Tuple[int, int]:
+ max_x = 1
+ max_y = 1
+
+ for item in structure.values():
+ max_x = max(max_x, item['gfx']['fit']['x'] + item['gfx']['size'][0])
+ max_y = max(max_y, item['gfx']['fit']['y'] + item['gfx']['size'][1])
+
+ return int(max_x), int(max_y)
+
+
+def calculate_adjusted_size(scn: Scene, size: Tuple[int, int]) -> Tuple[int, int]:
+ if scn.kkbp_size == 'PO2':
+ return cast(Tuple[int, int], tuple(1 << int(x - 1).bit_length() for x in size))
+ elif scn.kkbp_size == 'QUAD':
+ return (int(max(size)),) * 2
+ return size
+
+
+def get_atlas(scn: Scene, data: Structure, atlas_size: Tuple[int, int]) -> ImageType:
+ #create new atlas image
+ kkbp_size = (scn.kkbp_size_width, scn.kkbp_size_height)
+ img = Image.new('RGBA', atlas_size)
+ half_gaps = int(scn.kkbp_gaps / 2)
+
+ #for every material in data items,
+ for mat, item in data.items():
+ _set_image_or_color(item, mat)
+ _paste_gfx(scn, item, mat, img, half_gaps)
+
+ if scn.kkbp_size in ['CUST', 'STRICTCUST']:
+ img.thumbnail(kkbp_size, resampling)
+
+ if scn.kkbp_size == 'STRICTCUST':
+ canvas_img = Image.new('RGBA', kkbp_size)
+ canvas_img.paste(img)
+ return canvas_img
+
+ return img
+
+
+def _set_image_or_color(item: StructureItem, mat: bpy.types.Material) -> None:
+ shader = get_shader_type(mat) if mat else None
+ node_name = shader_image_nodes.get(shader)
+ item['gfx']['img_or_color'] = get_packed_file(mat.node_tree.nodes.get(node_name).image) if node_name else None
+
+ if not item['gfx']['img_or_color']:
+ item['gfx']['img_or_color'] = get_diffuse(mat)
+
+
+def _paste_gfx(scn: Scene, item: StructureItem, mat: bpy.types.Material, img: ImageType, half_gaps: int) -> None:
+ if not item['gfx']['fit']:
+ return
+
+ img.paste(
+ _get_gfx(scn, mat, item, item['gfx']['img_or_color']),
+ (int(item['gfx']['fit']['x'] + half_gaps), int(item['gfx']['fit']['y'] + half_gaps))
+ )
+
+
+def _get_gfx(scn: Scene, mat: bpy.types.Material, item: StructureItem,
+ img_or_color: Union[bpy.types.PackedFile, Tuple, None]) -> ImageType:
+ size = cast(Tuple[int, int], tuple(int(size - scn.kkbp_gaps) for size in item['gfx']['size']))
+
+ if not img_or_color:
+ return Image.new('RGBA', size, (1, 1, 1, 1))
+
+ if isinstance(img_or_color, tuple):
+ return Image.new('RGBA', size, img_or_color)
+
+ img = Image.open(io.BytesIO(img_or_color.data))
+ if img.size != size:
+ img.resize(size, resampling)
+ if mat.kkbp_size:
+ img.thumbnail((mat.kkbp_size_width, mat.kkbp_size_height), resampling)
+ if max(item['gfx']['uv_size'], default=0) > 1:
+ img = _get_uv_image(item, img, size)
+ if mat.kkbp_diffuse:
+ diffuse_img = Image.new(img.mode, size, get_diffuse(mat))
+ img = ImageChops.multiply(img, diffuse_img)
+
+ return img
+
+
+def _get_uv_image(item: StructureItem, img: ImageType, size: Tuple[int, int]) -> ImageType:
+ uv_img = Image.new('RGBA', size)
+ size_height = size[1]
+ img_width, img_height = img.size
+ uv_width, uv_height = (math.ceil(x) for x in item['gfx']['uv_size'])
+
+ for h in range(uv_height):
+ y = size_height - img_height - h * img_height
+ for w in range(uv_width):
+ x = w * img_width
+ uv_img.paste(img, (x, y))
+
+ return uv_img
+
+
+def align_uvs(scn: Scene, data: Structure, atlas_size: Tuple[int, int], size: Tuple[int, int]) -> None:
+ size_width, size_height = size
+
+ scaled_width, scaled_height = _get_scale_factors(atlas_size, size)
+
+ margin = scn.kkbp_gaps + (0 if scn.kkbp_pixel_art else 2)
+ border_margin = int(scn.kkbp_gaps / 2) + (0 if scn.kkbp_pixel_art else 1)
+
+ for item in data.values():
+ gfx_size = item['gfx']['size']
+ gfx_height = gfx_size[1]
+
+ gfx_width_margin, gfx_height_margin = (x - margin for x in gfx_size)
+
+ uv_width, uv_height = item['gfx']['uv_size']
+
+ x_offset = item['gfx']['fit']['x'] + border_margin
+ y_offset = item['gfx']['fit']['y'] - border_margin
+
+ for uv in item['uv']:
+ reset_x = uv.x / uv_width * gfx_width_margin
+ reset_y = uv.y / uv_height * gfx_height_margin - gfx_height
+
+ uv_x = (reset_x + x_offset) / size_width
+ uv_y = (reset_y - y_offset) / size_height
+
+ uv.x = uv_x * scaled_width
+ uv.y = uv_y * scaled_height + 1
+
+
+def _get_scale_factors(atlas_size: Tuple[int, int], size: Tuple[int, int]) -> Tuple[float, float]:
+ scaled_factors = tuple(x / y for x, y in zip(size, atlas_size))
+
+ if all(factor <= 1 for factor in scaled_factors):
+ return cast(Tuple[float, float], scaled_factors)
+
+ atlas_width, atlas_height = atlas_size
+ size_width, size_height = size
+
+ aspect_ratio = (size_width * atlas_height) / (size_height * atlas_width)
+ return (1, 1 / aspect_ratio) if aspect_ratio > 1 else (aspect_ratio, 1)
+
+
+def get_comb_mats(scn: Scene, atlas: ImageType, mats_uv: MatsUV, type: str, atlas_index) -> CombMats:
+ layers = _get_layers(scn, mats_uv)
+ path = _save_atlas(scn, atlas, atlas_index, type)
+ texture = _create_texture(path, atlas_index)
+ return cast(CombMats, {idx: _create_material(texture, atlas_index, idx) for idx in layers})
+
+
+def _get_layers(scn: Scene, mats_uv: MatsUV) -> Set[int]:
+ return {}
+
+
+def _get_unique_id(scn: Scene) -> str:
+ existed_ids = set()
+ _add_its_from_existing_materials(scn, existed_ids)
+
+ if not os.path.isdir(scn.kkbp_save_path):
+ return _generate_random_unique_id(existed_ids)
+
+ _add_ids_from_existing_files(scn, existed_ids)
+ unique_id = next(x for x in itertools.count(start=1) if x not in existed_ids)
+ return '{:05d}'.format(unique_id)
+
+
+def _add_its_from_existing_materials(scn: Scene, existed_ids: Set[int]) -> None:
+ atlas_material_pattern = re.compile(r'{0}(\d+)_\d+'.format(atlas_material_prefix))
+ for item in scn.kkbp_ob_data:
+ if item.type != globs.CL_MATERIAL:
+ continue
+
+ match = atlas_material_pattern.fullmatch(item.mat.name)
+ if match:
+ existed_ids.add(int(match.group(1)))
+
+
+def _generate_random_unique_id(existed_ids: Set[int]) -> str:
+ unused_ids = set(range(10000, 99999)) - existed_ids
+ return str(random.choice(list(unused_ids)))
+
+
+def _add_ids_from_existing_files(scn: Scene, existed_ids: Set[int]) -> None:
+ atlas_file_pattern = re.compile(r'{0}(\d+).png'.format(atlas_prefix))
+ for file_name in os.listdir(scn.kkbp_save_path):
+ match = atlas_file_pattern.fullmatch(file_name)
+ if match:
+ existed_ids.add(int(match.group(1)))
+
+
+def _save_atlas(scn: Scene, atlas: ImageType, atlas_index: str, type: str) -> str:
+ path = os.path.join(scn.kkbp_save_path, f'{atlas_index}_{type}.png')
+ try:
+ atlas.save(path)
+ except:
+ #atlas folder didn't exist
+ os.mkdir(scn.kkbp_save_path)
+ atlas.save(path)
+ return path
+
+
+def _create_texture(path: str, unique_id: str) -> bpy.types.Texture:
+ texture = bpy.data.textures.new('{0}{1}'.format(atlas_texture_prefix, unique_id), 'IMAGE')
+ image = bpy.data.images.load(path)
+ texture.image = image
+ return texture
+
+
+def _create_material(texture: bpy.types.Texture, unique_id: str, idx: int) -> bpy.types.Material:
+ mat = bpy.data.materials.new(name='{0}{1}_{2}'.format(atlas_material_prefix, unique_id, idx))
+ _configure_material(mat, texture)
+ return mat
+
+
+def _configure_material(mat: bpy.types.Material, texture: bpy.types.Texture) -> None:
+ mat['atlas'] = True
+ mat.blend_method = 'CLIP'
+ mat.use_backface_culling = True
+ mat.use_nodes = True
+
+ node_texture = mat.node_tree.nodes.new(type='ShaderNodeTexImage')
+ node_texture.image = texture.image
+ node_texture.label = 'Material Combiner Texture'
+ node_texture.location = -300, 300
+
+ mat.node_tree.links.new(node_texture.outputs['Color'],
+ mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'])
+ mat.node_tree.links.new(node_texture.outputs['Alpha'],
+ mat.node_tree.nodes['Principled BSDF'].inputs['Alpha'])
+
+def assign_comb_mats(scn: Scene, data: SMCObData, comb_mats: CombMats) -> None:
+ for ob_n, item in data.items():
+ ob = scn.objects[ob_n]
+ ob_materials = ob.data.materials
+ _assign_mats(item, comb_mats, ob_materials)
+ _assign_mats_to_polys(item, comb_mats, ob, ob_materials)
+
+
+def _assign_mats(item: SMCObDataItem, comb_mats: CombMats, ob_materials: ObMats) -> None:
+ for idx in set(item.values()):
+ if idx in comb_mats:
+ ob_materials.append(comb_mats[idx])
+
+
+def _assign_mats_to_polys(item: SMCObDataItem, comb_mats: CombMats, ob: bpy.types.Object, ob_materials: ObMats) -> None:
+ for idx, polys in get_polys(ob).items():
+ if ob_materials[idx] not in item:
+ continue
+ mat_name = comb_mats[item[ob_materials[idx]]].name
+ mat_idx = ob_materials.find(mat_name)
+ for poly in polys:
+ poly.material_index = mat_idx
+
+
+def clear_mats(scn: Scene, mats_uv: MatsUV) -> None:
+ for ob_n, item in mats_uv.items():
+ ob = scn.objects[ob_n]
+ for mat in item:
+ _delete_material(ob, mat.name)
diff --git a/exporting/material_combiner/extend_types.py b/exporting/material_combiner/extend_types.py
new file mode 100644
index 0000000..918018f
--- /dev/null
+++ b/exporting/material_combiner/extend_types.py
@@ -0,0 +1,141 @@
+import bpy
+from bpy.props import *
+
+class CombineList(bpy.types.PropertyGroup):
+ ob = PointerProperty(
+ name='Current Object',
+ type=bpy.types.Object,
+ )
+ ob_id = IntProperty(default=0)
+ mat = PointerProperty(
+ name='Current Object Material',
+ type=bpy.types.Material,
+ )
+ layer = IntProperty(
+ name='Material Layers',
+ description='Materials with the same number will be merged together.'
+ '\nUse this to create multiple materials linked to the same atlas file',
+ min=1,
+ max=99,
+ step=1,
+ default=1,
+ )
+ used = BoolProperty(default=True)
+ type = IntProperty(default=0)
+
+def register_smc_types():
+ bpy.types.Scene.kkbp_ob_data = CollectionProperty(type=CombineList)
+ bpy.types.Scene.kkbp_ob_data_id = IntProperty(default=0)
+ bpy.types.Scene.kkbp_list_id = IntProperty(default=0)
+ bpy.types.Scene.kkbp_size = EnumProperty(
+ name='Atlas size',
+ items=[
+ ('PO2', 'Power of 2', 'Combined image size is power of 2'),
+ ('QUAD', 'Quadratic', 'Combined image has same width and height'),
+ ('AUTO', 'Automatic', 'Combined image has minimal size'),
+ ('CUST', 'Custom', 'Combined image has proportionally scaled to fit in custom size'),
+ ('STRICTCUST', 'Strict Custom', 'Combined image has exact custom width and height'),
+ ],
+ description='Select atlas size',
+ default='QUAD',
+ )
+ bpy.types.Scene.kkbp_size_width = IntProperty(
+ name='Max width (px)',
+ description='Select max width for combined image',
+ min=8,
+ max=8192,
+ step=1,
+ default=4096,
+ )
+ bpy.types.Scene.kkbp_size_height = IntProperty(
+ name='Max height (px)',
+ description='Select max height for combined image',
+ min=8,
+ max=8192,
+ step=1,
+ default=4096,
+ )
+ bpy.types.Scene.kkbp_crop = BoolProperty(
+ name='Crop outside images by UV',
+ description='Crop images by UV if materials UV outside of bounds',
+ default=True,
+ )
+ bpy.types.Scene.kkbp_pixel_art = BoolProperty(
+ name='Pixel Art / Small Textures',
+ description='Avoids 1-pixel UV scaling for small textures.'
+ '\nDisable for larger textures to avoid blending with nearby pixels',
+ default=False,
+ )
+ bpy.types.Scene.kkbp_diffuse_size = IntProperty(
+ name='Size of materials without image',
+ description='Select the size of materials that only consist of a color',
+ min=8,
+ max=256,
+ step=1,
+ default=32,
+ )
+ bpy.types.Scene.kkbp_gaps = IntProperty(
+ name='Size of gaps between images',
+ description='Select size of gaps between images',
+ min=0,
+ max=32,
+ step=200,
+ default=0,
+ options={'HIDDEN'},
+ )
+ bpy.types.Scene.kkbp_save_path = StringProperty(
+ description='Select the directory in which the generated texture atlas will be saved',
+ default='',
+ )
+
+ bpy.types.Material.kkbp_root_mat = PointerProperty(
+ name='Material Root',
+ type=bpy.types.Material,
+ )
+ bpy.types.Material.kkbp_diffuse = BoolProperty(
+ name='Multiply image with diffuse color',
+ description='Multiply the materials image with its diffuse color.'
+ '\nINFO: If this color is white the final image will be the same',
+ default=True,
+ )
+ bpy.types.Material.kkbp_size = BoolProperty(
+ name='Custom image size',
+ description='Select the max size for this materials image in the texture atlas',
+ default=False,
+ )
+ bpy.types.Material.kkbp_size_width = IntProperty(
+ name='Max width (px)',
+ description='Select max width for material image',
+ min=8,
+ max=8192,
+ step=1,
+ default=2048,
+ )
+ bpy.types.Material.kkbp_size_height = IntProperty(
+ name='Max height (px)',
+ description='Select max height for material image',
+ min=8,
+ max=8192,
+ step=1,
+ default=2048,
+ )
+
+
+def unregister_smc_types() -> None:
+ del bpy.types.Scene.kkbp_ob_data
+ del bpy.types.Scene.kkbp_ob_data_id
+ del bpy.types.Scene.kkbp_list_id
+ del bpy.types.Scene.kkbp_size
+ del bpy.types.Scene.kkbp_size_width
+ del bpy.types.Scene.kkbp_size_height
+ del bpy.types.Scene.kkbp_crop
+ del bpy.types.Scene.kkbp_pixel_art
+ del bpy.types.Scene.kkbp_diffuse_size
+ del bpy.types.Scene.kkbp_gaps
+ del bpy.types.Scene.kkbp_save_path
+
+ del bpy.types.Material.kkbp_root_mat
+ del bpy.types.Material.kkbp_diffuse
+ del bpy.types.Material.kkbp_size
+ del bpy.types.Material.kkbp_size_width
+ del bpy.types.Material.kkbp_size_height
diff --git a/exporting/material_combiner/get_pillow.py b/exporting/material_combiner/get_pillow.py
new file mode 100644
index 0000000..b0c48c1
--- /dev/null
+++ b/exporting/material_combiner/get_pillow.py
@@ -0,0 +1,30 @@
+import os
+import subprocess
+import sys
+from typing import Set
+from . import globs
+from ... import common as c
+from ...interface.dictionary_en import t
+import bpy
+
+class InstallPIL(bpy.types.Operator):
+ bl_idname = 'kkbp.get_pillow'
+ bl_label = 'Install Pillow'
+ bl_description = t('pillow_tt')
+
+ def execute(self, context: bpy.types.Context) -> Set[str]:
+ try:
+ from PIL import Image, ImageChops
+ except ImportError:
+ self._install_pillow()
+
+ globs.pil_exist = 'restart'
+
+ self.report({'INFO'}, 'Installation complete')
+ return {'FINISHED'}
+
+ @staticmethod
+ def _install_pillow() -> None:
+ from pip import _internal
+ _internal.main(['install', 'pip', 'setuptools', 'wheel', '-U', '--user'])
+ _internal.main(['install', 'Pillow', '--user'])
diff --git a/exporting/material_combiner/globs.py b/exporting/material_combiner/globs.py
new file mode 100644
index 0000000..4c75c79
--- /dev/null
+++ b/exporting/material_combiner/globs.py
@@ -0,0 +1,23 @@
+import bpy
+import sys
+import site
+
+sys.path.insert(0, site.getusersitepackages())
+
+try:
+ from PIL import Image
+ from PIL import ImageChops
+ pil_exist = 'yup'
+except ImportError:
+ pil_exist = 'no'
+
+is_blender_2_79_or_older = bpy.app.version < (2, 80, 0)
+is_blender_2_80_or_newer = bpy.app.version >= (2, 80, 0)
+is_blender_2_92_or_newer = bpy.app.version >= (2, 92, 0)
+is_blender_3_or_newer = bpy.app.version >= (3, 0, 0)
+
+smc_pi = False
+
+CL_OBJECT = 0
+CL_MATERIAL = 1
+CL_SEPARATOR = 2
diff --git a/exporting/material_combiner/images.py b/exporting/material_combiner/images.py
new file mode 100644
index 0000000..b7e7c14
--- /dev/null
+++ b/exporting/material_combiner/images.py
@@ -0,0 +1,19 @@
+import os
+from typing import Union
+
+import bpy
+
+
+def get_image(tex: bpy.types.Texture) -> bpy.types.Image:
+ return tex.image if tex and hasattr(tex, 'image') and tex.image else None
+
+
+def get_packed_file(image: Union[bpy.types.Image, None]) -> Union[bpy.types.PackedFile, None]:
+ if image and not image.packed_file and _get_image_path(image):
+ image.pack()
+ return image.packed_file if image and image.packed_file else None
+
+
+def _get_image_path(img: Union[bpy.types.Image, None]) -> Union[str, None]:
+ path = os.path.abspath(bpy.path.abspath(img.filepath)) if img else ''
+ return path if os.path.isfile(path) and not path.lower().endswith(('.spa', '.sph')) else None
diff --git a/exporting/material_combiner/materials.py b/exporting/material_combiner/materials.py
new file mode 100644
index 0000000..fe58ee6
--- /dev/null
+++ b/exporting/material_combiner/materials.py
@@ -0,0 +1,131 @@
+from collections import OrderedDict
+from collections import defaultdict
+from typing import List
+from typing import Union
+from typing import ValuesView
+from typing import cast
+
+import bpy
+import numpy as np
+
+from .images import get_image
+from .images import get_packed_file
+from .textures import get_texture
+from . import globs
+from .type_annotations import Diffuse
+from .type_annotations import MatDict
+from .type_annotations import MatDictItem
+
+shader_types = OrderedDict([
+ ('mmd', {'mmd_shader', 'mmd_base_tex'}),
+ ('mmdCol', {'mmd_shader'}),
+ ('mtoon', {'Mtoon1BaseColorTexture.Image'}),
+ ('mtoonCol', {'Mtoon1Material.Mtoon1Output'}),
+ ('principled', {'Principled BSDF', 'Image Texture'}),
+ ('principledCol', {'Principled BSDF'}),
+ ('diffuse', {'Diffuse BSDF', 'Image Texture'}),
+ ('diffuseCol', {'Diffuse BSDF'}),
+ ('emission', {'Emission', 'Image Texture'}),
+ ('emissionCol', {'Emission'}),
+])
+
+shader_image_nodes = {
+ 'mmd': 'mmd_base_tex',
+ 'mtoon': 'Mtoon1BaseColorTexture.Image',
+ 'vrm': 'Image Texture',
+ 'xnalara': 'Image Texture',
+ 'principled': 'Image Texture',
+ 'diffuse': 'Image Texture',
+ 'emission': 'Image Texture',
+}
+
+
+def get_materials(ob: bpy.types.Object) -> List[bpy.types.Material]:
+ return [mat_slot.material for mat_slot in ob.material_slots if mat_slot.material]
+
+
+def get_shader_type(mat: bpy.types.Material) -> Union[str, None]:
+ if not mat.node_tree or not mat.node_tree.nodes:
+ return
+
+ node_tree = mat.node_tree.nodes
+
+ if 'Group' in node_tree:
+ node_tree_name = node_tree['Group'].node_tree.name
+ if node_tree_name == 'Group':
+ return 'xnalaraNewCol'
+ if node_tree_name == 'MToon_unversioned':
+ return 'vrm' if 'Image Texture' in node_tree else 'vrmCol'
+ elif node_tree_name == 'XPS Shader' and 'Image Texture' in node_tree:
+ return 'xnalara'
+
+ node_names_set = set(node_tree.keys())
+ return next(
+ (
+ shader_type
+ for shader_type, node_names in shader_types.items()
+ if node_names.issubset(node_names_set)
+ ),
+ None,
+ )
+
+
+def sort_materials(mat_list: List[bpy.types.Material]) -> ValuesView[MatDictItem]:
+ for mat in bpy.data.materials:
+ mat.kkbp_root_mat = None
+
+ mat_dict = cast(MatDict, defaultdict(list))
+ for mat in mat_list:
+ node_tree = mat.node_tree if mat else None
+
+ packed_file = None
+
+ if globs.is_blender_2_79_or_older:
+ packed_file = get_packed_file(get_image(get_texture(mat)))
+ elif node_tree:
+ shader = get_shader_type(mat)
+ node_name = shader_image_nodes.get(shader)
+ if node_name:
+ packed_file = get_packed_file(node_tree.nodes[node_name].image)
+
+ if packed_file:
+ mat_dict[(packed_file, get_diffuse(mat) if mat.kkbp_diffuse else None)].append(mat)
+ else:
+ mat_dict[get_diffuse(mat)].append(mat)
+
+ return mat_dict.values()
+
+
+def rgb_to_255_scale(diffuse: Diffuse) -> Diffuse:
+ rgb = np.empty(shape=(0,), dtype=int)
+ for c in diffuse:
+ if c < 0.0:
+ srgb = 0
+ elif c < 0.0031308:
+ srgb = c * 12.92
+ else:
+ srgb = 1.055 * pow(c, 1.0 / 2.4) - 0.055
+ rgb = np.append(rgb, np.clip(round(srgb * 255), 0, 255))
+ return tuple(rgb)
+
+
+def get_diffuse(mat: bpy.types.Material) -> Diffuse:
+ if globs.is_blender_2_79_or_older:
+ return rgb_to_255_scale(mat.diffuse_color)
+
+ shader = get_shader_type(mat) if mat else False
+ if shader == 'mmdCol':
+ return rgb_to_255_scale(mat.node_tree.nodes['mmd_shader'].inputs['Diffuse Color'].default_value)
+ elif shader == 'mtoonCol':
+ return rgb_to_255_scale(mat.node_tree.nodes['Mtoon1PbrMetallicRoughness.BaseColorFactor'].color)
+ elif shader == 'vrm':
+ return rgb_to_255_scale(mat.node_tree.nodes['RGB'].outputs[0].default_value)
+ elif shader == 'vrmCol':
+ return rgb_to_255_scale(mat.node_tree.nodes['Group'].inputs[10].default_value)
+ elif shader == 'diffuseCol':
+ return rgb_to_255_scale(mat.node_tree.nodes['Diffuse BSDF'].inputs['Color'].default_value)
+ elif shader == 'xnalaraNewCol':
+ return rgb_to_255_scale(mat.node_tree.nodes['Group'].inputs['Diffuse'].default_value)
+ elif shader in ['principledCol', 'xnalaraCol']:
+ return rgb_to_255_scale(mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value)
+ return 255, 255, 255
diff --git a/exporting/material_combiner/objects.py b/exporting/material_combiner/objects.py
new file mode 100644
index 0000000..6bd4b47
--- /dev/null
+++ b/exporting/material_combiner/objects.py
@@ -0,0 +1,38 @@
+import math
+from collections import defaultdict
+from typing import List, Dict
+
+import bpy
+from mathutils import Vector
+
+
+def get_polys(ob: bpy.types.Object) -> Dict[int, bpy.types.MeshPolygon]:
+ polys = defaultdict(list)
+ for poly in ob.data.polygons:
+ polys[poly.material_index].append(poly)
+ return polys
+
+
+def get_uv(ob: bpy.types.Object, poly: bpy.types.MeshPolygon) -> List[Vector]:
+ data = ob.data.uv_layers.active.data
+ return [data[loop_idx].uv if loop_idx < len(data) else Vector((0, 0, 0)) for loop_idx in poly.loop_indices]
+
+
+def align_uv(face_uv: List[Vector]) -> List[Vector]:
+ min_x = float('inf')
+ min_y = float('inf')
+
+ for uv in face_uv:
+ if not math.isnan(uv.x):
+ min_x = min(min_x, uv.x)
+ if not math.isnan(uv.y):
+ min_y = min(min_y, uv.y)
+
+ min_x = math.floor(min_x)
+ min_y = math.floor(min_y)
+
+ if min_x != 0 or min_y != 0:
+ for uv in face_uv:
+ uv.x -= min_x
+ uv.y -= min_y
+ return face_uv
diff --git a/exporting/material_combiner/packer.py b/exporting/material_combiner/packer.py
new file mode 100644
index 0000000..9668b5d
--- /dev/null
+++ b/exporting/material_combiner/packer.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
+#
+# 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.
+
+from typing import Union
+from typing import Dict
+
+
+class BinPacker(object):
+ def __init__(self, images: Dict) -> None:
+ self.root = {}
+ self.bin = images
+
+ def fit(self) -> Dict:
+ self.root = {'x': 0, 'y': 0, 'w': 0, 'h': 0}
+
+ if not self.bin:
+ return self.bin
+
+ self.root['w'], self.root['h'] = next(iter(self.bin.values()))['gfx']['size']
+
+ for img in self.bin.values():
+ w, h = img['gfx']['size']
+ node = self.find_node(self.root, w, h)
+ img['gfx']['fit'] = self.split_node(node, w, h) if node else self.grow_node(w, h)
+
+ return self.bin
+
+ def find_node(self, root: Dict, w: int, h: int) -> Union[Dict, None]:
+ if 'used' in root and root['used']:
+ return self.find_node(root['right'], w, h) or self.find_node(root['down'], w, h)
+ elif w <= root['w'] and h <= root['h']:
+ return root
+ return None
+
+ @staticmethod
+ def split_node(node: Dict, w: int, h: int) -> Dict:
+ node['used'] = True
+ node['down'] = {'x': node['x'], 'y': node['y'] + h, 'w': node['w'], 'h': node['h'] - h}
+ node['right'] = {'x': node['x'] + w, 'y': node['y'], 'w': node['w'] - w, 'h': h}
+ return node
+
+ def grow_node(self, w: int, h: int) -> Union[Dict, None]:
+ can_grow_right = h <= self.root['h']
+ can_grow_down = w <= self.root['w']
+
+ should_grow_right = can_grow_right and self.root['h'] >= self.root['w'] + w
+ should_grow_down = can_grow_down and self.root['w'] >= self.root['h'] + h
+
+ if should_grow_right or not should_grow_down and can_grow_right:
+ return self.grow_right(w, h)
+ elif should_grow_down or can_grow_down:
+ return self.grow_down(w, h)
+ return None
+
+ def grow_right(self, w: int, h: int) -> Union[Dict, None]:
+ self.root = {
+ 'used': True,
+ 'x': 0,
+ 'y': 0,
+ 'w': self.root['w'] + w,
+ 'h': self.root['h'],
+ 'down': self.root,
+ 'right': {'x': self.root['w'], 'y': 0, 'w': w, 'h': self.root['h']}
+ }
+ node = self.find_node(self.root, w, h)
+ return self.split_node(node, w, h) if node else None
+
+ def grow_down(self, w: int, h: int) -> Union[Dict, None]:
+ self.root = {
+ 'used': True,
+ 'x': 0,
+ 'y': 0,
+ 'w': self.root['w'],
+ 'h': self.root['h'] + h,
+ 'down': {'x': 0, 'y': self.root['h'], 'w': self.root['w'], 'h': h},
+ 'right': self.root
+ }
+ node = self.find_node(self.root, w, h)
+ return self.split_node(node, w, h) if node else None
diff --git a/exporting/material_combiner/textures.py b/exporting/material_combiner/textures.py
new file mode 100644
index 0000000..a5b2dcd
--- /dev/null
+++ b/exporting/material_combiner/textures.py
@@ -0,0 +1,13 @@
+from typing import Dict
+
+import bpy
+
+
+def get_texture(mat: bpy.types.Material) -> bpy.types.Texture:
+ return next((slot.texture for idx, slot in enumerate(mat.texture_slots) if
+ slot is not None and mat.use_textures[idx]), None)
+
+
+def get_textures(mat: bpy.types.Material) -> Dict[int, bpy.types.Texture]:
+ return {idx: slot.texture for idx, slot in enumerate(mat.texture_slots) if
+ slot is not None and mat.use_textures[idx]}
diff --git a/exporting/material_combiner/type_annotations.py b/exporting/material_combiner/type_annotations.py
new file mode 100644
index 0000000..d490057
--- /dev/null
+++ b/exporting/material_combiner/type_annotations.py
@@ -0,0 +1,39 @@
+from typing import DefaultDict
+from typing import Dict
+from typing import List
+from typing import Tuple
+from typing import Union
+
+import bpy
+from mathutils import Vector
+
+from . import globs
+
+BlClasses = Union[
+ bpy.types.Panel, bpy.types.Operator, bpy.types.PropertyGroup, bpy.types.AddonPreferences, bpy.types.UIList
+]
+
+# SMCIcons = Union[bpy.utils.previews.ImagePreviewCollection, Dict[str, bpy.types.ImagePreview], None]
+
+Scene = bpy.types.ViewLayer if globs.is_blender_2_80_or_newer else bpy.types.Scene
+
+SMCObDataItem = Dict[bpy.types.Material, int]
+SMCObData = Dict[str, SMCObDataItem]
+
+MatsUV = Dict[str, DefaultDict[bpy.types.Material, List[Vector]]]
+
+StructureItem = Dict[str, Union[List, Dict[str, Union[Dict[str, int], Tuple, bpy.types.PackedFile, None]]]]
+Structure = Dict[bpy.types.Material, StructureItem]
+
+ObMats = Union[bpy.types.bpy_prop_collection, List[bpy.types.Material]]
+
+CombMats = Dict[int, bpy.types.Material]
+
+MatDictItem = List[bpy.types.Material]
+MatDict = DefaultDict[Tuple, MatDictItem]
+
+CombineListDataMat = Dict[str, Union[int, bool]]
+CombineListDataItem = Dict[str, Union[Dict[bpy.types.Material, CombineListDataMat], bool]]
+CombineListData = Dict[bpy.types.Object, CombineListDataItem]
+
+Diffuse = Union[bpy.types.bpy_prop_collection, Tuple[float, float, float], Tuple[int, int, int]]
diff --git a/extras/__pycache__/createanimationlibrary.cpython-311.pyc b/extras/__pycache__/createanimationlibrary.cpython-311.pyc
new file mode 100644
index 0000000..068165a
Binary files /dev/null and b/extras/__pycache__/createanimationlibrary.cpython-311.pyc differ
diff --git a/extras/__pycache__/createmapassetlibrary.cpython-311.pyc b/extras/__pycache__/createmapassetlibrary.cpython-311.pyc
new file mode 100644
index 0000000..1964da8
Binary files /dev/null and b/extras/__pycache__/createmapassetlibrary.cpython-311.pyc differ
diff --git a/extras/__pycache__/imageconvert.cpython-311.pyc b/extras/__pycache__/imageconvert.cpython-311.pyc
new file mode 100644
index 0000000..b683023
Binary files /dev/null and b/extras/__pycache__/imageconvert.cpython-311.pyc differ
diff --git a/extras/__pycache__/importanimation.cpython-311.pyc b/extras/__pycache__/importanimation.cpython-311.pyc
new file mode 100644
index 0000000..e79923f
Binary files /dev/null and b/extras/__pycache__/importanimation.cpython-311.pyc differ
diff --git a/extras/__pycache__/importstudio.cpython-311.pyc b/extras/__pycache__/importstudio.cpython-311.pyc
new file mode 100644
index 0000000..93a8197
Binary files /dev/null and b/extras/__pycache__/importstudio.cpython-311.pyc differ
diff --git a/extras/__pycache__/linkhair.cpython-311.pyc b/extras/__pycache__/linkhair.cpython-311.pyc
new file mode 100644
index 0000000..5ab0410
Binary files /dev/null and b/extras/__pycache__/linkhair.cpython-311.pyc differ
diff --git a/extras/__pycache__/linkshapekeys.cpython-311.pyc b/extras/__pycache__/linkshapekeys.cpython-311.pyc
new file mode 100644
index 0000000..04563d2
Binary files /dev/null and b/extras/__pycache__/linkshapekeys.cpython-311.pyc differ
diff --git a/extras/__pycache__/matcombsetup.cpython-311.pyc b/extras/__pycache__/matcombsetup.cpython-311.pyc
new file mode 100644
index 0000000..c66deac
Binary files /dev/null and b/extras/__pycache__/matcombsetup.cpython-311.pyc differ
diff --git a/extras/__pycache__/matcombswitch.cpython-311.pyc b/extras/__pycache__/matcombswitch.cpython-311.pyc
new file mode 100644
index 0000000..26ee179
Binary files /dev/null and b/extras/__pycache__/matcombswitch.cpython-311.pyc differ
diff --git a/extras/__pycache__/resetmaterials.cpython-311.pyc b/extras/__pycache__/resetmaterials.cpython-311.pyc
new file mode 100644
index 0000000..c5213f3
Binary files /dev/null and b/extras/__pycache__/resetmaterials.cpython-311.pyc differ
diff --git a/extras/__pycache__/rigifywrapper.cpython-311.pyc b/extras/__pycache__/rigifywrapper.cpython-311.pyc
new file mode 100644
index 0000000..fd7b89b
Binary files /dev/null and b/extras/__pycache__/rigifywrapper.cpython-311.pyc differ
diff --git a/extras/__pycache__/updatebones.cpython-311.pyc b/extras/__pycache__/updatebones.cpython-311.pyc
new file mode 100644
index 0000000..99fd596
Binary files /dev/null and b/extras/__pycache__/updatebones.cpython-311.pyc differ
diff --git a/extras/animationlibrary/ARP retargeting list (koikatsu fbx to KKBP Rigify).json b/extras/animationlibrary/ARP retargeting list (koikatsu fbx to KKBP Rigify).json
new file mode 100644
index 0000000..4c90194
--- /dev/null
+++ b/extras/animationlibrary/ARP retargeting list (koikatsu fbx to KKBP Rigify).json
@@ -0,0 +1,283 @@
+{
+ "Rokoko_custom_names": true,
+ "version": 1,
+ "bones": {
+ "custom_bone_cf_j_hips": [
+ "cf_j_hips",
+ "torso"
+ ],
+ "custom_bone_cf_j_spine01": [
+ "cf_j_spine01",
+ "Spine_fk"
+ ],
+ "custom_bone_cf_j_spine02": [
+ "cf_j_spine02",
+ "Chest_fk"
+ ],
+ "custom_bone_cf_j_spine03": [
+ "cf_j_spine03",
+ "Upper Chest_fk"
+ ],
+ "custom_bone_cf_j_hand_L": [
+ "cf_j_hand_L",
+ "Left wrist_fk"
+ ],
+ "custom_bone_cf_j_arm_L": [
+ "cf_j_arm00_L",
+ "Left arm_fk"
+ ],
+ "custom_bone_cf_j_elbow_L": [
+ "cf_j_forearm01_L",
+ "Left elbow_fk"
+ ],
+ "custom_bone_cf_j_index01_L": [
+ "cf_j_index01_L",
+ "IndexFinger1_L"
+ ],
+ "custom_bone_cf_j_index02_L": [
+ "cf_j_index02_L",
+ "IndexFinger2_L"
+ ],
+ "custom_bone_cf_j_index03_L": [
+ "cf_j_index03_L",
+ "IndexFinger3_L"
+ ],
+ "custom_bone_cf_j_Little01_L": [
+ "cf_j_little01_L",
+ "LittleFinger1_L"
+ ],
+ "custom_bone_cf_j_Little02_L": [
+ "cf_j_little02_L",
+ "LittleFinger2_L"
+ ],
+ "custom_bone_cf_j_Little03_L": [
+ "cf_j_little03_L",
+ "LittleFinger3_L"
+ ],
+ "custom_bone_cf_j_middle01_L": [
+ "cf_j_middle01_L",
+ "MiddleFinger1_L"
+ ],
+ "custom_bone_cf_j_middle02_L": [
+ "cf_j_middle02_L",
+ "MiddleFinger2_L"
+ ],
+ "custom_bone_cf_j_middle03_L": [
+ "cf_j_middle03_L",
+ "MiddleFinger3_L"
+ ],
+ "custom_bone_cf_j_ring01_L": [
+ "cf_j_ring01_L",
+ "RingFinger1_L"
+ ],
+ "custom_bone_cf_j_ring02_L": [
+ "cf_j_ring02_L",
+ "RingFinger2_L"
+ ],
+ "custom_bone_cf_j_ring03_L": [
+ "cf_j_ring03_L",
+ "RingFinger3_L"
+ ],
+ "custom_bone_cf_j_thumb01_L": [
+ "cf_j_thumb01_L",
+ "Thumb0_L"
+ ],
+ "custom_bone_cf_j_thumb02_L": [
+ "cf_j_thumb02_L",
+ "Thumb1_L"
+ ],
+ "custom_bone_cf_j_thumb03_L": [
+ "cf_j_thumb03_L",
+ "Thumb2_L"
+ ],
+ "custom_bone_cf_j_arm_R": [
+ "cf_j_arm00_R",
+ "Right arm_fk"
+ ],
+ "custom_bone_cf_j_elbow": [
+ "cf_j_forearm01_R",
+ "Right elbow_fk"
+ ],
+ "custom_bone_cf_j_hand_R": [
+ "cf_j_hand_R",
+ "Right wrist_fk"
+ ],
+ "custom_bone_cf_j_index01_R": [
+ "cf_j_index01_R",
+ "IndexFinger1_R"
+ ],
+ "custom_bone_cf_j_index02_R": [
+ "cf_j_index02_R",
+ "IndexFinger2_R"
+ ],
+ "custom_bone_cf_j_index03_R": [
+ "cf_j_index03_R",
+ "IndexFinger3_R"
+ ],
+ "custom_bone_cf_j_Little01_R": [
+ "cf_j_little01_R",
+ "LittleFinger1_R"
+ ],
+ "custom_bone_cf_j_Little02_R": [
+ "cf_j_little02_R",
+ "LittleFinger2_R"
+ ],
+ "custom_bone_cf_j_Little03_R": [
+ "cf_j_little03_R",
+ "LittleFinger3_R"
+ ],
+ "custom_bone_cf_j_middle01_R": [
+ "cf_j_middle01_R",
+ "MiddleFinger1_R"
+ ],
+ "custom_bone_cf_j_middle02_R": [
+ "cf_j_middle02_R",
+ "MiddleFinger2_R"
+ ],
+ "custom_bone_cf_j_middle03_R": [
+ "cf_j_middle03_R",
+ "MiddleFinger3_R"
+ ],
+ "custom_bone_cf_j_ring01_R": [
+ "cf_j_ring01_R",
+ "RingFinger1_R"
+ ],
+ "custom_bone_cf_j_ring02_R": [
+ "cf_j_ring02_R",
+ "RingFinger2_R"
+ ],
+ "custom_bone_cf_j_ring03_R": [
+ "cf_j_ring03_R",
+ "RingFinger3_R"
+ ],
+ "custom_bone_cf_j_thumb01_R": [
+ "cf_j_thumb01_R",
+ "Thumb0_R"
+ ],
+ "custom_bone_cf_j_thumb02_R": [
+ "cf_j_thumb02_R",
+ "Thumb1_R"
+ ],
+ "custom_bone_cf_j_thumb03_R": [
+ "cf_j_thumb03_R",
+ "Thumb2_R"
+ ],
+ "custom_bone_cf_j_neck": [
+ "cf_j_neck",
+ "neck"
+ ],
+ "custom_bone_cf_j_head": [
+ "cf_j_head",
+ "head"
+ ],
+ "custom_bone_cf_j_waist01": [
+ "cf_j_waist01",
+ "Hips_fk"
+ ],
+ "custom_bone_cf_j_foot_L": [
+ "cf_j_foot_L",
+ "Left ankle_fk"
+ ],
+ "custom_bone_cf_j_toes_L": [
+ "cf_j_toes_L",
+ "Left toe_fk"
+ ],
+ "custom_bone_cf_j_thigh00_l": [
+ "cf_j_thigh00_L",
+ "Left leg_fk"
+ ],
+ "custom_bone_cf_j_leg01_l": [
+ "cf_j_leg01_L",
+ "Left knee_fk"
+ ],
+ "custom_bone_cf_j_foot_R": [
+ "cf_j_foot_R",
+ "Right ankle_fk"
+ ],
+ "custom_bone_cf_j_toes_R": [
+ "cf_j_toes_R",
+ "Right toe_fk"
+ ],
+ "custom_bone_cf_j_thigh00_r": [
+ "cf_j_thigh00_R",
+ "Right leg_fk"
+ ],
+ "custom_bone_cf_j_leg01_r": [
+ "cf_j_leg01_R",
+ "Right knee_fk"
+ ],
+ "custom_bone_cf_j_shoulder_L": [
+ "cf_j_shoulder_L",
+ "Left shoulder"
+ ],
+ "custom_bone_cf_j_waist02": [
+ "cf_j_waist02",
+ "cf_j_waist02"
+ ],
+ "custom_bone_cf_j_shoulder_R": [
+ "cf_j_shoulder_R",
+ "Right shoulder"
+ ],
+ "custom_bone_cf_j_kokan": [
+ "cf_j_kokan",
+ "cf_j_kokan"
+ ],
+ "custom_bone_cf_j_ana": [
+ "cf_j_ana",
+ "cf_j_ana"
+ ],
+ "custom_bone_cf_d_bust00": [
+ "cf_d_bust00",
+ "Breasts handle"
+ ],
+ "custom_bone_cf_j_bust01_L": [
+ "cf_j_bust01_L",
+ "Left Breast handle"
+ ],
+ "custom_bone_cf_j_bust02_L": [
+ "cf_j_bust02_L",
+ "cf_j_bust02_L"
+ ],
+ "custom_bone_cf_j_bust03_L": [
+ "cf_j_bust03_L",
+ "cf_j_bust03_L"
+ ],
+ "custom_bone_cf_j_bnip02root_L": [
+ "cf_j_bnip02root_L",
+ "cf_j_bnip02root_L"
+ ],
+ "custom_bone_cf_j_bnip02_L": [
+ "cf_j_bnip02_L",
+ "cf_j_bnip02_L"
+ ],
+ "custom_bone_cf_j_bust01_R": [
+ "cf_j_bust01_R",
+ "Right Breast handle"
+ ],
+ "custom_bone_cf_j_bust02_R": [
+ "cf_j_bust02_R",
+ "cf_j_bust02_R"
+ ],
+ "custom_bone_cf_j_bust03_R": [
+ "cf_j_bust03_R",
+ "cf_j_bust03_R"
+ ],
+ "custom_bone_cf_j_bnip02root_R": [
+ "cf_j_bnip02root_R",
+ "cf_j_bnip02root_R"
+ ],
+ "custom_bone_cf_j_bnip02_R": [
+ "cf_j_bnip02_R",
+ "cf_j_bnip02_R"
+ ],
+ "custom_bone_cf_j_siri_L": [
+ "cf_j_siri_L",
+ "Left Buttock handle"
+ ],
+ "custom_bone_cf_j_siri_R": [
+ "cf_j_siri_R",
+ "Right Buttock handle"
+ ]
+ },
+ "shapes": {}
+}
\ No newline at end of file
diff --git a/extras/animationlibrary/Rokoko retargeting list (koikatsu fbx to KKBP Rigify).json b/extras/animationlibrary/Rokoko retargeting list (koikatsu fbx to KKBP Rigify).json
new file mode 100644
index 0000000..7231bcf
--- /dev/null
+++ b/extras/animationlibrary/Rokoko retargeting list (koikatsu fbx to KKBP Rigify).json
@@ -0,0 +1,284 @@
+{
+ "rokoko_custom_names": true,
+ "version": 1,
+ "bones": {
+ "custom_bone_cf_j_hips": [
+ "cf_j_hips",
+ "torso"
+ ],
+ "custom_bone_cf_j_spine01": [
+ "cf_j_spine01",
+ "Spine_fk"
+ ],
+ "custom_bone_cf_j_spine02": [
+ "cf_j_spine02",
+ "Chest_fk"
+ ],
+ "custom_bone_cf_j_spine03": [
+ "cf_j_spine03",
+ "Upper Chest_fk"
+ ],
+ "custom_bone_cf_j_arm00_l": [
+ "cf_j_arm00_l",
+ "Left arm_fk"
+ ],
+ "custom_bone_cf_j_forearm01_l": [
+ "cf_j_forearm01_l",
+ "Left elbow_fk"
+ ],
+ "custom_bone_cf_j_hand_l": [
+ "cf_j_hand_l",
+ "Left wrist_fk"
+ ],
+ "custom_bone_cf_j_index01_l": [
+ "cf_j_index01_l",
+ "IndexFinger1_L"
+ ],
+ "custom_bone_cf_j_index02_l": [
+ "cf_j_index02_l",
+ "IndexFinger2_L"
+ ],
+ "custom_bone_cf_j_index03_l": [
+ "cf_j_index03_l",
+ "IndexFinger3_L"
+ ],
+ "custom_bone_cf_j_little01_l": [
+ "cf_j_little01_l",
+ "LittleFinger1_L"
+ ],
+ "custom_bone_cf_j_little02_l": [
+ "cf_j_little02_l",
+ "LittleFinger2_L"
+ ],
+ "custom_bone_cf_j_little03_l": [
+ "cf_j_little03_l",
+ "LittleFinger3_L"
+ ],
+ "custom_bone_cf_j_middle01_l": [
+ "cf_j_middle01_l",
+ "MiddleFinger1_L"
+ ],
+ "custom_bone_cf_j_middle02_l": [
+ "cf_j_middle02_l",
+ "MiddleFinger2_L"
+ ],
+ "custom_bone_cf_j_middle03_l": [
+ "cf_j_middle03_l",
+ "MiddleFinger3_L"
+ ],
+ "custom_bone_cf_j_ring01_l": [
+ "cf_j_ring01_l",
+ "RingFinger1_L"
+ ],
+ "custom_bone_cf_j_ring02_l": [
+ "cf_j_ring02_l",
+ "RingFinger2_L"
+ ],
+ "custom_bone_cf_j_ring03_l": [
+ "cf_j_ring03_l",
+ "RingFinger3_L"
+ ],
+ "custom_bone_cf_j_thumb01_l": [
+ "cf_j_thumb01_l",
+ "Thumb0_L"
+ ],
+ "custom_bone_cf_j_thumb02_l": [
+ "cf_j_thumb02_l",
+ "Thumb1_l"
+ ],
+ "custom_bone_cf_j_thumb03_l": [
+ "cf_j_thumb03_l",
+ "Thumb2_L"
+ ],
+ "custom_bone_cf_j_arm00_r": [
+ "cf_j_arm00_r",
+ "Right arm_fk"
+ ],
+ "custom_bone_cf_j_forearm01_r": [
+ "cf_j_forearm01_r",
+ "Right elbow_fk"
+ ],
+ "custom_bone_cf_j_hand_r": [
+ "cf_j_hand_r",
+ "Right wrist_fk"
+ ],
+ "custom_bone_cf_j_index01_r": [
+ "cf_j_index01_r",
+ "IndexFinger1_R"
+ ],
+ "custom_bone_cf_j_index02_r": [
+ "cf_j_index02_r",
+ "IndexFinger2_R"
+ ],
+ "custom_bone_cf_j_index03_r": [
+ "cf_j_index03_r",
+ "IndexFinger3_R"
+ ],
+ "custom_bone_cf_j_little01_r": [
+ "cf_j_little01_r",
+ "LittleFinger1_R"
+ ],
+ "custom_bone_cf_j_little02_r": [
+ "cf_j_little02_r",
+ "LittleFinger2_R"
+ ],
+ "custom_bone_cf_j_little03_r": [
+ "cf_j_little03_r",
+ "LittleFinger3_R"
+ ],
+ "custom_bone_cf_j_middle01_r": [
+ "cf_j_middle01_r",
+ "MiddleFinger1_R"
+ ],
+ "custom_bone_cf_j_middle02_r": [
+ "cf_j_middle02_r",
+ "MiddleFinger2_R"
+ ],
+ "custom_bone_cf_j_middle03_r": [
+ "cf_j_middle03_r",
+ "MiddleFinger3_R"
+ ],
+ "custom_bone_cf_j_ring01_r": [
+ "cf_j_ring01_r",
+ "RingFinger1_R"
+ ],
+ "custom_bone_cf_j_ring02_r": [
+ "cf_j_ring02_r",
+ "RingFinger2_R"
+ ],
+ "custom_bone_cf_j_ring03_r": [
+ "cf_j_ring03_r",
+ "RingFinger3_R"
+ ],
+ "custom_bone_cf_j_thumb01_r": [
+ "cf_j_thumb01_r",
+ "Thumb0_R"
+ ],
+ "custom_bone_cf_j_thumb02_r": [
+ "cf_j_thumb02_r",
+ "Thumb1_R"
+ ],
+ "custom_bone_cf_j_thumb03_r": [
+ "cf_j_thumb03_r",
+ "Thumb2_R"
+ ],
+ "custom_bone_cf_j_neck": [
+ "cf_j_neck",
+ "neck"
+ ],
+ "custom_bone_cf_j_head": [
+ "cf_j_head",
+ "head"
+ ],
+ "custom_bone_cf_j_waist01": [
+ "cf_j_waist01",
+ "Hips_fk"
+ ],
+ "custom_bone_cf_j_thigh00_l": [
+ "cf_j_thigh00_l",
+ "Left leg_fk"
+ ],
+ "custom_bone_cf_j_leg01_l": [
+ "cf_j_leg01_l",
+ "Left knee_fk"
+ ],
+ "custom_bone_cf_j_foot_l": [
+ "cf_j_foot_l",
+ "Left ankle_fk"
+ ],
+ "custom_bone_cf_j_toes_l": [
+ "cf_j_toes_l",
+ "Left toe_fk"
+ ],
+ "custom_bone_cf_j_thigh00_r": [
+ "cf_j_thigh00_r",
+ "Right leg_fk"
+ ],
+ "custom_bone_cf_j_leg01_r": [
+ "cf_j_leg01_r",
+ "Right knee_fk"
+ ],
+ "custom_bone_cf_j_foot_r": [
+ "cf_j_foot_r",
+ "Right ankle_fk"
+ ],
+ "custom_bone_cf_j_toes_r": [
+ "cf_j_toes_r",
+ "Right toe_fk"
+ ],
+ "custom_bone_cf_j_shoulder_l": [
+ "cf_j_shoulder_l",
+ "Left shoulder"
+ ],
+ "custom_bone_cf_j_waist02": [
+ "cf_j_waist02",
+ "cf_j_waist02"
+ ],
+ "custom_bone_cf_j_shoulder_r": [
+ "cf_j_shoulder_r",
+ "Right shoulder"
+ ],
+ "custom_bone_cf_j_kokan": [
+ "cf_j_kokan",
+ "cf_j_kokan"
+ ],
+ "custom_bone_cf_j_ana": [
+ "cf_j_ana",
+ "cf_j_ana"
+ ],
+ "custom_bone_cf_d_bust00": [
+ "cf_d_bust00",
+ "Breasts handle"
+ ],
+ "custom_bone_cf_j_bust01_l": [
+ "cf_j_bust01_l",
+ "Left Breast handle"
+ ],
+ "custom_bone_cf_j_bust02_l": [
+ "cf_j_bust02_l",
+ "cf_j_bust02_L"
+ ],
+ "custom_bone_cf_j_bust03_l": [
+ "cf_j_bust03_l",
+ "cf_j_bust03_L"
+ ],
+ "custom_bone_cf_j_bnip02root_l": [
+ "cf_j_bnip02root_l",
+ "cf_j_bnip02root_L"
+ ],
+ "custom_bone_cf_j_bnip02_l": [
+ "cf_j_bnip02_l",
+ "cf_j_bnip02_L"
+ ],
+ "custom_bone_cf_j_bust01_r": [
+ "cf_j_bust01_r",
+ "Right Breast handle"
+ ],
+ "custom_bone_cf_j_bust02_r": [
+ "cf_j_bust02_r",
+ "cf_j_bust02_R"
+ ],
+ "custom_bone_cf_j_bust03_r": [
+ "cf_j_bust03_r",
+ "cf_j_bust03_R"
+ ],
+ "custom_bone_cf_j_bnip02root_r": [
+ "cf_j_bnip02root_r",
+ "cf_j_bnip02root_R"
+ ],
+ "custom_bone_cf_j_bnip02_r": [
+ "cf_j_bnip02_r",
+ "cf_j_bnip02_R"
+ ],
+ "custom_bone_cf_j_siri_l": [
+ "cf_j_siri_l",
+ "Left Buttock handle"
+ ],
+ "custom_bone_cf_j_siri_r": [
+ "cf_j_siri_r",
+ "Right Buttock handle"
+ ]
+
+ },
+ "shapes": {}
+}
\ No newline at end of file
diff --git a/extras/animationlibrary/Rokoko retargeting list (mixamo to KKBP Rigify).json b/extras/animationlibrary/Rokoko retargeting list (mixamo to KKBP Rigify).json
new file mode 100644
index 0000000..4d30253
--- /dev/null
+++ b/extras/animationlibrary/Rokoko retargeting list (mixamo to KKBP Rigify).json
@@ -0,0 +1,215 @@
+{
+ "Rokoko_custom_names": true,
+ "version": 1,
+ "bones": {
+ "custom_bone_mixamorig:Hips": [
+ "mixamorig:Hips",
+ "torso"
+ ],
+ "custom_bone_mixamorig:Spine": [
+ "mixamorig:Spine",
+ "Spine_fk"
+ ],
+ "custom_bone_mixamorig:Spine1": [
+ "mixamorig:Spine1",
+ "Chest_fk"
+ ],
+ "custom_bone_mixamorig:Spine2": [
+ "mixamorig:Spine2",
+ "Upper Chest_fk"
+ ],
+ "custom_bone_mixamorig:LeftHand": [
+ "mixamorig:LeftHand",
+ "Left wrist_fk"
+ ],
+ "custom_bone_mixamorig:LeftArm": [
+ "mixamorig:LeftArm",
+ "Left arm_fk"
+ ],
+ "custom_bone_mixamorig:LeftForeArm": [
+ "mixamorig:LeftForeArm",
+ "Left elbow_fk"
+ ],
+ "custom_bone_mixamorig:LeftHandIndex1": [
+ "mixamorig:LeftHandIndex1",
+ "IndexFinger1_L"
+ ],
+ "custom_bone_mixamorig:LeftHandIndex2": [
+ "mixamorig:LeftHandIndex2",
+ "IndexFinger2_L"
+ ],
+ "custom_bone_cf_j_index03_L": [
+ "mixamorig:LeftHandIndex3",
+ "IndexFinger3_L"
+ ],
+ "custom_bone_cf_j_Little01_L": [
+ "mixamorig:LeftHandPinky1",
+ "LittleFinger1_L"
+ ],
+ "custom_bone_cf_j_Little02_L": [
+ "mixamorig:LeftHandPinky2",
+ "LittleFinger2_L"
+ ],
+ "custom_bone_cf_j_Little03_L": [
+ "mixamorig:LeftHandPinky3",
+ "LittleFinger3_L"
+ ],
+ "custom_bone_cf_j_middle01_L": [
+ "mixamorig:LeftHandMiddle1",
+ "MiddleFinger1_L"
+ ],
+ "custom_bone_cf_j_middle02_L": [
+ "mixamorig:LeftHandMiddle2",
+ "MiddleFinger2_L"
+ ],
+ "custom_bone_cf_j_middle03_L": [
+ "mixamorig:LeftHandMiddle3",
+ "MiddleFinger3_L"
+ ],
+ "custom_bone_cf_j_ring01_L": [
+ "mixamorig:LeftHandRing1",
+ "RingFinger1_L"
+ ],
+ "custom_bone_cf_j_ring02_L": [
+ "mixamorig:LeftHandRing2",
+ "RingFinger2_L"
+ ],
+ "custom_bone_cf_j_ring03_L": [
+ "mixamorig:LeftHandRing3",
+ "RingFinger3_L"
+ ],
+ "custom_bone_cf_j_thumb01_L": [
+ "mixamorig:LeftHandThumb1",
+ "Thumb0_L"
+ ],
+ "custom_bone_cf_j_thumb02_L": [
+ "mixamorig:LeftHandThumb2",
+ "Thumb1_L"
+ ],
+ "custom_bone_cf_j_thumb03_L": [
+ "mixamorig:LeftHandThumb3",
+ "Thumb2_L"
+ ],
+ "custom_bone_cf_j_arm_R": [
+ "mixamorig:RightArm",
+ "Right arm_fk"
+ ],
+ "custom_bone_cf_j_elbow": [
+ "mixamorig:RightForeArm",
+ "Right elbow_fk"
+ ],
+ "custom_bone_cf_j_hand_R": [
+ "mixamorig:RightHand",
+ "Right wrist_fk"
+ ],
+ "custom_bone_cf_j_index01_R": [
+ "mixamorig:RightHandIndex1",
+ "IndexFinger1_R"
+ ],
+ "custom_bone_cf_j_index02_R": [
+ "mixamorig:RightHandIndex2",
+ "IndexFinger2_R"
+ ],
+ "custom_bone_cf_j_index03_R": [
+ "mixamorig:RightHandIndex3",
+ "IndexFinger3_R"
+ ],
+ "custom_bone_cf_j_Little01_R": [
+ "mixamorig:RightHandPinky1",
+ "LittleFinger1_R"
+ ],
+ "custom_bone_cf_j_Little02_R": [
+ "mixamorig:RightHandPinky2",
+ "LittleFinger2_R"
+ ],
+ "custom_bone_cf_j_Little03_R": [
+ "mixamorig:RightHandPinky.",
+ "LittleFinger3_R"
+ ],
+ "custom_bone_cf_j_middle01_R": [
+ "mixamorig:RightHandMiddle1",
+ "MiddleFinger1_R"
+ ],
+ "custom_bone_cf_j_middle02_R": [
+ "mixamorig:RightHandMiddle2",
+ "MiddleFinger2_R"
+ ],
+ "custom_bone_cf_j_middle03_R": [
+ "mixamorig:RightHandMiddle3",
+ "MiddleFinger3_R"
+ ],
+ "custom_bone_cf_j_ring01_R": [
+ "mixamorig:RightHandRing1",
+ "RingFinger1_R"
+ ],
+ "custom_bone_cf_j_ring02_R": [
+ "mixamorig:RightHandRing2",
+ "RingFinger2_R"
+ ],
+ "custom_bone_cf_j_ring03_R": [
+ "mixamorig:RightHandRing3",
+ "RingFinger3_R"
+ ],
+ "custom_bone_cf_j_thumb01_R": [
+ "mixamorig:RightHandThumb1",
+ "Thumb0_R"
+ ],
+ "custom_bone_cf_j_thumb02_R": [
+ "mixamorig:RightHandThumb2",
+ "Thumb1_R"
+ ],
+ "custom_bone_cf_j_thumb03_R": [
+ "mixamorig:RightHandThumb3",
+ "Thumb2_R"
+ ],
+ "custom_bone_cf_j_neck": [
+ "mixamorig:Neck",
+ "neck"
+ ],
+ "custom_bone_cf_j_head": [
+ "mixamorig:Head",
+ "head"
+ ],
+ "custom_bone_mixamorig:LeftFoot": [
+ "mixamorig:LeftFoot",
+ "Left ankle_fk"
+ ],
+ "custom_bone_cf_j_toes_L": [
+ "mixamorig:LeftToeBase",
+ "Left toe_fk"
+ ],
+ "custom_bone_cf_j_thigh00_l": [
+ "mixamorig:LeftUpLeg",
+ "Left leg_fk"
+ ],
+ "custom_bone_cf_j_leg01_l": [
+ "mixamorig:LeftLeg",
+ "Left knee_fk"
+ ],
+ "custom_bone_cf_j_foot_R": [
+ "mixamorig:RightFoot",
+ "Right ankle_fk"
+ ],
+ "custom_bone_cf_j_toes_R": [
+ "mixamorig:RightToeBase",
+ "Right toe_fk"
+ ],
+ "custom_bone_cf_j_thigh00_r": [
+ "mixamorig:RightUpLeg",
+ "Right leg_fk"
+ ],
+ "custom_bone_cf_j_leg01_r": [
+ "mixamorig:RightLeg",
+ "Right knee_fk"
+ ],
+ "custom_bone_cf_j_shoulder_L": [
+ "mixamorig:LeftShoulder",
+ "Left shoulder"
+ ],
+ "custom_bone_cf_j_shoulder_R": [
+ "mixamorig:RightShoulder",
+ "Right shoulder"
+ ]
+ },
+ "shapes": {}
+}
\ No newline at end of file
diff --git a/extras/catsscripts/__pycache__/armature_manual.cpython-311.pyc b/extras/catsscripts/__pycache__/armature_manual.cpython-311.pyc
new file mode 100644
index 0000000..c9d76c2
Binary files /dev/null and b/extras/catsscripts/__pycache__/armature_manual.cpython-311.pyc differ
diff --git a/extras/catsscripts/__pycache__/common.cpython-311.pyc b/extras/catsscripts/__pycache__/common.cpython-311.pyc
new file mode 100644
index 0000000..d8d9c14
Binary files /dev/null and b/extras/catsscripts/__pycache__/common.cpython-311.pyc differ
diff --git a/extras/catsscripts/armature_manual.py b/extras/catsscripts/armature_manual.py
new file mode 100644
index 0000000..ca8f943
--- /dev/null
+++ b/extras/catsscripts/armature_manual.py
@@ -0,0 +1,105 @@
+# MIT License
+
+# Copyright (c) 2017 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: Hotox
+# Repo: https://github.com/michaeldegroot/cats-blender-plugin
+# Edits by: GiveMeAllYourCats
+
+import bpy, io
+
+from contextlib import redirect_stdout
+from . import common as Common
+from ...common import kklog
+#from .register import register_wrap
+#from .translations import t
+
+#@register_wrap
+class MergeWeights(bpy.types.Operator):
+ bl_idname = 'kkbp.cats_merge_weights'
+ bl_label = 'cats merge weights'
+ #bl_description = t('MergeWeights.desc')
+ bl_options = {'REGISTER', 'UNDO'}
+ '''
+ @classmethod
+ def poll(cls, context):
+ active_obj = bpy.context.active_object
+ if not active_obj or not bpy.context.active_object.type == 'ARMATURE':
+ return False
+ if active_obj.mode == 'EDIT' and bpy.context.selected_editable_bones:
+ return True
+ if active_obj.mode == 'POSE' and bpy.context.selected_pose_bones:
+ return True
+
+ return False
+ '''
+ def execute(self, context):
+ saved_data = Common.SavedData()
+
+ armature = bpy.context.object
+
+ Common.switch('EDIT')
+
+ # Find which bones to work on and put their name and their parent in a list
+ parenting_list = {}
+ for bone in bpy.context.selected_editable_bones:
+ parent = bone.parent
+ while parent and parent.parent and parent in bpy.context.selected_editable_bones:
+ parent = parent.parent
+ if not parent:
+ continue
+ parenting_list[bone.name] = parent.name
+
+ # Merge all the bones in the parenting list
+ merge_weights(armature, parenting_list)
+
+ saved_data.load()
+
+ self.report({'INFO'}, 'cats merge weights success')
+ return {'FINISHED'}
+
+def merge_weights(armature, parenting_list):
+ Common.switch('OBJECT')
+ # Merge the weights on the meshes
+ stdout = io.StringIO()
+ meshes = Common.get_meshes_objects(armature_name=armature.name, visible_only=bpy.context.scene.merge_visible_meshes_only if bpy.context.scene.get('merge_visible_meshes_only') != None else True)
+ for mindex, mesh in enumerate(meshes):
+ Common.set_active(mesh)
+
+ for index, bone in enumerate(parenting_list):
+ parent = parenting_list[bone]
+ kklog('Merging bone: {} object {} / {}, bone {} / {}'.format(bone, mindex, len(meshes), index, len(parenting_list)))
+ if not mesh.vertex_groups.get(bone):
+ continue
+ if not mesh.vertex_groups.get(parent):
+ mesh.vertex_groups.new(name=parent)
+ with redirect_stdout(stdout):
+ Common.mix_weights(mesh, bone, parent)
+
+ # Select armature
+ Common.unselect_all()
+ Common.set_active(armature)
+ Common.switch('EDIT')
+
+ # Delete merged bones
+ if True:
+ for bone in parenting_list.keys():
+ armature.data.edit_bones.remove(armature.data.edit_bones.get(bone))
diff --git a/extras/catsscripts/common.py b/extras/catsscripts/common.py
new file mode 100644
index 0000000..1751237
--- /dev/null
+++ b/extras/catsscripts/common.py
@@ -0,0 +1,2290 @@
+# MIT License
+
+# Copyright (c) 2017 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: GiveMeAllYourCats
+# Repo: https://github.com/michaeldegroot/cats-blender-plugin
+# Edits by: GiveMeAllYourCats, Hotox
+
+import re
+import bpy
+import time
+import bmesh
+import platform
+import numpy as np
+
+from math import degrees
+from mathutils import Vector
+from datetime import datetime
+from html.parser import HTMLParser
+from html.entities import name2codepoint
+
+from . import common as Common
+#from . import supporter as Supporter
+#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 .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
+
+
+def version_2_79_or_older():
+ return bpy.app.version < (2, 80)
+
+def version_2_93_or_older():
+ return bpy.app.version < (2, 90)
+
+
+def get_objects():
+ return bpy.context.scene.objects if version_2_79_or_older() else bpy.context.view_layer.objects
+
+
+class SavedData:
+ __object_properties = {}
+ __active_object = None
+
+ def __init__(self):
+ # initialize as instance attributes rather than class attributes
+ self.__object_properties = {}
+ self.__active_object = None
+
+ for obj in get_objects():
+ mode = obj.mode
+ selected = is_selected(obj)
+ hidden = is_hidden(obj)
+ pose = None
+ if obj.type == 'ARMATURE':
+ pose = obj.data.pose_position
+ self.__object_properties[obj.name] = [mode, selected, hidden, pose]
+
+ active = get_active()
+ if active:
+ self.__active_object = active.name
+
+ def load(self, ignore=None, load_mode=True, load_select=True, load_hide=True, load_active=True, hide_only=False):
+ if not ignore:
+ ignore = []
+ if hide_only:
+ load_mode = False
+ load_select = False
+ load_active = False
+
+ for obj_name, values in self.__object_properties.items():
+ # print(obj_name, ignore)
+ if obj_name in ignore:
+ continue
+
+ obj = get_objects().get(obj_name)
+ if not obj:
+ continue
+
+ mode, selected, hidden, pose = values
+ # print(obj_name, mode, selected, hidden)
+ print(obj_name, pose)
+
+ if load_mode and obj.mode != mode:
+ set_active(obj, skip_sel=True)
+ switch(mode, check_mode=False)
+ if pose:
+ obj.data.pose_position = pose
+
+ if load_select:
+ select(obj, selected)
+ if load_hide:
+ hide(obj, hidden)
+
+ # Set the active object
+ if load_active and self.__active_object and get_objects().get(self.__active_object):
+ if self.__active_object not in ignore and self.__active_object != get_active():
+ set_active(get_objects().get(self.__active_object), skip_sel=True)
+
+
+def get_armature(armature_name=None):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+ for obj in get_objects():
+ if obj.type == 'ARMATURE':
+ if (armature_name and obj.name == armature_name) or not armature_name:
+ return obj
+ return None
+
+
+def get_armature_objects():
+ armatures = []
+ for obj in get_objects():
+ if obj.type == 'ARMATURE':
+ armatures.append(obj)
+ return armatures
+
+
+def get_top_parent(child):
+ if child.parent:
+ return get_top_parent(child.parent)
+ return child
+
+
+def unhide_all_unnecessary():
+ try:
+ bpy.ops.object.hide_view_clear()
+ except RuntimeError:
+ pass
+
+ for collection in bpy.data.collections:
+ collection.hide_select = False
+ collection.hide_viewport = False
+
+
+def unhide_all():
+ for obj in get_objects():
+ hide(obj, False)
+ set_unselectable(obj, False)
+
+ if not version_2_79_or_older():
+ unhide_all_unnecessary()
+
+
+def unhide_children(parent):
+ for child in parent.children:
+ hide(child, False)
+ set_unselectable(child, False)
+ unhide_children(child)
+
+
+def unhide_all_of(obj_to_unhide=None):
+ if not obj_to_unhide:
+ return
+
+ top_parent = get_top_parent(obj_to_unhide)
+ hide(top_parent, False)
+ set_unselectable(top_parent, False)
+ unhide_children(top_parent)
+
+
+def unselect_all():
+ for obj in get_objects():
+ select(obj, False)
+
+
+def set_active(obj, skip_sel=False):
+ if not skip_sel:
+ select(obj)
+ if version_2_79_or_older():
+ bpy.context.scene.objects.active = obj
+ else:
+ bpy.context.view_layer.objects.active = obj
+
+
+def get_active():
+ if version_2_79_or_older():
+ return bpy.context.scene.objects.active
+ return bpy.context.view_layer.objects.active
+
+
+def select(obj, sel=True):
+ if sel:
+ hide(obj, False)
+ obj.select_set(sel)
+
+
+def is_selected(obj):
+ if version_2_79_or_older():
+ return obj.select
+ return obj.select_get()
+
+
+def hide(obj, val=True):
+ obj.hide_set(val)
+
+def is_hidden(obj):
+ return obj.hide_get()
+
+
+def set_unselectable(obj, val=True):
+ obj.hide_select = val
+
+
+def switch(new_mode, check_mode=True):
+ if check_mode and get_active() and get_active().mode == new_mode:
+ return
+ if bpy.ops.object.mode_set.poll():
+ bpy.ops.object.mode_set(mode=new_mode, toggle=False)
+
+
+def set_default_stage_old():
+ switch('OBJECT')
+ unhide_all()
+ unselect_all()
+ armature = get_armature()
+ set_active(armature)
+ return armature
+
+
+def set_default_stage():
+ """
+
+ Selects the armature, unhides everything and sets the modes of every object to object mode
+
+ :return: the armature
+ """
+
+ # Remove rigidbody collections, as they cause issues if they are not in the view_layer
+ if not version_2_79_or_older() and bpy.context.scene.remove_rigidbodies_joints:
+ print('Collections:')
+ for collection in bpy.data.collections:
+ print(' ' + collection.name, collection.name.lower())
+ if 'rigidbody' in collection.name.lower():
+ print('DELETE')
+ for obj in collection.objects:
+ delete(obj)
+ bpy.data.collections.remove(collection)
+
+ if not bpy.context.scene.cats_is_unittest:
+ unhide_all()
+ unselect_all()
+
+ for obj in get_objects():
+ set_active(obj)
+ switch('OBJECT')
+ if obj.type == 'ARMATURE':
+ # obj.data.pose_position = 'REST'
+ pass
+
+ select(obj, False)
+
+ armature = get_armature()
+ if armature:
+ set_active(armature)
+ # if version_2_79_or_older():
+ # armature.layers[0] = True
+
+ # Fix broken armatures
+ if not bpy.context.scene.armature:
+ bpy.context.scene.armature = armature.name
+
+ 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)
+ return
+
+ 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')
+ for bone in armature.data.edit_bones:
+ if bone.name == find_bone:
+ armature.data.edit_bones.remove(bone)
+
+
+def remove_empty():
+ armature = set_default_stage()
+ if armature.parent and armature.parent.type == 'EMPTY':
+ unselect_all()
+ set_active(armature.parent)
+ bpy.ops.object.delete(use_global=False)
+ unselect_all()
+
+
+def get_bone_angle(p1, p2):
+ try:
+ ret = degrees((p1.head - p1.tail).angle(p2.head - p2.tail))
+ except ValueError:
+ ret = 0
+
+ return ret
+
+
+def remove_unused_vertex_groups(ignore_main_bones=False):
+ remove_count = 0
+ unselect_all()
+ for mesh in get_meshes_objects(mode=2):
+ mesh.update_from_editmode()
+
+ vgroup_used = {i: False for i, k in enumerate(mesh.vertex_groups)}
+
+ for v in mesh.data.vertices:
+ for g in v.groups:
+ if g.weight > 0.0:
+ vgroup_used[g.group] = True
+
+ for i, used in sorted(vgroup_used.items(), reverse=True):
+ if not used:
+ if ignore_main_bones and mesh.vertex_groups[i].name in Bones.dont_delete_these_main_bones:
+ continue
+ mesh.vertex_groups.remove(mesh.vertex_groups[i])
+ remove_count += 1
+ return remove_count
+
+
+def remove_unused_vertex_groups_of_mesh(mesh):
+ remove_count = 0
+ unselect_all()
+ mesh.update_from_editmode()
+
+ vgroup_used = {i: False for i, k in enumerate(mesh.vertex_groups)}
+
+ for v in mesh.data.vertices:
+ for g in v.groups:
+ if g.weight > 0.0:
+ vgroup_used[g.group] = True
+
+ for i, used in sorted(vgroup_used.items(), reverse=True):
+ if not used:
+ mesh.vertex_groups.remove(mesh.vertex_groups[i])
+ remove_count += 1
+ return remove_count
+
+
+def find_center_vector_of_vertex_group(mesh, vertex_group):
+ data = mesh.data
+ verts = data.vertices
+ verts_in_group = []
+
+ for vert in verts:
+ i = vert.index
+ try:
+ if mesh.vertex_groups[vertex_group].weight(i) > 0:
+ verts_in_group.append(vert)
+ except RuntimeError:
+ # vertex is not in the group
+ pass
+
+ # Find the average vector point of the vertex cluster
+ divide_by = len(verts_in_group)
+ total = Vector()
+
+ if divide_by == 0:
+ return False
+
+ for vert in verts_in_group:
+ total += vert.co
+
+ average = total / divide_by
+
+ return average
+
+
+def vertex_group_exists(mesh_name, bone_name):
+ mesh = get_objects()[mesh_name]
+ data = mesh.data
+ verts = data.vertices
+
+ for vert in verts:
+ i = vert.index
+ try:
+ mesh.vertex_groups[bone_name].weight(i)
+ return True
+ except:
+ pass
+
+ return False
+
+
+def get_meshes(self, context):
+ # Modes:
+ # 0 = With Armature only
+ # 1 = Without armature only
+ # 2 = All meshes
+
+ choices = []
+
+ for mesh in get_meshes_objects(mode=0, check=False):
+ choices.append((mesh.name, mesh.name, mesh.name))
+
+ bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower()))
+ return bpy.types.Object.Enum
+
+
+def get_top_meshes(self, context):
+ choices = []
+
+ for mesh in get_meshes_objects(mode=1, check=False):
+ choices.append((mesh.name, mesh.name, mesh.name))
+
+ bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower()))
+ return bpy.types.Object.Enum
+
+
+def get_all_meshes(self, context):
+ choices = []
+
+ for mesh in get_meshes_objects(mode=2, check=False):
+ choices.append((mesh.name, mesh.name, mesh.name))
+
+ bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower()))
+ return bpy.types.Object.Enum
+
+
+def get_armature_list(self, context):
+ choices = []
+
+ for armature in get_armature_objects():
+ # Set name displayed in list
+ name = armature.data.name
+ if name.startswith('Armature ('):
+ name = armature.name + ' (' + name.replace('Armature (', '')[:-1] + ')'
+
+ # 1. Will be returned by context.scene
+ # 2. Will be shown in lists
+ # 3. will be shown in the hover description (below description)
+ choices.append((armature.name, name, armature.name))
+
+ if len(choices) == 0:
+ choices.append(('None', 'None', 'None'))
+
+ bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower()))
+ return bpy.types.Object.Enum
+
+
+def get_armature_merge_list(self, context):
+ choices = []
+ current_armature = context.scene.merge_armature_into
+
+ for armature in get_armature_objects():
+ if armature.name != current_armature:
+ # Set name displayed in list
+ name = armature.data.name
+ if name.startswith('Armature ('):
+ name = armature.name + ' (' + name.replace('Armature (', '')[:-1] + ')'
+
+ # 1. Will be returned by context.scene
+ # 2. Will be shown in lists
+ # 3. will be shown in the hover description (below description)
+ choices.append((armature.name, name, armature.name))
+
+ if len(choices) == 0:
+ choices.append(('None', 'None', 'None'))
+
+ bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower()))
+ return bpy.types.Object.Enum
+
+
+def get_meshes_decimation(self, context):
+ choices = []
+
+ for object in bpy.context.scene.objects:
+ if object.type == 'MESH':
+ if object.parent and object.parent.type == 'ARMATURE' and object.parent.name == bpy.context.scene.armature:
+ if object.name in Decimation.ignore_meshes:
+ continue
+ # 1. Will be returned by context.scene
+ # 2. Will be shown in lists
+ # 3. will be shown in the hover description (below description)
+ choices.append((object.name, object.name, object.name))
+
+ bpy.types.Object.Enum = sorted(choices, key=lambda x: tuple(x[0].lower()))
+ return bpy.types.Object.Enum
+
+
+def get_bones_head(self, context):
+ return get_bones(names=['Head'])
+
+
+def get_bones_eye_l(self, context):
+ return get_bones(names=['Eye_L', 'EyeReturn_L'])
+
+
+def get_bones_eye_r(self, context):
+ return get_bones(names=['Eye_R', 'EyeReturn_R'])
+
+
+def get_bones_merge(self, context):
+ return get_bones(armature_name=bpy.context.scene.merge_armature_into)
+
+
+# names - The first object will be the first one in the list. So the first one has to be the one that exists in the most models
+def get_bones(names=None, armature_name=None, check_list=False):
+ if not names:
+ names = []
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+
+ choices = []
+ armature = get_armature(armature_name=armature_name)
+
+ if not armature:
+ bpy.types.Object.Enum = choices
+ return bpy.types.Object.Enum
+
+ # print("")
+ # print("START DEBUG UNICODE")
+ # print("")
+ for bone in armature.data.bones:
+ # print(bone.name)
+ try:
+ # 1. Will be returned by context.scene
+ # 2. Will be shown in lists
+ # 3. will be shown in the hover description (below description)
+ choices.append((bone.name, bone.name, bone.name))
+ except UnicodeDecodeError:
+ print("ERROR", bone.name)
+
+ choices.sort(key=lambda x: tuple(x[0].lower()))
+
+ choices2 = []
+ for name in names:
+ if name in armature.data.bones and choices[0][0] != name:
+ choices2.append((name, name, name))
+
+ if not check_list:
+ for choice in choices:
+ choices2.append(choice)
+
+ bpy.types.Object.Enum = choices2
+
+ return bpy.types.Object.Enum
+
+
+def get_shapekeys_mouth_ah(self, context):
+ return get_shapekeys(context, ['MTH A', 'Ah', 'A'], True, False, False, False)
+
+
+def get_shapekeys_mouth_oh(self, context):
+ return get_shapekeys(context, ['MTH U', 'Oh', 'O', 'Your'], True, False, False, False)
+
+
+def get_shapekeys_mouth_ch(self, context):
+ return get_shapekeys(context, ['MTH I', 'Glue', 'Ch', 'I', 'There'], True, False, False, False)
+
+
+def get_shapekeys_eye_blink_l(self, context):
+ return get_shapekeys(context, ['EYE Close L', 'Wink 2', 'Wink', 'Wink left', 'Wink Left', 'Blink (Left)', 'Blink', 'Basis'], False, False, False, False)
+
+
+def get_shapekeys_eye_blink_r(self, context):
+ return get_shapekeys(context, ['EYE Close R', 'Wink 2 right', 'Wink 2 Right', 'Wink right 2', 'Wink Right 2', 'Wink right', 'Wink Right', 'Blink (Right)', 'Basis'], False, False, False, False)
+
+
+def get_shapekeys_eye_low_l(self, context):
+ return get_shapekeys(context, ['Basis'], False, False, False, False)
+
+
+def get_shapekeys_eye_low_r(self, context):
+ return get_shapekeys(context, ['Basis'], False, False, False, False)
+
+
+def get_shapekeys_decimation(self, context):
+ return get_shapekeys(context,
+ ['MTH A', 'Ah', 'A', 'MTH U', 'Oh', 'O', 'Your', 'MTH I', 'Glue', 'Ch', 'I', 'There', 'Wink 2', 'Wink', 'Wink left', 'Wink Left', 'Blink (Left)', 'Wink 2 right',
+ 'EYE Close R', 'EYE Close L', 'Wink 2 Right', 'Wink right 2', 'Wink Right 2', 'Wink right', 'Wink Right', 'Blink (Right)', 'Blink'], False, True, True, False)
+
+
+def get_shapekeys_decimation_list(self, context):
+ return get_shapekeys(context,
+ ['MTH A', 'Ah', 'A', 'MTH U', 'Oh', 'O', 'Your', 'MTH I', 'Glue', 'Ch', 'I', 'There', 'Wink 2', 'Wink', 'Wink left', 'Wink Left', 'Blink (Left)', 'Wink 2 right',
+ 'EYE Close R', 'EYE Close L', 'Wink 2 Right', 'Wink right 2', 'Wink Right 2', 'Wink right', 'Wink Right', 'Blink (Right)', 'Blink'], False, True, True, True)
+
+
+# names - The first object will be the first one in the list. So the first one has to be the one that exists in the most models
+# no_basis - If this is true the Basis will not be available in the list
+def get_shapekeys(context, names, is_mouth, no_basis, decimation, return_list):
+ choices = []
+ choices_simple = []
+ meshes_list = get_meshes_objects(check=False)
+
+ if decimation:
+ meshes = meshes_list
+ elif meshes_list:
+ if is_mouth:
+ meshes = [get_objects().get(context.scene.mesh_name_viseme)]
+ else:
+ meshes = [get_objects().get(context.scene.mesh_name_eye)]
+ else:
+ bpy.types.Object.Enum = choices
+ return bpy.types.Object.Enum
+
+ for mesh in meshes:
+ if not mesh or not has_shapekeys(mesh):
+ bpy.types.Object.Enum = choices
+ return bpy.types.Object.Enum
+
+ for shapekey in mesh.data.shape_keys.key_blocks:
+ name = shapekey.name
+ if name in choices_simple:
+ continue
+ if no_basis and name == 'Basis':
+ continue
+ if decimation and name in Decimation.ignore_shapes:
+ continue
+ # 1. Will be returned by context.scene
+ # 2. Will be shown in lists
+ # 3. will be shown in the hover description (below description)
+ choices.append((name, name, name))
+ choices_simple.append(name)
+
+ choices.sort(key=lambda x: tuple(x[0].lower()))
+
+ choices2 = []
+ for name in names:
+ if name in choices_simple and len(choices) > 1 and choices[0][0] != name:
+ if decimation and name in Decimation.ignore_shapes:
+ continue
+ choices2.append((name, name, name))
+
+ for choice in choices:
+ choices2.append(choice)
+
+ bpy.types.Object.Enum = choices2
+
+ if return_list:
+ shape_list = []
+ for choice in choices2:
+ shape_list.append(choice[0])
+ return shape_list
+
+ return bpy.types.Object.Enum
+
+
+def fix_armature_names(armature_name=None):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+ base_armature = get_armature(armature_name=bpy.context.scene.merge_armature_into)
+ merge_armature = get_armature(armature_name=bpy.context.scene.merge_armature)
+
+ # Armature should be named correctly (has to be at the end because of multiple armatures)
+ armature = get_armature(armature_name=armature_name)
+ armature.name = 'Armature'
+ if not armature.data.name.startswith('Armature'):
+ Translate.update_dictionary(armature.data.name)
+ armature.data.name = 'Armature (' + Translate.translate(armature.data.name, add_space=True)[0] + ')'
+
+ # Reset the armature lists
+ try:
+ bpy.context.scene.armature = armature.name
+ except TypeError:
+ pass
+
+ try:
+ if base_armature:
+ bpy.context.scene.merge_armature_into = base_armature.name
+ except TypeError:
+ pass
+
+ try:
+ if merge_armature:
+ bpy.context.scene.merge_armature = merge_armature.name
+ except TypeError:
+ pass
+
+
+def get_texture_sizes(self, context):
+ bpy.types.Object.Enum = [
+ ("1024", "1024 (low)", "1024"),
+ ("2048", "2048 (medium)", "2048"),
+ ("4096", "4096 (high)", "4096")
+ ]
+
+ return bpy.types.Object.Enum
+
+
+def get_meshes_objects(armature_name=None, mode=0, check=True, visible_only=False):
+ # Modes:
+ # 0 = With armatures only
+ # 1 = Top level only
+ # 2 = All meshes
+ # 3 = Selected only
+
+ if not armature_name:
+ armature = get_armature()
+ if armature:
+ armature_name = armature.name
+
+ meshes = []
+ for ob in get_objects():
+ if ob.type == 'MESH':
+ if mode == 0 or mode == 5:
+ if ob.parent:
+ if ob.parent.type == 'ARMATURE' and ob.parent.name == armature_name:
+ meshes.append(ob)
+ elif ob.parent.parent and ob.parent.parent.type == 'ARMATURE' and ob.parent.parent.name == armature_name:
+ meshes.append(ob)
+
+ elif mode == 1:
+ if not ob.parent:
+ meshes.append(ob)
+
+ elif mode == 2:
+ meshes.append(ob)
+
+ elif mode == 3:
+ if is_selected(ob):
+ meshes.append(ob)
+
+ if visible_only:
+ for mesh in meshes:
+ if is_hidden(mesh):
+ meshes.remove(mesh)
+
+ # Check for broken meshes and delete them
+ if check:
+ current_active = get_active()
+ to_remove = []
+ for mesh in meshes:
+ selected = is_selected(mesh)
+ # print(mesh.name, mesh.users)
+ set_active(mesh)
+
+ if not get_active():
+ to_remove.append(mesh)
+
+ if not selected:
+ select(mesh, False)
+
+ for mesh in to_remove:
+ print('DELETED CORRUPTED MESH:', mesh.name, mesh.users)
+ meshes.remove(mesh)
+ delete(mesh)
+
+ if current_active:
+ set_active(current_active)
+
+ return meshes
+
+
+def join_meshes(armature_name=None, mode=0, apply_transformations=True, repair_shape_keys=True):
+ # Modes:
+ # 0 - Join all meshes
+ # 1 - Join selected only
+
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+
+ # Get meshes to join
+ meshes_to_join = get_meshes_objects(armature_name=armature_name, mode=3 if mode == 1 else 0)
+ if not meshes_to_join:
+ reset_context_scenes()
+ return None
+
+ set_default_stage()
+ unselect_all()
+
+ if apply_transformations:
+ apply_transforms(armature_name=armature_name)
+
+ unselect_all()
+
+ # Apply existing decimation modifiers and select the meshes for joining
+ for mesh in meshes_to_join:
+ set_active(mesh)
+
+ # Apply decimation modifiers
+ for mod in mesh.modifiers:
+ if mod.type == 'DECIMATE':
+ if mod.decimate_type == 'COLLAPSE' and mod.ratio == 1:
+ mesh.modifiers.remove(mod)
+ continue
+ if mod.decimate_type == 'UNSUBDIV' and mod.iterations == 0:
+ mesh.modifiers.remove(mod)
+ continue
+
+ if has_shapekeys(mesh):
+ bpy.ops.object.shape_key_remove(all=True)
+ apply_modifier(mod)
+ elif mod.type == 'SUBSURF':
+ mesh.modifiers.remove(mod)
+ elif mod.type == 'MIRROR':
+ if not has_shapekeys(mesh):
+ apply_modifier(mod)
+
+ # Standardize UV maps name
+ if version_2_79_or_older():
+ if mesh.data.uv_textures:
+ mesh.data.uv_textures[0].name = 'UVMap'
+ for mat_slot in mesh.material_slots:
+ if mat_slot and mat_slot.material:
+ for tex_slot in mat_slot.material.texture_slots:
+ if tex_slot and tex_slot.texture and tex_slot.texture_coords == 'UV':
+ tex_slot.uv_layer = 'UVMap'
+ else:
+ if mesh.data.uv_layers:
+ mesh.data.uv_layers[0].name = 'UVMap'
+
+ # Get the name of the active mesh in order to check if it was deleted later
+ active_mesh_name = get_active().name
+
+ # Join the meshes
+ if bpy.ops.object.join.poll():
+ bpy.ops.object.join()
+ else:
+ print('NO MESH COMBINED!')
+
+ # Delete meshes that somehow weren't deleted. Both pre and post join mesh deletion methods are needed!
+ for mesh in get_meshes_objects(armature_name=armature_name):
+ if mesh.name == active_mesh_name:
+ set_active(mesh)
+ elif mesh.name in meshes_to_join:
+ delete(mesh)
+ print('DELETED', mesh.name, mesh.users)
+
+ # Rename result to Body and correct modifiers
+ mesh = get_active()
+ if mesh:
+ # If its the only mesh in the armature left, rename it to Body
+ if len(get_meshes_objects(armature_name=armature_name)) == 1:
+ mesh.name = 'Body'
+ mesh.parent_type = 'OBJECT'
+
+ repair_mesh(mesh, armature_name)
+
+ if repair_shape_keys:
+ repair_shapekey_order(mesh.name)
+
+ reset_context_scenes()
+
+ # Update the material list of the Material Combiner
+ update_material_list()
+
+ return mesh
+
+
+def repair_mesh(mesh, armature_name):
+ mesh.parent_type = 'OBJECT'
+
+ # Remove duplicate armature modifiers
+ mod_count = 0
+ for mod in mesh.modifiers:
+ mod.show_expanded = False
+ if mod.type == 'ARMATURE':
+ mod_count += 1
+ if mod_count > 1:
+ bpy.ops.object.modifier_remove(modifier=mod.name)
+ continue
+ mod.object = get_armature(armature_name=armature_name)
+ mod.show_viewport = True
+
+ # Add armature mod if there is none
+ if mod_count == 0:
+ mod = mesh.modifiers.new("Armature", 'ARMATURE')
+ mod.object = get_armature(armature_name=armature_name)
+
+
+def apply_transforms(armature_name=None):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+ armature = get_armature(armature_name=armature_name)
+
+ # Apply transforms on armature
+ unselect_all()
+ set_active(armature)
+ switch('OBJECT')
+ bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
+
+ # Apply transforms of meshes
+ for mesh in get_meshes_objects(armature_name=armature_name):
+ unselect_all()
+ set_active(mesh)
+ bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
+
+
+def apply_all_transforms():
+
+ def apply_transforms_with_children(parent):
+ unselect_all()
+ set_active(parent)
+ switch('OBJECT')
+ bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
+ for child in parent.children:
+ apply_transforms_with_children(child)
+
+ for obj in get_objects():
+ if not obj.parent:
+ apply_transforms_with_children(obj)
+
+
+def reset_transforms(armature_name=None):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+ armature = get_armature(armature_name=armature_name)
+
+ # Reset transforms on armature
+ for i in range(0, 3):
+ armature.location[i] = 0
+ armature.rotation_euler[i] = 0
+ armature.scale[i] = 1
+
+ # Apply transforms of meshes
+ for mesh in get_meshes_objects(armature_name=armature_name):
+ for i in range(0, 3):
+ mesh.location[i] = 0
+ mesh.rotation_euler[i] = 0
+ mesh.scale[i] = 1
+
+
+def separate_by_materials(context, mesh):
+ prepare_separation(mesh)
+
+ utils.separateByMaterials(mesh)
+
+ for ob in context.selected_objects:
+ if ob.type == 'MESH':
+ hide(ob, False)
+ clean_shapekeys(ob)
+
+ utils.clearUnusedMeshes()
+
+ # Update the material list of the Material Combiner
+ update_material_list()
+
+
+def separate_by_loose_parts(context, mesh):
+ prepare_separation(mesh)
+
+ # Correctly put mesh together. This is done to prevent extremely small pieces.
+ # This essentially does nothing but merges the extremely small parts together.
+ remove_doubles(mesh, 0, save_shapes=True)
+
+ utils.separateByMaterials(mesh)
+
+ meshes = []
+ for ob in context.selected_objects:
+ if ob.type == 'MESH':
+ hide(ob, False)
+ meshes.append(ob)
+
+ wm = bpy.context.window_manager
+ current_step = 0
+ wm.progress_begin(current_step, len(meshes))
+
+ for mesh in meshes:
+ unselect_all()
+ set_active(mesh)
+ bpy.ops.mesh.separate(type='LOOSE')
+
+ meshes2 = []
+ for ob in context.selected_objects:
+ if ob.type == 'MESH':
+ meshes2.append(ob)
+
+ ## This crashes blender, but would be better
+ # unselect_all()
+ # for mesh2 in meshes2:
+ # if len(mesh2.data.vertices) <= 3:
+ # select(mesh2)
+ # elif bpy.ops.object.join.poll():
+ # bpy.ops.object.join()
+ # unselect_all()
+
+ for mesh2 in meshes2:
+ clean_shapekeys(mesh2)
+
+ current_step += 1
+ wm.progress_update(current_step)
+
+ wm.progress_end()
+
+ utils.clearUnusedMeshes()
+
+ # Update the material list of the Material Combiner
+ update_material_list()
+
+
+def separate_by_shape_keys(context, mesh):
+ prepare_separation(mesh)
+
+ switch('EDIT')
+ bpy.ops.mesh.select_mode(type="VERT")
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ switch('OBJECT')
+ selected_count = 0
+ max_count = 0
+ if has_shapekeys(mesh):
+ for kb in mesh.data.shape_keys.key_blocks:
+ for i, (v0, v1) in enumerate(zip(kb.relative_key.data, kb.data)):
+ max_count += 1
+ if v0.co != v1.co:
+ mesh.data.vertices[i].select = True
+ selected_count += 1
+
+ if not selected_count or selected_count == max_count:
+ return False
+
+ switch('EDIT')
+ bpy.ops.mesh.select_all(action='INVERT')
+
+ bpy.ops.mesh.separate(type='SELECTED')
+
+ for ob in context.selected_objects:
+ if ob.type == 'MESH':
+ if ob != get_active():
+ print('not active', ob.name)
+ active_tmp = get_active()
+ ob.name = ob.name.replace('.001', '') + '.no_shapes'
+ set_active(ob)
+ bpy.ops.object.shape_key_remove(all=True)
+ set_active(active_tmp)
+ select(ob, False)
+ else:
+ print('active', ob.name)
+ clean_shapekeys(ob)
+ switch('OBJECT')
+
+ utils.clearUnusedMeshes()
+
+ # Update the material list of the Material Combiner
+ update_material_list()
+ return True
+
+
+def separate_by_cats_protection(context, mesh):
+ prepare_separation(mesh)
+
+ switch('EDIT')
+ bpy.ops.mesh.select_mode(type="VERT")
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ switch('OBJECT')
+ selected_count = 0
+ max_count = 0
+ if has_shapekeys(mesh):
+ for kb in mesh.data.shape_keys.key_blocks:
+ if kb.name == 'Basis Original':
+ for i, (v0, v1) in enumerate(zip(kb.relative_key.data, kb.data)):
+ max_count += 1
+ if v0.co != v1.co:
+ mesh.data.vertices[i].select = True
+ selected_count += 1
+
+ if not selected_count or selected_count == max_count:
+ return False
+
+ switch('EDIT')
+ bpy.ops.mesh.select_all(action='INVERT')
+
+ bpy.ops.mesh.separate(type='SELECTED')
+
+ for ob in context.selected_objects:
+ if ob.type == 'MESH':
+ if ob != get_active():
+ print('not active', ob.name)
+ active_tmp = get_active()
+ ob.name = ob.name.replace('.001', '') + '.no_shapes'
+ set_active(ob)
+ bpy.ops.object.shape_key_remove(all=True)
+ set_active(active_tmp)
+ select(ob, False)
+ else:
+ print('active', ob.name)
+ clean_shapekeys(ob)
+ switch('OBJECT')
+
+ utils.clearUnusedMeshes()
+
+ # Update the material list of the Material Combiner
+ update_material_list()
+ return True
+
+
+def prepare_separation(mesh):
+ set_default_stage()
+ unselect_all()
+
+ # Remove Rigidbodies and joints
+ if bpy.context.scene.remove_rigidbodies_joints:
+ for obj in get_objects():
+ if 'rigidbodies' in obj.name or 'joints' in obj.name:
+ delete_hierarchy(obj)
+
+ save_shapekey_order(mesh.name)
+ set_active(mesh)
+
+ for mod in mesh.modifiers:
+ if mod.type == 'DECIMATE':
+ mesh.modifiers.remove(mod)
+ else:
+ mod.show_expanded = False
+
+ clean_material_names(mesh)
+
+
+def clean_shapekeys(mesh):
+ # Remove empty shapekeys
+ if has_shapekeys(mesh):
+ for kb in mesh.data.shape_keys.key_blocks:
+ if can_remove_shapekey(kb):
+ mesh.shape_key_remove(kb)
+ if len(mesh.data.shape_keys.key_blocks) == 1:
+ mesh.shape_key_remove(mesh.data.shape_keys.key_blocks[0])
+
+
+def can_remove_shapekey(key_block):
+ if 'mmd_' in key_block.name:
+ return True
+ if key_block.relative_key == key_block:
+ return False # Basis
+ for v0, v1 in zip(key_block.relative_key.data, key_block.data):
+ if v0.co != v1.co:
+ return False
+ return True
+
+
+def separate_by_verts():
+ for obj in bpy.context.selected_objects:
+ if obj.type == 'MESH' and len(obj.vertex_groups) > 0:
+ Common.set_active(obj)
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_mode(type='VERT')
+ for vgroup in obj.vertex_groups:
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bpy.ops.object.vertex_group_set_active(group=vgroup.name)
+ bpy.ops.object.vertex_group_select()
+ bpy.ops.mesh.separate(type='SELECTED')
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+
+def reset_context_scenes():
+ head_bones = get_bones_head(None, bpy.context)
+ if len(head_bones) > 0:
+ bpy.context.scene.head = head_bones[0][0]
+ bpy.context.scene.eye_left = get_bones_eye_l(None, bpy.context)[0][0]
+ bpy.context.scene.eye_right = get_bones_eye_r(None, bpy.context)[0][0]
+
+ meshes = get_meshes(None, bpy.context)
+ if len(meshes) > 0:
+ mesh = meshes[0][0]
+ if not bpy.context.scene.mesh_name_eye:
+ bpy.context.scene.mesh_name_eye = mesh
+ if not bpy.context.scene.mesh_name_viseme:
+ bpy.context.scene.mesh_name_viseme = mesh
+ if not bpy.context.scene.merge_mesh:
+ bpy.context.scene.merge_mesh = mesh
+
+
+def save_shapekey_order(mesh_name):
+ mesh = get_objects()[mesh_name]
+ armature = get_armature()
+
+ if not armature:
+ return
+
+ # Get current custom data
+ custom_data = armature.get('CUSTOM')
+ if not custom_data:
+ # print('NEW DATA!')
+ custom_data = {}
+
+ # Create shapekey order
+ shape_key_order = []
+ if has_shapekeys(mesh):
+ for index, shapekey in enumerate(mesh.data.shape_keys.key_blocks):
+ shape_key_order.append(shapekey.name)
+
+ # Check if there is already a shapekey order
+ if custom_data.get('shape_key_order'):
+ # print('SHAPEKEY ORDER ALREADY EXISTS!')
+ # print(custom_data['shape_key_order'])
+ old_len = len(custom_data.get('shape_key_order'))
+
+ if type(shape_key_order) is str:
+ old_len = len(shape_key_order.split(',,,'))
+
+ if len(shape_key_order) <= old_len:
+ # print('ABORT')
+ return
+
+ # Save order to custom data
+ # print('SAVE NEW ORDER')
+ custom_data['shape_key_order'] = shape_key_order
+
+ # Save custom data in armature
+ armature['CUSTOM'] = custom_data
+
+ # print(armature.get('CUSTOM').get('shape_key_order'))
+
+
+def repair_shapekey_order(mesh_name):
+ # Get current custom data
+ armature = get_armature()
+ custom_data = armature.get('CUSTOM')
+ if not custom_data:
+ custom_data = {}
+
+ # Extract shape keys from string
+ shape_key_order = custom_data.get('shape_key_order')
+ if not shape_key_order:
+ custom_data['shape_key_order'] = []
+ armature['CUSTOM'] = custom_data
+
+ if type(shape_key_order) is str:
+ shape_key_order_temp = []
+ for shape_name in shape_key_order.split(',,,'):
+ shape_key_order_temp.append(shape_name)
+ custom_data['shape_key_order'] = shape_key_order_temp
+ armature['CUSTOM'] = custom_data
+
+ sort_shape_keys(mesh_name, custom_data['shape_key_order'])
+
+
+def update_shapekey_orders():
+ for armature in get_armature_objects():
+ shape_key_order_translated = []
+
+ # Get current custom data
+ custom_data = armature.get('CUSTOM')
+ if not custom_data:
+ continue
+ order = custom_data.get('shape_key_order')
+ if not order:
+ continue
+
+ if type(order) is str:
+ shape_key_order_temp = order.split(',,,')
+ order = []
+ for shape_name in shape_key_order_temp:
+ order.append(shape_name)
+
+ # Get shape keys and translate them
+ for shape_name in order:
+ shape_key_order_translated.append(Translate.translate(shape_name, add_space=True, translating_shapes=True)[0])
+
+ # print(armature.name, shape_key_order_translated)
+ custom_data['shape_key_order'] = shape_key_order_translated
+ armature['CUSTOM'] = custom_data
+
+
+def sort_shape_keys(mesh_name, shape_key_order=None):
+ mesh = get_objects()[mesh_name]
+ if not has_shapekeys(mesh):
+ return
+ set_active(mesh)
+
+ if not shape_key_order:
+ shape_key_order = []
+
+ order = [
+ 'Basis',
+ 'vrc.blink_left',
+ 'vrc.blink_right',
+ 'vrc.lowerlid_left',
+ 'vrc.lowerlid_right',
+ 'vrc.v_aa',
+ 'vrc.v_ch',
+ 'vrc.v_dd',
+ 'vrc.v_e',
+ 'vrc.v_ff',
+ 'vrc.v_ih',
+ 'vrc.v_kk',
+ 'vrc.v_nn',
+ 'vrc.v_oh',
+ 'vrc.v_ou',
+ 'vrc.v_pp',
+ 'vrc.v_rr',
+ 'vrc.v_sil',
+ 'vrc.v_ss',
+ 'vrc.v_th',
+ 'Basis Original'
+ ]
+
+ for shape in shape_key_order:
+ if shape not in order:
+ order.append(shape)
+
+ wm = bpy.context.window_manager
+ current_step = 0
+ wm.progress_begin(current_step, len(order))
+
+ i = 0
+ for name in order:
+ if name == 'Basis' and 'Basis' not in mesh.data.shape_keys.key_blocks:
+ i += 1
+ current_step += 1
+ wm.progress_update(current_step)
+ continue
+
+ for index, shapekey in enumerate(mesh.data.shape_keys.key_blocks):
+ if shapekey.name == name:
+
+ mesh.active_shape_key_index = index
+ new_index = i
+ index_diff = (index - new_index)
+
+ if new_index >= len(mesh.data.shape_keys.key_blocks):
+ bpy.ops.object.shape_key_move(type='BOTTOM')
+ break
+
+ position_correct = False
+ if 0 <= index_diff <= (new_index - 1):
+ while position_correct is False:
+ if mesh.active_shape_key_index != new_index:
+ bpy.ops.object.shape_key_move(type='UP')
+ else:
+ position_correct = True
+ else:
+ if mesh.active_shape_key_index > new_index:
+ bpy.ops.object.shape_key_move(type='TOP')
+
+ position_correct = False
+ while position_correct is False:
+ if mesh.active_shape_key_index != new_index:
+ bpy.ops.object.shape_key_move(type='DOWN')
+ else:
+ position_correct = True
+
+ i += 1
+ break
+
+ current_step += 1
+ wm.progress_update(current_step)
+
+ mesh.active_shape_key_index = 0
+
+ wm.progress_end()
+
+
+def isEmptyGroup(group_name):
+ mesh = get_objects().get('Body')
+ if mesh is None:
+ return True
+ vgroup = mesh.vertex_groups.get(group_name)
+ if vgroup is None:
+ return True
+
+ for vert in mesh.data.vertices:
+ for group in vert.groups:
+ if group.group == vgroup.index:
+ if group.weight > 0:
+ return False
+
+ return True
+
+
+def removeEmptyGroups(obj, thres=0):
+ z = []
+ for v in obj.data.vertices:
+ for g in v.groups:
+ if g.weight > thres:
+ if g not in z:
+ z.append(obj.vertex_groups[g.group])
+ for r in obj.vertex_groups:
+ if r not in z:
+ obj.vertex_groups.remove(r)
+
+
+def removeZeroVerts(obj, thres=0):
+ for v in obj.data.vertices:
+ z = []
+ for g in v.groups:
+ if not g.weight > thres:
+ z.append(g)
+ for r in z:
+ obj.vertex_groups[g.group].remove([v.index])
+
+
+def delete_hierarchy(parent):
+ unselect_all()
+ to_delete = []
+
+ def get_child_names(obj):
+ for child in obj.children:
+ to_delete.append(child)
+ if child.children:
+ get_child_names(child)
+
+ get_child_names(parent)
+ to_delete.append(parent)
+
+ objs = bpy.data.objects
+ for obj in to_delete:
+ objs.remove(objs[obj.name], do_unlink=True)
+
+
+def delete(obj):
+ if obj.parent:
+ for child in obj.children:
+ child.parent = obj.parent
+
+ objs = bpy.data.objects
+ objs.remove(objs[obj.name], do_unlink=True)
+
+
+def days_between(d1, d2, time_format):
+ d1 = datetime.strptime(d1, time_format)
+ d2 = datetime.strptime(d2, time_format)
+ return abs((d2 - d1).days)
+
+
+def delete_bone_constraints(armature_name=None):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+
+ armature = get_armature(armature_name=armature_name)
+ switch('POSE')
+
+ for bone in armature.pose.bones:
+ if len(bone.constraints) > 0:
+ for constraint in bone.constraints:
+ bone.constraints.remove(constraint)
+
+ switch('EDIT')
+
+
+def delete_zero_weight(armature_name=None, ignore=''):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+
+ armature = get_armature(armature_name=armature_name)
+ switch('EDIT')
+
+ bone_names_to_work_on = set([bone.name for bone in armature.data.edit_bones])
+
+ bone_name_to_edit_bone = dict()
+ for edit_bone in armature.data.edit_bones:
+ bone_name_to_edit_bone[edit_bone.name] = edit_bone
+
+ vertex_group_names_used = set()
+ vertex_group_name_to_objects_having_same_named_vertex_group = dict()
+ for objects in get_meshes_objects(armature_name=armature_name):
+ vertex_group_id_to_vertex_group_name = dict()
+ for vertex_group in objects.vertex_groups:
+ vertex_group_id_to_vertex_group_name[vertex_group.index] = vertex_group.name
+ if vertex_group.name not in vertex_group_name_to_objects_having_same_named_vertex_group:
+ vertex_group_name_to_objects_having_same_named_vertex_group[vertex_group.name] = set()
+ vertex_group_name_to_objects_having_same_named_vertex_group[vertex_group.name].add(objects)
+ for vertex in objects.data.vertices:
+ for group in vertex.groups:
+ if group.weight > 0:
+ vertex_group_names_used.add(vertex_group_id_to_vertex_group_name.get(group.group))
+
+ not_used_bone_names = bone_names_to_work_on - vertex_group_names_used
+
+ count = 0
+ for bone_name in not_used_bone_names:
+ if not bpy.context.scene.keep_end_bones or not is_end_bone(bone_name, armature_name):
+ if bone_name not in Bones.dont_delete_these_bones and 'Root_' not in bone_name and bone_name != ignore:
+ armature.data.edit_bones.remove(bone_name_to_edit_bone[bone_name]) # delete bone
+ count += 1
+ if bone_name in vertex_group_name_to_objects_having_same_named_vertex_group:
+ for objects in vertex_group_name_to_objects_having_same_named_vertex_group[bone_name]: # delete vertex groups
+ vertex_group = objects.vertex_groups.get(bone_name)
+ if vertex_group is not None:
+ objects.vertex_groups.remove(vertex_group)
+
+ return count
+
+
+def remove_unused_objects():
+ default_scene_objects = []
+ for obj in get_objects():
+ if (obj.type == 'CAMERA' and obj.name == 'Camera') \
+ or (obj.type == 'LAMP' and obj.name == 'Lamp') \
+ or (obj.type == 'LIGHT' and obj.name == 'Light') \
+ or (obj.type == 'MESH' and obj.name == 'Cube'):
+ default_scene_objects.append(obj)
+
+ if len(default_scene_objects) == 3:
+ for obj in default_scene_objects:
+ delete_hierarchy(obj)
+
+
+def remove_no_user_objects():
+ # print('\nREMOVE OBJECTS')
+ for block in get_objects():
+ # print(block.name, block.users)
+ if block.users == 0:
+ print('Removing obj ', block.name)
+ delete(block)
+ # print('\nREMOVE MESHES')
+ for block in bpy.data.meshes:
+ # print(block.name, block.users)
+ if block.users == 0:
+ print('Removing mesh ', block.name)
+ bpy.data.meshes.remove(block)
+ # print('\nREMOVE MATERIALS')
+ for block in bpy.data.materials:
+ # print(block.name, block.users)
+ if block.users == 0:
+ print('Removing material ', block.name)
+ bpy.data.materials.remove(block)
+
+ # print('\nREMOVE MATS')
+ # for block in bpy.data.materials:
+ # print(block.name, block.users)
+ # if block.users == 0:
+ # bpy.data.materials.remove(block)
+
+
+def is_end_bone(name, armature_name):
+ armature = get_armature(armature_name=armature_name)
+ end_bone = armature.data.edit_bones.get(name)
+ if end_bone and end_bone.parent and len(end_bone.parent.children) == 1:
+ return True
+ return False
+
+
+def correct_bone_positions(armature_name=None):
+ if not armature_name:
+ armature_name = bpy.context.scene.armature
+ armature = get_armature(armature_name=armature_name)
+
+ upper_chest = armature.data.edit_bones.get('Upper Chest')
+ chest = armature.data.edit_bones.get('Chest')
+ neck = armature.data.edit_bones.get('Neck')
+ head = armature.data.edit_bones.get('Head')
+ if chest and neck:
+ if upper_chest and bpy.context.scene.keep_upper_chest:
+ chest.tail = upper_chest.head
+ upper_chest.tail = neck.head
+ else:
+ chest.tail = neck.head
+ if neck and head:
+ neck.tail = head.head
+
+ if 'Left shoulder' in armature.data.edit_bones:
+ if 'Left arm' in armature.data.edit_bones:
+ if 'Left elbow' in armature.data.edit_bones:
+ if 'Left wrist' in armature.data.edit_bones:
+ shoulder = armature.data.edit_bones.get('Left shoulder')
+ arm = armature.data.edit_bones.get('Left arm')
+ elbow = armature.data.edit_bones.get('Left elbow')
+ wrist = armature.data.edit_bones.get('Left wrist')
+ shoulder.tail = arm.head
+ arm.tail = elbow.head
+ elbow.tail = wrist.head
+
+ if 'Right shoulder' in armature.data.edit_bones:
+ if 'Right arm' in armature.data.edit_bones:
+ if 'Right elbow' in armature.data.edit_bones:
+ if 'Right wrist' in armature.data.edit_bones:
+ shoulder = armature.data.edit_bones.get('Right shoulder')
+ arm = armature.data.edit_bones.get('Right arm')
+ elbow = armature.data.edit_bones.get('Right elbow')
+ wrist = armature.data.edit_bones.get('Right wrist')
+ shoulder.tail = arm.head
+ arm.tail = elbow.head
+ elbow.tail = wrist.head
+
+ if 'Left leg' in armature.data.edit_bones:
+ if 'Left knee' in armature.data.edit_bones:
+ if 'Left ankle' in armature.data.edit_bones:
+ leg = armature.data.edit_bones.get('Left leg')
+ knee = armature.data.edit_bones.get('Left knee')
+ ankle = armature.data.edit_bones.get('Left ankle')
+
+ if 'Left leg 2' in armature.data.edit_bones:
+ leg = armature.data.edit_bones.get('Left leg 2')
+
+ leg.tail = knee.head
+ knee.tail = ankle.head
+
+ if 'Right leg' in armature.data.edit_bones:
+ if 'Right knee' in armature.data.edit_bones:
+ if 'Right ankle' in armature.data.edit_bones:
+ leg = armature.data.edit_bones.get('Right leg')
+ knee = armature.data.edit_bones.get('Right knee')
+ ankle = armature.data.edit_bones.get('Right ankle')
+
+ if 'Right leg 2' in armature.data.edit_bones:
+ leg = armature.data.edit_bones.get('Right leg 2')
+
+ leg.tail = knee.head
+ knee.tail = ankle.head
+
+
+dpi_scale = 3
+error = []
+override = False
+
+
+def show_error(scale, error_list, override_header=False):
+ global override, dpi_scale, error
+ override = override_header
+ dpi_scale = scale
+
+ if type(error_list) is str:
+ error_list = error_list.split('\n')
+
+ error = error_list
+
+ header = 'error'
+ if override:
+ header = error_list[0]
+
+ print('')
+ print('Report: Error')
+ for line in error:
+ print(' ' + line)
+
+
+def remove_doubles(mesh, threshold, save_shapes=True):
+ if not mesh:
+ return 0
+
+ # If the mesh has no shapekeys, don't remove doubles
+ if not has_shapekeys(mesh) or len(mesh.data.shape_keys.key_blocks) == 1:
+ return 0
+
+ pre_tris = len(mesh.data.polygons)
+
+ set_active(mesh)
+ switch('EDIT')
+ bpy.ops.mesh.select_mode(type="VERT")
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ if save_shapes and has_shapekeys(mesh):
+ switch('OBJECT')
+ for kb in mesh.data.shape_keys.key_blocks:
+ i = 0
+ for v0, v1 in zip(kb.relative_key.data, kb.data):
+ if v0.co != v1.co:
+ mesh.data.vertices[i].select = True
+ i += 1
+ switch('EDIT')
+ bpy.ops.mesh.select_all(action='INVERT')
+ else:
+ bpy.ops.mesh.select_all(action='SELECT')
+
+ bpy.ops.mesh.remove_doubles(threshold=threshold)
+ bpy.ops.mesh.select_all(action='DESELECT')
+ switch('OBJECT')
+
+ 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
+ z_cord = 2
+ fbx = False
+ # armature = get_armature()
+ #
+ # for index, bone in enumerate(armature.pose.bones):
+ # if 'Head' in bone.name:
+ # #if index == 5:
+ # bone_pos = bone.matrix
+ # print(bone_pos)
+ # world_pos = armature.matrix_world * bone.matrix
+ # print(world_pos)
+ # print(bone_pos[0][0], world_pos[0][0])
+ # if round(abs(bone_pos[0][0]), 4) != round(abs(world_pos[0][0]), 4):
+ # z_cord = 1
+ # y_cord = 2
+ # fbx = True
+ # break
+
+ return x_cord, y_cord, z_cord, fbx
+
+
+def clean_material_names(mesh):
+ for j, mat in enumerate(mesh.material_slots):
+ if mat.name.endswith('.001'):
+ mesh.active_material_index = j
+ mesh.active_material.name = mat.name[:-4]
+ if mat.name.endswith(('. 001', ' .001')):
+ mesh.active_material_index = j
+ mesh.active_material.name = mat.name[:-5]
+
+
+def mix_weights(mesh, vg_from, vg_to, mix_strength=1.0, mix_mode='ADD', delete_old_vg=True):
+ mesh.active_shape_key_index = 0
+ mod = mesh.modifiers.new("VertexWeightMix", 'VERTEX_WEIGHT_MIX')
+ mod.vertex_group_a = vg_to
+ mod.vertex_group_b = vg_from
+ mod.mix_mode = mix_mode
+ mod.mix_set = 'B'
+ mod.mask_constant = mix_strength
+ 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.90 which causes random weights to be stuck after being merged
+
+
+def get_user_preferences():
+ return bpy.context.user_preferences if hasattr(bpy.context, 'user_preferences') else bpy.context.preferences
+
+
+def has_shapekeys(mesh):
+ if not hasattr(mesh.data, 'shape_keys'):
+ return False
+ return hasattr(mesh.data.shape_keys, 'key_blocks')
+
+
+def matmul(a, b):
+ if version_2_79_or_older():
+ return a * b
+ return a @ b
+
+
+def ui_refresh():
+ # A way to refresh the ui
+ refreshed = False
+ while not refreshed:
+ if hasattr(bpy.data, 'window_managers'):
+ for windowManager in bpy.data.window_managers:
+ for window in windowManager.windows:
+ for area in window.screen.areas:
+ area.tag_redraw()
+ refreshed = True
+ # print('Refreshed UI')
+ else:
+ time.sleep(0.5)
+
+
+def fix_zero_length_bones(armature, x_cord, y_cord, z_cord):
+ pre_mode = armature.mode
+ set_active(armature)
+ switch('EDIT')
+
+ for bone in armature.data.edit_bones:
+ if round(bone.head[x_cord], 4) == round(bone.tail[x_cord], 4) \
+ and round(bone.head[y_cord], 4) == round(bone.tail[y_cord], 4) \
+ and round(bone.head[z_cord], 4) == round(bone.tail[z_cord], 4):
+ bone.tail[z_cord] += 0.1
+
+ switch(pre_mode)
+
+
+def fix_bone_orientations(armature):
+ # Connect all bones with their children if they have exactly one
+ for bone in armature.data.edit_bones:
+ if len(bone.children) == 1 and bone.name not in ['LeftEye', 'RightEye', 'Head', 'Hips']:
+ p1 = bone.head
+ p2 = bone.children[0].head
+ dist = ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2) ** (1/2)
+
+ # Only connect them if the other bone is a certain distance away, otherwise blender will delete them
+ if dist > 0.005:
+ bone.tail = bone.children[0].head
+ if bone.parent:
+ if len(bone.parent.children) == 1: # if the bone's parent bone only has one child, connect the bones (Don't connect them all because that would mess up hand/finger bones)
+ bone.use_connect = True
+
+
+def update_material_list(self=None, context=None):
+ try:
+ if hasattr(bpy.context.scene, 'kkbp_ob_data') and bpy.context.scene.kkbp_ob_data:
+ bpy.ops.kkbp.refresh_ob_data()
+ except AttributeError:
+ print('Material Combiner not found')
+
+
+def unify_materials():
+ textures = []
+
+ for ob in get_objects():
+ if ob.type == "MESH":
+ for mat_slot in ob.material_slots:
+ if mat_slot.material:
+ mat_slot.material.blend_method = 'HASHED'
+ # mat_slot.material.blend_method = 'BLEND' # Use this for transparent textures only
+ print('MAT: ', mat_slot.material.name)
+ if mat_slot.material.node_tree:
+ nodes = mat_slot.material.node_tree.nodes
+ image = None
+ for node in nodes:
+ # print(' ' + node.name + ', ' + node.type + ', ' + node.label)
+ if node.type == 'TEX_IMAGE' and 'toon' not in node.name and 'sphere' not in node.name:
+ image = node.image
+ # textures.append(node.image.name)
+ mat_slot.material.node_tree.nodes.remove(node)
+
+ # Create Image node
+ node_texture = nodes.new(type='ShaderNodeTexImage')
+ node_texture.location = 0, 0
+ node_texture.image = image
+ node_texture.label = 'Cats Texture'
+
+ # Create Principled BSDF node
+ node_prinipled = nodes.new(type='ShaderNodeBsdfPrincipled')
+ node_prinipled.location = 300, -220
+ node_prinipled.label = 'Cats Emission'
+ node_prinipled.inputs['Specular'].default_value = 0
+ node_prinipled.inputs['Roughness'].default_value = 0
+ node_prinipled.inputs['Sheen Tint'].default_value = 0
+ node_prinipled.inputs['Clearcoat Roughness'].default_value = 0
+ node_prinipled.inputs['IOR'].default_value = 0
+
+ # Create Transparency BSDF node
+ node_transparent = nodes.new(type='ShaderNodeBsdfTransparent')
+ node_transparent.location = 325, -100
+ node_transparent.label = 'Cats Transparency'
+
+ # Create Mix Shader node
+ node_mix = nodes.new(type='ShaderNodeMixShader')
+ node_mix.location = 600, 0
+ node_mix.label = 'Cats Mix'
+
+ # Create Output node
+ node_output = nodes.new(type='ShaderNodeOutputMaterial')
+ node_output.location = 800, 0
+ node_output.label = 'Cats Output'
+
+ # Create 2nd Output node
+ node_output2 = nodes.new(type='ShaderNodeOutputMaterial')
+ node_output2.location = 800, -200
+ node_output2.label = 'Cats Export'
+
+ # Link nodes together
+ mat_slot.material.node_tree.links.new(node_texture.outputs['Color'], node_prinipled.inputs['Base Color'])
+ mat_slot.material.node_tree.links.new(node_texture.outputs['Alpha'], node_mix.inputs['Fac'])
+
+ mat_slot.material.node_tree.links.new(node_prinipled.outputs['BSDF'], node_mix.inputs[2])
+ mat_slot.material.node_tree.links.new(node_transparent.outputs['BSDF'], node_mix.inputs[1])
+
+ mat_slot.material.node_tree.links.new(node_mix.outputs['Shader'], node_output.inputs['Surface'])
+
+ mat_slot.material.node_tree.links.new(node_prinipled.outputs['BSDF'], node_output2.inputs['Surface'])
+
+ # break
+
+ print(textures, len(textures))
+ return {'FINISHED'}
+
+
+def add_principled_shader(mesh):
+ # This adds a principled shader and material output node in order for
+ # Unity to automatically detect exported materials
+ principled_shader_pos = (501, -500)
+ output_shader_pos = (801, -500)
+ mmd_texture_bake_pos = (1101, -500)
+ principled_shader_label = 'Cats Export Shader'
+ output_shader_label = 'Cats Export'
+
+ for mat_slot in mesh.material_slots:
+ if mat_slot.material and mat_slot.material.node_tree:
+ nodes = mat_slot.material.node_tree.nodes
+ node_image = None
+ node_image_count = 0
+ node_mmd_shader = None
+ needsmmdcolor = False
+
+ # Check if the new nodes should be added and to which image node they should be attached to
+ for node in nodes:
+ # Cancel if the cats nodes are already found
+ if node.type == 'BSDF_PRINCIPLED' and node.label == principled_shader_label:
+ node_image = None
+ break
+ elif node.type == 'OUTPUT_MATERIAL' and node.label == output_shader_label:
+ node_image = None
+ break
+ elif node.type == 'OUTPUT_MATERIAL': #So that blender doesn't get confused on which to output
+ nodes.remove(node)
+ continue
+ if node.name == "mmd_shader":
+ node_mmd_shader = node
+ needsmmdcolor = True
+ continue
+
+ # Skip if this node is not an image node
+ if node.type != 'TEX_IMAGE':
+ continue
+ node_image_count += 1
+
+ # If an mmd_texture is found, link it to the principled shader later
+ if node.name == 'mmd_base_tex' or node.label == 'MainTexture':
+ node_image = node
+ node_image_count = 0
+ break
+
+ # This is an image node, so link it to the principled shader later
+ node_image = node
+ #this material doesn't have a texture and doesn't have a MMD AO+Diffuse so skip
+ if (not node_image or node_image_count > 1) and not needsmmdcolor:
+ continue
+ elif needsmmdcolor and node_mmd_shader: #this needs to implement mmd color and has a shader node
+ #bake AO and Diffuse color into pixels for MMD texture. if texture exists, multiply over
+ #Thank this guy for pixel manipulation: https://blender.stackexchange.com/a/652
+
+
+ basecolor = [x*0.6 for x in node_mmd_shader.inputs[1].default_value[:]] #multply color of diffuse by .6 which is MMD's addition factor
+ for rgba,num in enumerate(basecolor):
+ basecolor[rgba] = max(0,min(1,basecolor[rgba]+node_mmd_shader.inputs[0].default_value[rgba])) #add AO to diffuse and clamp between 0-1 for each channel
+
+ if not node_image:
+ node_image = mat_slot.material.node_tree.nodes.new(type="ShaderNodeTexImage")
+ node_image.location = mmd_texture_bake_pos
+ node_image.label = "Mmd Base Tex"
+ node_image.name = "mmd_base_tex"
+ node_image.image = bpy.data.images.new("MMDCatsBaked", width=8, height=8, alpha=True)
+
+ #make pixels using AO color
+
+
+ #assign to image so it's baked
+ node_image.image.generated_color = basecolor
+ node_image.image.filepath = bpy.path.abspath("//"+node_image.image.name+".png")
+ node_image.image.file_format = 'PNG'
+ if bpy.data.is_saved:
+ node_image.image.save()
+ elif node_image:
+
+ #multiply color on top of default color.
+ pixels = np.array(node_image.image.pixels[:])
+
+ multiply_image = np.tile(np.array(basecolor),int(len(pixels)/4))
+
+ new_pixels = pixels*multiply_image
+
+ #create new image as to not touch old one
+ node_image.image = bpy.data.images.new(node_image.image.name+"MMDCatsBaked", width=node_image.image.size[0], height=node_image.image.size[1], alpha=True)
+ node_image.image.filepath = bpy.path.abspath("//"+node_image.image.name+".png")
+ node_image.image.file_format = 'PNG'
+
+ node_image.image.pixels = new_pixels
+ if bpy.data.is_saved:
+ node_image.image.save()
+
+
+ # Create Principled BSDF node
+ node_prinipled = nodes.new(type='ShaderNodeBsdfPrincipled')
+ node_prinipled.label = 'Cats Export Shader'
+ node_prinipled.location = principled_shader_pos
+ node_prinipled.inputs['Specular'].default_value = 0
+ node_prinipled.inputs['Roughness'].default_value = 0
+ node_prinipled.inputs['Sheen Tint'].default_value = 0
+ node_prinipled.inputs['Clearcoat Roughness'].default_value = 0
+ node_prinipled.inputs['IOR'].default_value = 0
+
+ # Create Output node for correct image exports
+ node_output = nodes.new(type='ShaderNodeOutputMaterial')
+ node_output.label = 'Cats Export'
+ node_output.location = output_shader_pos
+
+ # Link nodes together
+ mat_slot.material.node_tree.links.new(node_image.outputs['Color'], node_prinipled.inputs['Base Color'])
+ mat_slot.material.node_tree.links.new(node_prinipled.outputs['BSDF'], node_output.inputs['Surface'])
+
+
+def remove_toon_shader(mesh):
+ for mat_slot in mesh.material_slots:
+ if mat_slot.material and mat_slot.material.node_tree:
+ nodes = mat_slot.material.node_tree.nodes
+ for node in nodes:
+ if node.name == 'mmd_toon_tex':
+ print('Toon tex removed from material', mat_slot.material.name)
+ nodes.remove(node)
+ # if not node.image or not node.image.filepath:
+ # print('Toon tex removed: Empty, from material', mat_slot.material.name)
+ # nodes.remove(node)
+ # continue
+ #
+ # image_filepath = bpy.path.abspath(node.image.filepath)
+ # if not os.path.isfile(image_filepath):
+ # print('Toon tex removed:', node.image.name, 'from material', mat_slot.material.name)
+ # nodes.remove(node)
+
+
+def fix_mmd_shader(mesh):
+ for mat_slot in mesh.material_slots:
+ if mat_slot.material and mat_slot.material.node_tree:
+ nodes = mat_slot.material.node_tree.nodes
+ for node in nodes:
+ if node.name == 'mmd_shader':
+ node.inputs['Reflect'].default_value = 1
+
+
+def fix_vrm_shader(mesh):
+ for mat_slot in mesh.material_slots:
+ if mat_slot.material and mat_slot.material.node_tree:
+ is_vrm_mat = False
+ nodes = mat_slot.material.node_tree.nodes
+ for node in nodes:
+ if hasattr(node, 'node_tree') and 'MToon_unversioned' in node.node_tree.name:
+ node.location[0] = 200
+ node.inputs['ReceiveShadow_Texture_alpha'].default_value = -10000
+ node.inputs['ShadeTexture'].default_value = (1.0, 1.0, 1.0, 1.0)
+ node.inputs['Emission_Texture'].default_value = (0.0, 0.0, 0.0, 0.0)
+ node.inputs['SphereAddTexture'].default_value = (0.0, 0.0, 0.0, 0.0)
+
+ # Support typo in old vrm importer
+ node_input = node.inputs.get('NomalmapTexture')
+ 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:
+ continue
+
+ nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture']
+ if 'HAIR' in mat_slot.material.name:
+ nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture', 'SphereAddTexture']
+
+ for node in nodes:
+ # Delete all unneccessary nodes
+ if 'RGB' in node.name \
+ or 'Value' in node.name \
+ or 'Image Texture' in node.name \
+ or 'UV Map' in node.name \
+ or 'Mapping' in node.name:
+ if node.label not in nodes_to_keep:
+ for output in node.outputs:
+ for link in output.links:
+ mat_slot.material.node_tree.links.remove(link)
+ continue
+
+ # if hasattr(node, 'node_tree') and 'matcap_vector' in node.node_tree.name:
+ # for output in node.outputs:
+ # for link in output.links:
+ # mat_slot.material.node_tree.links.remove(link)
+ # continue
+
+
+def fix_twist_bones(mesh, bones_to_delete):
+ # This will fix MMD twist bones
+
+ for bone_type in ['Hand', 'Arm']:
+ for suffix in ['L', 'R']:
+ prefix = 'Left' if suffix == 'L' else 'Right'
+ bone_parent_name = prefix + ' ' + ('elbow' if bone_type == 'Hand' else 'arm')
+
+ vg_twist = mesh.vertex_groups.get(bone_type + 'Twist_' + suffix)
+ vg_parent = mesh.vertex_groups.get(bone_parent_name)
+
+ if not vg_twist:
+ print('1. no ' + bone_type + 'Twist_' + suffix)
+ continue
+ if not vg_parent:
+ print('2. no ' + bone_parent_name)
+ vg_parent = mesh.vertex_groups.new(name=bone_parent_name)
+
+ vg_twist1 = mesh.vertex_groups.get(bone_type + 'Twist1_' + suffix)
+ vg_twist2 = mesh.vertex_groups.get(bone_type + 'Twist2_' + suffix)
+ vg_twist3 = mesh.vertex_groups.get(bone_type + 'Twist3_' + suffix)
+
+ mix_weights(mesh, vg_twist.name, vg_parent.name, mix_strength=0.2, delete_old_vg=False)
+ mix_weights(mesh, vg_twist.name, vg_twist.name, mix_strength=0.2, mix_mode='SUB', delete_old_vg=False)
+
+ if vg_twist1:
+ twistname = bone_type + 'Twist1_' + suffix
+ bones_to_delete.append(twistname)
+ mix_weights(mesh, twistname, vg_twist.name, mix_strength=0.25, delete_old_vg=False)
+ mix_weights(mesh, twistname, vg_parent.name, mix_strength=0.75) #if we are adding to bones to delete, then don't delete prematurely please (added don't delete argument) - @989onan
+
+ if vg_twist2:
+ twistname = bone_type + 'Twist2_' + suffix
+ bones_to_delete.append(twistname)
+ mix_weights(mesh, twistname, vg_twist.name, mix_strength=0.5, delete_old_vg=False)
+ mix_weights(mesh, twistname, vg_parent.name, mix_strength=0.5, delete_old_vg=False) #if we are adding to bones to delete, then don't delete prematurely please (added don't delete argument) - @989onan
+
+ if vg_twist3:
+ twistname = bone_type + 'Twist3_' + suffix
+ bones_to_delete.append(twistname)
+ mix_weights(mesh, twistname, vg_twist.name, mix_strength=0.75, delete_old_vg=False)
+ mix_weights(mesh, twistname, vg_parent.name, mix_strength=0.25, delete_old_vg=False) #if we are adding to bones to delete, then don't delete prematurely please. (added don't delete argument) - @989onan
+
+
+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)
+ if bone_twist:
+ bone_twist.name = 'z' + bone_twist.name
+
+
+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,
+ mmd_tool.MMDMorphToolsPanel,
+ mmd_tool.MMDRigidbodySelectorPanel,
+ mmd_tool.MMDJointSelectorPanel,
+ mmd_util_tools.MMDMaterialSorter,
+ mmd_util_tools.MMDMeshSorter,
+ mmd_util_tools.MMDBoneOrder,
+ ]
+ mmd_cls_shading = [
+ mmd_view_prop.MMDViewPanel,
+ mmd_view_prop.MMDSDEFPanel,
+ ]
+
+ 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 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)
+
+
+
+"""
+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()
+
+
+""" === THIS CODE COULD BE USEFUL === """
+
+# def addvertex(meshname, shapekey_name):
+# mesh = get_objects()[meshname].data
+# bm = bmesh.new()
+# bm.from_mesh(mesh)
+# bm.verts.ensure_lookup_table()
+#
+# print(" ")
+# if shapekey_name in bm.verts.layers.shape.keys():
+# val = bm.verts.layers.shape.get(shapekey_name)
+# print("%s = %s" % (shapekey_name, val))
+# sk = mesh.shape_keys.key_blocks[shapekey_name]
+# print("v=%f, f=%f" % (sk.value, sk.frame))
+# for i in range(len(bm.verts)):
+# v = bm.verts[i]
+# delta = v[val] - v.co
+# if (delta.length > 0):
+# print("v[%d]+%s" % (i, delta))
+#
+# print(" ")
+
+# === THIS CODE COULD BE USEFUL ===
+
+# Check which shape keys will be deleted on export by Blender
+# def checkshapekeys():
+# for ob in get_objects():
+# if ob.type == 'MESH':
+# mesh = ob
+# bm = bmesh.new()
+# bm.from_mesh(mesh.data)
+# bm.verts.ensure_lookup_table()
+#
+# deleted_shapes = []
+# for key in bm.verts.layers.shape.keys():
+# if key == 'Basis':
+# continue
+# val = bm.verts.layers.shape.get(key)
+# delete = True
+# for vert in bm.verts:
+# delta = vert[val] - vert.co
+# if delta.length > 0:
+# delete = False
+# break
+# if delete:
+# deleted_shapes.append(key)
+#
+# return deleted_shapes
+
+# # Repair vrc shape keys old
+# def repair_shapekeys():
+# for ob in get_objects():
+# if ob.type == 'MESH':
+# mesh = ob
+# bm = bmesh.new()
+# bm.from_mesh(mesh.data)
+# bm.verts.ensure_lookup_table()
+#
+# for key in bm.verts.layers.shape.keys():
+# if not key.startswith('vrc'):
+# continue
+#
+# value = bm.verts.layers.shape.get(key)
+# for vert in bm.verts:
+# shapekey = vert
+# shapekey_coords = mesh.matrix_world * shapekey[value]
+# shapekey_coords[2] -= 0.00001
+# shapekey[value] = mesh.matrix_world.inverted() * shapekey_coords
+# break
+#
+# bm.to_mesh(mesh.data)
+
+# === THIS CODE COULD BE USEFUL ===
diff --git a/extras/createanimationlibrary.py b/extras/createanimationlibrary.py
new file mode 100644
index 0000000..44ae4c5
--- /dev/null
+++ b/extras/createanimationlibrary.py
@@ -0,0 +1,451 @@
+#This will take a folder full of fbx animation files ripped from the game and create a blender animation library using the current model (for thumbnails)
+
+# Usage instructions:
+# Export koikatsu fbx animation files using https://www.youtube.com/watch?v=XFt12n7ByBI&t=465s
+# You can also export multiple animations at the same time by shift clicking them in the window
+# put all the fbx files you exported into one folder
+# any subfolder names in that folder will be used to tag them
+
+# import a character with a rigify armature
+# make sure you've got a camera and light pointed at the model (put it at an angle for better thumbnails)
+# make sure you've disabled the armature visibility (in the 3d view settings on the top bar, not in the outliner)
+# Suggested X and Y axis lines: ON
+# Suggested sun location and rotation [-1 m, 0 m, 0 m] , [78°, -40°, 27°]
+# Suggested world color [0,0,0]
+# Suggested camera location + rotation [-0.7 m, -1.9 m, 0.8 m] , [83.6°, -0°, -20°]
+# Suggested camera resolution [150 x 150]
+# Suggested Tpose model location + rotation [0,0,0], [0°,0°,0°]
+# Use https://www.youtube.com/watch?v=Nyxeb48mUfs&t=713s to setup the Rokoko retargeting addon with a random koikatsu fbx animation file from your folder
+# (make sure torso, torso tweak, arm fk, fingers detail, leg fk Rigify layers are visible)
+# (make sure ALL rokoko remapping / naming schemes are already setup using the random file)
+# (Alternatively, you can import the included "Rokoko custom target naming" .json file included in the /extras/animationlibrary/ directory to set it automatically)
+# (it just needs to be done once, then you can hit the save button in the rokoko retargeting panel to use it in any other file)
+# delete the random fbx animation you imported (setup is complete at this point)
+# save the file
+
+# enter rendered view
+# Run the script by pressing the button in the panel
+# It will take about two hours on a good CPU to generate the library (three minutes per 11 poses, or 2 hours for ~440 poses / animations which adds up to about 1.2gb of fbx files)
+# You can also do it in small batches and rotate out the already imported fbx files for new ones (change your filename after each batch or the previous batches will be overwritten)
+# Or you can put a large list and kill blender when you want to pause the import process (it will automatically save a new file for every subfolder, and pick up from the previous subfolder)
+
+# when finished, open every asset file and run the script on the bottom of this file to reduce the filesize
+# (You can also copy paste the script into a text editor before running the script so you can have it automatically loaded on every asset file that will be produced)
+# also recall that blender resets the fps to the framerate of the fbx file when importing
+
+import bpy, os, time, mathutils, json, pathlib
+from bpy.props import StringProperty
+from .. import common as c
+from ..interface.dictionary_en import t
+def main(folder):
+
+ #stop if the file was not saved
+ if not bpy.data.filepath:
+ raise('File must be saved first!')
+
+ kkbp_character = False
+ #delete the armature before starting to reduce console spam
+ if c.get_body() and c.get_rig():
+ kkbp_character = True
+ if bpy.data.objects.get('Armature') and kkbp_character:
+ n = bpy.data.objects['Armature'].data.name
+ bpy.data.objects.remove(bpy.data.objects['Armature'])
+ bpy.data.armatures.remove(bpy.data.armatures[n])
+ c.toggle_console() #open console for some kind of progression
+ start = time.time()
+ rigify_armature = bpy.data.objects['RIG-Armature']
+ bpy.context.view_layer.objects.active=rigify_armature
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bpy.ops.rsl.clear_custom_bones()
+
+ if kkbp_character:
+ #disable IKs
+ rigify_armature.pose.bones["Right arm_parent"]["IK_FK"] = 1
+ rigify_armature.pose.bones["Left arm_parent"]["IK_FK"] = 1
+ rigify_armature.pose.bones["Right leg_parent"]["IK_FK"] = 1
+ rigify_armature.pose.bones["Left leg_parent"]["IK_FK"] = 1
+
+ #disable head follow
+ rigify_armature.pose.bones['torso']['neck_follow'] = 1.0
+ rigify_armature.pose.bones['torso']['head_follow'] = 1.0
+
+ '''#reset all bone transforms
+ for pb in bpy.context.selected_pose_bones_from_active_object:
+ pb.matrix_basis = mathutils.Matrix() # == Matrix.Identity(4)
+ '''
+
+ #import the fbx files
+ fbx_files = []
+ for subdir, dirs, files in os.walk(folder):
+ for file in files:
+ if '.fbx' in file:
+ fbx_files.append((subdir, file, os.path.join(subdir, file)))
+
+ actions_from_set = []
+ last_category = ''
+ first_import = True
+ original_file_name = bpy.data.filepath
+
+ for file in fbx_files:
+ category = os.path.basename(file[0])
+ original_file_number = os.path.basename(os.path.dirname(file[0]))
+ filename = file[1]
+
+ #skip this file if the animation is for larger characters, or is a partial animation
+ skip = False
+ no_use = ['L_', 'M_', 'ML_', 'SM_', 'Te_', 'Yubi_', 'Sita_', 'Denma_', 'Vibe_']
+ for item in no_use:
+ if filename.startswith(item):
+ skip = True
+ if skip:
+ continue
+
+ #also skip if this a file for this category already exists
+ if os.path.exists(bpy.data.filepath.replace('.blend', ' ' + category + ' ' + str(original_file_number) + '.blend')):
+ continue
+
+ #create a new file for every category because the asset browser seems to behave better with multiple small files vs one large file
+ if last_category != category:
+ #save, then create new file
+ if last_category: #skip save for first file
+ bpy.ops.wm.save_as_mainfile(filepath = bpy.data.filepath)
+ bpy.ops.wm.save_as_mainfile(filepath = original_file_name.replace('.blend', ' ' + category + ' ' + str(original_file_number) + '.blend'))
+ last_category = category
+
+ #new file, so delete the previous imported asset animations
+ for act in actions_from_set:
+ bpy.data.actions.remove(bpy.data.actions[act])
+ actions_from_set = []
+
+ #load a script into the layout tab
+ if bpy.data.screens.get('Layout'):
+ for area in bpy.data.screens['Layout'].areas:
+ if area.type == 'DOPESHEET_EDITOR':
+ area.ui_type = 'TEXT_EDITOR'
+ area.spaces[0].text = bpy.data.texts.new(name='Reduce filesize, save and close')
+ area.spaces[0].text.write(
+'''#this script reduces the filesize of the library file to just the action data
+import bpy, time
+for obj in bpy.data.objects:
+ bpy.data.objects.remove(obj)
+for arm in bpy.data.armatures:
+ bpy.data.armatures.remove(arm)
+for mesh in bpy.data.meshes:
+ bpy.data.meshes.remove(mesh)
+for mat in bpy.data.materials:
+ bpy.data.materials.remove(mat)
+for img in bpy.data.images:
+ bpy.data.images.remove(img)
+for node in bpy.data.node_groups:
+ bpy.data.node_groups.remove(node)
+bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
+time.sleep(3)
+bpy.ops.wm.quit_blender()
+
+''')
+ bpy.context.scene.frame_end = 40
+ bpy.ops.import_scene.fbx(filepath=str(file[2]), global_scale=96)
+
+ imported_armature = bpy.context.object
+ '''bpy.ops.object.mode_set(mode = 'EDIT')
+ bpy.ops.armature.select_all(action='DESELECT')
+ for bone in imported_armature.data.edit_bones:
+ if (
+ 'k_f_' in bone.name
+ or ('cf_hit_' in bone.name)
+ or ('a_n_' in bone.name)
+ ):
+ bone.select_head, bone.select_tail, bone.select = True, True, True
+ bpy.ops.object.mode_set(mode = 'OBJECT')'''
+
+ #rokoko remap list still shows deleted bones for some fucking reason
+ #clear all animation data from source armature, then re-key all bones for just this frame
+ #duplicate source armature then delete the original source armature
+ #rokoko list now shows correct bone list on source armature.002
+
+ #save the source action name so I can delete it later
+ current_action_name = imported_armature.animation_data.action.name
+
+ #setup rokoko remapping
+ my_areas = bpy.context.workspace.screens[0].areas
+ for area in my_areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.show_object_viewport_armature = True
+ bpy.context.scene.rsl_retargeting_armature_source = imported_armature
+ bpy.context.scene.rsl_retargeting_armature_target = rigify_armature
+ bpy.ops.rsl.build_bone_list()
+ #setup all remapping stuff if this is the first imported animation
+ if first_import and kkbp_character:
+ first_import = False
+ script_dir=pathlib.Path(__file__).parent
+ remap_file = os.path.join(script_dir, "animationlibrary", "Rokoko retargeting list (koikatsu fbx to KKBP Rigify).json")
+ json_file = open(remap_file)
+ data = json.load(json_file)
+ for row in bpy.context.scene.rsl_retargeting_bone_list:
+ row.bone_name_target = "" #clear all bones
+ for bone in data['bones']:
+ for row in bpy.context.scene.rsl_retargeting_bone_list:
+ if row.bone_name_source.lower() == data['bones'][bone][0].lower():
+ row.bone_name_target = data['bones'][bone][1]
+ bpy.ops.rsl.retarget_animation()
+
+ # then remove the fcurves of the retargeted bones that were not present in the retargeting list.
+ # A lot of popping and fluctuation is present if these aren't removed
+ retargeting_list = [
+ 'torso',
+ 'Spine_fk',
+ 'Chest_fk',
+ 'Upper Chest_fk',
+ 'Left arm_fk',
+ 'Left elbow_fk',
+ 'Left wrist_fk',
+ 'IndexFinger1_L',
+ 'IndexFinger2_L',
+ 'IndexFinger3_L',
+ 'LittleFinger1_L',
+ 'LittleFinger2_L',
+ 'LittleFinger3_L',
+ 'MiddleFinger1_L',
+ 'MiddleFinger2_L',
+ 'MiddleFinger3_L',
+ 'RingFinger1_L',
+ 'RingFinger2_L',
+ 'RingFinger3_L',
+ 'Thumb0_L',
+ 'Thumb1_L',
+ 'Thumb2_L',
+ 'Right arm_fk',
+ 'Right elbow_fk',
+ 'Right wrist_fk',
+ 'IndexFinger1_R',
+ 'IndexFinger2_R',
+ 'IndexFinger3_R',
+ 'LittleFinger1_R',
+ 'LittleFinger2_R',
+ 'LittleFinger3_R',
+ 'MiddleFinger1_R',
+ 'MiddleFinger2_R',
+ 'MiddleFinger3_R',
+ 'RingFinger1_R',
+ 'RingFinger2_R',
+ 'RingFinger3_R',
+ 'Thumb0_R',
+ 'Thumb1_R',
+ 'Thumb2_R',
+ 'neck',
+ 'head',
+ 'Hips_fk',
+ 'Left leg_fk',
+ 'Left knee_fk',
+ 'Left ankle_fk',
+ 'Left toe_fk',
+ 'Right leg_fk',
+ 'Right knee_fk',
+ 'Right ankle_fk',
+ 'Right toe_fk',
+ 'Left shoulder',
+ 'cf_j_waist02',
+ 'Right shoulder',
+ 'cf_j_kokan',
+ #'cf_j_ana',
+ 'Breasts handle',
+ 'Left Breast handle',
+ 'cf_j_bust02_l',
+ 'cf_j_bust03_l',
+ 'cf_j_bnip02root_l',
+ 'cf_j_bnip02_l',
+ 'Right Breast handle',
+ 'cf_j_bust02_r',
+ 'cf_j_bust03_r',
+ 'cf_j_bnip02root_r',
+ 'cf_j_bnip02_r',
+ #'Left Buttock handle',
+ #'Right Buttock handle',
+ ]
+ if kkbp_character:
+ for index, action in enumerate(rigify_armature.animation_data.action.fcurves):
+ bone_name = action.data_path[action.data_path.find('[')+2:action.data_path.find('].')-1]
+ print('{} {} / {}'.format(action.data_path, index, len(rigify_armature.animation_data.action.fcurves)))
+ if bone_name not in retargeting_list:
+ rigify_armature.animation_data.action.fcurves.remove(action) #remove keyframes
+ if rigify_armature.pose.bones.get(bone_name):
+ rigify_armature.pose.bones[bone_name].matrix_basis = mathutils.Matrix() #reset rotation matrix
+
+ if bpy.context.scene.kkbp.animation_library_scale:
+ #also scale the arms on the y axis by 5% because it makes the animation more accurate to the in-game one for some poses
+ already_got_both_arms = False
+ if kkbp_character:
+ for y_scale_curve in [curve for curve in rigify_armature.animation_data.action.fcurves if (curve.array_index == 1) and ('scale' in curve.data_path)]:
+ bone_name = y_scale_curve.data_path[y_scale_curve.data_path.find('[')+2:y_scale_curve.data_path.find('].')-1]
+ if bone_name in ['Left arm_fk', 'Right arm_fk']:
+ keyframe_limits = [int(y_scale_curve.keyframe_points[0].co.x), int(y_scale_curve.keyframe_points[-1].co.x)]
+ for frame_number in range(keyframe_limits[0], keyframe_limits[1]+1):
+ original_scale = y_scale_curve.evaluate(frame_number)
+ y_scale_curve.keyframe_points.insert(frame_number, original_scale * 1.05)
+ if already_got_both_arms:
+ break #skip the rest
+ already_got_both_arms = True
+
+ #select all rigify armature bones and create pose asset
+ bpy.ops.object.select_all(action='DESELECT')
+ rigify_armature.select_set(True)
+ bpy.context.view_layer.objects.active=rigify_armature
+ bpy.ops.object.mode_set(mode = 'POSE')
+ bpy.ops.pose.select_all(action='SELECT')
+ bpy.data.workspaces["Layout"].asset_library_reference = 'LOCAL'
+ #translate name
+ name = filename.replace('-p_cf_body_bone-0.fbx', '')
+ name = name.replace('-p_cf_body_bone-1.fbx', '')
+ translation_dict_normal = {
+ 'isu_':'Seated_',
+ 'suwari':'Sitting',
+ 'syagami':'Squat',
+ 'sadou':'Tea',
+ 'undou':'Excercise',
+ 'suiei':'Swimming',
+ 'tachi':'Standing',
+ 'manken':'Reading',
+ 'soine':'Laying2',
+ 'kasa':'Umbrella',
+ 'konbou':'Club',
+ 'aruki':'Walking',
+ 'haruki':'Running',
+ 'ne_0':'Sleep',
+ 'nugi':'Undressing',
+ }
+ translation_dict_h = {
+ '_A_Idle':'Crotch grope idle',
+ '_A_Loop':'Crotch grope',
+ '_A_Touch':'Crotch grope start',
+ '_K_Loop':'Kiss',
+ '_K_Touch':'Kiss start',
+ '_M_Idle':'Breast grope idle',
+ '_M_Loop':'Breast grope',
+ '_M_Touch':'Breast grope start',
+ '_MLoop1': 'Masturbate',
+ '_MLoop2': 'Masturbate2',
+ '_Orgasm-': 'Climax-',
+ '_Orgasm_A':'Climax end',
+ '_Orgasm_B':'Climax end',
+ '_Orgasm_Loop':'Climax',
+ '_Orgasm_Start':'Climax start',
+ '_S_Idle':'Butt grope idle',
+ '_S_Loop':'Butt grope',
+ '_S_Touch':'Butt grope start',
+ '_Back_Dislikes':'Embarrassed back',
+ '_Front_Dislikes':'Embarrassed front',
+
+ '_Oral_Idle_IN': 'Cum in mouth start',
+ '_Oral_Idle': 'Cum in mouth',
+ '_Stop_Idle': 'Idling stop',
+ '_InsertIdle': 'Insert idle',
+ '_Idle-': 'Idling-',
+ '_Insert-': 'Insert-',
+ '_M_IN_Start':'Climax inside start',
+ '_SF_IN_Start':'Climax inside start',
+ '_A_SS_IN_Start':'Climax inside start',
+ '_SS_IN_Start':'Climax inside start',
+ '_A_WF_IN_Start':'Climax inside start',
+ '_A_WS_IN_Start':'Climax inside start',
+ '_WF_IN_Start':'Climax inside start',
+ '_A_M_IN_Start':'Climax inside start',
+ '_WS_IN_Start':'Climax inside start',
+ '_IN_Start':'Climax inside start',
+ '_M_IN_Loop':'Climax inside',
+ '_SF_IN_Loop':'Climax inside',
+ '_SS_IN_Loop':'Climax inside',
+ '_WF_IN_Loop':'Climax inside',
+ '_WS_IN_Loop':'Climax inside',
+ '_IN_Loop':'Climax inside',
+ '_A_IN_A':'Climax inside end',
+ '_SS_IN_A':'Climax inside end',
+ '_A_WS_IN_A':'Climax inside end',
+ '_WS_IN_A':'Climax inside end',
+ '_IN_A':'Climax inside end',
+ '_Pull':'Pull out',
+ '_OUT_Start':'Climax outside start',
+ '_M_OUT_Loop':'Climax outside',
+ '_OUT_Loop':'Climax outside',
+ '_OUT_A':'Climax outside end',
+ '_OLoop': 'Loop',
+ '_SLoop':'Fast loop',
+ '_WLoop':'Slow loop',
+ '_Drink_IN':'Swallow cum start',
+ '_Drink_A':'Swallow cum end',
+ '_Drink':'Swallow cum',
+ '_Vomit_IN': 'Spit out cum start',
+ '_Vomit_A': 'Spit out cum end',
+ '_Vomit': 'Spit out cum',
+ }
+
+ for item in translation_dict_normal:
+ name = name.replace(item, translation_dict_normal[item])
+ h_dict_used = False
+ for item in translation_dict_h:
+ if item in name:
+ name = category + ' ' + name[1:] #add the description onto it because h animations don't have names, remove the S at the beginning of the name because all of them start with that
+ name = name.replace(item, translation_dict_h[item])
+ h_dict_used = True
+ break
+
+ action = rigify_armature.animation_data.action
+ action.asset_mark()
+ rigify_armature.asset_mark()
+ action.use_fake_user = True
+ #render the first frame of the animation and set it as the preview
+ bpy.context.scene.render.filepath = os.path.join(folder, "preview.png")
+ #print(file[2])
+ my_areas = bpy.context.workspace.screens[0].areas
+ for area in my_areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.show_object_viewport_armature = False
+ bpy.ops.render.opengl(write_still = True)
+ with bpy.context.temp_override(id=action):
+ bpy.ops.ed.lib_id_load_custom_preview(filepath=os.path.join(folder, "preview.png"))
+ my_areas = bpy.context.workspace.screens[0].areas
+ for area in my_areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.show_object_viewport_armature = True
+ action.name = name
+ #bpy.ops.poselib.create_pose_asset(activate_new_action=True, pose_name = name) #for pose assets instead
+ action.asset_data.tags.new(filename)
+ action.asset_data.tags.new(category)
+ action.asset_data.tags.new(original_file_number)
+ if h_dict_used:
+ action.asset_data.tags.new('NSFW')
+ action.asset_data.description = filename
+ actions_from_set.append(action.name)
+
+ #delete the imported action, object and armature
+ bpy.data.actions.remove(bpy.data.actions[current_action_name])
+ imported_armature_armaturename = imported_armature.data.name
+ bpy.data.objects.remove(imported_armature)
+ bpy.data.armatures.remove(bpy.data.armatures[imported_armature_armaturename])
+ print(file)
+
+ print(str(time.time() - start))
+ bpy.ops.wm.save_as_mainfile(filepath = original_file_name.replace('.blend', ' ' + category + ' ' + str(original_file_number) + '.blend'))
+ c.toggle_console() #close console
+
+class anim_asset_lib(bpy.types.Operator):
+ bl_idname = "kkbp.createanimassetlib"
+ bl_label = "Create animation asset library"
+ bl_description = t('animation_library_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ directory : StringProperty(maxlen=1024, default='', subtype='FILE_PATH', options={'HIDDEN'})
+ filter_glob : StringProperty(default='', options={'HIDDEN'})
+ data = None
+ mats_uv = None
+ structure = None
+
+ def execute(self, context):
+ main(self.directory)
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
\ No newline at end of file
diff --git a/extras/createmapassetlibrary.py b/extras/createmapassetlibrary.py
new file mode 100644
index 0000000..a077023
--- /dev/null
+++ b/extras/createmapassetlibrary.py
@@ -0,0 +1,355 @@
+#This will take a folder full of map files ripped from the game and create a blender asset library from them
+
+# Usage instructions:
+# Export koikatsu fbx animation files using https://www.youtube.com/watch?v=PeryYTsAN6E
+# put all the map folders you exported into one folder
+# any subfolder names in that folder will be used to tag them
+# (i.e. /Desktop/all maps/map1/map1 fbx files + textures, then /Desktop/all maps/map2/map2 fbx files + textures, etc )
+
+# Run this button from the kkbp extras section of the panel
+# You can also do it in small batches and rotate out the already imported fbx files for new ones
+# Or you can put a large list and kill blender when you want to pause the import process (it will resume from where it left off next time you run the script)
+# remember to save the library file when it's done
+
+import bpy, os, time, pathlib
+from bpy.props import StringProperty
+from .importstudio import import_studio_objects
+from .. import common as c
+from ..importing.modifymaterial import modify_material
+from ..interface.dictionary_en import t
+
+def better_fbx_map_import(directory):
+ already_loaded_images = [image.name for image in bpy.data.images]
+ #import
+ path = pathlib.Path(directory).rglob('*')
+ obj_list = []
+ for item in path:
+ if '.fbx' in str(item):
+ obj_list.append(str(item))
+ for obj in obj_list:
+ bpy.ops.better_import.fbx(filepath = obj, my_scale = 1.0)
+
+ #if nothing was imported, skip
+ if len(bpy.data.objects) < 2:
+ return
+
+ #delete duplicate objects and sky mesh
+ objs_to_delete = [obj for obj in bpy.data.objects if '.001' in obj.name or '.002' in obj.name or '_koi_sky_' in obj.name]
+ for obj in objs_to_delete:
+ data_name = obj.data.name
+ bpy.data.objects.remove(obj)
+ bpy.data.meshes.remove(bpy.data.meshes[data_name])
+
+ #remove all unused slots
+ bpy.ops.object.select_all(action='SELECT')
+ bpy.context.view_layer.objects.active = bpy.data.objects[1]
+ bpy.ops.object.material_slot_remove_unused()
+ bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
+
+ #setup simple toon shader for all objects
+ for obj in [mesh for mesh in bpy.data.objects if mesh.type == 'MESH' and mesh.material_slots[0].material]:
+ #if the material already exists, use that, else create it
+ try:
+ obj.material_slots[0].material = bpy.data.materials['KK ' + obj.material_slots[0].material.name]
+ c.kklog('Material already exists: ' + obj.material_slots[0].material.name)
+ except:
+ try:
+ template = bpy.data.materials['KK Simple'].copy()
+ except:
+ script_dir=pathlib.Path(__file__).parent.parent
+ template_path=(script_dir / '../KK Shader V8.0.blend').resolve()
+ filepath = str(template_path)
+
+ innerpath = 'Material'
+ templateList = ['KK Simple']
+
+ for template in templateList:
+ bpy.ops.wm.append(
+ filepath=os.path.join(filepath, innerpath, template),
+ directory=os.path.join(filepath, innerpath),
+ filename=template,
+ set_fake=False
+ )
+ template = bpy.data.materials['KK Simple'].copy()
+ if obj.material_slots[0].material:
+ template.name = 'KK ' + obj.material_slots[0].material.name
+ new_node = template.node_tree.nodes['Gentex'].node_tree.copy()
+ template.node_tree.nodes['Gentex'].node_tree = new_node
+ new_node.name = template.name
+ main_image_link = [node for node in obj.material_slots[0].material.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'][0].inputs[0]
+ if main_image_link.is_linked:
+ main_image = main_image_link.links[0].from_node.image
+ #replace the dds image with a png version
+ new_path = main_image.filepath.replace(".dds", ".png").replace(".DDS", ".png")
+ new_image_name = main_image.name.replace(".dds", ".png").replace(".DDS", ".png")
+ main_image.save_render(bpy.path.abspath(new_path))
+ bpy.data.images.load(filepath=bpy.path.abspath(new_path))
+ bpy.data.images[new_image_name].pack()
+
+ #create darktex
+ bpy.context.scene.kkbp.import_dir = os.path.dirname(bpy.data.filepath) + '\\'
+ main_image = bpy.data.images[new_image_name]
+ dark_image = modify_material.create_darktex(main_image, [.764, .880, 1])
+ bpy.context.scene.kkbp.import_dir = ''
+
+ #saturate both with color code
+ lut_light = 'Lut_TimeDay.png'
+ lut_dark = 'Lut_TimeDay.png'
+ modify_material.load_luts(lut_light, lut_dark)
+
+ for image in [main_image, dark_image]:
+ print('converting ' + image.name)
+ image.save()
+ image.reload()
+ image.colorspace_settings.name = 'sRGB'
+ modify_material.image_to_KK(image, lut_light) #run twice because of bug
+ new_pixels, width, height = modify_material.image_to_KK(image, lut_light)
+ image.pixels = new_pixels
+
+ #then load it in
+ new_node.nodes['light'].image = main_image
+ new_node.nodes['dark'].image = dark_image
+
+ norm_image_link = [node for node in obj.material_slots[0].material.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'][0].inputs[22]
+
+ if norm_image_link.is_linked:
+ norm_image = norm_image_link.links[0].from_node.inputs[2].links[0].from_node.image
+ new_node.nodes['norm'].image == norm_image
+ obj.material_slots[0].material = template
+
+def main(folder):
+ #Use the Better FBX importer addon if installed, else fallback to internal fbx importer
+ use_better_fbx_importer = 'better_fbx' in [addon.module for addon in bpy.context.preferences.addons]
+
+ c.toggle_console()
+ start = time.time()
+
+ #delete the default scene if present
+ if len(bpy.data.objects) == 3:
+ for obj in ['Camera', 'Light', 'Cube']:
+ if bpy.data.objects.get(obj):
+ bpy.data.objects.remove(bpy.data.objects[obj])
+
+ #Set the view transform
+ bpy.context.scene.view_settings.view_transform = 'Standard'
+
+ #import the fbx files
+ fbx_folders = [(folder + sub) for sub in os.listdir(folder) if '.blend' not in sub]
+ print(fbx_folders)
+ for map in fbx_folders:
+ category = map.replace(folder, '')
+ filename = map
+ blend_filepath = map.replace(category,'') + category + '.blend'
+
+ #if a .blend file already exists, don't process this fbx file
+ if os.path.exists(blend_filepath):
+ continue
+
+ collection = bpy.data.collections.new(category)
+ bpy.context.scene.collection.children.link(collection)
+ layer_collection = bpy.context.view_layer.layer_collection
+ #Recursivly transverse layer_collection for a particular name
+ def recurLayerCollection(layerColl, collName):
+ found = None
+ if (layerColl.name == collName):
+ return layerColl
+ for layer in layerColl.children:
+ found = recurLayerCollection(layer, collName)
+ if found:
+ return found
+
+ layerColl = recurLayerCollection(layer_collection, collection.name)
+ bpy.context.view_layer.active_layer_collection = layerColl
+
+ #import map object based on Better FBX availability
+ if use_better_fbx_importer:
+ better_fbx_map_import(map)
+ else:
+ import_studio_objects(map)
+
+ #skip if this was an empty folder
+ if len(bpy.data.objects) < 2:
+ bpy.data.collections.remove(collection)
+ continue
+
+ #apply all armature transforms
+ arm = bpy.data.objects['Armature']
+ bpy.ops.object.select_all(action='SELECT')
+ bpy.context.view_layer.objects.active = arm
+ bpy.ops.object.transform_apply(location=True, rotation=True, scale=False)
+ #delete armature because the asset browser sucks
+ bpy.data.objects.remove(arm)
+ bpy.context.view_layer.objects.active = bpy.data.objects[1]
+ bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
+
+ #mark the collection as an asset and mark each object as an asset as long as it's not a wall, floor, ceiling or sky mesh
+ collection.asset_mark()
+ collection.asset_data.description = filename
+ collection.asset_data.tags.new(filename)
+ collection.asset_data.tags.new(category)
+
+ reject_list = [
+ '_yuka',
+ '_wall',
+ '_kabe',
+ '_ten',
+ '_black',
+ '_yuka',
+ 'Armature',
+ '_sotokabe',
+ '_yane',
+ '_hana',
+ '_kuki',
+ '_kusa',
+ '_enkei',
+ 'fensu',
+ 'gura',
+ '_kage',
+ '_kmichi',
+ '_kmichi',
+ '_ie0',
+ '_seimon',
+ '_shadow',
+ 'hana_0',
+ 'kuki_0',
+ 'kusa_0',
+ '_kyusui',
+ '_saku',
+ '_kumo',
+ '_kaidan',
+ '_nav',
+ 'nob',
+ '_bushitu',
+ 'map20_kk00',
+ '_styuka00',
+ '_ueki0',
+ '_kousya',
+ '_kanten',
+ 'map21_light',
+ 'sky_hoshi',
+ '_over',
+ '_suido',
+ '_kagamiwaku',
+ 'o_koi_map100h_00_0',
+ 'o_koi_map100_shawer00_00_0',
+ '_kar0',
+ 'm00_o_box1__0',
+ '_reef_tga',
+ '_sky_star',
+ '_o_tree',
+ 'o_ikegaki_0',
+ 'i_stu_kiha',
+ 'kouen_kakiwari_',
+ 'kouen_road',
+ 'kouen_pole',
+ 'kouen_sibahu',
+ 'mob_swimclub_',
+ 'map34_kk0',
+ 'p34_pool',
+ 'ap36_kikai',
+ '_shitugaiki0',
+ '_tatemono',
+ 'o_koi_map36_03_0',
+ 'i_mizukri',
+ 'koi_map17_kk00',
+ '_karten',
+ 'i_gabyou',
+ '_pos0',
+ '_highyuka',
+ 'map70_mob',
+ '_tesuri',
+ 'o_soto_doa',
+ 'o_soto_glass',
+ 'o_soto_mado',
+ 'o_koi_map80_00_s_0',
+ ]
+ for obj in collection.all_objects:
+ mark_it = True
+ for item in reject_list:
+ if item in obj.name:
+ mark_it = False
+ break
+ if mark_it:
+ #apply transform then mark as asset
+ #bpy.ops.object.select_all(action='DESELECT')
+ #obj.select_set(True)
+ #bpy.context.view_layer.objects.active = obj
+ #bpy.ops.object.transform_apply(location=True, rotation=True, scale=False)
+ obj.asset_mark()
+ obj.asset_data.description = filename
+ obj.asset_data.tags.new(obj.name)
+ obj.asset_data.tags.new(category)
+
+ #hide the collection afterwards
+ bpy.context.scene.view_layers[0].active_layer_collection.exclude = True
+
+ #load a script into the layout tab
+# if bpy.data.screens.get('Layout'):
+# for area in bpy.data.screens['Layout'].areas:
+# if area.type == 'DOPESHEET_EDITOR':
+# area.ui_type = 'TEXT_EDITOR'
+# area.spaces[0].text = bpy.data.texts.new(name='Generate previews, save and close')
+# area.spaces[0].text.write(
+# '''#stolen script to generate thumbnails for all objects
+# import bpy, functools, time
+
+# assets = [o for o in bpy.data.objects if o.asset_data] # Select all object assets
+# assets.extend([m for m in bpy.data.materials if m.asset_data]) # Select all material assets
+
+# for asset in assets:
+# asset.asset_generate_preview()
+
+# while bpy.app.is_job_running('RENDER_PREVIEW') == True:
+# time.sleep(5)
+
+# bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
+# time.sleep(3)
+# bpy.ops.wm.quit_blender()
+
+# ''')
+
+ print(blend_filepath)
+ bpy.ops.wm.save_as_mainfile(filepath = blend_filepath)
+ bpy.data.collections.remove(collection)
+ for block in bpy.data.objects:
+ bpy.data.objects.remove(block)
+ for block in bpy.data.meshes:
+ bpy.data.meshes.remove(block)
+ for block in bpy.data.materials:
+ bpy.data.materials.remove(block)
+ for block in bpy.data.armatures:
+ bpy.data.armatures.remove(block)
+ for block in bpy.data.images:
+ bpy.data.images.remove(block)
+ for block in bpy.data.node_groups:
+ bpy.data.node_groups.remove(block)
+
+ print(str(time.time() - start))
+ c.toggle_console()
+
+
+class map_asset_lib(bpy.types.Operator):
+ bl_idname = "kkbp.createmapassetlib"
+ bl_label = "Create map asset library"
+ bl_description = t('map_library_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ directory : StringProperty(maxlen=1024, default='', subtype='FILE_PATH', options={'HIDDEN'})
+ filter_glob : StringProperty(default='', options={'HIDDEN'})
+ data = None
+ mats_uv = None
+ structure = None
+
+ def execute(self, context):
+ main(self.directory)
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
+if __name__ == "__main__":
+ bpy.utils.register_class(map_asset_lib)
+
+ # test call
+ print((bpy.ops.kkbp.createmapassetlib('INVOKE_DEFAULT')))
\ No newline at end of file
diff --git a/extras/imageconvert.py b/extras/imageconvert.py
new file mode 100644
index 0000000..ea7a7bf
--- /dev/null
+++ b/extras/imageconvert.py
@@ -0,0 +1,62 @@
+from pathlib import Path
+import bpy
+from .. import common as c
+from ..importing.modifymaterial import modify_material
+
+class image_convert(bpy.types.Operator):
+ bl_idname = "kkbp.imageconvert"
+ bl_label = "Convert light image"
+ bl_description = "Click this to saturate the currently loaded image using the selected Koikatsu LUT"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ c.kklog("Converting image: ".format(context.space_data.image))
+
+ scene = context.scene.kkbp
+ lut_selection = scene.image_dropdown
+
+ if lut_selection == 'A':
+ lut_choice = 'Lut_TimeDay.png'
+ elif lut_selection == 'B':
+ lut_choice = 'Lut_TimeNight.png'
+ else:
+ lut_choice = 'Lut_TimeSunset.png'
+
+ image = context.space_data.image
+ image.reload()
+ image.colorspace_settings.name = 'sRGB'
+
+ # Use code from importcolors to convert the current image
+ modify_material.load_luts(lut_choice, lut_choice)
+ # Need to run image_to_KK twice for the first image due to a weird bug
+ modify_material.image_to_KK(image, lut_choice)
+
+ new_pixels, width, height = modify_material.image_to_KK(image, lut_choice)
+ image.pixels = new_pixels
+ #image.save()
+
+ return {'FINISHED'}
+
+class image_dark_convert(bpy.types.Operator):
+ bl_idname = "kkbp.imagedarkconvert"
+ bl_label = "Convert dark image"
+ bl_description = "Click this to create a dark version of the currently loaded maintex image. The new image will end in 'MT_DT.png' The new image will be automatically loaded to the dark color node group"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ body = bpy.data.objects['Body']
+ c.kklog("Converting image: ".format(context.space_data.image))
+
+ image = context.space_data.image
+ material_name = image.name[:-10]
+ try:
+ shadow_color = [body['KKBP shadow colors'][material_name]['r'], body['KKBP shadow colors'][material_name]['g'], body['KKBP shadow colors'][material_name]['b']]
+ darktex = modify_material.create_darktex(bpy.data.images[image.name], shadow_color)
+ material_name = 'KK ' + image.name[:-10]
+ bpy.data.materials[material_name].node_tree.nodes['Gentex'].node_tree.nodes['Darktex'].image = darktex
+ bpy.data.materials[material_name].node_tree.nodes['Shader'].node_tree.nodes['colorsDark'].inputs['Use dark maintex?'].default_value = 1
+ bpy.data.materials[material_name].node_tree.nodes['Shader'].node_tree.nodes['colorsDark'].inputs['Ignore colormask?'].default_value = 1
+ except:
+ c.kklog('Tried to create a dark version of {} but there was no shadow color available. \nDark color conversion is only available for Koikatsu images that end in \'_MT_CT.png\''.format(image.name), type='error')
+
+ return {'FINISHED'}
diff --git a/extras/importanimation.py b/extras/importanimation.py
new file mode 100644
index 0000000..08d4e8d
--- /dev/null
+++ b/extras/importanimation.py
@@ -0,0 +1,315 @@
+import bpy, os, time, mathutils, json, pathlib
+from .. import common as c
+from ..interface.dictionary_en import t
+def main(file):
+
+ kkbp_character = False
+ #delete the armature before starting to reduce console spam
+ if c.get_body() and c.get_rig():
+ kkbp_character = True
+ c.toggle_console() #open console for some kind of progression
+ start = time.time()
+ rigify_armature = bpy.data.objects['RIG-Armature']
+ bpy.context.view_layer.objects.active=rigify_armature
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bpy.ops.rsl.clear_custom_bones()
+
+ if kkbp_character:
+ #disable IKs
+ rigify_armature.pose.bones["Right arm_parent"]["IK_FK"] = 1
+ rigify_armature.pose.bones["Left arm_parent"]["IK_FK"] = 1
+ rigify_armature.pose.bones["Right leg_parent"]["IK_FK"] = 1
+ rigify_armature.pose.bones["Left leg_parent"]["IK_FK"] = 1
+
+ #disable head follow
+ rigify_armature.pose.bones['torso']['neck_follow'] = 1.0
+ rigify_armature.pose.bones['torso']['head_follow'] = 1.0
+
+ bpy.ops.import_scene.fbx(filepath=str(file), global_scale=96)
+
+ imported_armature = bpy.context.object
+
+ #save the source action name so I can delete it later
+ current_action_name = imported_armature.animation_data.action.name
+
+ #setup rokoko remapping
+ my_areas = bpy.context.workspace.screens[0].areas
+ for area in my_areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.show_object_viewport_armature = True
+ bpy.context.scene.rsl_retargeting_armature_source = imported_armature
+ bpy.context.scene.rsl_retargeting_armature_target = rigify_armature
+ bpy.ops.rsl.build_bone_list()
+ #setup all remapping stuff if this is the first imported animation
+ script_dir=pathlib.Path(__file__).parent
+ if bpy.context.scene.kkbp.animation_import_type:
+ remap_file = os.path.join(script_dir, "animationlibrary", "Rokoko retargeting list (mixamo to KKBP Rigify).json")
+ else:
+ remap_file = os.path.join(script_dir, "animationlibrary", "Rokoko retargeting list (koikatsu fbx to KKBP Rigify).json")
+
+ if kkbp_character:
+ script_dir=pathlib.Path(__file__).parent
+ json_file = open(remap_file)
+ data = json.load(json_file)
+ for row in bpy.context.scene.rsl_retargeting_bone_list:
+ row.bone_name_target = "" #clear all bones
+ for bone in data['bones']:
+ for row in bpy.context.scene.rsl_retargeting_bone_list:
+ if row.bone_name_source.lower() == data['bones'][bone][0].lower():
+ row.bone_name_target = data['bones'][bone][1]
+ bpy.ops.rsl.retarget_animation()
+
+ # then remove the fcurves of the retargeted bones that were not present in the retargeting list.
+ # A lot of popping and fluctuation is present if these aren't removed
+ retargeting_list = [
+ 'torso',
+ 'Spine_fk',
+ 'Chest_fk',
+ 'Upper Chest_fk',
+ 'Left arm_fk',
+ 'Left elbow_fk',
+ 'Left wrist_fk',
+ 'IndexFinger1_L',
+ 'IndexFinger2_L',
+ 'IndexFinger3_L',
+ 'LittleFinger1_L',
+ 'LittleFinger2_L',
+ 'LittleFinger3_L',
+ 'MiddleFinger1_L',
+ 'MiddleFinger2_L',
+ 'MiddleFinger3_L',
+ 'RingFinger1_L',
+ 'RingFinger2_L',
+ 'RingFinger3_L',
+ 'Thumb0_L',
+ 'Thumb1_L',
+ 'Thumb2_L',
+ 'Right arm_fk',
+ 'Right elbow_fk',
+ 'Right wrist_fk',
+ 'IndexFinger1_R',
+ 'IndexFinger2_R',
+ 'IndexFinger3_R',
+ 'LittleFinger1_R',
+ 'LittleFinger2_R',
+ 'LittleFinger3_R',
+ 'MiddleFinger1_R',
+ 'MiddleFinger2_R',
+ 'MiddleFinger3_R',
+ 'RingFinger1_R',
+ 'RingFinger2_R',
+ 'RingFinger3_R',
+ 'Thumb0_R',
+ 'Thumb1_R',
+ 'Thumb2_R',
+ 'neck',
+ 'head',
+ 'Hips_fk',
+ 'Left leg_fk',
+ 'Left knee_fk',
+ 'Left ankle_fk',
+ 'Left toe_fk',
+ 'Right leg_fk',
+ 'Right knee_fk',
+ 'Right ankle_fk',
+ 'Right toe_fk',
+ 'Left shoulder',
+ 'cf_j_waist02',
+ 'Right shoulder',
+ 'cf_j_kokan',
+ #'cf_j_ana',
+ 'Breasts handle',
+ 'Left Breast handle',
+ 'cf_j_bust02_l',
+ 'cf_j_bust03_l',
+ 'cf_j_bnip02root_l',
+ 'cf_j_bnip02_l',
+ 'Right Breast handle',
+ 'cf_j_bust02_r',
+ 'cf_j_bust03_r',
+ 'cf_j_bnip02root_r',
+ 'cf_j_bnip02_r',
+ #'Left Buttock handle',
+ #'Right Buttock handle',
+ ]
+ if kkbp_character:
+ for index, action in enumerate(rigify_armature.animation_data.action.fcurves):
+ bone_name = action.data_path[action.data_path.find('[')+2:action.data_path.find('].')-1]
+ print('{} {} / {}'.format(action.data_path, index, len(rigify_armature.animation_data.action.fcurves)))
+ if bone_name not in retargeting_list:
+ rigify_armature.animation_data.action.fcurves.remove(action) #remove keyframes
+ if rigify_armature.pose.bones.get(bone_name):
+ rigify_armature.pose.bones[bone_name].matrix_basis = mathutils.Matrix() #reset rotation matrix
+
+
+ if bpy.context.scene.kkbp.animation_library_scale:
+ #also scale the arms on the y axis by 5% because it makes the animation more accurate to the in-game one for some poses
+ already_got_both_arms = False
+ if kkbp_character:
+ for y_scale_curve in [curve for curve in rigify_armature.animation_data.action.fcurves if (curve.array_index == 1) and ('scale' in curve.data_path)]:
+ bone_name = y_scale_curve.data_path[y_scale_curve.data_path.find('[')+2:y_scale_curve.data_path.find('].')-1]
+ if bone_name in ['Left arm_fk', 'Right arm_fk']:
+ keyframe_limits = [int(y_scale_curve.keyframe_points[0].co.x), int(y_scale_curve.keyframe_points[-1].co.x)]
+ for frame_number in range(keyframe_limits[0], keyframe_limits[1]+1):
+ original_scale = y_scale_curve.evaluate(frame_number)
+ y_scale_curve.keyframe_points.insert(frame_number, original_scale * 1.05)
+ if already_got_both_arms:
+ break #skip the rest
+ already_got_both_arms = True
+
+ #select all rigify armature bones and create pose asset
+ bpy.ops.object.select_all(action='DESELECT')
+ rigify_armature.select_set(True)
+ bpy.context.view_layer.objects.active=rigify_armature
+ bpy.ops.object.mode_set(mode = 'POSE')
+ bpy.ops.pose.select_all(action='SELECT')
+ bpy.data.workspaces["Layout"].asset_library_reference = 'LOCAL'
+ #translate name
+ name = file.replace('-p_cf_body_bone-0.fbx', '')
+ name = name.replace('-p_cf_body_bone-1.fbx', '')
+ translation_dict_normal = {
+ 'isu_':'Seated_',
+ 'suwari':'Sitting',
+ 'syagami':'Squat',
+ 'sadou':'Tea',
+ 'undou':'Excercise',
+ 'suiei':'Swimming',
+ 'tachi':'Standing',
+ 'manken':'Reading',
+ 'soine':'Laying2',
+ 'kasa':'Umbrella',
+ 'konbou':'Club',
+ 'aruki':'Walking',
+ 'haruki':'Running',
+ 'ne_0':'Sleep',
+ 'nugi':'Undressing',
+ }
+ translation_dict_h = {
+ '_A_Idle':'Crotch grope idle',
+ '_A_Loop':'Crotch grope',
+ '_A_Touch':'Crotch grope start',
+ '_K_Loop':'Kiss',
+ '_K_Touch':'Kiss start',
+ '_M_Idle':'Breast grope idle',
+ '_M_Loop':'Breast grope',
+ '_M_Touch':'Breast grope start',
+ '_MLoop1': 'Masturbate',
+ '_MLoop2': 'Masturbate2',
+ '_Orgasm-': 'Climax-',
+ '_Orgasm_A':'Climax end',
+ '_Orgasm_B':'Climax end',
+ '_Orgasm_Loop':'Climax',
+ '_Orgasm_Start':'Climax start',
+ '_S_Idle':'Butt grope idle',
+ '_S_Loop':'Butt grope',
+ '_S_Touch':'Butt grope start',
+ '_Back_Dislikes':'Embarrassed back',
+ '_Front_Dislikes':'Embarrassed front',
+
+ '_Oral_Idle_IN': 'Cum in mouth start',
+ '_Oral_Idle': 'Cum in mouth',
+ '_Stop_Idle': 'Idling stop',
+ '_InsertIdle': 'Insert idle',
+ '_Idle-': 'Idling-',
+ '_Insert-': 'Insert-',
+ '_M_IN_Start':'Climax inside start',
+ '_SF_IN_Start':'Climax inside start',
+ '_A_SS_IN_Start':'Climax inside start',
+ '_SS_IN_Start':'Climax inside start',
+ '_A_WF_IN_Start':'Climax inside start',
+ '_A_WS_IN_Start':'Climax inside start',
+ '_WF_IN_Start':'Climax inside start',
+ '_A_M_IN_Start':'Climax inside start',
+ '_WS_IN_Start':'Climax inside start',
+ '_IN_Start':'Climax inside start',
+ '_M_IN_Loop':'Climax inside',
+ '_SF_IN_Loop':'Climax inside',
+ '_SS_IN_Loop':'Climax inside',
+ '_WF_IN_Loop':'Climax inside',
+ '_WS_IN_Loop':'Climax inside',
+ '_IN_Loop':'Climax inside',
+ '_A_IN_A':'Climax inside end',
+ '_SS_IN_A':'Climax inside end',
+ '_A_WS_IN_A':'Climax inside end',
+ '_WS_IN_A':'Climax inside end',
+ '_IN_A':'Climax inside end',
+ '_Pull':'Pull out',
+ '_OUT_Start':'Climax outside start',
+ '_M_OUT_Loop':'Climax outside',
+ '_OUT_Loop':'Climax outside',
+ '_OUT_A':'Climax outside end',
+ '_OLoop': 'Loop',
+ '_SLoop':'Fast loop',
+ '_WLoop':'Slow loop',
+ '_Drink_IN':'Swallow cum start',
+ '_Drink_A':'Swallow cum end',
+ '_Drink':'Swallow cum',
+ '_Vomit_IN': 'Spit out cum start',
+ '_Vomit_A': 'Spit out cum end',
+ '_Vomit': 'Spit out cum',
+ }
+
+ for item in translation_dict_normal:
+ name = name.replace(item, translation_dict_normal[item])
+ h_dict_used = False
+ for item in translation_dict_h:
+ if item in name:
+ name = name[1:] #add the description onto it because h animations don't have names, remove the S at the beginning of the name because all of them start with that
+ name = name.replace(item, translation_dict_h[item])
+ h_dict_used = True
+ break
+
+ action = rigify_armature.animation_data.action
+ rigify_armature.asset_mark()
+ action.asset_mark()
+ action.use_fake_user = True
+ my_areas = bpy.context.workspace.screens[0].areas
+ for area in my_areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.show_object_viewport_armature = False
+ my_areas = bpy.context.workspace.screens[0].areas
+ for area in my_areas:
+ for space in area.spaces:
+ if space.type == 'VIEW_3D':
+ space.show_object_viewport_armature = True
+ action.name = os.path.basename(str(name))
+ action.asset_data.tags.new(file)
+ if h_dict_used:
+ action.asset_data.tags.new('NSFW')
+ action.asset_data.description = file
+
+ #delete the imported action, object and armature
+ bpy.data.actions.remove(bpy.data.actions[current_action_name])
+ imported_armature_armaturename = imported_armature.data.name
+ bpy.data.objects.remove(imported_armature)
+ try:
+ bpy.data.objects.remove(bpy.data.objects['Beta_Joints'])
+ bpy.data.objects.remove(bpy.data.objects['Beta_Surface'])
+ except:
+ #oh well
+ pass
+ bpy.data.armatures.remove(bpy.data.armatures[imported_armature_armaturename])
+ print(file)
+
+ print(str(time.time() - start))
+ c.toggle_console() #close console
+
+class anim_import(bpy.types.Operator):
+ bl_idname = "kkbp.importanimation"
+ bl_label = "Import .fbx animation file"
+ bl_description = t('single_animation_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ filepath : bpy.props.StringProperty(maxlen=1024, default='', options={'HIDDEN'})
+ filter_glob : bpy.props.StringProperty(default='*.fbx', options={'HIDDEN'})
+
+ def execute(self, context):
+ main(self.filepath)
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
diff --git a/extras/importstudio.py b/extras/importstudio.py
new file mode 100644
index 0000000..076458a
--- /dev/null
+++ b/extras/importstudio.py
@@ -0,0 +1,413 @@
+import bpy, os, time, glob
+from pathlib import Path
+from bpy.props import StringProperty
+from ..importing.modifymaterial import modify_material
+from .. import common as c
+from subprocess import Popen, PIPE
+from ..interface.dictionary_en import t
+
+def import_studio_objects(directory):
+ #Stop if no files were detected
+ def fileError(self, context):
+ self.layout.label(text="No fbx files were detected in the folder you selected (including subfolders)")
+
+ #Stop if no custom studio node group was detected
+ def nodeError(self, context):
+ self.layout.label(text="You need a node group named \"Custom_studio\" with at least three inputs and one output to use the Custom shader.")
+ self.layout.label(text="Input 1: Maintex || Input 2: Image Alpha || Input 3: Normal || Input 4 (optional): Detailmask || Input 5 (optional): Colormask")
+
+ bpy.context.scene.view_settings.view_transform = 'Standard'
+ scene = bpy.context.scene.kkbp
+ shader_type = scene.dropdown_box
+ shadow_type = scene.shadows_dropdown
+ blend_type = scene.blend_dropdown
+ use_lut = scene.studio_lut_bool
+
+ #shader_dict = {"A": "principled", "B": "emission", "C": "kkshader", "D": "custom"}
+ shadow_dict = {"A": "NONE", "B": "OPAQUE", "C": "CLIP", "D": "HASHED"}
+ blend_dict = {"A": "OPAQUE", "B": "CLIP", "C": "HASHED", "D": "BLEND"}
+
+ path = Path(directory).rglob('*')
+ fbx_list = []
+ image_list = []
+ for item in path:
+ if '.fbx' in str(item):
+ fbx_list.append(item)
+ if '.dds' in str(item) or '.tga' in str(item):
+ image_list.append(item)
+
+ #attempt to detect detail masks, color masks and main tex files from the selected folder/subfolders
+ #normal map will always be attached on import if it's present
+
+ conversion_image_list = []
+ for image in image_list:
+ if '_md-DXT' in str(image):
+ detected_detailmask = image.name
+ if '_mc-DXT' in str(image):
+ detected_colormask = image.name
+ if '_t-DXT' in str(image):
+ detected_maintex = image.name
+
+ #pack certain images for later
+ if '_md-DXT' in str(image) or '_mc-DXT' in str(image) or '_t-DXT' in str(image):
+ bpy.data.images.load(filepath=str(image))
+ bpy.data.images[image.name].pack()
+
+ #save the images in this directory for later
+ conversion_image_list.append(image.name)
+
+ if len(fbx_list) == 0:
+ bpy.context.window_manager.popup_menu(fileError, title="Error", icon='ERROR')
+ return
+
+ #get the images currently loaded into the file
+ already_loaded_images = [image.name for image in bpy.data.images]
+
+ for fbx in fbx_list:
+ bpy.ops.import_scene.fbx(filepath=str(fbx))
+ bpy.ops.object.mode_set(mode='OBJECT')
+ for object in bpy.context.selected_objects:
+ if object.type == 'ARMATURE':
+ object.scale = [1, 1, 1]
+ if len(object.data.bones) == 1:
+ bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
+ bpy.data.objects.remove(object)
+
+ elif object.type == 'MESH':
+ #set scale and rename the first UV map
+ object.scale = [1, 1, 1]
+ if object.data.uv_layers[0]:
+ object.data.uv_layers[0].name = 'UVMap'
+
+ for material_slot in object.material_slots:
+ material = material_slot.material
+ nodes = material.node_tree.nodes
+
+ material.use_backface_culling = True
+ material.show_transparent_back = False
+ material.blend_method = blend_dict[blend_type]
+ material.shadow_method = shadow_dict[shadow_type]
+
+ #rename all principled BSDF nodes to 'Principled BSDF' just in case
+ for node in nodes:
+ if node.type == 'BSDF_PRINCIPLED':
+ node.name = 'Principled BSDF'
+
+ #rename all Material Output nodes to 'Material Output' just in case
+ for node in nodes:
+ if node.type == 'OUTPUT_MATERIAL':
+ node.name = 'Material Output'
+
+ #Remove duplicate images if they exist
+ for node in nodes:
+ if node.type == 'TEX_IMAGE':
+ (base, sep, ext) = node.image.name.rpartition('.')
+ if ext.isnumeric():
+ if bpy.data.images.get(base):
+ node.image = bpy.data.images.get(base)
+
+ #DDS files need to be converted to pngs or tgas or the color conversion scripts won't work
+ #also set images to srgb
+ for node in nodes:
+ if node.type == 'TEX_IMAGE':
+ bpy.data.images[node.image.name].colorspace_settings.name = 'sRGB'
+ image = bpy.data.images[node.image.name]
+ if ('.dds' in image.name or '.DDS' in image.name) and image.name.replace('.dds', '.png') not in already_loaded_images:
+ new_path = image.filepath.replace(".dds", ".png").replace(".DDS", ".png")
+ new_image_name = image.name.replace(".dds", ".png").replace(".DDS", ".png")
+ image.colorspace_settings.name = 'sRGB'
+ image.save_render(bpy.path.abspath(new_path))
+ bpy.data.images.load(filepath=bpy.path.abspath(new_path))
+ bpy.data.images[new_image_name].pack()
+ node.image = bpy.data.images[new_image_name]
+
+ #if two objects have the same material, and the material was already operated on, skip it
+ if nodes.get('Principled BSDF') == None:
+ continue
+
+ emission_input = 26
+ metallic_input = 1
+ normal_input = 5
+ alpha_input = 4
+
+ #standardize dist and subsurf because the number of nodes on the principled bsdf changes with these choices
+ nodes['Principled BSDF'].distribution = 'GGX'
+ nodes['Principled BSDF'].subsurface_method = 'RANDOM_WALK'
+
+ #set emission to black
+ nodes['Principled BSDF'].inputs[emission_input].default_value = (0, 0, 0, 1)
+
+ #set metallic value to zero
+ nodes['Principled BSDF'].inputs[metallic_input].default_value = 0.0
+
+ try:
+ image_alpha = nodes['Principled BSDF'].inputs[alpha_input].links[0].from_node
+ except:
+ #there was no image attached to the alpha node
+ image_alpha = 'noalpha'
+
+ try:
+ image = nodes['Principled BSDF'].inputs[0].links[0].from_node
+ except:
+ image = 'noimage'
+
+ if image_alpha != 'noalpha':
+ material.node_tree.links.new(image_alpha.outputs[1], nodes['Principled BSDF'].inputs[alpha_input])
+
+ #if set to emission
+ if shader_type == 'B':
+ nodes.remove(nodes['Principled BSDF'])
+ output_node = nodes['Material Output']
+ transparency_mix = nodes.new('ShaderNodeMixShader')
+ transparency_mix.location = output_node.location[0], output_node.location[1] - 300
+ transparency_node = nodes.new('ShaderNodeBsdfTransparent')
+ transparency_node.location = transparency_mix.location[0] - 300, transparency_mix.location[1]
+ emissive_node = nodes.new('ShaderNodeEmission')
+ emissive_node.location = transparency_node.location[0], transparency_node.location[1] + 300
+ output_node.location = transparency_mix.location[0] + 300, transparency_mix.location[1]
+
+ if image != 'noimage':
+ material.node_tree.links.new(image.outputs[0], emissive_node.inputs[0])
+ else:
+ #if there's no image, try falling back to the colormask
+ try:
+ color_node = nodes.new('ShaderNodeTexImage')
+ color_node.image = bpy.data.images[detected_colormask]
+ color_node.location = emissive_node.location[0] - 300, emissive_node.location[1]
+ material.node_tree.links.new(color_node.outputs[0], emissive_node.inputs[0])
+ except:
+ nodes.remove(color_node)
+
+ material.node_tree.links.new(emissive_node.outputs[0], transparency_mix.inputs[2])
+ material.node_tree.links.new(transparency_node.outputs[0], transparency_mix.inputs[1])
+ if image_alpha != 'noalpha':
+ material.node_tree.links.new(image_alpha.outputs[1], transparency_mix.inputs[0])
+ elif image != 'noimage':
+ material.node_tree.links.new(image.outputs[1], transparency_mix.inputs[0])
+
+ material.node_tree.links.new(output_node.inputs[0], transparency_mix.outputs[0])
+
+ elif shader_type == 'C':
+ #if set to KK shader
+ if image_alpha != 'noalpha':
+ image_alpha = image_alpha.image
+ if image != 'noimage':
+ image = image.image
+ else:
+ #if there's no image, try to fallback to the maintex
+ try:
+ image = bpy.data.images.get(detected_maintex)
+ except:
+ pass
+
+ #get normal image
+ if nodes['Principled BSDF'].inputs[normal_input].links[0].from_node.inputs[1].links != ():
+ normal = nodes['Principled BSDF'].inputs[normal_input].links[0].from_node.inputs[1].links[0].from_node.image
+ else:
+ normal = 'nonormal'
+
+ try:
+ template = bpy.data.materials['KK General'].copy()
+ except:
+ script_dir=Path(__file__).parent
+ template_path=(script_dir / '../KK Shader V8.0.blend').resolve()
+ filepath = str(template_path)
+
+ innerpath = 'Material'
+ templateList = ['KK General']
+
+ for template in templateList:
+ bpy.ops.wm.append(
+ filepath=os.path.join(filepath, innerpath, template),
+ directory=os.path.join(filepath, innerpath),
+ filename=template,
+ set_fake=False
+ )
+ template = bpy.data.materials['KK General'].copy()
+
+ template.name = 'KK ' + material.name
+ material_slot.material = bpy.data.materials[template.name]
+ material = material_slot.material
+ nodes = material.node_tree.nodes
+
+ def image_load(group, node, image, raw = False):
+ try:
+ nodes[group].node_tree.nodes[node].image = bpy.data.images[image]
+ if raw:
+ nodes[group].node_tree.nodes[node].image.colorspace_settings.name = 'Raw'
+ except:
+ c.kklog('Image not found, skipping: ' + str(image), type = 'warn')
+
+ gen_type = material_slot.name.replace('KK ','')
+
+ #make a copy of the node group, use it to replace the current node group and rename it so each mat has a unique texture group
+ new_node = material_slot.material.node_tree.nodes['Gentex'].node_tree.copy()
+ material_slot.material.node_tree.nodes['Gentex'].node_tree = new_node
+ new_node.name = gen_type + ' Textures'
+
+ if image != 'noimage':
+ image_load('Gentex', 'Maintex', image.name)#, True)
+ else:
+ #if there's no image, fallback to the detected maintex
+ try:
+ image_load('Gentex', 'Maintex', detected_maintex)#, True)
+ except:
+ #oh well
+ pass
+
+ if normal != 'nonormal':
+ image_load('Gentex', 'MainNorm', normal.name, True)
+
+ #try importing the detail mask if there is one
+ try:
+ image_load('Gentex', 'MainDet', detected_detailmask)#, True)
+ except:
+ #or not
+ pass
+
+ try:
+ image_load('Gentex', 'MainCol', detected_colormask)#, True)
+ except:
+ #or not
+ pass
+
+ #Also, make a copy of the General shader node group, as it's unlikely everything using it will be the same color
+ new_node = material_slot.material.node_tree.nodes['Shader'].node_tree.copy()
+ material_slot.material.node_tree.nodes['Shader'].node_tree = new_node
+ new_node.name = gen_type + ' Shader'
+
+ main_image = material_slot.material.node_tree.nodes['Gentex'].node_tree.nodes['Maintex'].image
+ alpha_image = material_slot.material.node_tree.nodes['Gentex'].node_tree.nodes['Alphamask'].image
+
+ #If no main image was loaded in, there's no alpha channel being fed into the KK Shader.
+ #Unlink the input node and make the alpha channel pure white
+ if main_image == None:
+ getOut = material_slot.material.node_tree.nodes['Shader'].node_tree.nodes['alphatoggle'].inputs['maintex alpha'].links[0]
+ material_slot.material.node_tree.nodes['Shader'].node_tree.links.remove(getOut)
+ material_slot.material.node_tree.nodes['Shader'].node_tree.nodes['alphatoggle'].inputs['maintex alpha'].default_value = (1,1,1,1)
+ else:
+ #but if there is a main image, create a darktex for it and load it in
+ original_path = bpy.context.scene.kkbp.import_dir
+ bpy.context.scene.kkbp.import_dir = directory + 'saturated_files'
+ darktex = modify_material.create_darktex(bpy.data.images[image.name], [.764, .880, 1]) #create the darktex now and load it in later
+ bpy.context.scene.kkbp.import_dir = original_path
+
+ image_load('Gentex', 'Darktex', darktex.name)
+ material_slot.material.node_tree.nodes['Shader'].node_tree.nodes['colorsDark'].inputs['Use dark maintex?'].default_value = 1
+ material_slot.material.node_tree.nodes['Shader'].node_tree.nodes['colorsDark'].inputs['Ignore colormask?'].default_value = 1
+
+ material.use_backface_culling = True
+ material.show_transparent_back = False
+ material.blend_method = blend_dict[blend_type]
+ material.shadow_method = shadow_dict[shadow_type]
+
+ elif shader_type == 'D':
+
+ if nodes['Principled BSDF'].inputs[20].links[0].from_node.inputs[1].links != ():
+ normal = nodes['Principled BSDF'].inputs[20].links[0].from_node.inputs[1].links[0].from_node
+ else:
+ normal = 'nonormal'
+
+ nodes.remove(nodes['Principled BSDF'])
+ output_node = nodes['Material Output']
+ custom_group = nodes.new('ShaderNodeGroup')
+ try:
+ custom_group.node_tree = bpy.data.node_groups['Custom_studio']
+ except:
+ try:
+ custom_group.node_tree = bpy.data.node_groups['custom_studio']
+ except:
+ #no custom studio node group was detected
+ bpy.context.window_manager.popup_menu(nodeError, title="Error", icon='ERROR')
+ return
+
+ custom_group.location = output_node.location[0], output_node.location[1] - 300
+
+ if image != 'noimage':
+ material.node_tree.links.new(image.outputs[0], custom_group.inputs[0])
+ if image_alpha != 'noalpha':
+ material.node_tree.links.new(image_alpha.outputs[1], custom_group.inputs[1])
+ if normal != 'nonormal':
+ material.node_tree.links.new(normal.outputs[0], custom_group.inputs[2])
+
+ #if the custom studio node group has a fourth input, and a detail mask is available, put the detail mask in there
+ try:
+ detail_node = nodes.new('ShaderNodeTexImage')
+ detail_node.image = bpy.data.images[detected_detailmask]
+ detail_node.location = custom_group.location[0] - 300, custom_group.location[1] - 300
+ material.node_tree.links.new(detail_node.outputs[0], custom_group.inputs[3])
+ except:
+ nodes.remove(detail_node)
+
+ #if the custom studio node group has a fifth input, and a color mask is available, put the color mask in there
+ try:
+ color_node = nodes.new('ShaderNodeTexImage')
+ color_node.image = bpy.data.images[detected_colormask]
+ color_node.location = custom_group.location[0] - 300, custom_group.location[1] - 600
+ material.node_tree.links.new(color_node.outputs[0], custom_group.inputs[4])
+ except:
+ nodes.remove(color_node)
+
+ material.node_tree.links.new(output_node.inputs[0], custom_group.outputs[0])
+
+ #basic strat: load in the .dds files to this version of blender, set them to srgb, save them as .pngs and saturate the .pngs with the older blender version
+ if use_lut:
+ image_list = [image for image in bpy.data.images if (image.name not in already_loaded_images and 'Template:' not in image.name)]
+
+ def convert_and_import_textures():
+ c.kklog('Opening older version of Blender to convert model textures...')
+ time.sleep(5)
+ # You have to supply a blend file or it won't execute the script automatically. Choose the video editing template blend because it's the first one I tried
+ if 'blender.exe' in bpy.context.scene.kkbp.blender_path:
+ version_path = [i for i in glob.glob(os.path.dirname(bpy.context.scene.kkbp.blender_path) + '/*/')][0]
+ else:
+ bpy.context.scene.kkbp.blender_path = bpy.context.scene.kkbp.blender_path + '/blender.exe'
+ version_path = [i for i in glob.glob(os.path.dirname(bpy.context.scene.kkbp.blender_path) + '/*/')][0]
+ blender_file = os.path.join(version_path, 'scripts', 'startup', 'bl_app_templates_system', 'Video_Editing', 'startup.blend')
+ secondscriptname = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'importing', 'converttextures.py')
+ process = Popen([bpy.context.scene.kkbp.blender_path, blender_file, "-P", secondscriptname, os.path.dirname(os.path.dirname(__file__)) + r'\importing', directory, '0'], stdout=PIPE, universal_newlines=True)
+ r = process.stdout.readline()[:-1]
+ while r:
+ if '|' in r:
+ c.kklog(r.replace('|','')) # these are lines printed from the second script
+ r = process.stdout.readline()[:-1]
+ convert_and_import_textures()
+
+ #load in the saturated images and remap
+ for image in image_list:
+ if image.filepath:
+ try:
+ saturated_image = bpy.data.images.load(image.filepath.replace(image.name, 'saturated_files\\' + image.name.replace('.dds','.png').replace('.DDS','.png')))
+ image.user_remap(saturated_image)
+ except:
+ saturated_image = bpy.data.images.load(image.filepath.replace(image.name, image.name.replace('.dds','.png').replace('.DDS','.png')))
+ image.user_remap(saturated_image)
+
+ #delete orphan data
+ for cat in [bpy.data.armatures, bpy.data.objects, bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.node_groups]:
+ for block in cat:
+ if block.users == 0:
+ cat.remove(block)
+
+class import_studio(bpy.types.Operator):
+ bl_idname = "kkbp.importstudio"
+ bl_label = "Import studio object"
+ bl_description = t('studio_object_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ directory : StringProperty(maxlen=1024, default='', subtype='FILE_PATH', options={'HIDDEN'})
+ filter_glob : StringProperty(default='', options={'HIDDEN'})
+ data = None
+ mats_uv = None
+ structure = None
+
+ def execute(self, context):
+ import_studio_objects(self.directory)
+ c.toggle_console()
+ c.toggle_console()
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
\ No newline at end of file
diff --git a/extras/linkhair.py b/extras/linkhair.py
new file mode 100644
index 0000000..2226b92
--- /dev/null
+++ b/extras/linkhair.py
@@ -0,0 +1,26 @@
+#This script takes a source hair material and copies all of it's attributes to all target hair materials on the same object
+# so you don't have to manually change every single hair material yourself
+
+import bpy
+from ..interface.dictionary_en import t
+
+class link_hair(bpy.types.Operator):
+ bl_idname = "kkbp.linkhair"
+ bl_label = "Link hair"
+ bl_description = t('link_hair_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ #get the currently selected hair material
+ object = bpy.context.object
+ source_material = object.active_material
+ for type in ['light', 'dark']:
+ if source_material.node_tree.nodes.get(type):
+ for target_material in [s.material for s in object.material_slots if s.material.get('hair')]:
+ if target_material.node_tree.nodes.get(type):
+ #copy all of the hair settings for this node group to the target material
+ for index, input in enumerate(target_material.node_tree.nodes[type].inputs):
+ input.default_value = source_material.node_tree.nodes[type].inputs[index].default_value
+
+ return {'FINISHED'}
+
diff --git a/extras/linkshapekeys.py b/extras/linkshapekeys.py
new file mode 100644
index 0000000..2c05600
--- /dev/null
+++ b/extras/linkshapekeys.py
@@ -0,0 +1,99 @@
+'''
+LINK EYEBROW SHAPEKEYS TO BODY SCRIPT
+Usage:
+- Seperate the eyebrow mesh into its own object
+- Select the eyebrows object, then shift click the body object and run the script to control the eyebrow shapekeys from the body object
+
+Script 90% stolen from https://blender.stackexchange.com/questions/86757/python-how-to-connect-shapekeys-via-drivers
+'''
+import bpy
+from ..interface.dictionary_en import t
+from .. import common as c
+
+def link_keys(shapekey_holder_object, objects_to_link):
+
+ shapekey_list_string = str(shapekey_holder_object.data.shape_keys.key_blocks.keys()).lower()
+ for obj in objects_to_link:
+ bpy.ops.object.select_all(action = 'DESELECT')
+ bpy.context.view_layer.objects.active = obj
+ obj.select_set(True)
+ bpy.ops.object.material_slot_remove_unused()
+ for key in obj.data.shape_keys.key_blocks:
+ if key.name.lower() in shapekey_list_string:
+ if not key.name == obj.data.shape_keys.key_blocks[0]:
+ skey_driver = key.driver_add('value')
+ skey_driver.driver.type = 'AVERAGE'
+ #skey_driver.driver.show_debug_info = True
+ if skey_driver.driver.variables:
+ for v in skey_driver.driver.variables:
+ skey_driver.driver.variables.remove(v)
+ newVar = skey_driver.driver.variables.new()
+ newVar.name = "value"
+ newVar.type = 'SINGLE_PROP'
+ newVar.targets[0].id_type = 'KEY'
+ newVar.targets[0].id = shapekey_holder_object.data.shape_keys
+ newVar.targets[0].data_path = 'key_blocks["' + key.name+ '"].value'
+ skey_driver = key.driver_add('mute')
+ skey_driver.driver.type = 'AVERAGE'
+ #skey_driver.driver.show_debug_info = True
+ if skey_driver.driver.variables:
+ for v in skey_driver.driver.variables:
+ skey_driver.driver.variables.remove(v)
+ newVar = skey_driver.driver.variables.new()
+ newVar.name = "hide"
+ newVar.type = 'SINGLE_PROP'
+ newVar.targets[0].id_type = 'KEY'
+ newVar.targets[0].id = shapekey_holder_object.data.shape_keys
+ newVar.targets[0].data_path = 'key_blocks["' + key.name+ '"].mute'
+
+
+class link_shapekeys(bpy.types.Operator):
+ bl_idname = "kkbp.linkshapekeys"
+ bl_label = "Link shapekeys"
+ bl_description = t('sep_eye_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ #separate the eyes from the body object
+ body = c.get_body()
+ c.switch(body, 'EDIT')
+
+ def separateMaterial(matList):
+ for mat in matList:
+ try:
+ def moveUp():
+ return bpy.ops.object.material_slot_move(direction='UP')
+ while moveUp() != {"CANCELLED"}:
+ pass
+ bpy.context.object.active_material_index = body.data.materials.find(mat)
+ bpy.ops.object.material_slot_select()
+ except:
+ print('material wasn\'t found: ' + mat)
+ bpy.ops.mesh.separate(type='SELECTED')
+
+ eye_list = ['KK EyeR (hitomi) ' + c.get_name(),
+ 'KK EyeL (hitomi) ' + c.get_name(),
+ 'KK Eyewhites (sirome) ' + c.get_name(),
+ 'KK Eyeline up ' + c.get_name(),
+ 'KK Eyeline down ' + c.get_name()]
+ separateMaterial(eye_list)
+
+ eyes = bpy.data.objects[body.name + '.001']
+ eyes.name = 'Eyes'
+ if eyes.modifiers.get('Outline Modifier'):
+ eyes.modifiers['Outline Modifier'].show_viewport = False
+ eyes.modifiers['Outline Modifier'].show_render = False
+
+ #do the same for the eyebrows
+ separateMaterial(['KK Eyebrows (mayuge) ' + c.get_name()])
+ eyebrows = bpy.data.objects[body.name + '.001']
+ eyebrows.name = 'Eyebrows'
+ if eyebrows.modifiers.get('Outline Modifier'):
+ eyebrows.modifiers['Outline Modifier'].show_viewport = False
+ eyebrows.modifiers['Outline Modifier'].show_render = False
+
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ link_keys(body, [eyes, eyebrows])
+
+ return {'FINISHED'}
+
diff --git a/extras/matcombsetup.py b/extras/matcombsetup.py
new file mode 100644
index 0000000..2d4e7dd
--- /dev/null
+++ b/extras/matcombsetup.py
@@ -0,0 +1,114 @@
+import bpy
+from ..interface.dictionary_en import t
+from .. import common as c
+
+class mat_comb_setup(bpy.types.Operator):
+ bl_idname = "kkbp.matcombsetup"
+ bl_label = "Material combiner setup"
+ bl_description = t('mat_comb_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ def recurLayerCollection(layerColl, collName):
+ found = None
+ if (layerColl.name == collName):
+ return layerColl
+ for layer in layerColl.children:
+ found = recurLayerCollection(layer, collName)
+ if found:
+ return found
+
+ def remove_orphan_data():
+ #revert the image back from the atlas file to the baked file
+ for mat in bpy.data.materials:
+ if mat.name[-4:] == '-ORG':
+ simplified_name = mat.name[:-4]
+ if bpy.data.materials.get(simplified_name):
+ simplified_mat = bpy.data.materials[simplified_name]
+ for bake_type in ['light', 'dark', 'normal']:
+ simplified_mat.node_tree.nodes['textures'].node_tree.nodes[bake_type].image = bpy.data.images.get(simplified_name + ' ' + bake_type + '.png')
+ #delete orphan data
+ for cat in [bpy.data.armatures, bpy.data.objects, bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.node_groups]:
+ for block in cat:
+ if block.users == 0:
+ cat.remove(block)
+
+ if bpy.data.collections.get(c.get_name() + ' atlas'):
+ c.kklog('deleting previous collection "Model with atlas" and regenerating atlas model...')
+ def del_collection(coll):
+ for c in coll.children:
+ del_collection(c)
+ bpy.data.collections.remove(coll,do_unlink=True)
+ del_collection(bpy.data.collections[c.get_name() + ' atlas'])
+ remove_orphan_data()
+ #show the original collection again
+ layer_collection = bpy.context.view_layer.layer_collection
+ layerColl = recurLayerCollection(layer_collection, 'Scene Collection')
+ bpy.context.view_layer.active_layer_collection = layerColl
+ bpy.context.scene.view_layers[0].active_layer_collection.children[0].exclude = False
+
+ #Change the Active LayerCollection to 'My Collection'
+ layer_collection = bpy.context.view_layer.layer_collection
+ layerColl = recurLayerCollection(layer_collection, c.get_name())
+ bpy.context.view_layer.active_layer_collection = layerColl
+
+ # https://blender.stackexchange.com/questions/157828/how-to-duplicate-a-certain-collection-using-python
+ from collections import defaultdict
+ def copy_objects(from_col, to_col, linked, dupe_lut):
+ for o in from_col.objects:
+ dupe = o.copy()
+ if not linked and o.data:
+ dupe.data = dupe.data.copy()
+ to_col.objects.link(dupe)
+ dupe_lut[o] = dupe
+ def copy(parent, collection, linked=False):
+ dupe_lut = defaultdict(lambda : None)
+ def _copy(parent, collection, linked=False):
+ if collection.name == 'Bone Widgets':
+ return
+ cc = bpy.data.collections.new(collection.name)
+ copy_objects(collection, cc, linked, dupe_lut)
+ for c in collection.children:
+ _copy(cc, c, linked)
+ parent.children.link(cc)
+ _copy(parent, collection, linked)
+ for o, dupe in tuple(dupe_lut.items()):
+ parent = dupe_lut[o.parent]
+ if parent:
+ dupe.parent = parent
+
+ context = bpy.context
+ scene = context.scene
+ col = context.collection
+ assert(col is not scene.collection)
+ copy(scene.collection, col)
+
+ #setup materials for the combiner script
+ for obj in [o for o in bpy.data.collections[c.get_name() + '.001'].all_objects if not o.hide_get() and o.type == 'MESH']:
+ for mat in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
+ nodes = mat.node_tree.nodes
+ links = mat.node_tree.links
+ emissive_node = nodes.new('ShaderNodeEmission')
+ image_node = nodes.new('ShaderNodeTexImage')
+ links.new(emissive_node.inputs[0], image_node.outputs[0])
+ image_node.image = nodes['textures'].node_tree.nodes['light'].image
+ context.view_layer.objects.active = obj
+ bpy.ops.object.material_slot_remove_unused()
+
+ #update the modifiers
+ for mod in obj.modifiers:
+ if mod.type == 'ARMATURE':
+ #fix the armature modifier to use the copied aramture
+ copied_armature = [o for o in bpy.data.collections[c.get_name() + '.001'].all_objects if o.type == 'ARMATURE'][0]
+ mod.object = copied_armature
+ elif mod.type == 'SOLIDIFY':
+ #disable the outline on the atlased object because I don't feel like fixing it
+ obj.modifiers['Outline Modifier'].show_render = False
+ obj.modifiers['Outline Modifier'].show_viewport = False
+ elif mod.type == 'UV_WARP':
+ #fix the UV warp modifier to use the copied armature
+ copied_armature = [o for o in bpy.data.collections[c.get_name() + '.001'].all_objects if o.type == 'ARMATURE'][0]
+ mod.object_from = copied_armature
+ mod.object_to = copied_armature
+
+ return {'FINISHED'}
diff --git a/extras/matcombswitch.py b/extras/matcombswitch.py
new file mode 100644
index 0000000..ea1c8f4
--- /dev/null
+++ b/extras/matcombswitch.py
@@ -0,0 +1,27 @@
+import bpy
+from ..interface.dictionary_en import t
+from .. import common as c
+
+class mat_comb_switch(bpy.types.Operator):
+ bl_idname = "kkbp.matcombswitch"
+ bl_label = "Material combiner switch"
+ bl_description = t('mat_comb_switch_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ #toggle textures for the combiner script
+ for obj in [o for o in bpy.data.collections[c.get_name() + '.001'].all_objects if not o.hide_get() and o.type == 'MESH']:
+ for mat in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
+ nodes = mat.node_tree.nodes
+ #find the image node
+ image_node = None
+ for node in nodes:
+ if node.type == 'TEX_IMAGE':
+ image_node = node
+ if image_node:
+ #toggle light / dark state
+ image_node.image = nodes['textures'].node_tree.nodes['dark' if image_node.image == nodes['textures'].node_tree.nodes['light'].image else 'light'].image
+ context.view_layer.objects.active = obj
+ bpy.ops.object.material_slot_remove_unused()
+
+ return {'FINISHED'}
diff --git a/extras/resetmaterials.py b/extras/resetmaterials.py
new file mode 100644
index 0000000..e326700
--- /dev/null
+++ b/extras/resetmaterials.py
@@ -0,0 +1,18 @@
+import bpy
+from ..interface.dictionary_en import t
+from .. import common as c
+
+class reset_materials(bpy.types.Operator):
+ bl_idname = "kkbp.resetmaterials"
+ bl_label = "Reset KKBP materials"
+ bl_description = t('reset_mats_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ #setup materials for the combiner script
+ for obj in [o for o in bpy.data.collections[c.get_name()].all_objects if not o.hide_get() and o.type == 'MESH']:
+ for mat in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
+ if bpy.data.materials.get(mat.name + '-ORG'):
+ org_mat = bpy.data.materials.get(mat.name + '-ORG')
+ mat.user_remap(org_mat)
+ return {'FINISHED'}
diff --git a/extras/rigifyscripts/__pycache__/commons.cpython-311.pyc b/extras/rigifyscripts/__pycache__/commons.cpython-311.pyc
new file mode 100644
index 0000000..d361620
Binary files /dev/null and b/extras/rigifyscripts/__pycache__/commons.cpython-311.pyc differ
diff --git a/extras/rigifyscripts/__pycache__/rigify_after.cpython-311.pyc b/extras/rigifyscripts/__pycache__/rigify_after.cpython-311.pyc
new file mode 100644
index 0000000..3b4a360
Binary files /dev/null and b/extras/rigifyscripts/__pycache__/rigify_after.cpython-311.pyc differ
diff --git a/extras/rigifyscripts/__pycache__/rigify_before.cpython-311.pyc b/extras/rigifyscripts/__pycache__/rigify_before.cpython-311.pyc
new file mode 100644
index 0000000..939ff61
Binary files /dev/null and b/extras/rigifyscripts/__pycache__/rigify_before.cpython-311.pyc differ
diff --git a/extras/rigifyscripts/commons.py b/extras/rigifyscripts/commons.py
new file mode 100644
index 0000000..737cb81
--- /dev/null
+++ b/extras/rigifyscripts/commons.py
@@ -0,0 +1,1474 @@
+import bpy
+from typing import NamedTuple
+import mathutils
+from mathutils import Matrix
+import os
+import collections
+import json
+import re
+import bmesh
+from rna_prop_ui import rna_idprop_ui_create
+import random
+import string
+from ... import common as c
+
+def generateRandomAlphanumericString():
+ randomString = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
+ return randomString
+
+metarigIdBonePrefix = "Metarig_ID:"
+leftNamePrefix = "Left "
+rightNamePrefix = "Right "
+leftNameSuffix1 = "_L"
+rightNameSuffix1 = "_R"
+leftNameSuffix2 = ".L"
+rightNameSuffix2 = ".R"
+copyNameSuffix = " copy"
+renamedNameSuffix = " renamed"
+
+def leftNameToRightName(leftName):
+ if leftName.startswith(leftNamePrefix):
+ leftName = rightNamePrefix + leftName[len(leftNamePrefix):]
+ if leftName.endswith(leftNameSuffix1):
+ leftName = leftName[:len(leftName) - len(leftNameSuffix1)] + rightNameSuffix1
+ if leftName.endswith(leftNameSuffix2):
+ leftName = leftName[:len(leftName) - len(leftNameSuffix2)] + rightNameSuffix2
+ return leftName
+
+def bodyName():
+ return 'Body ' + c.get_name()
+def riggedTongueName():
+ return "Tongue (rigged) " + c.get_name()
+
+eyelidsShapeKeyName = "KK Eyes_default_cl"
+eyelidsShapeKeyCopyName = eyelidsShapeKeyName + copyNameSuffix
+
+widgetCollectionName = "Bone Widgets"
+widgetEyesName = "WidgetEyesRigify"
+widgetEyeLeftName = "WidgetEyeLeftRigify"
+widgetEyeRightName = "WidgetEyeRightRigify"
+widgetFaceName = "WidgetFace"
+originalWidgetBreastsName = "WidgetBust"
+originalWidgetBreastLeftName = "WidgetBreastL"
+originalWidgetBreastRightName = "WidgetBreastR"
+widgetBreastsName = "WidgetBreasts"
+widgetBreastLeftName = "WidgetBreastLeft"
+widgetBreastRightName = "WidgetBreastRight"
+widgetButtocksName = "WidgetButtocks"
+widgetButtockLeftName = "WidgetButtockLeft"
+widgetButtockRightName = "WidgetButtockRight"
+
+handleBoneSuffix = " handle"
+trackBoneSuffix = " track"
+markerBoneSuffix = " marker"
+parentBoneSuffix = " parent"
+placeholderBoneSuffix = " placeholder"
+xBoneSuffix = " x"
+yBoneSuffix = " y"
+zBoneSuffix = " z"
+
+originalRootBoneName = "Center"
+originalRootUpperBoneName = "cf_pv_root_upper"
+rootBoneName = "root"
+eyesXBoneName = "Eyesx"
+originalEyesBoneName = "Eye Controller"
+eyesBoneName = "Eyes target"
+leftEyeBoneName = "Left eye target"
+rightEyeBoneName = leftNameToRightName(leftEyeBoneName)
+eyesHandleBoneName = "Eyes" + handleBoneSuffix
+leftEyeHandleBoneName = "Left eye" + handleBoneSuffix
+rightEyeHandleBoneName = leftNameToRightName(leftEyeHandleBoneName)
+eyesTrackTargetBoneName = "Eyes track target"
+eyesTrackTargetParentBoneName = eyesTrackTargetBoneName + parentBoneSuffix
+eyesHandleMarkerBoneName = eyesHandleBoneName + markerBoneSuffix
+leftEyeHandleMarkerBoneName = leftEyeHandleBoneName + markerBoneSuffix
+rightEyeHandleMarkerBoneName = leftNameToRightName(leftEyeHandleMarkerBoneName)
+leftEyeHandleMarkerXBoneName = leftEyeHandleBoneName + markerBoneSuffix + xBoneSuffix
+rightEyeHandleMarkerXBoneName = leftNameToRightName(leftEyeHandleMarkerXBoneName)
+leftEyeHandleMarkerZBoneName = leftEyeHandleBoneName + markerBoneSuffix + zBoneSuffix
+rightEyeHandleMarkerZBoneName = leftNameToRightName(leftEyeHandleMarkerZBoneName)
+eyeballsBoneName = "Eyeballs"
+leftEyeballBoneName = "Left eyeball"
+rightEyeballBoneName = leftNameToRightName(leftEyeballBoneName)
+eyeballsTrackBoneName = eyeballsBoneName + trackBoneSuffix
+leftEyeballTrackBoneName = leftEyeballBoneName + trackBoneSuffix
+rightEyeballTrackBoneName = leftNameToRightName(leftEyeballTrackBoneName)
+leftEyeballTrackCorrectionBoneName = leftEyeballTrackBoneName + " correction"
+rightEyeballTrackCorrectionBoneName = leftNameToRightName(leftEyeballTrackCorrectionBoneName)
+riggedTongueLeftBoneBaseName = "cf_j_tang_L"
+riggedTongueRightBoneBaseName = leftNameToRightName(riggedTongueLeftBoneBaseName)
+riggedTongueBone1Name = "cf_j_tang_01"
+riggedTongueBone2Name = "cf_j_tang_02"
+riggedTongueBone3Name = "cf_j_tang_03"
+riggedTongueLeftBone3Name = riggedTongueLeftBoneBaseName + "_03"
+riggedTongueRightBone3Name = riggedTongueRightBoneBaseName + "_03"
+riggedTongueBone4Name = "cf_j_tang_04"
+riggedTongueLeftBone4Name = riggedTongueLeftBoneBaseName + "_04"
+riggedTongueRightBone4Name = riggedTongueRightBoneBaseName + "_04"
+riggedTongueBone5Name = "cf_j_tang_05"
+riggedTongueLeftBone5Name = riggedTongueLeftBoneBaseName + "_05"
+riggedTongueRightBone5Name = riggedTongueRightBoneBaseName + "_05"
+headBoneName = "Head"
+leftHeadMarkerXBoneName = leftNamePrefix + headBoneName + markerBoneSuffix + xBoneSuffix
+rightHeadMarkerXBoneName = leftNameToRightName(leftHeadMarkerXBoneName)
+leftHeadMarkerZBoneName = leftNamePrefix + headBoneName + markerBoneSuffix + zBoneSuffix
+rightHeadMarkerZBoneName = leftNameToRightName(leftHeadMarkerZBoneName)
+headTrackTargetBoneName = "Head track target"
+headTrackTargetParentBoneName = headTrackTargetBoneName + parentBoneSuffix
+headTrackBoneName = "Head" + trackBoneSuffix
+neckBoneName = "Neck"
+torsoBoneName = "torso"
+upperChestBoneName = "Upper Chest"
+chestBoneName = "Chest"
+spineBoneName = "Spine"
+hipsBoneName = "Hips"
+pelvisBoneName = "Pelvis"
+waistBoneName = "cf_j_waist02"
+crotchBoneName = "cf_j_kokan"
+anusBoneName = "cf_j_ana"
+betterPenetrationRootCrotchBoneName = "cf_J_Vagina_root"
+betterPenetrationFrontCrotchBoneName = "cf_J_Vagina_F"
+betterPenetrationLeftCrotchBoneBaseName = "cf_J_Vagina_L"
+betterPenetrationRightCrotchBoneBaseName = leftNameToRightName(betterPenetrationLeftCrotchBoneBaseName)
+betterPenetrationLeftCrotchBone1Name = betterPenetrationLeftCrotchBoneBaseName + ".001"
+betterPenetrationRightCrotchBone1Name = betterPenetrationRightCrotchBoneBaseName + ".001"
+betterPenetrationLeftCrotchBone2Name = betterPenetrationLeftCrotchBoneBaseName + ".002"
+betterPenetrationRightCrotchBone2Name = betterPenetrationRightCrotchBoneBaseName + ".002"
+betterPenetrationLeftCrotchBone3Name = betterPenetrationLeftCrotchBoneBaseName + ".003"
+betterPenetrationRightCrotchBone3Name = betterPenetrationRightCrotchBoneBaseName + ".003"
+betterPenetrationLeftCrotchBone4Name = betterPenetrationLeftCrotchBoneBaseName + ".004"
+betterPenetrationRightCrotchBone4Name = betterPenetrationRightCrotchBoneBaseName + ".004"
+betterPenetrationLeftCrotchBone5Name = betterPenetrationLeftCrotchBoneBaseName + ".005"
+betterPenetrationRightCrotchBone5Name = betterPenetrationRightCrotchBoneBaseName + ".005"
+betterPenetrationBackCrotchBoneName = "cf_J_Vagina_B"
+buttocksBoneName = "Buttocks"
+leftButtockBoneName = "cf_j_siri_L"
+rightButtockBoneName = leftNameToRightName(leftButtockBoneName)
+buttocksHandleBoneName = "Buttocks" + handleBoneSuffix
+leftButtockHandleBoneName = "Left Buttock" + handleBoneSuffix
+rightButtockHandleBoneName = leftNameToRightName(leftButtockHandleBoneName)
+breastsBoneName = "cf_d_bust00"
+leftBreastBone1Name = "cf_j_bust01_L"
+rightBreastBone1Name = leftNameToRightName(leftBreastBone1Name)
+leftBreastBone2Name = "cf_j_bust02_L"
+rightBreastBone2Name = leftNameToRightName(leftBreastBone2Name)
+leftBreastBone3Name = "cf_j_bust03_L"
+rightBreastBone3Name = leftNameToRightName(leftBreastBone3Name)
+leftNippleBone1Name = "cf_j_bnip02root_L"
+rightNippleBone1Name = leftNameToRightName(leftNippleBone1Name)
+leftNippleBone2Name = "cf_j_bnip02_L"
+rightNippleBone2Name = leftNameToRightName(leftNippleBone2Name)
+breastsHandleBoneName = "Breasts" + handleBoneSuffix
+leftBreastHandleBoneName = "Left Breast" + handleBoneSuffix
+rightBreastHandleBoneName = leftNameToRightName(leftBreastHandleBoneName)
+leftShoulderBoneName = "Left shoulder"
+rightShoulderBoneName = leftNameToRightName(leftShoulderBoneName)
+leftArmBoneName = "Left arm"
+rightArmBoneName = leftNameToRightName(leftArmBoneName)
+leftElbowBoneName = "Left elbow"
+rightElbowBoneName = leftNameToRightName(leftElbowBoneName)
+originalLeftElbowPoleBoneName = "cf_pv_elbo_L"
+originalRightElbowPoleBoneName = leftNameToRightName(originalLeftElbowPoleBoneName)
+originalLeftWristBoneName = "cf_pv_hand_L"
+originalRightWristBoneName = leftNameToRightName(originalLeftWristBoneName)
+leftWristBoneName = "Left wrist"
+rightWristBoneName = leftNameToRightName(leftWristBoneName)
+leftThumbBone1Name = "Thumb0_L"
+rightThumbBone1Name = leftNameToRightName(leftThumbBone1Name)
+leftThumbBone2Name = "Thumb1_L"
+rightThumbBone2Name = leftNameToRightName(leftThumbBone2Name)
+leftThumbBone3Name = "Thumb2_L"
+rightThumbBone3Name = leftNameToRightName(leftThumbBone3Name)
+leftIndexFingerPalmBoneName = "IndexFingerPalm_L"
+rightIndexFingerPalmBoneName = leftNameToRightName(leftIndexFingerPalmBoneName)
+leftIndexFingerBone1Name = "IndexFinger1_L"
+rightIndexFingerBone1Name = leftNameToRightName(leftIndexFingerBone1Name)
+leftIndexFingerBone2Name = "IndexFinger2_L"
+rightIndexFingerBone2Name = leftNameToRightName(leftIndexFingerBone2Name)
+leftIndexFingerBone3Name = "IndexFinger3_L"
+rightIndexFingerBone3Name = leftNameToRightName(leftIndexFingerBone3Name)
+leftMiddleFingerPalmBoneName = "MiddleFingerPalm_L"
+rightMiddleFingerPalmBoneName = leftNameToRightName(leftMiddleFingerPalmBoneName)
+leftMiddleFingerBone1Name = "MiddleFinger1_L"
+rightMiddleFingerBone1Name = leftNameToRightName(leftMiddleFingerBone1Name)
+leftMiddleFingerBone2Name = "MiddleFinger2_L"
+rightMiddleFingerBone2Name = leftNameToRightName(leftMiddleFingerBone2Name)
+leftMiddleFingerBone3Name = "MiddleFinger3_L"
+rightMiddleFingerBone3Name = leftNameToRightName(leftMiddleFingerBone3Name)
+leftRingFingerPalmBoneName = "RingFingerPalm_L"
+rightRingFingerPalmBoneName = leftNameToRightName(leftRingFingerPalmBoneName)
+leftRingFingerBone1Name = "RingFinger1_L"
+rightRingFingerBone1Name = leftNameToRightName(leftRingFingerBone1Name)
+leftRingFingerBone2Name = "RingFinger2_L"
+rightRingFingerBone2Name = leftNameToRightName(leftRingFingerBone2Name)
+leftRingFingerBone3Name = "RingFinger3_L"
+rightRingFingerBone3Name = leftNameToRightName(leftRingFingerBone3Name)
+leftLittleFingerPalmBoneName = "LittleFingerPalm_L"
+rightLittleFingerPalmBoneName = leftNameToRightName(leftLittleFingerPalmBoneName)
+leftLittleFingerBone1Name = "LittleFinger1_L"
+rightLittleFingerBone1Name = leftNameToRightName(leftLittleFingerBone1Name)
+leftLittleFingerBone2Name = "LittleFinger2_L"
+rightLittleFingerBone2Name = leftNameToRightName(leftLittleFingerBone2Name)
+leftLittleFingerBone3Name = "LittleFinger3_L"
+rightLittleFingerBone3Name = leftNameToRightName(leftLittleFingerBone3Name)
+leftLegBoneName = "Left leg"
+rightLegBoneName = leftNameToRightName(leftLegBoneName)
+leftKneeBoneName = "Left knee"
+rightKneeBoneName = leftNameToRightName(leftKneeBoneName)
+originalLeftKneePoleBoneName = "cf_pv_knee_L"
+originalRightKneePoleBoneName = leftNameToRightName(originalLeftKneePoleBoneName)
+originalLeftAnkleBoneName = "MasterFootIK.L"
+originalRightAnkleBoneName = leftNameToRightName(originalLeftAnkleBoneName)
+leftAnkleBoneName = "Left ankle"
+rightAnkleBoneName = leftNameToRightName(leftAnkleBoneName)
+originalLeftToeBoneName = "ToeRotator.L"
+originalRightToeBoneName = leftNameToRightName(originalLeftToeBoneName)
+leftToeBoneName = "Left toe"
+rightToeBoneName = leftNameToRightName(leftToeBoneName)
+originalLeftHeelIkBoneName = "HeelIK.L"
+originalRightHeelIkBoneName = leftNameToRightName(originalLeftHeelIkBoneName)
+originalLeftHeelBoneName = "a_n_heel_L"
+originalRightHeelBoneName = leftNameToRightName(originalLeftHeelBoneName)
+leftHeelBoneName = "Left heel"
+rightHeelBoneName = leftNameToRightName(leftHeelBoneName)
+
+skirtPalmBonePrefix = "cf_d_sk"
+skirtParentBoneName = skirtPalmBonePrefix + "_top"
+skirtParentBoneCopyName = skirtParentBoneName + copyNameSuffix
+skirtBonePrefix = "cf_j_sk"
+
+def getSkirtBoneName(palm, primaryIndex, secondaryIndex = 0):
+ if palm:
+ prefix = skirtPalmBonePrefix
+ else:
+ prefix = skirtBonePrefix
+ return prefix + "_" + str(primaryIndex).zfill(2) + "_" + str(secondaryIndex).zfill(2)
+
+originalBonePrefix = "ORG-"
+
+deformBonePrefix = "DEF-"
+leftEyeDeformBoneName = "Left Eye"
+rightEyeDeformBoneName = "Right Eye"
+headDeformBoneName = deformBonePrefix + headBoneName
+neckDeformBoneName = deformBonePrefix + neckBoneName
+upperChestDeformBoneName = deformBonePrefix + upperChestBoneName
+chestDeformBoneName = deformBonePrefix + chestBoneName
+spineDeformBoneName = deformBonePrefix + spineBoneName
+hipsDeformBoneName = deformBonePrefix + hipsBoneName
+leftButtockDeformBoneName = "cf_s_siri_L"
+rightButtockDeformBoneName = leftNameToRightName(leftButtockDeformBoneName)
+leftBreastDeformBone1Name = "cf_s_bust01_L"
+rightBreastDeformBone1Name = leftNameToRightName(leftBreastDeformBone1Name)
+leftBreastDeformBone2Name = "cf_s_bust02_L"
+rightBreastDeformBone2Name = leftNameToRightName(leftBreastDeformBone2Name)
+leftBreastDeformBone3Name = "cf_s_bust03_L"
+rightBreastDeformBone3Name = leftNameToRightName(leftBreastDeformBone3Name)
+leftNippleDeformBone1Name = "cf_s_bnip01_L"
+rightNippleDeformBone1Name = leftNameToRightName(leftNippleDeformBone1Name)
+leftNippleDeformBone2Name = "cf_s_bnip025_L"
+rightNippleDeformBone2Name = leftNameToRightName(leftNippleDeformBone2Name)
+leftShoulderDeformBoneName = "cf_s_shoulder02_L"
+rightShoulderDeformBoneName = leftNameToRightName(leftShoulderDeformBoneName)
+leftArmDeformBone1Name = deformBonePrefix + leftArmBoneName
+rightArmDeformBone1Name = deformBonePrefix + rightArmBoneName
+leftArmDeformBone2Name = deformBonePrefix + leftArmBoneName + ".001"
+rightArmDeformBone2Name = deformBonePrefix + rightArmBoneName + ".001"
+leftArmDeformBone3Name = deformBonePrefix + leftArmBoneName + ".002"
+rightArmDeformBone3Name = deformBonePrefix + rightArmBoneName + ".002"
+leftElbowDeformBone1Name = deformBonePrefix + leftElbowBoneName
+rightElbowDeformBone1Name = deformBonePrefix + rightElbowBoneName
+leftElbowDeformBone2Name = deformBonePrefix + leftElbowBoneName + ".001"
+rightElbowDeformBone2Name = deformBonePrefix + rightElbowBoneName + ".001"
+leftElbowDeformBone3Name = deformBonePrefix + leftElbowBoneName + ".002"
+rightElbowDeformBone3Name = deformBonePrefix + rightElbowBoneName + ".002"
+leftWristDeformBoneName = deformBonePrefix + leftWristBoneName
+rightWristDeformBoneName = deformBonePrefix + rightWristBoneName
+leftThighDeformBone1Name = deformBonePrefix + leftLegBoneName
+rightThighDeformBone1Name = deformBonePrefix + rightLegBoneName
+leftThighDeformBone2Name = deformBonePrefix + leftLegBoneName + ".001"
+rightThighDeformBone2Name = deformBonePrefix + rightLegBoneName + ".001"
+leftThighDeformBone3Name = deformBonePrefix + leftLegBoneName + ".002"
+rightThighDeformBone3Name = deformBonePrefix + rightLegBoneName + ".002"
+leftLegDeformBone1Name = deformBonePrefix + leftKneeBoneName
+rightLegDeformBone1Name = deformBonePrefix + rightKneeBoneName
+leftLegDeformBone2Name = deformBonePrefix + leftKneeBoneName + ".001"
+rightLegDeformBone2Name = deformBonePrefix + rightKneeBoneName + ".001"
+leftLegDeformBone3Name = deformBonePrefix + leftKneeBoneName + ".002"
+rightLegDeformBone3Name = deformBonePrefix + rightKneeBoneName + ".002"
+leftAnkleDeformBoneName = deformBonePrefix + leftAnkleBoneName
+rightAnkleDeformBoneName = deformBonePrefix + rightAnkleBoneName
+leftToeDeformBoneName = deformBonePrefix + leftToeBoneName
+rightToeDeformBoneName = deformBonePrefix + rightToeBoneName
+leftThumbDeformBone1Name = deformBonePrefix + leftThumbBone1Name
+rightThumbDeformBone1Name = deformBonePrefix + rightThumbBone1Name
+leftThumbDeformBone2Name = deformBonePrefix + leftThumbBone2Name
+rightThumbDeformBone2Name = deformBonePrefix + rightThumbBone2Name
+leftThumbDeformBone3Name = deformBonePrefix + leftThumbBone3Name
+rightThumbDeformBone3Name = deformBonePrefix + rightThumbBone3Name
+leftIndexFingerPalmDeformBoneName = deformBonePrefix + leftIndexFingerPalmBoneName
+rightIndexFingerPalmDeformBoneName = deformBonePrefix + rightIndexFingerPalmBoneName
+leftIndexFingerDeformBone1Name = deformBonePrefix + leftIndexFingerBone1Name
+rightIndexFingerDeformBone1Name = deformBonePrefix + rightIndexFingerBone1Name
+leftIndexFingerDeformBone2Name = deformBonePrefix + leftIndexFingerBone2Name
+rightIndexFingerDeformBone2Name = deformBonePrefix + rightIndexFingerBone2Name
+leftIndexFingerDeformBone3Name = deformBonePrefix + leftIndexFingerBone3Name
+rightIndexFingerDeformBone3Name = deformBonePrefix + rightIndexFingerBone3Name
+leftMiddleFingerPalmDeformBoneName = deformBonePrefix + leftMiddleFingerPalmBoneName
+rightMiddleFingerPalmDeformBoneName = deformBonePrefix + rightMiddleFingerPalmBoneName
+leftMiddleFingerDeformBone1Name = deformBonePrefix + leftMiddleFingerBone1Name
+rightMiddleFingerDeformBone1Name = deformBonePrefix + rightMiddleFingerBone1Name
+leftMiddleFingerDeformBone2Name = deformBonePrefix + leftMiddleFingerBone2Name
+rightMiddleFingerDeformBone2Name = deformBonePrefix + rightMiddleFingerBone2Name
+leftMiddleFingerDeformBone3Name = deformBonePrefix + leftMiddleFingerBone3Name
+rightMiddleFingerDeformBone3Name = deformBonePrefix + rightMiddleFingerBone3Name
+leftRingFingerPalmDeformBoneName = deformBonePrefix + leftRingFingerPalmBoneName
+rightRingFingerPalmDeformBoneName = deformBonePrefix + rightRingFingerPalmBoneName
+leftRingFingerDeformBone1Name = deformBonePrefix + leftRingFingerBone1Name
+rightRingFingerDeformBone1Name = deformBonePrefix + rightRingFingerBone1Name
+leftRingFingerDeformBone2Name = deformBonePrefix + leftRingFingerBone2Name
+rightRingFingerDeformBone2Name = deformBonePrefix + rightRingFingerBone2Name
+leftRingFingerDeformBone3Name = deformBonePrefix + leftRingFingerBone3Name
+rightRingFingerDeformBone3Name = deformBonePrefix + rightRingFingerBone3Name
+leftLittleFingerPalmDeformBoneName = deformBonePrefix + leftLittleFingerPalmBoneName
+rightLittleFingerPalmDeformBoneName = deformBonePrefix + rightLittleFingerPalmBoneName
+leftLittleFingerDeformBone1Name = deformBonePrefix + leftLittleFingerBone1Name
+rightLittleFingerDeformBone1Name = deformBonePrefix + rightLittleFingerBone1Name
+leftLittleFingerDeformBone2Name = deformBonePrefix + leftLittleFingerBone2Name
+rightLittleFingerDeformBone2Name = deformBonePrefix + rightLittleFingerBone2Name
+leftLittleFingerDeformBone3Name = deformBonePrefix + leftLittleFingerBone3Name
+rightLittleFingerDeformBone3Name = deformBonePrefix + rightLittleFingerBone3Name
+
+def getSkirtDeformBoneName(primaryIndex, secondaryIndex):
+ return deformBonePrefix + getSkirtBoneName(False, primaryIndex, secondaryIndex)
+
+originalFaceUpDeformBoneName = "cf_J_FaceUp_ty"
+originalFaceBaseDeformBoneName = "cf_J_FaceBase"
+originalHeadDeformBoneName = "cf_s_head"
+originalNeckDeformBoneName = "cf_s_neck"
+originalUpperChestDeformBoneName = "cf_s_spine03"
+originalChestDeformBoneName = "cf_s_spine02"
+originalSpineDeformBoneName = "cf_s_spine01"
+originalHipsDeformBoneName = "cf_s_waist01"
+originalLeftShoulderDeformBoneName = "cf_s_shoulder02_L"
+originalRightShoulderDeformBoneName = leftNameToRightName(originalLeftShoulderDeformBoneName)
+originalLeftArmDeformBone1Name = "cf_s_arm01_L"
+originalRightArmDeformBone1Name = leftNameToRightName(originalLeftArmDeformBone1Name)
+originalLeftArmDeformBone2Name = "cf_s_arm02_L"
+originalRightArmDeformBone2Name = leftNameToRightName(originalLeftArmDeformBone2Name)
+originalLeftArmDeformBone3Name = "cf_s_arm03_L"
+originalRightArmDeformBone3Name = leftNameToRightName(originalLeftArmDeformBone3Name)
+originalLeftElbowDeformBone1Name = "cf_s_forearm01_L"
+originalRightElbowDeformBone1Name = leftNameToRightName(originalLeftElbowDeformBone1Name)
+originalLeftElbowDeformBone2Name = "cf_s_forearm02_L"
+originalRightElbowDeformBone2Name = leftNameToRightName(originalLeftElbowDeformBone2Name)
+originalLeftElbowDeformBone3Name = "cf_s_wrist_L"
+originalRightElbowDeformBone3Name = leftNameToRightName(originalLeftElbowDeformBone3Name)
+originalLeftWristDeformBoneName = "cf_s_hand_L"
+originalRightWristDeformBoneName = leftNameToRightName(originalLeftWristDeformBoneName)
+originalLeftThumbDeformBone1Name = leftThumbBone1Name
+originalRightThumbDeformBone1Name = rightThumbBone1Name
+originalLeftThumbDeformBone2Name = leftThumbBone2Name
+originalRightThumbDeformBone2Name = rightThumbBone2Name
+originalLeftThumbDeformBone3Name = leftThumbBone3Name
+originalRightThumbDeformBone3Name = rightThumbBone3Name
+originalLeftIndexFingerDeformBone1Name = leftIndexFingerBone1Name
+originalRightIndexFingerDeformBone1Name = rightIndexFingerBone1Name
+originalLeftIndexFingerDeformBone2Name = leftIndexFingerBone2Name
+originalRightIndexFingerDeformBone2Name = rightIndexFingerBone2Name
+originalLeftIndexFingerDeformBone3Name = leftIndexFingerBone3Name
+originalRightIndexFingerDeformBone3Name = rightIndexFingerBone3Name
+originalLeftMiddleFingerDeformBone1Name = leftMiddleFingerBone1Name
+originalRightMiddleFingerDeformBone1Name = rightMiddleFingerBone1Name
+originalLeftMiddleFingerDeformBone2Name = leftMiddleFingerBone2Name
+originalRightMiddleFingerDeformBone2Name = rightMiddleFingerBone2Name
+originalLeftMiddleFingerDeformBone3Name = leftMiddleFingerBone3Name
+originalRightMiddleFingerDeformBone3Name = rightMiddleFingerBone3Name
+originalLeftRingFingerDeformBone1Name = leftRingFingerBone1Name
+originalRightRingFingerDeformBone1Name = rightRingFingerBone1Name
+originalLeftRingFingerDeformBone2Name = leftRingFingerBone2Name
+originalRightRingFingerDeformBone2Name = rightRingFingerBone2Name
+originalLeftRingFingerDeformBone3Name = leftRingFingerBone3Name
+originalRightRingFingerDeformBone3Name = rightRingFingerBone3Name
+originalLeftLittleFingerDeformBone1Name = leftLittleFingerBone1Name
+originalRightLittleFingerDeformBone1Name = rightLittleFingerBone1Name
+originalLeftLittleFingerDeformBone2Name = leftLittleFingerBone2Name
+originalRightLittleFingerDeformBone2Name = rightLittleFingerBone2Name
+originalLeftLittleFingerDeformBone3Name = leftLittleFingerBone3Name
+originalRightLittleFingerDeformBone3Name = rightLittleFingerBone3Name
+originalLeftThighDeformBone1Name = "cf_s_thigh01_L"
+originalRightThighDeformBone1Name = leftNameToRightName(originalLeftThighDeformBone1Name)
+originalLeftThighDeformBone2Name = "cf_s_thigh02_L"
+originalRightThighDeformBone2Name = leftNameToRightName(originalLeftThighDeformBone2Name)
+originalLeftThighDeformBone3Name = "cf_s_thigh03_L"
+originalRightThighDeformBone3Name = leftNameToRightName(originalLeftThighDeformBone3Name)
+originalLeftLegDeformBone1Name = "cf_s_leg01_L"
+originalRightLegDeformBone1Name = leftNameToRightName(originalLeftLegDeformBone1Name)
+originalLeftLegDeformBone2Name = "cf_s_leg02_L"
+originalRightLegDeformBone2Name = leftNameToRightName(originalLeftLegDeformBone2Name)
+originalLeftLegDeformBone3Name = "cf_s_leg03_L"
+originalRightLegDeformBone3Name = leftNameToRightName(originalLeftLegDeformBone3Name)
+
+tweakBoneSuffix = "_tweak"
+parentBoneSuffix = "_parent"
+fkBoneSuffix = "_fk"
+ikBoneSuffix = "_ik"
+masterBoneSuffix = "_master"
+headTweakBoneName = "head"
+leftArmTweakBone1Name = leftArmBoneName + tweakBoneSuffix
+rightArmTweakBone1Name = leftNameToRightName(leftArmTweakBone1Name)
+leftArmParentBoneName = leftArmBoneName + parentBoneSuffix
+rightArmParentBoneName = leftNameToRightName(leftArmParentBoneName)
+leftArmIkBoneName = leftArmBoneName + ikBoneSuffix
+rightArmArmIkBoneName = leftNameToRightName(leftArmIkBoneName)
+leftWristFkBoneName = leftWristBoneName + fkBoneSuffix
+rightWristFkBoneName = leftNameToRightName(leftWristFkBoneName)
+leftWristIkBoneName = leftWristBoneName + ikBoneSuffix
+rightWristIkBoneName = leftNameToRightName(leftWristIkBoneName)
+leftWristTweakBoneName = leftWristBoneName + tweakBoneSuffix
+rightWristTweakBoneName = leftNameToRightName(leftWristTweakBoneName)
+leftThumbTweakBoneName = leftThumbBone1Name + ".001"
+rightThumbTweakBoneName = leftNameToRightName(leftThumbTweakBoneName)
+leftThumbIkBoneName = leftThumbBone1Name[:leftThumbBone1Name.index(leftNameSuffix1)] + ikBoneSuffix + leftNameSuffix1
+rightThumbIkBoneName = leftNameToRightName(leftThumbIkBoneName)
+leftThumbMasterBoneName = leftThumbBone1Name[:leftThumbBone1Name.index(leftNameSuffix1)] + masterBoneSuffix + leftNameSuffix1
+rightThumbMasterBoneName = leftNameToRightName(leftThumbMasterBoneName)
+leftIndexFingerTweakBoneName = leftIndexFingerBone1Name + ".001"
+rightIndexFingerTweakBoneName = leftNameToRightName(leftIndexFingerTweakBoneName)
+leftIndexFingerIkBoneName = leftIndexFingerBone1Name[:leftIndexFingerBone1Name.index(leftNameSuffix1)] + ikBoneSuffix + leftNameSuffix1
+rightIndexFingerIkBoneName = leftNameToRightName(leftIndexFingerIkBoneName)
+leftIndexFingerMasterBoneName = leftIndexFingerBone1Name[:leftIndexFingerBone1Name.index(leftNameSuffix1)] + masterBoneSuffix + leftNameSuffix1
+rightIndexFingerMasterBoneName = leftNameToRightName(leftIndexFingerMasterBoneName)
+leftMiddleFingerTweakBoneName = leftMiddleFingerBone1Name + ".001"
+rightMiddleFingerTweakBoneName = leftNameToRightName(leftMiddleFingerTweakBoneName)
+leftMiddleFingerIkBoneName = leftMiddleFingerBone1Name[:leftMiddleFingerBone1Name.index(leftNameSuffix1)] + ikBoneSuffix + leftNameSuffix1
+rightMiddleFingerIkBoneName = leftNameToRightName(leftMiddleFingerIkBoneName)
+leftMiddleFingerMasterBoneName = leftMiddleFingerBone1Name[:leftMiddleFingerBone1Name.index(leftNameSuffix1)] + masterBoneSuffix + leftNameSuffix1
+rightMiddleFingerMasterBoneName = leftNameToRightName(leftMiddleFingerMasterBoneName)
+leftRingFingerTweakBoneName = leftRingFingerBone1Name + ".001"
+rightRingFingerTweakBoneName = leftNameToRightName(leftRingFingerTweakBoneName)
+leftRingFingerIkBoneName = leftRingFingerBone1Name[:leftRingFingerBone1Name.index(leftNameSuffix1)] + ikBoneSuffix + leftNameSuffix1
+rightRingFingerIkBoneName = leftNameToRightName(leftRingFingerIkBoneName)
+leftRingFingerMasterBoneName = leftRingFingerBone1Name[:leftRingFingerBone1Name.index(leftNameSuffix1)] + masterBoneSuffix + leftNameSuffix1
+rightingFingerMasterBoneName = leftNameToRightName(leftRingFingerMasterBoneName)
+leftLittleFingerPalmFkBoneName = leftLittleFingerPalmBoneName[:leftLittleFingerPalmBoneName.index(leftNameSuffix1)] + fkBoneSuffix + leftNameSuffix1
+rightLittleFingerPalmFkBoneName = leftNameToRightName(leftLittleFingerPalmFkBoneName)
+leftLittleFingerTweakBoneName = leftLittleFingerBone1Name + ".001"
+rightLittleFingerTweakBoneName = leftNameToRightName(leftLittleFingerTweakBoneName)
+leftLittleFingerIkBoneName = leftLittleFingerBone1Name[:leftLittleFingerBone1Name.index(leftNameSuffix1)] + ikBoneSuffix + leftNameSuffix1
+rightLittleFingerIkBoneName = leftNameToRightName(leftLittleFingerIkBoneName)
+leftLittleFingerMasterBoneName = leftLittleFingerBone1Name[:leftLittleFingerBone1Name.index(leftNameSuffix1)] + masterBoneSuffix + leftNameSuffix1
+rightLittleFingerMasterBoneName = leftNameToRightName(leftLittleFingerMasterBoneName)
+leftLegTweakBone1Name = leftLegBoneName + tweakBoneSuffix
+rightLegTweakBone1Name = leftNameToRightName(leftLegTweakBone1Name)
+leftLegTweakBone2Name = leftLegBoneName + tweakBoneSuffix + ".001"
+rightLegTweakBone2Name = leftNameToRightName(leftLegTweakBone2Name)
+leftLegTweakBone3Name = leftLegBoneName + tweakBoneSuffix + ".002"
+rightLegTweakBone3Name = leftNameToRightName(leftLegTweakBone3Name)
+leftLegParentBoneName = leftLegBoneName + parentBoneSuffix
+rightLegParentBoneName = leftNameToRightName(leftLegParentBoneName)
+leftLegIkBoneName = leftLegBoneName + ikBoneSuffix
+rightLegIkBoneName = leftNameToRightName(leftLegIkBoneName)
+leftLegFkBoneName = leftLegBoneName + fkBoneSuffix
+rightLegFkBoneName = leftNameToRightName(leftLegFkBoneName)
+leftKneeTweakBone1Name = leftKneeBoneName + tweakBoneSuffix
+rightKneeTweakBone1Name = leftNameToRightName(leftKneeTweakBone1Name)
+leftKneeTweakBone2Name = leftKneeBoneName + tweakBoneSuffix + ".001"
+rightKneeTweakBone2Name = leftNameToRightName(leftKneeTweakBone2Name)
+leftKneeTweakBone3Name = leftKneeBoneName + tweakBoneSuffix + ".002"
+rightKneeTweakBone3Name = leftNameToRightName(leftKneeTweakBone3Name)
+leftKneeFkBoneName = leftKneeBoneName + fkBoneSuffix
+rightKneeFkBoneName = leftNameToRightName(leftKneeFkBoneName)
+
+waistJointCorrectionBoneName = "cf_s_waist02"
+leftButtockJointCorrectionBoneName = "cf_d_siri_L"
+rightButtockJointCorrectionBoneName = leftNameToRightName(leftButtockJointCorrectionBoneName)
+leftShoulderJointCorrectionBoneName = "cf_d_shoulder02_L"
+rightShoulderJointCorrectionBoneName = leftNameToRightName(leftShoulderJointCorrectionBoneName)
+frontLeftElbowJointCorrectionBoneName = "cf_s_elbo_L"
+frontRightElbowJointCorrectionBoneName = leftNameToRightName(frontLeftElbowJointCorrectionBoneName)
+midLeftElbowJointCorrectionBoneName = "cf_s_forearm01_L"
+midRightElbowJointCorrectionBoneName = leftNameToRightName(midLeftElbowJointCorrectionBoneName)
+backLeftElbowJointCorrectionBoneName = "cf_s_elboback_L";
+backRightElbowJointCorrectionBoneName = leftNameToRightName(backLeftElbowJointCorrectionBoneName)
+leftWristJointCorrectionBoneName = "cf_d_hand_L"
+rightWristJointCorrectionBoneName = leftNameToRightName(leftWristJointCorrectionBoneName)
+leftLegJointCorrectionBoneName = "cf_s_leg_L"
+rightLegJointCorrectionBoneName = leftNameToRightName(leftLegJointCorrectionBoneName)
+frontLeftKneeJointCorrectionBoneName = "cf_d_kneeF_L";
+frontRightKneeJointCorrectionBoneName = leftNameToRightName(frontLeftKneeJointCorrectionBoneName)
+midLeftKneeJointCorrectionBoneName = "cf_s_leg01_L"
+midRightKneeJointCorrectionBoneName = leftNameToRightName(midLeftKneeJointCorrectionBoneName)
+backLeftKneeJointCorrectionBoneName = "cf_s_kneeB_L";
+backRightKneeJointCorrectionBoneName = leftNameToRightName(backLeftKneeJointCorrectionBoneName)
+
+def duplicateShapeKey(objectName, shapeKeyName, shapeKeyCopyName):
+ object = bpy.data.objects[objectName]
+ for shapeKey in object.data.shape_keys.key_blocks:
+ if shapeKey.name == shapeKeyName:
+ shapeKey.mute = False
+ oldShapeKeyValue = shapeKey.value
+ shapeKey.value = 1
+ elif shapeKey.name == shapeKeyCopyName:
+ if object.data.shape_keys.animation_data:
+ for driver in object.data.shape_keys.animation_data.drivers:
+ if driver.data_path.startswith("key_blocks"):
+ ownerName = driver.data_path.split('"')[1]
+ if ownerName == shapeKey.name:
+ object.data.shape_keys.animation_data.drivers.remove(driver)
+ object.shape_key_remove(key = shapeKey)
+ else:
+ shapeKey.mute = True
+ shapeKeyCopy = object.shape_key_add(name = shapeKeyCopyName, from_mix = True)
+ shapeKeyCopy.value = 0
+ for shapeKey in object.data.shape_keys.key_blocks:
+ shapeKey.mute = False
+ if shapeKey.name == shapeKeyName:
+ shapeKey.value = oldShapeKeyValue
+ return shapeKeyCopy
+
+def isVertexGroupEmpty(vertexGroupName, objectName):
+ object = bpy.data.objects[objectName]
+ vertexGroup = object.vertex_groups[vertexGroupName]
+ return not any(vertexGroup.index in [g.group for g in v.groups] for v in object.data.vertices)
+
+class Extremities:
+ vertices = None
+ coordinates = None
+ minX = None
+ minY = None
+ minZ = None
+ maxX = None
+ maxY = None
+ maxZ = None
+
+def returnLower(value1, value2):
+ if value1 is None or value2 < value1:
+ return value2
+ return value1
+
+def returnHigher(value1, value2):
+ if value1 is None or value2 > value1:
+ return value2
+ return value1
+
+def findVertexGroupExtremities(vertexGroupName, objectName):
+ extremities = Extremities()
+ object = bpy.data.objects[objectName]
+ coordinates = [(object.matrix_world @ v.co) for v in object.data.vertices]
+ vertexGroupIndex = object.vertex_groups[vertexGroupName].index
+ vertices = [ v for v in object.data.vertices if vertexGroupIndex in [ vg.group for vg in v.groups ] ]
+ extremities.vertices = vertices
+ extremities.coordinates = coordinates
+ for vertex in vertices:
+ extremities.minX = returnLower(extremities.minX, coordinates[vertex.index][0])
+ extremities.minY = returnLower(extremities.minY, coordinates[vertex.index][1])
+ extremities.minZ = returnLower(extremities.minZ, coordinates[vertex.index][2])
+ extremities.maxX = returnHigher(extremities.maxX, coordinates[vertex.index][0])
+ extremities.maxY = returnHigher(extremities.maxY, coordinates[vertex.index][1])
+ extremities.maxZ = returnHigher(extremities.maxZ, coordinates[vertex.index][2])
+ return extremities
+
+copyTransformsConstraintBaseName = "Copy Transforms"
+copyRotationConstraintBaseName = "Copy Rotation"
+transformationConstraintBaseName = "Transformation"
+limitLocationConstraintBaseName = "Limit Location"
+limitRotationConstraintBaseName = "Limit Rotation"
+armatureConstraintBaseName = "Armature"
+dampedTrackConstraintBaseName = "Damped Track"
+handleConstraintSuffix = "_Handle"
+jointConstraintSuffix = "_Joint"
+eyeballConstraintSuffix = "_Eyeball"
+headConstraintSuffix = "_Head"
+parentConstraintSuffix = "_Parent"
+trackConstraintSuffix = "_Track"
+locationConstraintSuffix = " Location"
+rotationConstraintSuffix = " Rotation"
+scaleConstraintSuffix = " Scale"
+correctionConstraintSuffix = " Correction"
+minConstraintSuffix = " Min"
+maxConstraintSuffix = " Max"
+
+def removeConstraint(rig, boneName, constraintName):
+ constraint = rig.pose.bones[boneName].constraints.get(constraintName)
+ if constraint:
+ rig.pose.bones[boneName].constraints.remove(constraint)
+
+def changeConstraintIndex(rig, boneName, constraintName, newIndex):
+ bone = rig.pose.bones[boneName]
+ for index, constraint in enumerate(bone.constraints):
+ if constraint.name == constraintName:
+ bone.constraints.move(index, newIndex)
+ break
+
+def addCopyTransformsConstraint(rig, boneName, subTargetBoneName, mixMode, space, constraintName):
+ removeConstraint(rig, boneName, constraintName)
+ copyTransformsConstraint = rig.pose.bones[boneName].constraints.new('COPY_TRANSFORMS')
+ copyTransformsConstraint.name = constraintName
+ copyTransformsConstraint.target = rig
+ copyTransformsConstraint.subtarget = subTargetBoneName
+ copyTransformsConstraint.mix_mode = mixMode
+ copyTransformsConstraint.owner_space = space
+ copyTransformsConstraint.target_space = space
+ return copyTransformsConstraint
+
+def addCopyRotationConstraint(rig, boneName, subTargetBoneName, mixMode, space, constraintName,
+useX, invertX, useY, invertY, useZ, invertZ):
+ removeConstraint(rig, boneName, constraintName)
+ copyRotationConstraint = rig.pose.bones[boneName].constraints.new('COPY_ROTATION')
+ copyRotationConstraint.name = constraintName
+ copyRotationConstraint.target = rig
+ copyRotationConstraint.subtarget = subTargetBoneName
+ copyRotationConstraint.mix_mode = mixMode
+ copyRotationConstraint.owner_space = space
+ copyRotationConstraint.target_space = space
+ copyRotationConstraint.use_x = useX
+ copyRotationConstraint.invert_x = invertX
+ copyRotationConstraint.use_y = useY
+ copyRotationConstraint.invert_y = invertY
+ copyRotationConstraint.use_z = useZ
+ copyRotationConstraint.invert_z = invertZ
+ return copyRotationConstraint
+
+def addCopyScaleConstraint(rig, boneName, targetRig, subTargetBoneName, space, constraintName,
+useX, useY, useZ):
+ removeConstraint(rig, boneName, constraintName)
+ copyRotationConstraint = rig.pose.bones[boneName].constraints.new('COPY_SCALE')
+ copyRotationConstraint.name = constraintName
+ copyRotationConstraint.target = targetRig
+ copyRotationConstraint.subtarget = subTargetBoneName
+ copyRotationConstraint.owner_space = space
+ copyRotationConstraint.target_space = space
+ copyRotationConstraint.use_x = useX
+ copyRotationConstraint.use_y = useY
+ copyRotationConstraint.use_z = useZ
+ return copyRotationConstraint
+
+def addTransformationConstraint(rig, boneName, subTargetBoneName, mixMode, space, constraintName,
+mapFrom, fromRotationMode, fromMinX, fromMaxX, fromMinY, fromMaxY, fromMinZ, fromMaxZ,
+mapTo, toEulerOrder, toMinX, toMaxX, toMinY, toMaxY, toMinZ, toMaxZ,
+mapToXFrom = 'X', mapToYFrom = 'Y', mapToZFrom = 'Z'):
+ removeConstraint(rig, boneName, constraintName)
+ transformationConstraint = rig.pose.bones[boneName].constraints.new('TRANSFORM')
+ transformationConstraint.name = constraintName
+ transformationConstraint.target = rig
+ transformationConstraint.subtarget = subTargetBoneName
+ transformationConstraint.owner_space = space
+ transformationConstraint.target_space = space
+ transformationConstraint.map_from = mapFrom
+ if mapFrom == 'LOCATION':
+ transformationConstraint.from_min_x = fromMinX
+ transformationConstraint.from_max_x = fromMaxX
+ transformationConstraint.from_min_y = fromMinY
+ transformationConstraint.from_max_y = fromMaxY
+ transformationConstraint.from_min_z = fromMinZ
+ transformationConstraint.from_max_z = fromMaxZ
+ elif mapFrom == 'ROTATION':
+ transformationConstraint.from_rotation_mode = fromRotationMode
+ transformationConstraint.from_min_x_rot = fromMinX
+ transformationConstraint.from_max_x_rot = fromMaxX
+ transformationConstraint.from_min_y_rot = fromMinY
+ transformationConstraint.from_max_y_rot = fromMaxY
+ transformationConstraint.from_min_z_rot = fromMinZ
+ transformationConstraint.from_max_z_rot = fromMaxZ
+ elif mapFrom == 'SCALE':
+ transformationConstraint.from_min_x_scale = fromMinX
+ transformationConstraint.from_max_x_scale = fromMaxX
+ transformationConstraint.from_min_y_scale = fromMinY
+ transformationConstraint.from_max_y_scale = fromMaxY
+ transformationConstraint.from_min_z_scale = fromMinZ
+ transformationConstraint.from_max_z_scale = fromMaxZ
+ transformationConstraint.map_to = mapTo
+ if mapTo == 'LOCATION':
+ transformationConstraint.to_min_x = toMinX
+ transformationConstraint.to_max_x = toMaxX
+ transformationConstraint.to_min_y = toMinY
+ transformationConstraint.to_max_y = toMaxY
+ transformationConstraint.to_min_z = toMinZ
+ transformationConstraint.to_max_z = toMaxZ
+ transformationConstraint.mix_mode = mixMode
+ elif mapFrom == 'ROTATION':
+ transformationConstraint.to_euler_order = toEulerOrder
+ transformationConstraint.to_min_x_rot = toMinX
+ transformationConstraint.to_max_x_rot = toMaxX
+ transformationConstraint.to_min_y_rot = toMinY
+ transformationConstraint.to_max_y_rot = toMaxY
+ transformationConstraint.to_min_z_rot = toMinZ
+ transformationConstraint.to_max_z_rot = toMaxZ
+ transformationConstraint.mix_mode_rot = mixMode
+ elif mapFrom == 'SCALE':
+ transformationConstraint.to_min_x_scale = toMinX
+ transformationConstraint.to_max_x_scale = toMaxX
+ transformationConstraint.to_min_y_scale = toMinY
+ transformationConstraint.to_max_y_scale = toMaxY
+ transformationConstraint.to_min_z_scale = toMinZ
+ transformationConstraint.to_max_z_scale = toMaxZ
+ transformationConstraint.mix_mode_scale = mixMode
+ transformationConstraint.map_to_x_from = mapToXFrom
+ transformationConstraint.map_to_y_from = mapToYFrom
+ transformationConstraint.map_to_z_from = mapToZFrom
+ return transformationConstraint
+
+def addLimitLocationConstraint(rig, boneName, subTargetBoneName, space, constraintName,
+useMinX, minX, useMaxX, maxX, useMinY, minY, useMaxY, maxY, useMinZ, minZ, useMaxZ, maxZ):
+ removeConstraint(rig, boneName, constraintName)
+ limitLocationConstraint = rig.pose.bones[boneName].constraints.new('LIMIT_LOCATION')
+ limitLocationConstraint.name = constraintName
+ limitLocationConstraint.owner_space = space
+ if space == 'CUSTOM':
+ limitLocationConstraint.space_object = rig
+ limitLocationConstraint.space_subtarget = subTargetBoneName
+ limitLocationConstraint.use_min_x = useMinX
+ limitLocationConstraint.min_x = minX
+ limitLocationConstraint.use_max_x = useMaxX
+ limitLocationConstraint.max_x = maxX
+ limitLocationConstraint.use_min_y = useMinY
+ limitLocationConstraint.min_y = minY
+ limitLocationConstraint.use_max_y = useMaxY
+ limitLocationConstraint.max_y = maxY
+ limitLocationConstraint.use_min_z = useMinZ
+ limitLocationConstraint.min_z = minZ
+ limitLocationConstraint.use_max_z = useMaxZ
+ limitLocationConstraint.max_z = maxZ
+ return limitLocationConstraint
+
+def addLimitRotationConstraint(rig, boneName, subTargetBoneName, space, constraintName,
+useX, minX, maxX, useY, minY, maxY, useZ, minZ, maxZ):
+ removeConstraint(rig, boneName, constraintName)
+ limitRotationConstraint = rig.pose.bones[boneName].constraints.new('LIMIT_ROTATION')
+ limitRotationConstraint.name = constraintName
+ limitRotationConstraint.owner_space = space
+ if space == 'CUSTOM':
+ limitRotationConstraint.space_object = rig
+ limitRotationConstraint.space_subtarget = subTargetBoneName
+ limitRotationConstraint.use_limit_x = useX
+ limitRotationConstraint.min_x = minX
+ limitRotationConstraint.max_x = maxX
+ limitRotationConstraint.use_limit_y = useY
+ limitRotationConstraint.min_y = minY
+ limitRotationConstraint.max_y = maxY
+ limitRotationConstraint.use_limit_z = useZ
+ limitRotationConstraint.min_z = minZ
+ limitRotationConstraint.max_z = maxZ
+ return limitRotationConstraint
+
+def addArmatureConstraint(rig, boneName, subTargetBoneNames, constraintName):
+ removeConstraint(rig, boneName, constraintName)
+ armatureConstraint = rig.pose.bones[boneName].constraints.new('ARMATURE')
+ armatureConstraint.name = constraintName
+ for index, subTargetBoneName in enumerate(subTargetBoneNames):
+ armatureConstraint.targets.new()
+ armatureConstraint.targets[index].target = rig
+ armatureConstraint.targets[index].subtarget = subTargetBoneName
+ return armatureConstraint
+
+def addDampedTrackConstraint(rig, boneName, subTargetBoneName, constraintName):
+ removeConstraint(rig, boneName, constraintName)
+ dampedTrackConstraint = rig.pose.bones[boneName].constraints.new('DAMPED_TRACK')
+ dampedTrackConstraint.name = constraintName
+ dampedTrackConstraint.target = rig
+ dampedTrackConstraint.subtarget = subTargetBoneName
+ return dampedTrackConstraint
+
+class DriverVariable(NamedTuple):
+ name: str
+ type: str
+ targetObject1: bpy.types.Object
+ targetBone1: str
+ targetTransformSpace1: str
+ targetObject2: bpy.types.Object
+ targetBone2: str
+ targetTransformSpace2: str
+ targetCustomPropertyDataPath: str
+ targetTransformType: str
+ targetRotationMode: str
+
+def addDriver(object, objectProperty, objectPropertyCoordinateIndex, driverType, driverVariables, driverExpression):
+ if objectPropertyCoordinateIndex:
+ driver = object.driver_add(objectProperty, objectPropertyCoordinateIndex)
+ else:
+ driver = object.driver_add(objectProperty)
+ driver.driver.type = driverType
+ for driverVariable in driverVariables:
+ variable = driver.driver.variables.new()
+ variable.name = driverVariable.name
+ variable.type = driverVariable.type
+ variable.targets[0].id = driverVariable.targetObject1
+ if driverVariable.targetCustomPropertyDataPath:
+ variable.targets[0].data_path = driverVariable.targetCustomPropertyDataPath
+ if driverVariable.targetBone1:
+ variable.targets[0].bone_target = driverVariable.targetBone1
+ if driverVariable.targetTransformSpace1:
+ variable.targets[0].transform_space = driverVariable.targetTransformSpace1
+ if driverVariable.targetTransformType:
+ variable.targets[0].transform_type = driverVariable.targetTransformType
+ if driverVariable.targetRotationMode:
+ variable.targets[0].rotation_mode = driverVariable.targetRotationMode
+ if driverVariable.targetObject2:
+ variable.targets[1].id = driverVariable.targetObject2
+ if driverVariable.targetBone2:
+ variable.targets[1].bone_target = driverVariable.targetBone2
+ if driverVariable.targetTransformSpace2 :
+ variable.targets[1].transform_space = driverVariable.targetTransformSpace2
+ if driverExpression:
+ driver.driver.expression = driverExpression
+ return driver
+
+def removeAllConstraints(rig, boneName):
+ boneToMute = rig.pose.bones[boneName]
+ for constraint in boneToMute.constraints:
+ boneToMute.constraints.remove(constraint)
+
+def removeAllDrivers(rig, boneName):
+ if rig.animation_data:
+ for driver in rig.animation_data.drivers:
+ if driver.data_path.startswith("pose.bones"):
+ ownerName = driver.data_path.split('"')[1]
+ if ownerName == boneName:
+ rig.animation_data.drivers.remove(driver)
+
+def createBone(rig, newBoneName):
+ newBone = rig.data.edit_bones.new(newBoneName)
+ newBone.head = (0, 1, 1) # if the head and tail are the same, the bone is deleted
+ newBone.tail = (0, 1, 2)
+ return newBone
+
+def deleteBone(rig, boneName):
+ bone = rig.data.edit_bones.get(boneName)
+ removeAllConstraints(rig, boneName)
+ removeAllDrivers(rig, boneName)
+ rig.data.edit_bones.remove(bone)
+
+def copyBone(rig, sourceBoneName, newBoneName):
+ newBone = rig.data.edit_bones.get(newBoneName)
+ if newBone is not None:
+ deleteBone(rig, newBoneName)
+ newBone = rig.data.edit_bones.new(newBoneName)
+ sourceBone = rig.data.edit_bones.get(sourceBoneName)
+ newBone.tail = sourceBone.tail
+ newBone.head = sourceBone.head
+ newBone.roll = sourceBone.roll
+ newBone.parent = sourceBone.parent
+ return newBone
+
+def addBoneCustomProperty(rig, boneName, propertyName, propertyTooltip, propertyValue, propertyMinValue, propertyMaxValue):
+ """ malfunctioning version
+ bone = rig.pose.bones[boneName]
+ bone[propertyName] = propertyValue
+ if "_RNA_UI" not in bone.keys():
+ bone["_RNA_UI"] = {}
+ bone["_RNA_UI"].update({propertyName: {"description":propertyTooltip, "default":propertyValue, "min":propertyMinValue, "max":propertyMaxValue}})
+ """
+ rna_idprop_ui_create(rig.pose.bones[boneName], propertyName, default = propertyValue, min = propertyMinValue, max = propertyMaxValue, soft_min = None, soft_max = None, description = propertyTooltip)
+ return 'pose.bones["' + boneName + '"]["' + propertyName + '"]'
+
+def copyObject(collectionName, sourceObjectName, newObjectName):
+ if newObjectName in bpy.context.scene.objects or newObjectName in bpy.data.objects:
+ bpy.data.objects.remove(bpy.data.objects[newObjectName])
+ sourceObject = bpy.data.objects[sourceObjectName]
+ newObject = sourceObject.copy()
+ newObject.data = sourceObject.data.copy()
+ newObject.name = newObjectName
+ bpy.data.collections[collectionName].objects.link(newObject)
+ return newObject
+
+def moveObjectOriginToBoneHead(objectName, rig, boneName): #doesn't work after transform changes
+ bpy.context.view_layer.update() #updates matrices
+ object = bpy.data.objects[objectName]
+ globalBoneHeadLocation = rig.location + rig.pose.bones[boneName].head
+ localBoneHeadLocation = object.matrix_world.inverted() @ globalBoneHeadLocation
+ object.data.transform(Matrix.Translation(-localBoneHeadLocation))
+ object.matrix_world.translation = object.matrix_world @ localBoneHeadLocation
+
+def lockUnlockAllObjectTransforms(objectName, lock):
+ object = bpy.data.objects[objectName]
+ object.lock_location[0] = lock
+ object.lock_location[1] = lock
+ object.lock_location[2] = lock
+ object.lock_rotation[0] = lock
+ object.lock_rotation[1] = lock
+ object.lock_rotation[2] = lock
+ object.lock_scale[0] = lock
+ object.lock_scale[1] = lock
+ object.lock_scale[2] = lock
+
+eyesPrimaryLayerBoneNames = [eyesHandleBoneName, leftEyeHandleBoneName, rightEyeHandleBoneName, riggedTongueBone1Name, riggedTongueBone2Name,
+riggedTongueBone3Name, riggedTongueBone4Name, riggedTongueBone5Name]
+eyesSecondaryLayerBoneNames = [eyesTrackTargetBoneName, eyeballsBoneName, leftEyeballBoneName, rightEyeballBoneName, riggedTongueLeftBone3Name,
+riggedTongueRightBone3Name, riggedTongueLeftBone4Name, riggedTongueRightBone4Name, riggedTongueLeftBone5Name, riggedTongueRightBone5Name]
+torsoLayerBoneNames = [headBoneName, headTrackTargetBoneName, neckBoneName, upperChestBoneName, chestBoneName, spineBoneName,
+hipsBoneName, pelvisBoneName, waistBoneName, buttocksHandleBoneName, leftButtockHandleBoneName, rightButtockHandleBoneName,
+breastsHandleBoneName, leftBreastHandleBoneName, rightBreastHandleBoneName, leftShoulderBoneName, rightShoulderBoneName]
+torsoTweakLayerBoneNames = [crotchBoneName, anusBoneName, leftBreastBone2Name, rightBreastBone2Name, leftBreastBone3Name,
+rightBreastBone3Name, leftNippleBone1Name, rightNippleBone1Name, leftNippleBone2Name, rightNippleBone2Name, leftBreastDeformBone1Name,
+rightBreastDeformBone1Name, leftBreastDeformBone2Name, rightBreastDeformBone2Name, leftBreastDeformBone3Name, rightBreastDeformBone3Name,
+leftNippleDeformBone1Name, rightNippleDeformBone1Name, leftNippleDeformBone2Name, rightNippleDeformBone2Name, betterPenetrationRootCrotchBoneName,
+betterPenetrationFrontCrotchBoneName, betterPenetrationLeftCrotchBone1Name, betterPenetrationRightCrotchBone1Name, betterPenetrationLeftCrotchBone2Name,
+betterPenetrationRightCrotchBone2Name, betterPenetrationLeftCrotchBone3Name, betterPenetrationRightCrotchBone3Name, betterPenetrationLeftCrotchBone4Name,
+betterPenetrationRightCrotchBone4Name, betterPenetrationLeftCrotchBone5Name, betterPenetrationRightCrotchBone5Name, betterPenetrationBackCrotchBoneName]
+leftArmIkLayerBoneNames = [leftArmBoneName, leftElbowBoneName, leftWristBoneName]
+rightArmIkLayerBoneNames = [rightArmBoneName, rightElbowBoneName, rightWristBoneName]
+leftLegIkLayerBoneNames = [leftLegBoneName, leftKneeBoneName, leftAnkleBoneName, leftToeBoneName, leftHeelBoneName]
+rightLegIkLayerBoneNames = [rightLegBoneName, rightKneeBoneName, rightAnkleBoneName, rightToeBoneName, rightHeelBoneName]
+fingersLayerBoneNames = [leftThumbBone1Name, rightThumbBone1Name, leftThumbBone2Name, rightThumbBone2Name, leftThumbBone3Name,
+rightThumbBone3Name, leftIndexFingerPalmBoneName, rightIndexFingerPalmBoneName, leftIndexFingerBone1Name, rightIndexFingerBone1Name,
+leftIndexFingerBone2Name, rightIndexFingerBone2Name, leftIndexFingerBone3Name, rightIndexFingerBone3Name, leftMiddleFingerPalmBoneName,
+rightMiddleFingerPalmBoneName, leftMiddleFingerBone1Name, rightMiddleFingerBone1Name, leftMiddleFingerBone2Name, rightMiddleFingerBone2Name,
+leftMiddleFingerBone3Name, rightMiddleFingerBone3Name, leftRingFingerPalmBoneName, rightRingFingerPalmBoneName, leftRingFingerBone1Name,
+rightRingFingerBone1Name, leftRingFingerBone2Name, rightRingFingerBone2Name, leftRingFingerBone3Name, rightRingFingerBone3Name,
+leftLittleFingerPalmBoneName, rightLittleFingerPalmBoneName, leftLittleFingerBone1Name, rightLittleFingerBone1Name, leftLittleFingerBone2Name,
+rightLittleFingerBone2Name, leftLittleFingerBone3Name, rightLittleFingerBone3Name]
+
+originalIkLayerIndex = 0
+originalFkLayerIndex = 1
+originalPrimaryJointCorrectionLayerIndex = 2
+originalSecondaryJointCorrectionLayerIndex = 3
+originalBetterPenetrationLayerIndex = 4
+originalSkirtLayerIndex = 8
+originalAccessoryLayerIndex = 9
+originalMchLayerIndex = 10
+originalPhysicsLayerIndex = 11
+originalExtraLayerIndex = 12
+originalUpperFaceLayerIndex = 16
+originalLowerFaceLayerIndex = 17
+originalRiggedTongueLayerIndex = 18
+
+temporaryAccessoryMchLayerIndex = 7
+temporaryFaceMchLayerIndex = 19
+temporaryOriginalDeformLayerIndex = 26
+
+detailLayerSuffix = " (Detail)"
+primaryLayerSuffix = " (Primary)"
+secondaryLayerSuffix = " (Secondary)"
+mchLayerSuffix = " (MCH)"
+tweakLayerSuffix = " (Tweak)"
+ikLayerSuffix = " IK"
+fkLayerSuffix = " FK"
+
+hairLayerName = "Hair/Accessories"
+eyesLayerName = "Eyes/Rig.Tongue"
+faceLayerName = "Face"
+torsoLayerName = "Torso"
+leftArmLayerName = "Arm.L"
+rightArmLayerName = leftNameToRightName(leftArmLayerName)
+fingersLayerName = "Fingers"
+leftLegLayerName = "Leg.L"
+rightLegLayerName = leftNameToRightName(leftLegLayerName)
+skirtLayerName = "Skirt"
+junkLayerName = "Junk"
+rootLayerName = "Root"
+defLayerName = "DEF"
+mchLayerName = "MCH"
+orgLayerName = "ORG"
+
+noneBoneGroupIndex = 0
+rootBoneGroupIndex = 1
+ikBoneGroupIndex = 2
+specialBoneGroupIndex = 3
+tweakBoneGroupIndex = 4
+fkBoneGroupIndex = 5
+extraBoneGroupIndex = 6
+
+class RigifyLayer(NamedTuple):
+ name: str
+ row: int
+ group: int
+
+rigifyLayers = [
+RigifyLayer(hairLayerName, 1, extraBoneGroupIndex),
+RigifyLayer(hairLayerName + detailLayerSuffix, 2, fkBoneGroupIndex),
+RigifyLayer(hairLayerName + mchLayerSuffix, 3, rootBoneGroupIndex),
+RigifyLayer(eyesLayerName + primaryLayerSuffix, 4, extraBoneGroupIndex),
+RigifyLayer(eyesLayerName + secondaryLayerSuffix, 5, fkBoneGroupIndex),
+RigifyLayer(faceLayerName, 6, tweakBoneGroupIndex),
+RigifyLayer(faceLayerName + mchLayerSuffix, 7, rootBoneGroupIndex),
+RigifyLayer(torsoLayerName, 8, specialBoneGroupIndex),
+RigifyLayer(torsoLayerName + detailLayerSuffix, 9, tweakBoneGroupIndex),
+RigifyLayer(leftArmLayerName + ikLayerSuffix, 10, ikBoneGroupIndex),
+RigifyLayer(leftArmLayerName + fkLayerSuffix, 11, fkBoneGroupIndex),
+RigifyLayer(leftArmLayerName + tweakLayerSuffix, 12, tweakBoneGroupIndex),
+RigifyLayer(rightArmLayerName + ikLayerSuffix, 10, ikBoneGroupIndex),
+RigifyLayer(rightArmLayerName + fkLayerSuffix, 11, fkBoneGroupIndex),
+RigifyLayer(rightArmLayerName + tweakLayerSuffix, 12, tweakBoneGroupIndex),
+RigifyLayer(fingersLayerName, 13, extraBoneGroupIndex),
+RigifyLayer(fingersLayerName + detailLayerSuffix, 14, fkBoneGroupIndex),
+RigifyLayer(leftLegLayerName + ikLayerSuffix, 15, ikBoneGroupIndex),
+RigifyLayer(leftLegLayerName + fkLayerSuffix, 16, fkBoneGroupIndex),
+RigifyLayer(leftLegLayerName + tweakLayerSuffix, 17, tweakBoneGroupIndex),
+RigifyLayer(rightLegLayerName + ikLayerSuffix, 15, ikBoneGroupIndex),
+RigifyLayer(rightLegLayerName + fkLayerSuffix, 16, fkBoneGroupIndex),
+RigifyLayer(rightLegLayerName + tweakLayerSuffix, 17, tweakBoneGroupIndex),
+RigifyLayer(skirtLayerName, 18, extraBoneGroupIndex),
+RigifyLayer(skirtLayerName + detailLayerSuffix, 19, fkBoneGroupIndex),
+RigifyLayer("", 28, noneBoneGroupIndex),
+RigifyLayer("", 28, noneBoneGroupIndex),
+RigifyLayer(junkLayerName, 29, noneBoneGroupIndex)
+]
+
+rootLayerIndex = 28
+rootLayerRow = 30
+defLayerIndex = 29
+defLayerRow = 31
+mchLayerIndex = 30
+mchLayerRow = 31
+orgLayerIndex = 31
+orgLayerRow = 31
+
+def getRigifyLayerIndexByName(rigifyLayerName):
+ for index, rigifyLayer in enumerate(rigifyLayers):
+ if rigifyLayer.name == rigifyLayerName:
+ return index
+
+def setRigifyLayer(rig, index, rigifyLayer):
+ if bpy.app.version[0] == 3:
+ rig.data.rigify_layers[index].name = rigifyLayer.name
+ rig.data.rigify_layers[index].row = rigifyLayer.row
+ rig.data.rigify_layers[index].group = rigifyLayer.group
+ else:
+ if rig.data.collections_all.get(str(index)):
+ rig.data.collections_all[str(index)].rigify_ui_title = rigifyLayer.name
+ rig.data.collections_all[str(index)].rigify_ui_row = rigifyLayer.row
+ rig.data.collections_all[str(index)].rigify_color_set_id = rigifyLayer.group
+
+def setRootRigifyLayer(rig, boneGroupIndex):
+ if bpy.app.version[0] == 3:
+ rig.data.rigify_layers[rootLayerIndex].group = boneGroupIndex
+ else:
+ if rig.data.collections_all.get(str(rootLayerIndex)):
+ rig.data.collections_all[str(rootLayerIndex)].rigify_color_set_id = boneGroupIndex
+
+mmdOriginalBoneLayerName = "Original bones"
+mmdRenamedRequiredDictionaryLayerName = "Renamed required dictionary"
+mmdRenamedWordDictionaryLayerName = "Renamed word dictionary"
+mmdRenamedExtraDictionaryLayerName = "Renamed extra dictionary"
+mmdRenamedMmdBoneLayerName = "Renamed mmd_bone"
+mmdNotRenamedLayerName = "Not renamed"
+mmdOriginalShadowLayerName = "Original shadow"
+mmdOriginalDummyLayerName = "Original dummy"
+mmdShadowLayerName = "Shadow"
+mmdDummyLayerName = "Dummy"
+mmdHiddenBonesLayerName = "Hidden bones"
+mmdDeformBonesLayerName = "Deform bones"
+mmdUsefulBonesLayerName = "Useful bones"
+mmdUselessBonesLayerName = "Useless bones"
+
+class BoneManagerLayer(NamedTuple):
+ name: str
+ row: int
+
+mmdBoneManagerLayers = [
+BoneManagerLayer(mmdOriginalBoneLayerName, 0),
+BoneManagerLayer(mmdRenamedRequiredDictionaryLayerName, 1),
+BoneManagerLayer(mmdRenamedWordDictionaryLayerName, 2),
+BoneManagerLayer(mmdRenamedExtraDictionaryLayerName, 3),
+BoneManagerLayer(mmdRenamedMmdBoneLayerName, 4),
+BoneManagerLayer(mmdNotRenamedLayerName, 5),
+BoneManagerLayer(mmdShadowLayerName, 6),
+BoneManagerLayer(mmdDummyLayerName, 7),
+BoneManagerLayer(mmdOriginalShadowLayerName, 8),
+BoneManagerLayer(mmdOriginalDummyLayerName, 9),
+BoneManagerLayer(mmdHiddenBonesLayerName, 10),
+BoneManagerLayer(mmdDeformBonesLayerName, 11),
+BoneManagerLayer(mmdUsefulBonesLayerName, 12),
+BoneManagerLayer(mmdUselessBonesLayerName, 13),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28)
+]
+
+def getMmdBoneManagerLayerIndexByName(mmdBoneManagerLayerName):
+ for index, mmdBoneManagerLayer in enumerate(mmdBoneManagerLayers):
+ if mmdBoneManagerLayer.name == mmdBoneManagerLayerName:
+ return index
+
+koikatsuOriginalBoneLayerName = "Original bones"
+koikatsuRetargetingBonesLayerName = "Retargeting bones"
+koikatsuNonRetargetingBonesLayerName = "Non-retargeting bones"
+koikatsuHiddenBonesLayerName = "Hidden bones"
+koikatsuDeformBonesLayerName = "Deform bones"
+koikatsuUsefulBonesLayerName = "Useful bones"
+koikatsuUselessBonesLayerName = "Useless bones"
+
+koikatsuBoneManagerLayers = [
+BoneManagerLayer(koikatsuOriginalBoneLayerName, 0),
+BoneManagerLayer(koikatsuRetargetingBonesLayerName, 1),
+BoneManagerLayer(koikatsuNonRetargetingBonesLayerName, 2),
+BoneManagerLayer(koikatsuHiddenBonesLayerName, 3),
+BoneManagerLayer(koikatsuDeformBonesLayerName, 4),
+BoneManagerLayer(koikatsuUsefulBonesLayerName, 5),
+BoneManagerLayer(koikatsuUselessBonesLayerName, 6),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28),
+BoneManagerLayer("", 28)
+]
+
+def getKoikatsuBoneManagerLayerIndexByName(koikatsuBoneManagerLayerName):
+ for index, koikatsuBoneManagerLayer in enumerate(koikatsuBoneManagerLayers):
+ if koikatsuBoneManagerLayer.name == koikatsuBoneManagerLayerName:
+ return index
+
+def setBoneManagerLayer(rig, layer, boneManagerLayer):
+ bpy.data.armatures[rig.data.name]["layer_name_" + str(layer)] = boneManagerLayer.name
+ bpy.data.armatures[rig.data.name]["rigui_id_" + str(layer)] = boneManagerLayer.row
+
+def setBoneManagerLayersFromRigifyLayers(rig):
+ for index, rigifyLayer in enumerate(rigifyLayers):
+ if rigifyLayer.name != "":
+ setBoneManagerLayer(rig, index, BoneManagerLayer(rigifyLayer.name, rigifyLayer.row))
+ setBoneManagerLayer(rig, rootLayerIndex, BoneManagerLayer(rootLayerName, rootLayerRow))
+ setBoneManagerLayer(rig, defLayerIndex, BoneManagerLayer(defLayerName, defLayerRow))
+ setBoneManagerLayer(rig, mchLayerIndex, BoneManagerLayer(mchLayerName, mchLayerRow))
+ setBoneManagerLayer(rig, orgLayerIndex, BoneManagerLayer(orgLayerName, orgLayerRow))
+
+def assignSingleBoneLayer_except(rig, boneName, layerIndex):
+ try:
+ assignSingleBoneLayer(rig, boneName, layerIndex)
+ except:
+ pass
+ #bone didn't exist
+
+def assignSingleBoneLayer(rig, boneName, layerIndex):
+ if bpy.app.version[0] == 3:
+ original_mode = bpy.context.object.mode
+ bpy.ops.object.mode_set(mode = 'POSE')
+ bone = rig.data.bones[boneName]
+ bone.layers[layerIndex] = True
+ for index in range(32):
+ if index != layerIndex:
+ bone.layers[index] = False
+ else:
+ original_mode = bpy.context.object.mode
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bone = rig.data.bones[boneName]
+ bone.collections.clear()
+ if rig.data.collections.get(str(layerIndex)):
+ rig.data.collections[str(layerIndex)].assign(bone)
+ else:
+ rig.data.collections.new(str(layerIndex))
+ rig.data.collections[str(layerIndex)].assign(bone)
+ bpy.ops.object.mode_set(mode = original_mode)
+
+def assignMultipleBoneLayer(rig, boneName, layerIndexes):
+ if bpy.app.version[0] == 3:
+ for layerIndex in layerIndexes:
+ bone.layers[layerIndex] = True
+ for index in range(32):
+ if index not in layerIndexes:
+ bone.layers[index] = False
+ else:
+ original_mode = bpy.context.object.mode
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bone = rig.data.bones[boneName]
+ bone.collections.clear()
+ for layerIndex in layerIndexes:
+ if rig.data.collections.get(str(layerIndex)):
+ rig.data.collections[str(layerIndex)].assign(bone)
+ else:
+ rig.data.collections.new(str(layerIndex))
+ rig.data.collections[str(layerIndex)].assign(bone)
+ bpy.ops.object.mode_set(mode = original_mode)
+
+
+def assignSingleBoneLayerToList(rig, boneNamesList, layerIndex):
+ for boneName in boneNamesList:
+ assignSingleBoneLayer(rig, boneName, layerIndex)
+
+def getDeformBoneNames(rig):
+ deformBoneNames = []
+ for obj in bpy.context.scene.objects:
+ if obj.type == 'MESH':
+ me = obj.data
+ bm = bmesh.new()
+ bm.from_mesh(me)
+ #groupIndex = obj.vertex_groups.active_index
+ dvertLay = bm.verts.layers.deform.active
+ vgIndexes = []
+ if dvertLay is not None:
+ for vert in bm.verts:
+ dvert = vert[dvertLay]
+ for tuple in dvert.items():
+ if tuple[1] != 0 and tuple[0] not in vgIndexes:
+ vgIndexes.append(tuple[0])
+ indexBone = rig.pose.bones.get(obj.vertex_groups[tuple[0]].name)
+ if indexBone is not None and indexBone.name not in deformBoneNames:
+ deformBoneNames.append(indexBone.name)
+ return deformBoneNames
+
+def getRelatedBoneNames(rig, boneName):
+ relatedBoneNames = []
+ bone = rig.pose.bones[boneName]
+ if bone.parent and bone.parent.name not in relatedBoneNames and rig.pose.bones.get(bone.parent.name):
+ relatedBoneNames.append(bone.parent.name)
+ for constraint in bone.constraints:
+ try:
+ if constraint.subtarget not in relatedBoneNames and rig.pose.bones.get(constraint.subtarget):
+ relatedBoneNames.append(constraint.subtarget)
+ except AttributeError as ex:
+ pass
+ if rig.animation_data:
+ for driver in rig.animation_data.drivers:
+ if driver.data_path.startswith("pose.bones"):
+ ownerName = driver.data_path.split('"')[1]
+ if ownerName == boneName:
+ for variable in driver.driver.variables:
+ for target in variable.targets:
+ if target.bone_target not in relatedBoneNames and rig.pose.bones.get(target.bone_target):
+ relatedBoneNames.append(target.bone_target)
+ return relatedBoneNames
+
+def lockAllPoseTransforms(rig, boneName):
+ rig.pose.bones[boneName].lock_location[0] = True
+ rig.pose.bones[boneName].lock_location[1] = True
+ rig.pose.bones[boneName].lock_location[2] = True
+ rig.pose.bones[boneName].lock_rotation[0] = True
+ rig.pose.bones[boneName].lock_rotation[1] = True
+ rig.pose.bones[boneName].lock_rotation[2] = True
+ rig.pose.bones[boneName].lock_rotation_w = True
+ rig.pose.bones[boneName].lock_scale[0] = True
+ rig.pose.bones[boneName].lock_scale[1] = True
+ rig.pose.bones[boneName].lock_scale[2] = True
+
+bonesWithDrivers = [
+waistJointCorrectionBoneName,
+leftButtockJointCorrectionBoneName,
+rightButtockJointCorrectionBoneName,
+leftShoulderJointCorrectionBoneName,
+rightShoulderJointCorrectionBoneName,
+backLeftElbowJointCorrectionBoneName,
+backRightElbowJointCorrectionBoneName,
+frontLeftElbowJointCorrectionBoneName,
+frontRightElbowJointCorrectionBoneName,
+leftWristJointCorrectionBoneName,
+rightWristJointCorrectionBoneName,
+leftLegJointCorrectionBoneName,
+rightLegJointCorrectionBoneName,
+backRightKneeJointCorrectionBoneName,
+backLeftKneeJointCorrectionBoneName,
+frontLeftKneeJointCorrectionBoneName,
+frontRightKneeJointCorrectionBoneName
+]
+
+driverTargets = [leftKneeBoneName, rightKneeBoneName, leftLegBoneName, rightLegBoneName, leftWristBoneName, rightWristBoneName, leftElbowBoneName, rightElbowBoneName, leftArmBoneName, rightArmBoneName, waistBoneName]
+
+targetsToChange = [leftArmBoneName, rightArmBoneName, leftElbowBoneName, rightElbowBoneName, leftLegBoneName, rightLegBoneName, leftKneeBoneName, rightKneeBoneName]
+
+def setBoneCustomShapeScale(rig, boneName, scale):
+ if bpy.app.version[0] < 3:
+ rig.pose.bones[boneName].custom_shape_scale = scale
+ else:
+ rig.pose.bones[boneName].custom_shape_scale_xyz = [scale, scale, scale]
+
+japHalfToFullTuples = (
+('ヴ', 'ヴ'), ('ガ', 'ガ'), ('ギ', 'ギ'), ('グ', 'グ'), ('ゲ', 'ゲ'),
+('ゴ', 'ゴ'), ('ザ', 'ザ'), ('ジ', 'ジ'), ('ズ', 'ズ'), ('ゼ', 'ゼ'),
+('ゾ', 'ゾ'), ('ダ', 'ダ'), ('ヂ', 'ヂ'), ('ヅ', 'ヅ'), ('デ', 'デ'),
+('ド', 'ド'), ('バ', 'バ'), ('パ', 'パ'), ('ビ', 'ビ'), ('ピ', 'ピ'),
+('ブ', 'ブ'), ('プ', 'プ'), ('ベ', 'ベ'), ('ペ', 'ペ'), ('ボ', 'ボ'),
+('ポ', 'ポ'), ('。', '。'), ('「', '「'), ('」', '」'), ('、', '、'),
+('・', '・'), ('ヲ', 'ヲ'), ('ァ', 'ァ'), ('ィ', 'ィ'), ('ゥ', 'ゥ'),
+('ェ', 'ェ'), ('ォ', 'ォ'), ('ャ', 'ャ'), ('ュ', 'ュ'), ('ョ', 'ョ'),
+('ッ', 'ッ'), ('ー', 'ー'), ('ア', 'ア'), ('イ', 'イ'), ('ウ', 'ウ'),
+('エ', 'エ'), ('オ', 'オ'), ('カ', 'カ'), ('キ', 'キ'), ('ク', 'ク'),
+('ケ', 'ケ'), ('コ', 'コ'), ('サ', 'サ'), ('シ', 'シ'), ('ス', 'ス'),
+('セ', 'セ'), ('ソ', 'ソ'), ('タ', 'タ'), ('チ', 'チ'), ('ツ', 'ツ'),
+('テ', 'テ'), ('ト', 'ト'), ('ナ', 'ナ'), ('ニ', 'ニ'), ('ヌ', 'ヌ'),
+('ネ', 'ネ'), ('ノ', 'ノ'), ('ハ', 'ハ'), ('ヒ', 'ヒ'), ('フ', 'フ'),
+('ヘ', 'ヘ'), ('ホ', 'ホ'), ('マ', 'マ'), ('ミ', 'ミ'), ('ム', 'ム'),
+('メ', 'メ'), ('モ', 'モ'), ('ヤ', 'ヤ'), ('ユ', 'ユ'), ('ヨ', 'ヨ'),
+('ラ', 'ラ'), ('リ', 'リ'), ('ル', 'ル'), ('レ', 'レ'), ('ロ', 'ロ'),
+('ワ', 'ワ'), ('ン', 'ン')
+)
+
+def fixJapChars(name):
+ for values in japHalfToFullTuples:
+ if values[0] in name:
+ name = name.replace(values[0], values[1])
+ return name
+
+japEngRequiredBoneNamesDictionary = {
+'全ての親':'mother',
+'グルーブ':'groove',
+'センター':'center',
+'上半身':'upper body',
+'上半身2':'upper body 2',
+'首':'neck',
+'頭':'head',
+'左目':'eye L',
+'下半身':'lower body',
+'左肩':'shoulder L',
+'左腕':'arm L',
+'左ひじ':'elbow L',
+'左手首':'wrist L',
+'左親指0':'thumb0L',
+'左親指1':'thumb1L',
+'左親指2':'thumb2L',
+'左人指1':'fore1L',
+'左人指2':'fore2L',
+'左人指3':'fore3L',
+'左中指1':'middle1L',
+'左中指2':'middle2L',
+'左中指3':'middle3L',
+'左薬指1':'third1L',
+'左薬指2':'third2L',
+'左薬指3':'third3L',
+'左小指1':'little1L',
+'左小指2':'little2L',
+'左小指3':'little3L',
+'左足':'legL',
+'左ひざ':'kneeL',
+'左足首':'ankleL',
+'両目':'eyes',
+'右目':'eye R',
+'右肩':'shoulderR',
+'右腕':'armR',
+'右ひじ':'elbowR',
+'右手首':'wristR',
+'右親指0':'thumb0R',
+'右親指1':'thumb1R',
+'右親指2':'thumb2R',
+'右人指1':'fore1R',
+'右人指2':'fore2R',
+'右人指3':'fore3R',
+'右中指1':'middle1R',
+'右中指2':'middle2R',
+'右中指3':'middle3R',
+'右薬指1':'third1R',
+'右薬指2':'third2R',
+'右薬指3':'third3R',
+'右小指1':'little1R',
+'右小指2':'little2R',
+'右小指3':'little3R',
+'右足':'legR',
+'右ひざ':'kneeR',
+'右足首':'ankleR',
+'左足IK':'leg IK L',
+'左つま先IK':'toe IK L',
+'右足IK':'leg IK R',
+'右つま先IK':'toe IK R'
+}
+
+japEngWordsDictionaryFileName = "dictionary.json"
+
+def loadJsonDictionaryFile(filePath, fileName):
+ dictionaryFile = os.path.join(filePath, fileName)
+ with open(dictionaryFile, encoding="utf8") as file:
+ return json.load(file, object_pairs_hook=collections.OrderedDict)
+
+japEngExtraBoneNamesDictionary = {
+'テール1':'tail 1',
+'テール2':'tail 2'
+}
+
+japCharactersRegex = u'[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]+' # Regex to look for japanese chars
+
+def getContainedJapCharacters(string):
+ return re.findall(japCharactersRegex, string)
+
+koikatsuRetargetingBoneNames = [
+"cf_j_hips",
+"cf_j_spine01",
+"cf_j_spine02",
+"cf_j_spine03",
+"cf_j_arm00_L",
+"cf_j_forearm01_L",
+"cf_j_hand_L",
+"cf_j_index01_L",
+"cf_j_index02_L",
+"cf_j_index03_L",
+"cf_j_little01_L",
+"cf_j_little02_L",
+"cf_j_little03_L",
+"cf_j_middle01_L",
+"cf_j_middle02_L",
+"cf_j_middle03_L",
+"cf_j_ring01_L",
+"cf_j_ring02_L",
+"cf_j_ring03_L",
+"cf_j_thumb01_L",
+"cf_j_thumb02_L",
+"cf_j_thumb03_L",
+"cf_j_arm00_R",
+"cf_j_forearm01_R",
+"cf_j_hand_R",
+"cf_j_index01_R",
+"cf_j_index02_R",
+"cf_j_index03_R",
+"cf_j_little01_R",
+"cf_j_little02_R",
+"cf_j_little03_R",
+"cf_j_middle01_R",
+"cf_j_middle02_R",
+"cf_j_middle03_R",
+"cf_j_ring01_R",
+"cf_j_ring02_R",
+"cf_j_ring03_R",
+"cf_j_thumb01_R",
+"cf_j_thumb02_R",
+"cf_j_thumb03_R",
+"cf_j_neck",
+"cf_j_head",
+"cf_j_waist01",
+"cf_j_thigh00_L",
+"cf_j_leg01_L",
+"cf_j_foot_L",
+"cf_j_toes_L",
+"cf_j_thigh00_R",
+"cf_j_leg01_R",
+"cf_j_foot_R",
+"cf_j_toes_R",
+"cf_j_shoulder_L",
+"cf_j_shoulder_R",
+"cf_j_kokan",
+"cf_j_ana",
+"cf_j_waist02",
+"cf_j_siri_L",
+"cf_j_siri_R",
+"cf_j_bust01_L",
+"cf_j_bust01_R",
+"cf_j_bust02_L",
+"cf_j_bust02_R",
+"cf_j_bust03_L",
+"cf_j_bust03_R",
+"cf_j_bnip02root_L",
+"cf_j_bnip02root_R",
+"cf_j_bnip02_L",
+"cf_j_bnip02_R"
+]
+
+autoRigProCopyRotationConstraintName = "Copy RotationREMAP"
+autoRigProCopyScaleConstraintName = "Copy ScaleREMAP"
diff --git a/extras/rigifyscripts/rigify_after.py b/extras/rigifyscripts/rigify_after.py
new file mode 100644
index 0000000..a0127c8
--- /dev/null
+++ b/extras/rigifyscripts/rigify_after.py
@@ -0,0 +1,312 @@
+
+#Switch to Object Mode and select Generated Rig
+
+import bpy
+import traceback
+import sys
+import math
+from math import radians
+from . import commons as koikatsuCommons
+
+def main():
+ generatedRig = bpy.context.active_object
+
+ assert generatedRig.mode == "OBJECT", 'assert generated_rig.mode == "OBJECT"'
+ assert generatedRig.type == "ARMATURE", 'assert generatedRig.type == "ARMATURE"'
+
+ generatedRig.show_in_front = True
+ generatedRig.display_type = 'TEXTURED'
+
+ """
+ for i in range(32):
+ if i == 0 or i == 1:
+ generatedRig.data.layers[i] = True
+ else:
+ generatedRig.data.layers[i] = False
+ """
+
+ hasRiggedTongue = generatedRig.pose.bones.get(koikatsuCommons.riggedTongueBone1Name)
+ hasBetterPenetrationMod = generatedRig.pose.bones.get(koikatsuCommons.betterPenetrationRootCrotchBoneName)
+
+ metarig = None
+ for bone in generatedRig.pose.bones:
+ if bone.name.startswith(koikatsuCommons.metarigIdBonePrefix):
+ generatedRigIdBoneName = bone.name
+ for object in bpy.data.objects:
+ if object != generatedRig and object.type == "ARMATURE":
+ for objectBone in object.pose.bones:
+ if objectBone.name == generatedRigIdBoneName:
+ metarig = object
+ break
+ break
+
+ if metarig:
+ metarig.hide_set(True)
+ for object in bpy.data.objects:
+ if object.type == "MESH":
+ if object.parent == metarig:
+ object.parent = generatedRig
+ for modifier in object.modifiers:
+ if modifier.type == "ARMATURE" and modifier.object == metarig:
+ modifier.object = generatedRig
+ if modifier.type == "UV_WARP" and modifier.object_from == metarig:
+ if modifier.name == "Left Eye UV warp":
+ modifier.object_from = generatedRig
+ modifier.bone_from = koikatsuCommons.eyesXBoneName
+ modifier.object_to = generatedRig
+ modifier.bone_to = koikatsuCommons.leftEyeBoneName
+ elif modifier.name == "Right Eye UV warp":
+ modifier.object_from = generatedRig
+ modifier.bone_from = koikatsuCommons.eyesXBoneName
+ modifier.object_to = generatedRig
+ modifier.bone_to = koikatsuCommons.rightEyeBoneName
+
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.eyesTrackTargetBoneName, 1.5)
+ if hasRiggedTongue:
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.riggedTongueLeftBone3Name, 0.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.riggedTongueRightBone3Name, 0.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.riggedTongueLeftBone4Name, 0.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.riggedTongueRightBone4Name, 0.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.riggedTongueLeftBone5Name, 0.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.riggedTongueRightBone5Name, 0.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.headTweakBoneName, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.crotchBoneName, 2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.anusBoneName, 0.25)
+ if hasBetterPenetrationMod:
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationRootCrotchBoneName, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationFrontCrotchBoneName, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationLeftCrotchBone1Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationRightCrotchBone1Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationLeftCrotchBone2Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationRightCrotchBone2Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationLeftCrotchBone3Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationRightCrotchBone3Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationLeftCrotchBone4Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationRightCrotchBone4Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationLeftCrotchBone5Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationRightCrotchBone5Name, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.betterPenetrationBackCrotchBoneName, 0.09)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftBreastDeformBone1Name, 0.4)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightBreastDeformBone1Name, 0.4)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftBreastBone2Name, 3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightBreastBone2Name, 3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftBreastDeformBone2Name, 0.3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightBreastDeformBone2Name, 0.3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftBreastBone3Name, 2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightBreastBone3Name, 2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftBreastDeformBone3Name, 0.2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightBreastDeformBone3Name, 0.2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftNippleBone1Name, 1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightNippleBone1Name, 1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftNippleDeformBone1Name, 0.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightNippleDeformBone1Name, 0.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftNippleBone2Name, 0.5)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightNippleBone2Name, 0.5)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftNippleDeformBone2Name, 0.05)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightNippleDeformBone2Name, 0.05)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftShoulderBoneName, 1.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightShoulderBoneName, 1.25)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftThumbBone1Name, 1.3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightThumbBone1Name, 1.3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftThumbBone2Name, 1.3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightThumbBone2Name, 1.3)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftThumbBone3Name, 1.2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightThumbBone3Name, 1.2)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftIndexFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightIndexFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftIndexFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightIndexFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftMiddleFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightMiddleFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftMiddleFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightMiddleFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftRingFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightRingFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftRingFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightRingFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftLittleFingerPalmBoneName, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightLittleFingerPalmBoneName, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftLittleFingerPalmFkBoneName, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightLittleFingerPalmFkBoneName, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftLittleFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightLittleFingerBone2Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.leftLittleFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rightLittleFingerBone3Name, 1.1)
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, koikatsuCommons.rootBoneName, 0.35)
+
+ for bone in generatedRig.pose.bones:
+ if bpy.app.version[0] == 3:
+ if generatedRig.data.bones[bone.name].layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.faceLayerName + koikatsuCommons.mchLayerSuffix)] == True:
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, bone.name, 0.15)
+ else:
+ if generatedRig.data.bones[bone.name].collections.get(str(koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.faceLayerName + koikatsuCommons.mchLayerSuffix))):
+ koikatsuCommons.setBoneCustomShapeScale(generatedRig, bone.name, 0.15)
+
+ headBone = generatedRig.pose.bones[koikatsuCommons.originalBonePrefix + koikatsuCommons.headBoneName]
+ koikatsuCommons.changeConstraintIndex(generatedRig, headBone.name, koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.headConstraintSuffix + koikatsuCommons.rotationConstraintSuffix, len(headBone.constraints) - 1)
+ koikatsuCommons.changeConstraintIndex(generatedRig, headBone.name, koikatsuCommons.limitRotationConstraintBaseName + koikatsuCommons.headConstraintSuffix, len(headBone.constraints) - 1)
+
+ headTweakLimitRotationConstraint = koikatsuCommons.addLimitRotationConstraint(generatedRig, koikatsuCommons.headTweakBoneName, None, 'LOCAL', koikatsuCommons.limitRotationConstraintBaseName + koikatsuCommons.headConstraintSuffix,
+ True, math.radians(-180), math.radians(180), True, math.radians(-180), math.radians(180), True, math.radians(-180), math.radians(180))
+
+ for driver in generatedRig.animation_data.drivers:
+ if driver.data_path.startswith("pose.bones"):
+ driverOwnerName = driver.data_path.split('"')[1]
+ driverProperty = driver.data_path.rsplit('.', 1)[1]
+ if driverOwnerName == headBone.name:
+ constraintName = driver.data_path.split('"')[3]
+ if constraintName == koikatsuCommons.limitRotationConstraintBaseName + koikatsuCommons.headConstraintSuffix:
+ variable = driver.driver.variables[0]
+ newVariable = koikatsuCommons.DriverVariable(variable.name, variable.type, generatedRig, variable.targets[0].bone_target, None, None, None, None, variable.targets[0].data_path, None, None)
+
+
+ koikatsuCommons.addDriver(headTweakLimitRotationConstraint, driverProperty, None, driver.driver.type, [newVariable],
+ driver.driver.expression)
+
+ for bone in generatedRig.pose.bones:
+ for constraint in bone.constraints:
+ if constraint.type == 'ARMATURE':
+ for target in constraint.targets:
+ if target.subtarget:
+ target.target = generatedRig
+ if target.subtarget.endswith(koikatsuCommons.placeholderBoneSuffix):
+ target.subtarget = target.subtarget[:-len(koikatsuCommons.placeholderBoneSuffix)]
+
+ for driver in bpy.data.objects[koikatsuCommons.bodyName()].data.shape_keys.animation_data.drivers:
+ if driver.data_path.startswith("key_blocks"):
+ ownerName = driver.data_path.split('"')[1]
+ if ownerName == koikatsuCommons.eyelidsShapeKeyCopyName:
+ for variable in driver.driver.variables:
+ for target in variable.targets:
+ if target.id == metarig:
+ target.id = generatedRig
+
+ for driver in generatedRig.animation_data.drivers:
+ if driver.data_path.startswith("pose.bones"):
+ driverOwnerName = driver.data_path.split('"')[1]
+ driverProperty = driver.data_path.rsplit('.', 1)[1]
+ if driverOwnerName in koikatsuCommons.bonesWithDrivers and driverProperty == "location":
+ for variable in driver.driver.variables:
+ for target in variable.targets:
+ for targetToChange in koikatsuCommons.targetsToChange:
+ if target.bone_target == koikatsuCommons.originalBonePrefix + targetToChange:
+ target.bone_target = koikatsuCommons.deformBonePrefix + targetToChange
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ for bone in generatedRig.data.edit_bones:
+ if bpy.app.version[0] == 3:
+ if generatedRig.data.bones[bone.name].layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.junkLayerName)] == True:
+ koikatsuCommons.deleteBone(generatedRig, bone.name)
+ continue
+ if generatedRig.data.bones[bone.name].layers[koikatsuCommons.defLayerIndex] == True:
+ bone.use_deform = True
+ else:
+ if generatedRig.data.bones[bone.name].collections.get(str(koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.junkLayerName))):
+ koikatsuCommons.deleteBone(generatedRig, bone.name)
+ continue
+ if generatedRig.data.bones[bone.name].collections.get(str(koikatsuCommons.defLayerIndex)):
+ bone.use_deform = True
+
+ generatedRig.data.edit_bones[koikatsuCommons.eyesTrackTargetParentBoneName].parent = None
+ generatedRig.data.edit_bones[koikatsuCommons.headTrackTargetParentBoneName].parent = None
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ #koikatsuCommons.setBoneManagerLayersFromRigifyLayers(generatedRig)
+
+ #move some bones to the correct layer at the end because I don't feel like figuring out what went wrong
+ theme_dict = {
+ 'purple':[1.000000, 0.266356, 0.955974],
+ 'red':(0.8, 0, 0),
+ 'yellow':(0.956863, 0.788235, 0.047059),
+ 'blue':(0.005605, 0.104617, 0.947307),
+ 'green':(0.026241, 0.693872, 0.004025),
+ 'orange':(0.968628, 0.250980, 0.094118)
+ }
+ for bone in ['cf_j_tang_02', 'cf_j_tang_03', 'cf_j_tang_04', 'cf_j_tang_05', 'cf_j_tang_02.001']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 3)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['green']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['green']
+
+ #Torso detail
+ for bone in ['tweak_Neck', 'Upper Chest_fk', 'tweak_Upper Chest', 'Chest_fk', 'tweak_Upper Chest', 'Chest_fk','tweak_Chest','Spine_fk','tweak_Spine','Hips_fk','tweak_Hips']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 8)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['blue']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['blue']
+
+ for bone in ['Left wrist_fk', 'Left elbow_fk', 'Left arm_fk', 'Right arm_fk', 'Right elbow_fk', 'Right wrist_fk']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 10 if 'Left' in bone else 13)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['green']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['green']
+
+ for bone in ['Left arm_tweak', 'Left arm_tweak.001', 'Left arm_tweak.002', 'Left elbow_tweak', 'Left elbow_tweak.001', 'Left elbow_tweak.002', 'Left wrist_tweak',
+ 'Right arm_tweak', 'Right arm_tweak.001', 'Right arm_tweak.002', 'Right elbow_tweak', 'Right elbow_tweak.001', 'Right elbow_tweak.002', 'Right wrist_tweak']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 11 if 'Left' in bone else 14)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['blue']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['blue']
+
+ for bone in ['Thumb0_L', 'Thumb1_L', 'Thumb2_L', 'Thumb0_L.001', 'IndexFinger1_L', 'IndexFinger2_L', 'IndexFinger3_L', 'IndexFinger1_L.001', 'MiddleFinger1_L', 'MiddleFinger2_L', 'MiddleFinger3_L', 'MiddleFinger1_L.001', 'RingFinger1_L', 'RingFinger2_L', 'RingFinger3_L', 'RingFinger1_L.001', 'LittleFinger1_L', 'LittleFinger2_L', 'LittleFinger3_L', 'LittleFinger1_L.001', 'Thumb0_R', 'Thumb1_R', 'Thumb2_R', 'Thumb0_R.001', 'IndexFinger1_R', 'IndexFinger2_R', 'IndexFinger3_R', 'IndexFinger1_R.001', 'MiddleFinger1_R', 'MiddleFinger2_R', 'MiddleFinger3_R', 'MiddleFinger1_R.001', 'RingFinger1_R', 'RingFinger2_R', 'RingFinger3_R', 'RingFinger1_R.001', 'LittleFinger1_R', 'LittleFinger2_R', 'LittleFinger3_R', 'LittleFinger1_R.001']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 16)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['green']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['green']
+
+ for bone in ['Left leg_fk', 'Left knee_fk', 'Left ankle_fk', 'Left toe_fk', 'Right leg_fk', 'Right knee_fk', 'Right ankle_fk', 'Right toe_fk']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 18 if 'Left' in bone else 21)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['green']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['green']
+
+ for bone in ['Left leg_tweak', 'Left leg_tweak.001', 'Left leg_tweak.002', 'Left knee_tweak', 'Left knee_tweak.001', 'Left knee_tweak.002', 'Left ankle_tweak', 'Right leg_tweak', 'Right leg_tweak.001', 'Right leg_tweak.002', 'Right knee_tweak', 'Right knee_tweak.001', 'Right knee_tweak.002', 'Right ankle_tweak']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 19 if 'Left' in bone else 22)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['blue']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['blue']
+
+ for bone in ['cf_j_sk_00_00_ik', 'cf_j_sk_01_00_ik', 'cf_j_sk_02_00_ik', 'cf_j_sk_03_00_ik', 'cf_j_sk_04_00_ik', 'cf_j_sk_05_00_ik', 'cf_j_sk_06_00_ik', 'cf_j_sk_07_00_ik', 'cf_j_sk_00_00_master', 'cf_j_sk_01_00_master', 'cf_j_sk_02_00_master', 'cf_j_sk_03_00_master', 'cf_j_sk_04_00_master', 'cf_j_sk_05_00_master', 'cf_j_sk_06_00_master', 'cf_j_sk_07_00_master']:
+ koikatsuCommons.assignSingleBoneLayer_except(generatedRig, bone, 23)
+ if generatedRig.data.bones.get(bone) and bpy.app.version[0] != 3:
+ generatedRig.data.bones[bone].color.custom.normal = theme_dict['red']
+ generatedRig.pose.bones[bone].color.custom.normal = theme_dict['red']
+
+ if bpy.app.version[0] != 3:
+ bpy.context.object.pose.bones["root"].color.palette = 'CUSTOM'
+ generatedRig.data.bones['root'].color.custom.normal = theme_dict['yellow']
+ generatedRig.pose.bones['root'].color.custom.normal = theme_dict['yellow']
+ generatedRig.data.bones['root'].color.custom.select = (0.313989, 0.783538, 1.000000)
+ generatedRig.pose.bones['root'].color.custom.select = (0.313989, 0.783538, 1.000000)
+ generatedRig.data.bones['root'].color.custom.active = (0.552011, 1.000000, 1.000000)
+ generatedRig.pose.bones['root'].color.custom.active = (0.552011, 1.000000, 1.000000)
+
+ #set layer visibility
+ try:
+ for layer in ['None']:
+ generatedRig.data.collections_all[str(layer)].is_visible = False
+ except:
+ pass
+ for layer in [7,9,12,17,20]:
+ generatedRig.data.collections_all[str(layer)].is_visible = True
+ else:
+ #set layer visibility
+ generatedRig.data.layers[0] = True
+ for layer in range(1,32):
+ generatedRig.data.layers[layer] = False
+ for layer in [7,9,12,17,20,28]:
+ generatedRig.data.layers[layer] = True
+ generatedRig.data.layers[0] = False
+
+class rigify_after(bpy.types.Operator):
+ bl_idname = "kkbp.rigafter"
+ bl_label = "After Each Rigify Generate - Public"
+ bl_description = 'Performs cleanup after a Rigify generation'
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ main()
+ return {'FINISHED'}
+
diff --git a/extras/rigifyscripts/rigify_before.py b/extras/rigifyscripts/rigify_before.py
new file mode 100644
index 0000000..e4bd1be
--- /dev/null
+++ b/extras/rigifyscripts/rigify_before.py
@@ -0,0 +1,2164 @@
+
+#Switch to Object Mode and select Metarig
+
+import bpy
+import math
+from math import radians
+import statistics
+from mathutils import Matrix, Vector, Euler
+from . import commons as koikatsuCommons
+
+def main():
+ metarig = bpy.context.active_object
+
+ assert metarig.mode == "OBJECT", 'assert metarig.mode == "OBJECT"'
+ assert metarig.type == "ARMATURE", 'assert metarig.type == "ARMATURE"'
+
+ metarig.show_in_front = True
+ metarig.display_type = 'TEXTURED'
+ metarig.data.display_type = 'OCTAHEDRAL'
+
+ reservedBoneNames = [koikatsuCommons.rootBoneName, koikatsuCommons.torsoBoneName, koikatsuCommons.headTweakBoneName]
+
+ def fixReservedBoneName(rig, boneName):
+ bone = metarig.pose.bones.get(koikatsuCommons.rootBoneName)
+ if bone is not None:
+ bone.name = bone.name + koikatsuCommons.renamedNameSuffix
+
+ for boneName in reservedBoneNames:
+ fixReservedBoneName(metarig, boneName)
+
+ def fixDuplicateBoneName(rig, boneName, childBoneName):
+ childBone = metarig.pose.bones.get(childBoneName)
+ if childBone.parent.name != boneName:
+ metarig.pose.bones.get(boneName).name = boneName + koikatsuCommons.renamedNameSuffix
+ metarig.pose.bones.get(childBone.parent.name).name = boneName
+
+ fixDuplicateBoneName(metarig, koikatsuCommons.headBoneName, koikatsuCommons.originalEyesBoneName)
+ fixDuplicateBoneName(metarig, koikatsuCommons.neckBoneName, koikatsuCommons.headBoneName)
+ fixDuplicateBoneName(metarig, koikatsuCommons.upperChestBoneName, koikatsuCommons.neckBoneName)
+ fixDuplicateBoneName(metarig, koikatsuCommons.chestBoneName, koikatsuCommons.upperChestBoneName)
+ fixDuplicateBoneName(metarig, koikatsuCommons.spineBoneName, koikatsuCommons.chestBoneName)
+ fixDuplicateBoneName(metarig, koikatsuCommons.hipsBoneName, koikatsuCommons.spineBoneName)
+
+ selectedLayers = []
+ for i in range(32):
+ if bpy.app.version[0] == 3:
+ if metarig.data.layers[i] == True:
+ selectedLayers.append(i)
+ if i == koikatsuCommons.originalIkLayerIndex:
+ metarig.data.layers[i] = True
+ else:
+ metarig.data.layers[i] = False
+ else:
+ if metarig.data.collections.get(str(i)) == True:
+ selectedLayers.append(i)
+ if i == koikatsuCommons.originalIkLayerIndex:
+ if metarig.data.collections.get(str(i)):
+ metarig.data.collections.get(str(i)).is_visible = True
+ else:
+ if metarig.data.collections.get(str(i)):
+ metarig.data.collections.get(str(i)).is_visible = False
+
+ hasSkirt = True
+ if koikatsuCommons.skirtParentBoneName not in metarig.pose.bones:
+ hasSkirt = False
+ else:
+ for primaryIndex in range(8):
+ if koikatsuCommons.getSkirtBoneName(True, primaryIndex) not in metarig.pose.bones:
+ hasSkirt = False
+ break
+ for secondaryIndex in range(6):
+ if koikatsuCommons.getSkirtBoneName(False, primaryIndex, secondaryIndex) not in metarig.pose.bones:
+ hasSkirt = False
+ break
+
+ hasRiggedTongue = metarig.pose.bones.get(koikatsuCommons.riggedTongueBone1Name) and bpy.data.objects.get(koikatsuCommons.riggedTongueName())
+ hasBetterPenetrationMod = metarig.pose.bones.get(koikatsuCommons.betterPenetrationRootCrotchBoneName)
+ hasHeadMod = koikatsuCommons.isVertexGroupEmpty(koikatsuCommons.originalFaceUpDeformBoneName, koikatsuCommons.bodyName())
+ isMale = koikatsuCommons.isVertexGroupEmpty(koikatsuCommons.leftNippleDeformBone1Name, koikatsuCommons.bodyName())
+
+ def objToBone(obj, rig, boneName):
+ """
+ Places an object at the location/rotation/scale of the given bone.
+ """
+ assert bpy.context.mode != 'EDIT_ARMATURE', "assert bpy.context.mode != 'EDIT_ARMATURE'"
+
+ bone = rig.pose.bones[boneName]
+ if bpy.app.version[0] < 3:
+ scale = bone.custom_shape_scale
+ else:
+ loc = bone.custom_shape_translation
+ rot = bone.custom_shape_rotation_euler
+ scale = Vector(bone.custom_shape_scale_xyz)
+
+ if bone.use_custom_shape_bone_size:
+ scale *= bone.length
+
+ obj.rotation_mode = 'XYZ'
+
+ if bpy.app.version[0] < 3:
+ obj.matrix_basis = rig.matrix_world @ bone.bone.matrix_local @ Matrix.Scale(scale, 4)
+ else:
+ shape_mat = Matrix.LocRotScale(loc, Euler(rot), scale)
+ obj.matrix_basis = rig.matrix_world @ bone.bone.matrix_local @ shape_mat
+
+ def createWidget(objName, collectionName, rig, boneName):
+ """
+ Creates an empty widget object and returns the object.
+ """
+ scene = bpy.context.scene
+
+ # Delete object if it already exists in the scene or if it exists
+ # in blend data but not scene data.
+ # This is necessary so we can then create the object without
+ # name conflicts.
+ if objName in scene.objects or objName in bpy.data.objects:
+ obj = bpy.data.objects[objName]
+ #obj.user_clear() #gives errors/crashes
+ bpy.data.objects.remove(obj)
+
+ # Create mesh object
+ mesh = bpy.data.meshes.new(objName)
+ obj = bpy.data.objects.new(objName, mesh)
+ #scene.objects.link(obj)
+ bpy.data.collections[collectionName].objects.link(obj)
+ objToBone(obj, rig, boneName)
+ return obj
+
+ def createEyeWidget(objName, collectionName, rig, boneName, size = 1.0):
+ obj = createWidget(objName, collectionName, rig, boneName)
+ if obj is not None:
+ verts = [(1.1920928955078125e-07*size, 0.5000000596046448*size, 0.0*size), (-0.12940943241119385*size, 0.482962965965271*size, 0.0*size), (-0.24999988079071045*size, 0.4330127537250519*size, 0.0*size), (-0.35355329513549805*size, 0.35355344414711*size, 0.0*size), (-0.43301260471343994*size, 0.2500000596046448*size, 0.0*size), (-0.4829627275466919*size, 0.12940959632396698*size, 0.0*size), (-0.49999988079071045*size, 1.0094120739267964e-07*size, 0.0*size), (-0.482962965965271*size, -0.12940940260887146*size, 0.0*size), (-0.43301260471343994*size, -0.24999986588954926*size, 0.0*size), (-0.3535534143447876*size, -0.35355323553085327*size, 0.0*size), (-0.25*size, -0.43301257491111755*size, 0.0*size), (-0.1294095516204834*size, -0.48296281695365906*size, 0.0*size), (-1.1920928955078125e-07*size, -0.4999999403953552*size, 0.0*size), (0.12940943241119385*size, -0.4829629063606262*size, 0.0*size), (0.24999988079071045*size, -0.4330127537250519*size, 0.0*size), (0.35355329513549805*size, -0.35355353355407715*size, 0.0*size), (0.4330127239227295*size, -0.25000008940696716*size, 0.0*size), (0.482962965965271*size, -0.12940965592861176*size, 0.0*size), (0.5000001192092896*size, -1.6926388468618825e-07*size, 0.0*size), (0.48296308517456055*size, 0.1294093281030655*size, 0.0*size), (0.4330129623413086*size, 0.24999980628490448*size, 0.0*size), (0.35355377197265625*size, 0.35355323553085327*size, 0.0*size), (0.25000035762786865*size, 0.43301260471343994*size, 0.0*size), (0.1294100284576416*size, 0.48296287655830383*size, 0.0*size), ]
+ edges = [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (10, 9), (11, 10), (12, 11), (13, 12), (14, 13), (15, 14), (16, 15), (17, 16), (18, 17), (19, 18), (20, 19), (21, 20), (22, 21), (23, 22), (0, 23), ]
+ faces = []
+
+ mesh = obj.data
+ mesh.from_pydata(verts, edges, faces)
+ mesh.update()
+ return obj
+ else:
+ return None
+
+ def createEyesWidget(objName, collectionName, rig, boneName, size = 1.0):
+ obj = createWidget(objName, collectionName, rig, boneName)
+ if obj is not None:
+ verts = [(0.8928930759429932*size, -0.7071065902709961*size, 0.0*size), (0.8928932547569275*size, 0.7071067690849304*size, 0.0*size), (-1.8588197231292725*size, -0.9659252762794495*size, 0.0*size), (-2.100001096725464*size, -0.8660248517990112*size, 0.0*size), (-2.3071072101593018*size, -0.7071059942245483*size, 0.0*size), (-2.4660258293151855*size, -0.49999913573265076*size, 0.0*size), (-2.5659260749816895*size, -0.258818119764328*size, 0.0*size), (-2.5999999046325684*size, 8.575012770961621e-07*size, 0.0*size), (-2.5659255981445312*size, 0.2588198482990265*size, 0.0*size), (-2.4660253524780273*size, 0.5000006556510925*size, 0.0*size), (-2.3071064949035645*size, 0.7071075439453125*size, 0.0*size), (-2.099999189376831*size, 0.866025984287262*size, 0.0*size), (-1.8588184118270874*size, 0.9659261703491211*size, 0.0*size), (-1.5999996662139893*size, 1.000000238418579*size, 0.0*size), (-1.341180443763733*size, 0.9659258723258972*size, 0.0*size), (-1.0999995470046997*size, 0.8660253882408142*size, 0.0*size), (-0.8928929567337036*size, 0.7071067094802856*size, 0.0*size), (-0.892893373966217*size, -0.7071066498756409*size, 0.0*size), (-1.100000262260437*size, -0.8660252690315247*size, 0.0*size), (-1.3411810398101807*size, -0.9659255743026733*size, 0.0*size), (1.600000023841858*size, 1.0*size, 0.0*size), (1.3411810398101807*size, 0.9659258127212524*size, 0.0*size), (1.100000023841858*size, 0.8660253882408142*size, 0.0*size), (-1.600000262260437*size, -0.9999997615814209*size, 0.0*size), (1.0999997854232788*size, -0.8660252690315247*size, 0.0*size), (1.341180682182312*size, -0.9659257531166077*size, 0.0*size), (1.5999996662139893*size, -1.0*size, 0.0*size), (1.8588186502456665*size, -0.965925931930542*size, 0.0*size), (2.0999996662139893*size, -0.8660256266593933*size, 0.0*size), (2.3071064949035645*size, -0.7071071863174438*size, 0.0*size), (2.4660253524780273*size, -0.5000002980232239*size, 0.0*size), (2.5659255981445312*size, -0.25881943106651306*size, 0.0*size), (2.5999999046325684*size, -4.649122899991198e-07*size, 0.0*size), (2.5659260749816895*size, 0.25881853699684143*size, 0.0*size), (2.4660258293151855*size, 0.4999994933605194*size, 0.0*size), (2.3071072101593018*size, 0.707106351852417*size, 0.0*size), (2.1000006198883057*size, 0.8660250902175903*size, 0.0*size), (1.8588197231292725*size, 0.9659256339073181*size, 0.0*size), (-1.8070557117462158*size, -0.7727401852607727*size, 0.0*size), (-2.0000009536743164*size, -0.6928198337554932*size, 0.0*size), (-2.1656856536865234*size, -0.5656847357749939*size, 0.0*size), (-2.292820692062378*size, -0.3999992609024048*size, 0.0*size), (-2.3727407455444336*size, -0.20705445110797882*size, 0.0*size), (-2.3999998569488525*size, 7.336847716032935e-07*size, 0.0*size), (-2.3727405071258545*size, 0.207055926322937*size, 0.0*size), (-2.2928202152252197*size, 0.40000057220458984*size, 0.0*size), (-2.1656851768493652*size, 0.5656861066818237*size, 0.0*size), (-1.9999992847442627*size, 0.6928208470344543*size, 0.0*size), (-1.8070547580718994*size, 0.7727410197257996*size, 0.0*size), (-1.5999996662139893*size, 0.8000002503395081*size, 0.0*size), (-1.3929443359375*size, 0.7727407813072205*size, 0.0*size), (-1.1999995708465576*size, 0.6928203701972961*size, 0.0*size), (-1.0343143939971924*size, 0.5656854510307312*size, 0.0*size), (-1.0343146324157715*size, -0.5656852722167969*size, 0.0*size), (-1.2000001668930054*size, -0.6928201913833618*size, 0.0*size), (-1.3929448127746582*size, -0.7727404236793518*size, 0.0*size), (-1.6000001430511475*size, -0.7999997735023499*size, 0.0*size), (1.8070557117462158*size, 0.772739827632904*size, 0.0*size), (2.0000009536743164*size, 0.6928195953369141*size, 0.0*size), (2.1656856536865234*size, 0.5656843781471252*size, 0.0*size), (2.292820692062378*size, 0.39999890327453613*size, 0.0*size), (2.3727407455444336*size, 0.20705409348011017*size, 0.0*size), (2.3999998569488525*size, -1.0960745839838637e-06*size, 0.0*size), (2.3727405071258545*size, -0.20705628395080566*size, 0.0*size), (2.2928202152252197*size, -0.4000009298324585*size, 0.0*size), (2.1656851768493652*size, -0.5656863451004028*size, 0.0*size), (1.9999992847442627*size, -0.692821204662323*size, 0.0*size), (1.8070547580718994*size, -0.7727413773536682*size, 0.0*size), (1.5999996662139893*size, -0.8000004887580872*size, 0.0*size), (1.3929443359375*size, -0.7727410197257996*size, 0.0*size), (1.1999995708465576*size, -0.6928204894065857*size, 0.0*size), (1.0343143939971924*size, -0.5656855702400208*size, 0.0*size), (1.0343146324157715*size, 0.5656850337982178*size, 0.0*size), (1.2000004053115845*size, 0.6928199529647827*size, 0.0*size), (1.3929448127746582*size, 0.7727401852607727*size, 0.0*size), (1.6000001430511475*size, 0.7999995350837708*size, 0.0*size), ]
+ edges = [(24, 0), (1, 22), (16, 1), (17, 0), (23, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (21, 20), (22, 21), (13, 14), (14, 15), (15, 16), (17, 18), (18, 19), (19, 23), (25, 24), (26, 25), (27, 26), (28, 27), (29, 28), (30, 29), (31, 30), (32, 31), (33, 32), (34, 33), (35, 34), (36, 35), (37, 36), (20, 37), (56, 38), (38, 39), (39, 40), (40, 41), (41, 42), (42, 43), (43, 44), (44, 45), (45, 46), (46, 47), (47, 48), (48, 49), (49, 50), (50, 51), (51, 52), (53, 54), (54, 55), (55, 56), (75, 57), (57, 58), (58, 59), (59, 60), (60, 61), (61, 62), (62, 63), (63, 64), (64, 65), (65, 66), (66, 67), (67, 68), (68, 69), (69, 70), (70, 71), (72, 73), (73, 74), (74, 75), (52, 72), (53, 71), ]
+ faces = []
+
+ mesh = obj.data
+ mesh.from_pydata(verts, edges, faces)
+ mesh.update()
+ return obj
+ else:
+ return None
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ metarigIdBoneName = None
+ for bone in metarig.data.edit_bones:
+ if bone.name.startswith(koikatsuCommons.metarigIdBonePrefix):
+ metarigIdBoneName = bone.name
+ if not metarigIdBoneName:
+ metarigIdBoneName = koikatsuCommons.metarigIdBonePrefix + koikatsuCommons.generateRandomAlphanumericString()
+ koikatsuCommons.createBone(metarig, metarigIdBoneName)
+
+ eyesBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.originalEyesBoneName, koikatsuCommons.eyesBoneName)
+ leftEyeBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.originalEyesBoneName, koikatsuCommons.leftEyeBoneName)
+ rightEyeBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.originalEyesBoneName, koikatsuCommons.rightEyeBoneName)
+ referenceEyesBone = metarig.data.edit_bones[koikatsuCommons.originalEyesBoneName]
+ leftEyeBone.parent = eyesBone
+ rightEyeBone.parent = eyesBone
+
+ breastsBone = metarig.data.edit_bones[koikatsuCommons.breastsBoneName]
+ leftBreastBone1 = metarig.data.edit_bones[koikatsuCommons.leftBreastBone1Name]
+ rightBreastBone1 = metarig.data.edit_bones[koikatsuCommons.rightBreastBone1Name]
+ breastsBone.head.y = statistics.mean([leftBreastBone1.head.y, rightBreastBone1.head.y])
+ breastsBone.tail.y = statistics.mean([leftBreastBone1.tail.y, rightBreastBone1.tail.y])
+
+ buttocksBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.waistBoneName, koikatsuCommons.buttocksBoneName)
+ buttocksBone.parent = metarig.data.edit_bones[koikatsuCommons.waistBoneName]
+ leftButtockBone = metarig.data.edit_bones[koikatsuCommons.leftButtockBoneName]
+ leftButtockBone.parent = buttocksBone
+ leftButtockBone.length = buttocksBone.length
+ rightButtockBone = metarig.data.edit_bones[koikatsuCommons.rightButtockBoneName]
+ rightButtockBone.parent = buttocksBone
+ rightButtockBone.length = buttocksBone.length
+ buttocksBone.head.y = statistics.mean([leftButtockBone.head.y, rightButtockBone.head.y])
+ buttocksBone.tail.y = statistics.mean([leftButtockBone.tail.y, rightButtockBone.tail.y])
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ #set the newly created bones to layer 0 just in case
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.eyesBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.buttocksBoneName, 0)
+
+ def arrangeTripleWidgetSet(widgetCollectionName, parentWidget, leftChildWidget, rightChildWidget, snapGeometryToOrigin,
+ vertexGroupObjectName, leftVertexGroupName, rightVertexGroupName,
+ rotate, rotateXRadians, rotateYRadians, rotateZRadians,
+ translate, vertexGroupMidXFactor, vertexGroupMinYFactor, parentWidgetTranslateZFactor,
+ resize, parentWidgetResizeXFactor, parentWidgetResizeYFactor, parentWidgetResizeZFactor,
+ createHandleBones, rig, parentBoneName, parentHandleBoneName, leftChildBoneName, leftChildHandleBoneName, rightChildBoneName, rightChildHandleBoneName):
+ activeObject = bpy.context.view_layer.objects.active
+ widgetCollection = bpy.context.view_layer.layer_collection.children[0].children[widgetCollectionName]
+ bpy.ops.object.select_all(action='DESELECT')
+ widgetCollection.exclude = False
+ widgetCollection.hide_viewport = False
+ parentWidget.select_set(True)
+ bpy.context.view_layer.objects.active = parentWidget
+ if snapGeometryToOrigin:
+ bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS')
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ bpy.context.scene.tool_settings.transform_pivot_point = 'BOUNDING_BOX_CENTER'
+ bpy.ops.mesh.select_all(action='SELECT')
+ if rotate:
+ if rotateXRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateXRadians), orient_axis='X', orient_type='GLOBAL')
+ if rotateYRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateYRadians), orient_axis='Y', orient_type='GLOBAL')
+ if rotateZRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateZRadians), orient_axis='Z', orient_type='GLOBAL')
+ leftVertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(leftVertexGroupName, vertexGroupObjectName)
+ rightVertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(rightVertexGroupName, vertexGroupObjectName)
+ leftVertexGroupMidZ = (leftVertexGroupExtremities.maxZ + leftVertexGroupExtremities.minZ) / 2
+ rightVertexGroupMidZ = (rightVertexGroupExtremities.maxZ + rightVertexGroupExtremities.minZ) / 2
+ averageVertexGroupMidZ = statistics.mean([leftVertexGroupMidZ, rightVertexGroupMidZ])
+ averageVertexGroupMinY = statistics.mean([leftVertexGroupExtremities.minY, rightVertexGroupExtremities.minY])
+ if translate and not createHandleBones:
+ bpy.ops.transform.translate(value=(0, averageVertexGroupMinY * vertexGroupMinYFactor, -(parentWidget.location[2] - averageVertexGroupMidZ * parentWidgetTranslateZFactor)), orient_type='GLOBAL')
+ leftVertexGroupMidX = (leftVertexGroupExtremities.maxX + leftVertexGroupExtremities.minX) / 2
+ rightVertexGroupMidX = (rightVertexGroupExtremities.maxX + rightVertexGroupExtremities.minX) / 2
+ if resize:
+ bpy.ops.transform.resize(value=(parentWidgetResizeXFactor, parentWidgetResizeYFactor, parentWidgetResizeZFactor), orient_type='GLOBAL')
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ parentWidget.select_set(False)
+ leftChildWidget.select_set(True)
+ bpy.context.view_layer.objects.active = leftChildWidget
+ if snapGeometryToOrigin:
+ bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS')
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ bpy.context.scene.tool_settings.transform_pivot_point = 'BOUNDING_BOX_CENTER'
+ bpy.ops.mesh.select_all(action='SELECT')
+ if rotate:
+ if rotateXRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateXRadians), orient_axis='X', orient_type='GLOBAL')
+ if rotateYRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateYRadians), orient_axis='Y', orient_type='GLOBAL')
+ if rotateZRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateZRadians), orient_axis='Z', orient_type='GLOBAL')
+ if translate and not createHandleBones:
+ bpy.ops.transform.translate(value=(leftVertexGroupMidX * vertexGroupMidXFactor, leftVertexGroupExtremities.minY * vertexGroupMinYFactor, -(leftChildWidget.location[2] - leftVertexGroupMidZ * parentWidgetTranslateZFactor)), orient_type='GLOBAL')
+ #leftVertexGroupArea = (leftVertexGroupExtremities.maxX - leftVertexGroupExtremities.minX) * (leftVertexGroupExtremities.maxZ - leftVertexGroupExtremities.minZ)
+ #rightVertexGroupArea = (rightVertexGroupExtremities.maxX - rightVertexGroupExtremities.minX) * (rightVertexGroupExtremities.maxZ - rightVertexGroupExtremities.minZ)
+ #averageVertexGroupArea = statistics.mean([leftVertexGroupArea, rightVertexGroupArea])
+ #leftChildWidgetResizeXFactor = statistics.mean([parentWidgetResizeXFactor, parentWidgetResizeXFactor * (leftVertexGroupArea / averageVertexGroupArea)])
+ #leftChildWidgetResizeYFactor = statistics.mean([parentWidgetResizeYFactor, parentWidgetResizeYFactor * (leftVertexGroupArea / averageVertexGroupArea)])
+ #leftChildWidgetResizeZFactor = statistics.mean([parentWidgetResizeZFactor, parentWidgetResizeZFactor * (leftVertexGroupArea / averageVertexGroupArea)])
+ leftVertexGroupLengthX = leftVertexGroupExtremities.maxX - leftVertexGroupExtremities.minX
+ rightVertexGroupLengthX = rightVertexGroupExtremities.maxX - rightVertexGroupExtremities.minX
+ averageVertexGroupLengthX = statistics.mean([leftVertexGroupLengthX, rightVertexGroupLengthX])
+ leftVertexGroupLengthY = leftVertexGroupExtremities.maxY - leftVertexGroupExtremities.minY
+ rightVertexGroupLengthY = rightVertexGroupExtremities.maxY - rightVertexGroupExtremities.minY
+ averageVertexGroupLengthY = statistics.mean([leftVertexGroupLengthY, rightVertexGroupLengthY])
+ leftVertexGroupLengthZ = leftVertexGroupExtremities.maxZ - leftVertexGroupExtremities.minZ
+ rightVertexGroupLengthZ = rightVertexGroupExtremities.maxZ - rightVertexGroupExtremities.minZ
+ averageVertexGroupLengthZ = statistics.mean([leftVertexGroupLengthZ, rightVertexGroupLengthZ])
+ leftChildWidgetResizeXFactor = statistics.mean([parentWidgetResizeXFactor, parentWidgetResizeXFactor * (leftVertexGroupLengthX / averageVertexGroupLengthX)])
+ leftChildWidgetResizeYFactor = statistics.mean([parentWidgetResizeYFactor, parentWidgetResizeYFactor * (leftVertexGroupLengthY / averageVertexGroupLengthY)])
+ leftChildWidgetResizeZFactor = statistics.mean([parentWidgetResizeZFactor, parentWidgetResizeZFactor * (leftVertexGroupLengthZ / averageVertexGroupLengthZ)])
+ if resize:
+ bpy.ops.transform.resize(value=(leftChildWidgetResizeXFactor, leftChildWidgetResizeYFactor, leftChildWidgetResizeZFactor), orient_type='GLOBAL')
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ leftChildWidget.select_set(False)
+ rightChildWidget.select_set(True)
+ bpy.context.view_layer.objects.active = rightChildWidget
+ if snapGeometryToOrigin:
+ bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN', center='BOUNDS')
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ bpy.context.scene.tool_settings.transform_pivot_point = 'BOUNDING_BOX_CENTER'
+ bpy.ops.mesh.select_all(action='SELECT')
+ if rotate:
+ if rotateXRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateXRadians), orient_axis='X', orient_type='GLOBAL')
+ if rotateYRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateYRadians), orient_axis='Y', orient_type='GLOBAL')
+ if rotateZRadians:
+ bpy.ops.transform.rotate(value=math.radians(rotateZRadians), orient_axis='Z', orient_type='GLOBAL')
+ if translate and not createHandleBones:
+ bpy.ops.transform.translate(value=(rightVertexGroupMidX * vertexGroupMidXFactor, rightVertexGroupExtremities.minY * vertexGroupMinYFactor, -(rightChildWidget.location[2] - rightVertexGroupMidZ * parentWidgetTranslateZFactor)), orient_type='GLOBAL')
+ #rightChildWidgetResizeXFactor = statistics.mean([parentWidgetResizeXFactor, parentWidgetResizeXFactor * (rightVertexGroupArea / averageVertexGroupArea)])
+ #rightChildWidgetResizeYFactor = statistics.mean([parentWidgetResizeYFactor, parentWidgetResizeYFactor * (rightVertexGroupArea / averageVertexGroupArea)])
+ #rightChildWidgetResizeZFactor = statistics.mean([parentWidgetResizeZFactor, parentWidgetResizeZFactor * (rightVertexGroupArea / averageVertexGroupArea)])
+ rightChildWidgetResizeXFactor = statistics.mean([parentWidgetResizeXFactor, parentWidgetResizeXFactor * (rightVertexGroupLengthX / averageVertexGroupLengthX)])
+ rightChildWidgetResizeYFactor = statistics.mean([parentWidgetResizeYFactor, parentWidgetResizeYFactor * (rightVertexGroupLengthY / averageVertexGroupLengthY)])
+ rightChildWidgetResizeZFactor = statistics.mean([parentWidgetResizeZFactor, parentWidgetResizeZFactor * (rightVertexGroupLengthZ / averageVertexGroupLengthZ)])
+ if resize:
+ bpy.ops.transform.resize(value=(rightChildWidgetResizeXFactor, rightChildWidgetResizeYFactor, rightChildWidgetResizeZFactor), orient_type='GLOBAL')
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ rightChildWidget.select_set(False)
+ widgetCollection.exclude = True
+ widgetCollection.hide_viewport = True
+ activeObject.select_set(True)
+ bpy.context.view_layer.objects.active = activeObject
+ if createHandleBones:
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ parentHandleBone = koikatsuCommons.copyBone(rig, parentBoneName, parentHandleBoneName)
+ if translate:
+ parentHandleBone.tail.y = parentHandleBone.tail.y + averageVertexGroupMinY * vertexGroupMinYFactor
+ parentHandleBone.head.y = parentHandleBone.head.y + averageVertexGroupMinY * vertexGroupMinYFactor
+ parentHandleBone.tail.z = parentHandleBone.tail.z - (parentWidget.location[2] - averageVertexGroupMidZ * parentWidgetTranslateZFactor)
+ parentHandleBone.head.z = parentHandleBone.head.z - (parentWidget.location[2] - averageVertexGroupMidZ * parentWidgetTranslateZFactor)
+ leftChildHandleBone = koikatsuCommons.copyBone(rig, leftChildBoneName, leftChildHandleBoneName)
+ if translate:
+ leftChildHandleBone.tail.x = leftChildHandleBone.tail.x + leftVertexGroupMidX * vertexGroupMidXFactor
+ leftChildHandleBone.head.x = leftChildHandleBone.head.x + leftVertexGroupMidX * vertexGroupMidXFactor
+ leftChildHandleBone.tail.y = leftChildHandleBone.tail.y + leftVertexGroupExtremities.minY * vertexGroupMinYFactor
+ leftChildHandleBone.head.y = leftChildHandleBone.head.y + leftVertexGroupExtremities.minY * vertexGroupMinYFactor
+ leftChildHandleBone.tail.z = leftChildHandleBone.tail.z - (leftChildWidget.location[2] - leftVertexGroupMidZ * parentWidgetTranslateZFactor)
+ leftChildHandleBone.head.z = leftChildHandleBone.head.z - (leftChildWidget.location[2] - leftVertexGroupMidZ * parentWidgetTranslateZFactor)
+ leftChildHandleBone.parent = parentHandleBone
+ rightChildHandleBone = koikatsuCommons.copyBone(rig, rightChildBoneName, rightChildHandleBoneName)
+ if translate:
+ rightChildHandleBone.tail.x = rightChildHandleBone.tail.x + rightVertexGroupMidX * vertexGroupMidXFactor
+ rightChildHandleBone.head.x = rightChildHandleBone.head.x + rightVertexGroupMidX * vertexGroupMidXFactor
+ rightChildHandleBone.tail.y = rightChildHandleBone.tail.y + rightVertexGroupExtremities.minY * vertexGroupMinYFactor
+ rightChildHandleBone.head.y = rightChildHandleBone.head.y + rightVertexGroupExtremities.minY * vertexGroupMinYFactor
+ rightChildHandleBone.tail.z = rightChildHandleBone.tail.z - (rightChildWidget.location[2] - rightVertexGroupMidZ * parentWidgetTranslateZFactor)
+ rightChildHandleBone.head.z = rightChildHandleBone.head.z - (rightChildWidget.location[2] - rightVertexGroupMidZ * parentWidgetTranslateZFactor)
+ rightChildHandleBone.parent = parentHandleBone
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ #set new bones to layer 0, just in case
+ koikatsuCommons.assignSingleBoneLayer(rig, parentHandleBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(rig, leftChildHandleBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(rig, rightChildHandleBoneName, 0)
+
+ widgetEyes = createEyesWidget(koikatsuCommons.widgetEyesName, koikatsuCommons.widgetCollectionName, metarig, koikatsuCommons.eyesBoneName)
+ widgetEyeLeft = createEyeWidget(koikatsuCommons.widgetEyeLeftName, koikatsuCommons.widgetCollectionName, metarig, koikatsuCommons.leftEyeBoneName)
+ widgetEyeRight = createEyeWidget(koikatsuCommons.widgetEyeRightName, koikatsuCommons.widgetCollectionName, metarig, koikatsuCommons.rightEyeBoneName)
+
+ arrangeTripleWidgetSet(koikatsuCommons.widgetCollectionName, widgetEyes, widgetEyeLeft, widgetEyeRight, False,
+ koikatsuCommons.bodyName(), koikatsuCommons.leftEyeDeformBoneName, koikatsuCommons.rightEyeDeformBoneName,
+ True, 90, 0, 90,
+ True, 0.93, 1.66427, 1,
+ True, 1.16, 1, 1.65,
+ True, metarig, koikatsuCommons.eyesBoneName, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.leftEyeBoneName, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.rightEyeBoneName, koikatsuCommons.rightEyeHandleBoneName)
+
+ widgetBreasts = koikatsuCommons.copyObject(koikatsuCommons.widgetCollectionName, koikatsuCommons.originalWidgetBreastsName, koikatsuCommons.widgetBreastsName)
+ objToBone(widgetBreasts, metarig, koikatsuCommons.breastsBoneName)
+ widgetBreastLeft = koikatsuCommons.copyObject(koikatsuCommons.widgetCollectionName, koikatsuCommons.originalWidgetBreastLeftName, koikatsuCommons.widgetBreastLeftName)
+ objToBone(widgetBreastLeft, metarig, koikatsuCommons.breastsBoneName)
+ widgetBreastRight = koikatsuCommons.copyObject(koikatsuCommons.widgetCollectionName, koikatsuCommons.originalWidgetBreastRightName, koikatsuCommons.widgetBreastRightName)
+ objToBone(widgetBreastRight, metarig, koikatsuCommons.breastsBoneName)
+
+ if not isMale:
+ arrangeTripleWidgetSet(koikatsuCommons.widgetCollectionName, widgetBreasts, widgetBreastLeft, widgetBreastRight, True,
+ koikatsuCommons.bodyName(), koikatsuCommons.leftNippleDeformBone1Name, koikatsuCommons.rightNippleDeformBone1Name,
+ False, None, None, None,
+ True, 0.3, 1.66427, 0.9955,
+ True, 21, 1, 19,
+ True, metarig, koikatsuCommons.breastsBoneName, koikatsuCommons.breastsHandleBoneName, koikatsuCommons.leftBreastBone1Name, koikatsuCommons.leftBreastHandleBoneName, koikatsuCommons.rightBreastBone1Name, koikatsuCommons.rightBreastHandleBoneName)
+
+ widgetButtocks = koikatsuCommons.copyObject(koikatsuCommons.widgetCollectionName, koikatsuCommons.originalWidgetBreastsName, koikatsuCommons.widgetButtocksName)
+ objToBone(widgetButtocks, metarig, koikatsuCommons.buttocksBoneName)
+ widgetButtockLeft = koikatsuCommons.copyObject(koikatsuCommons.widgetCollectionName, koikatsuCommons.originalWidgetBreastLeftName, koikatsuCommons.widgetButtockLeftName)
+ objToBone(widgetButtockLeft, metarig, koikatsuCommons.buttocksBoneName)
+ widgetButtockRight = koikatsuCommons.copyObject(koikatsuCommons.widgetCollectionName, koikatsuCommons.originalWidgetBreastRightName, koikatsuCommons.widgetButtockRightName)
+ objToBone(widgetButtockRight, metarig, koikatsuCommons.buttocksBoneName)
+
+ arrangeTripleWidgetSet(koikatsuCommons.widgetCollectionName, widgetButtocks, widgetButtockLeft, widgetButtockRight, True,
+ koikatsuCommons.bodyName(), koikatsuCommons.leftButtockDeformBoneName, koikatsuCommons.rightButtockDeformBoneName,
+ False, None, None, None,
+ True, 0, -3.6, 1,
+ True, 30, 1, 25,
+ True, metarig, koikatsuCommons.buttocksBoneName, koikatsuCommons.buttocksHandleBoneName, koikatsuCommons.leftButtockBoneName, koikatsuCommons.leftButtockHandleBoneName, koikatsuCommons.rightButtockBoneName, koikatsuCommons.rightButtockHandleBoneName)
+
+ def finalizeHandleBone(isEyeHandleBone, rig, boneName, handleBoneName, widget):
+ rig.pose.bones[boneName].custom_shape = None
+ rig.pose.bones[handleBoneName].custom_shape = widget
+ if isEyeHandleBone:
+ rig.pose.bones[handleBoneName].lock_location[1] = True
+ rig.pose.bones[handleBoneName].lock_rotation[0] = True
+ rig.pose.bones[handleBoneName].lock_rotation[2] = True
+ rig.pose.bones[handleBoneName].lock_scale[1] = True
+ koikatsuCommons.addCopyTransformsConstraint(rig, boneName, handleBoneName, 'REPLACE', 'LOCAL', koikatsuCommons.copyTransformsConstraintBaseName + koikatsuCommons.handleConstraintSuffix)
+
+ finalizeHandleBone(True, metarig, koikatsuCommons.eyesBoneName, koikatsuCommons.eyesHandleBoneName, widgetEyes)
+ finalizeHandleBone(True, metarig, koikatsuCommons.leftEyeBoneName, koikatsuCommons.leftEyeHandleBoneName, widgetEyeLeft)
+ finalizeHandleBone(True, metarig, koikatsuCommons.rightEyeBoneName, koikatsuCommons.rightEyeHandleBoneName, widgetEyeRight)
+ metarig.pose.bones[koikatsuCommons.originalEyesBoneName].custom_shape = None
+ #metarig.data.bones[koikatsuCommons.originalEyesBoneName].layers[koikatsuCommons.originalMchLayerIndex] = True
+ #metarig.data.bones[koikatsuCommons.originalEyesBoneName].layers[koikatsuCommons.originalIkLayerIndex] = False
+ if not isMale:
+ finalizeHandleBone(False, metarig, koikatsuCommons.breastsBoneName, koikatsuCommons.breastsHandleBoneName, widgetBreasts)
+ finalizeHandleBone(False, metarig, koikatsuCommons.leftBreastBone1Name, koikatsuCommons.leftBreastHandleBoneName, widgetBreastLeft)
+ finalizeHandleBone(False, metarig, koikatsuCommons.rightBreastBone1Name, koikatsuCommons.rightBreastHandleBoneName, widgetBreastRight)
+ finalizeHandleBone(False, metarig, koikatsuCommons.buttocksBoneName, koikatsuCommons.buttocksHandleBoneName, widgetButtocks)
+ finalizeHandleBone(False, metarig, koikatsuCommons.leftButtockBoneName, koikatsuCommons.leftButtockHandleBoneName, widgetButtockLeft)
+ finalizeHandleBone(False, metarig, koikatsuCommons.rightButtockBoneName, koikatsuCommons.rightButtockHandleBoneName, widgetButtockRight)
+ copyTransformsConstraintName = koikatsuCommons.copyTransformsConstraintBaseName + koikatsuCommons.jointConstraintSuffix + koikatsuCommons.correctionConstraintSuffix
+ koikatsuCommons.addCopyTransformsConstraint(metarig, koikatsuCommons.leftButtockHandleBoneName, koikatsuCommons.leftButtockJointCorrectionBoneName, 'AFTER', 'LOCAL', copyTransformsConstraintName)
+ koikatsuCommons.addCopyTransformsConstraint(metarig, koikatsuCommons.rightButtockHandleBoneName, koikatsuCommons.rightButtockJointCorrectionBoneName, 'AFTER', 'LOCAL', copyTransformsConstraintName)
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ eyesTrackTargetBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyesTrackTargetBoneName)
+ eyesTrackTargetParentBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyesTrackTargetParentBoneName)
+ eyesHandleMarkerBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyesHandleMarkerBoneName)
+ leftEyeHandleMarkerBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeHandleMarkerBoneName)
+ rightEyeHandleMarkerBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeHandleMarkerBoneName)
+ leftEyeHandleMarkerXBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeHandleMarkerXBoneName)
+ rightEyeHandleMarkerXBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeHandleMarkerXBoneName)
+ leftEyeHandleMarkerXBone.head.z = eyesHandleMarkerBone.head.z
+ rightEyeHandleMarkerXBone.head.z = eyesHandleMarkerBone.head.z
+ leftEyeHandleMarkerXBone.tail.z = eyesHandleMarkerBone.head.z
+ rightEyeHandleMarkerXBone.tail.z = eyesHandleMarkerBone.head.z
+ leftEyeHandleMarkerZBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeHandleMarkerZBoneName)
+ rightEyeHandleMarkerZBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeHandleMarkerZBoneName)
+ leftEyeHandleMarkerZBone.head.x = eyesHandleMarkerBone.head.x
+ rightEyeHandleMarkerZBone.head.x = eyesHandleMarkerBone.head.x
+ leftEyeHandleMarkerZBone.tail.x = eyesHandleMarkerBone.head.x
+ rightEyeHandleMarkerZBone.tail.x = eyesHandleMarkerBone.head.x
+ eyesTrackTargetBone.parent = eyesTrackTargetParentBone
+ eyesTrackTargetParentBone.parent = None
+ headBone = metarig.data.edit_bones[koikatsuCommons.headBoneName]
+ leftEyeHandleMarkerBone.parent = headBone
+ rightEyeHandleMarkerBone.parent = headBone
+ leftEyeHandleMarkerXBone.parent = headBone
+ rightEyeHandleMarkerXBone.parent = headBone
+ leftEyeHandleMarkerZBone.parent = headBone
+ rightEyeHandleMarkerZBone.parent = headBone
+ eyeballsBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyeballsBoneName)
+ eyeballsBone.roll = radians(0)
+ leftEyeballBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeballBoneName)
+ rightEyeballBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeballBoneName)
+ leftEyeballBone.parent = headBone
+ rightEyeballBone.parent = headBone
+ leftEyeballBone.roll = radians(0)
+ rightEyeballBone.roll = radians(0)
+ leftEyeballBone.head.y = metarig.data.edit_bones[koikatsuCommons.leftEyeDeformBoneName].head.y
+ rightEyeballBone.head.y = metarig.data.edit_bones[koikatsuCommons.rightEyeDeformBoneName].head.y
+ eyeballsBone.head.y = statistics.mean([leftEyeballBone.head.y, rightEyeballBone.head.y])
+ eyeballsBone.length = metarig.data.edit_bones[koikatsuCommons.eyesHandleBoneName].length
+ leftEyeballBone.length = metarig.data.edit_bones[koikatsuCommons.leftEyeHandleBoneName].length
+ rightEyeballBone.length = metarig.data.edit_bones[koikatsuCommons.rightEyeHandleBoneName].length
+ eyeballsTrackBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyeballsBoneName, koikatsuCommons.eyeballsTrackBoneName)
+ leftEyeballTrackBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeballBoneName, koikatsuCommons.leftEyeballTrackBoneName)
+ rightEyeballTrackBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeballBoneName, koikatsuCommons.rightEyeballTrackBoneName)
+ leftEyeballTrackCorrectionBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeballTrackBoneName, koikatsuCommons.leftEyeballTrackCorrectionBoneName)
+ rightEyeballTrackCorrectionBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeballTrackBoneName, koikatsuCommons.rightEyeballTrackCorrectionBoneName)
+ leftHeadMarkerXBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftHeadMarkerXBoneName)
+ rightHeadMarkerXBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightHeadMarkerXBoneName)
+ leftHeadMarkerXBone.parent = headBone
+ rightHeadMarkerXBone.parent = headBone
+ leftHeadMarkerXBone.head.y = headBone.head.y
+ rightHeadMarkerXBone.head.y = headBone.head.y
+ leftHeadMarkerXBone.length = metarig.data.edit_bones[koikatsuCommons.leftEyeHandleBoneName].length
+ rightHeadMarkerXBone.length = metarig.data.edit_bones[koikatsuCommons.rightEyeHandleBoneName].length
+ leftHeadMarkerXBone.head.z = headBone.head.z
+ rightHeadMarkerXBone.head.z = headBone.head.z
+ leftHeadMarkerXBone.tail.z = headBone.head.z
+ rightHeadMarkerXBone.tail.z = headBone.head.z
+ leftHeadMarkerZBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftHeadMarkerZBoneName)
+ rightHeadMarkerZBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightHeadMarkerZBoneName)
+ leftHeadMarkerZBone.parent = headBone
+ rightHeadMarkerZBone.parent = headBone
+ leftHeadMarkerZBone.head.y = headBone.head.y
+ rightHeadMarkerZBone.head.y = headBone.head.y
+ leftHeadMarkerZBone.length = metarig.data.edit_bones[koikatsuCommons.leftEyeHandleBoneName].length
+ rightHeadMarkerZBone.length = metarig.data.edit_bones[koikatsuCommons.rightEyeHandleBoneName].length
+ leftHeadMarkerZBone.head.x = headBone.head.x
+ rightHeadMarkerZBone.head.x = headBone.head.x
+ leftHeadMarkerZBone.tail.x = headBone.head.x
+ rightHeadMarkerZBone.tail.x = headBone.head.x
+ headTrackTargetBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.headTrackTargetBoneName)
+ headTrackTargetBone.length = headBone.length
+ headTrackTargetParentBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.headTrackTargetBoneName, koikatsuCommons.headTrackTargetParentBoneName)
+ headTrackTargetBone.parent = headTrackTargetParentBone
+ headTrackTargetParentBone.parent = None
+ headTrackBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.eyeballsBoneName, koikatsuCommons.headTrackBoneName)
+ headTrackBone.parent = None
+ headTrackBone.head.y = headBone.head.y
+ placeholderHeadTweakBone = koikatsuCommons.createBone(metarig, koikatsuCommons.headTweakBoneName + koikatsuCommons.placeholderBoneSuffix)
+ placeholderTorsoBone = koikatsuCommons.createBone(metarig, koikatsuCommons.torsoBoneName + koikatsuCommons.placeholderBoneSuffix)
+ placeholderRootBone = koikatsuCommons.createBone(metarig, koikatsuCommons.rootBoneName + koikatsuCommons.placeholderBoneSuffix)
+
+ placeholderHeadTweakBoneName = placeholderHeadTweakBone.name
+ placeholderTorsoBoneName = placeholderTorsoBone.name
+ placeholderRootBoneName = placeholderRootBone.name
+
+ leftHeadDistX = leftHeadMarkerXBone.head.x - headBone.head.x
+ rightHeadDistX = rightHeadMarkerXBone.head.x - headBone.head.x
+ leftHeadDistZ = leftHeadMarkerZBone.head.z - headBone.head.z
+ rightHeadDistZ = rightHeadMarkerZBone.head.z - headBone.head.z
+ leftHeadDistXReference = 0.031159714929090754
+ rightHeadDistXReference = -0.031183381431473478
+ leftHeadDistZReference = 0.053977131843566895
+ rightHeadDistZReference = 0.05397462844848633
+
+ leftEyeballTrackCorrectionLeftEyeHandleDistY = leftEyeballTrackCorrectionBone.head.y - leftEyeHandleMarkerBone.head.y
+ rightEyeballTrackCorrectionRightEyeHandleDistY = rightEyeballTrackCorrectionBone.head.y - rightEyeHandleMarkerBone.head.y
+ leftEyeballTrackCorrectionLeftEyeHandleDistYReference = 0.09179527312517166
+ rightEyeballTrackCorrectionRightEyeHandleDistYReference = 0.0917946808040142
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ #set newly created bones to layer 0
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.eyesTrackTargetBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.eyesTrackTargetParentBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.eyesHandleMarkerBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeHandleMarkerBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeHandleMarkerBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeHandleMarkerXBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeHandleMarkerXBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeHandleMarkerZBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeHandleMarkerZBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.eyeballsBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeballBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeballBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.eyeballsTrackBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeballTrackBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeballTrackBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftEyeballTrackCorrectionBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightEyeballTrackCorrectionBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftHeadMarkerXBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightHeadMarkerXBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftHeadMarkerZBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightHeadMarkerZBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.headTrackTargetBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.headTrackTargetParentBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.headTrackBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.headTweakBoneName + koikatsuCommons.placeholderBoneSuffix, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.torsoBoneName + koikatsuCommons.placeholderBoneSuffix, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rootBoneName + koikatsuCommons.placeholderBoneSuffix, 0)
+
+ eyesSizeFactor = 100.0
+
+ """
+ leftEyeVertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.leftEyeDeformBoneName, koikatsuCommons.bodyName())
+ rightEyeVertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.rightEyeDeformBoneName, koikatsuCommons.bodyName())
+ leftEyeVertexGroupArea = (leftEyeVertexGroupExtremities.maxX - leftEyeVertexGroupExtremities.minX) * (leftEyeVertexGroupExtremities.maxZ - leftEyeVertexGroupExtremities.minZ)
+ rightEyeVertexGroupArea = (rightEyeVertexGroupExtremities.maxX - rightEyeVertexGroupExtremities.minX) * (rightEyeVertexGroupExtremities.maxZ - rightEyeVertexGroupExtremities.minZ)
+ averageEyeVertexGroupArea = statistics.mean([leftEyeVertexGroupArea, rightEyeVertexGroupArea])
+ averageEyeVertexGroupAreaReference = 0.0016523913975554083
+ eyesSizeFactorCurrent = eyesSizeFactor * math.sqrt(averageEyeVertexGroupArea / averageEyeVertexGroupAreaReference)
+ """
+
+ eyesHandleLimitLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "01A) Limit location", "Limits the maximum reachable X and Y locations based on the left and right eye handles settings", 0.0, 0.0, 1.0)
+ eyesHandleEyelidsAutomationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "02A) Eyelids automation", "Enables eyelids shape key automation when both eyes move up and down", 0.0, 0.0, 1.0)
+ eyesHandleDefaultEyelidsValueDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "02B) Default eyelids value", "Eyelids shape key automation value when both eyes are at their default position", 0.05, 0.0, 1.0)
+ eyesHandleMinEyelidsValueDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "02C) Min eyelids value", "Minimum reachable eyelids shape key automation value when both eyes move down", 0.0, 0.0, 1.0)
+ eyesHandleMaxEyelidsValueDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "02D) Max eyelids value", "Maximum reachable eyelids shape key automation value when both eyes move up", 0.25, 0.0, 1.0)
+ eyesHandleEyelidsSpeedFactorDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "02E) Eyelids speed factor", "Determines how fast the eyelids shape key automation value will change when both eyes move up and down", 2.0, 0.0, eyesSizeFactor * 10)
+ eyesHandleEyesSizeFactorDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesHandleBoneName, "03A) Eyes size factor", "Factors into many constraints and drivers for the eyes mechanics; it's better not to change this value", eyesSizeFactor, 0, eyesSizeFactor * 10)
+ leftEyeHandleLimitLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.leftEyeHandleBoneName, "01A) Limit location", "Limits the maximum reachable X and Y locations based on the settings below", 0.0, 0.0, 1.0)
+ leftEyeHandleMinXLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.leftEyeHandleBoneName, "01B) Min X location", "Minimum reachable X location limit", -0.075 - leftHeadDistXReference + leftHeadDistX, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ leftEyeHandleMaxXLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.leftEyeHandleBoneName, "01C) Max X location", "Maximum reachable X location limit", 0.25 - leftHeadDistXReference + leftHeadDistX, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ leftEyeHandleMinYLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.leftEyeHandleBoneName, "01D) Min Y location", "Minimum reachable Y location limit", -0.1 - leftHeadDistZReference + leftHeadDistZ, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ leftEyeHandleMaxYLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.leftEyeHandleBoneName, "01E) Max Y location", "Maximum reachable Y location limit", 0.13 - leftHeadDistZReference + leftHeadDistZ, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ leftEyeHandleSpeedCorrectionDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.leftEyeHandleBoneName, "02A) Speed correction", "Increases the speed at which the eye moves when not controlled directly in order to reach its maximum location limits at the same time as the other eye", 0.0, 0.0, 1.0)
+ rightEyeHandleLimitLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.rightEyeHandleBoneName, "01A) Limit location", "Limits the maximum reachable X and Y locations based on the settings below", 0.0, 0.0, 1.0)
+ rightEyeHandleMinXLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.rightEyeHandleBoneName, "01B) Min X location", "Minimum reachable X location limit", -0.25 - rightHeadDistXReference + rightHeadDistX, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ rightEyeHandleMaxXLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.rightEyeHandleBoneName, "01C) Max X location", "Maximum reachable X location limit", 0.075 - rightHeadDistXReference + rightHeadDistX, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ rightEyeHandleMinYLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.rightEyeHandleBoneName, "01D) Min Y location", "Minimum reachable Y location limit", -0.1 - rightHeadDistZReference + rightHeadDistZ, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ rightEyeHandleMaxYLocationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.rightEyeHandleBoneName, "01E) Max Y location", "Maximum reachable Y location limit", 0.13 - rightHeadDistZReference + rightHeadDistZ, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ rightEyeHandleSpeedCorrectionDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.rightEyeHandleBoneName, "02A) Speed correction", "Increases the speed at which the eye moves when not controlled directly in order to reach its maximum location limits at the same time as the other eye", 0.0, 0.0, 1.0)
+ eyesTrackTargetParentToHeadDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesTrackTargetBoneName, "01A) Parent to head", "The bone will act as if it was parented to the head bone", 1, 0, 1)
+ eyesTrackTargetParentToTorsoDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesTrackTargetBoneName, "01B) Parent to torso", "The bone will act as if it was parented to the torso bone", 0, 0, 1)
+ eyesTrackTargetParentToRootDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyesTrackTargetBoneName, "01C) Parent to root", "The bone will act as if it was parented to the root bone", 0, 0, 1)
+ eyeballsSpeedFactorDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyeballsBoneName, "01A) Eyeballs speed factor", "Determines how fast the eyes will move when rotating the eyeball bones manually", 0.26, 0.0, 1.0)
+ eyeballsTrackTargetDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyeballsBoneName, "02A) Track target", "Enables tracking of the eyeballs track target bone", 0.0, 0.0, 1.0)
+ eyeballsTrackSpeedFactorDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyeballsBoneName, "02B) Eyeballs track speed factor", "Determines how fast the eyes will move when the eyeballs are tracking their target", 0.04, 0.0, 1.0)
+ eyeballsNearbyTargetTrackCorrectionDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyeballsBoneName, "02C) Nearby target track correction", "When tracking a target that moves closer to the middle of the face than its original location, the individual eyes will gradually point towards it instead of keeping the original distance between them", 0.0, 0.0, 1.0)
+ eyeballsNearbyTargetSizeFactorDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.eyeballsBoneName, "02D) Nearby target track size factor", "Factors into the nearby target track correction, if the character is scaled up or down after running the first Rigify script this value should be changed proportionally", rightEyeballTrackCorrectionRightEyeHandleDistY / rightEyeballTrackCorrectionRightEyeHandleDistYReference, eyesSizeFactor * -10, eyesSizeFactor * 10)
+ headTrackTargetLimitRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01A) Limit rotation", "Limits the maximum reachable rotation of the head based on the settings below", 0.0, 0.0, 1.0)
+ headTrackTargetMinXRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01B) Min X rotation", "Minimum reachable X angle limit", -40.0, -180.0, 0.0)
+ headTrackTargetMaxXRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01C) Max X rotation", "Maximum reachable X angle limit", 50.0, 0.0, 180.0)
+ headTrackTargetMinYRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01D) Min Y rotation", "Minimum reachable Y angle limit", -80.0, -180.0, 0.0)
+ headTrackTargetMaxYRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01E) Max Y rotation", "Maximum reachable Y angle limit", 80.0, 0.0, 180.0)
+ headTrackTargetMinZRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01F) Min Z rotation", "Minimum reachable Z angle limit", -50.0, -180.0, 0.0)
+ headTrackTargetMaxZRotationDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "01G) Max Z rotation", "Maximum reachable Z angle limit", 50.0, 0.0, 180.0)
+ headTrackTargetTrackTargetDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "02A) Track target", "Enables tracking of the head track target bone", 0.0, 0.0, 1.0)
+ headTrackTargetParentToHeadDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "03A) Parent to head", "The bone will act as if it was parented to the head bone", 1, 0, 1)
+ headTrackTargetParentToTorsoDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "03B) Parent to torso", "The bone will act as if it was parented to the torso bone", 0, 0, 1)
+ headTrackTargetParentToRootDataPath = koikatsuCommons.addBoneCustomProperty(metarig, koikatsuCommons.headTrackTargetBoneName, "03C) Parent to root", "The bone will act as if it was parented to the root bone", 0, 0, 1)
+
+ eyesHandleTransformationConstraintEyeballLocation = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyeballsBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(-180), math.radians(180), math.radians(0), math.radians(0), math.radians(-180), math.radians(180),
+ 'LOCATION', None, eyesSizeFactor / -10, eyesSizeFactor / 10, 0, 0, eyesSizeFactor / -10, eyesSizeFactor / 10)
+ eyesHandleCopyRotationConstraintEyeball = koikatsuCommons.addCopyRotationConstraint(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyeballsBoneName, 'AFTER', 'LOCAL', koikatsuCommons.copyRotationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix,
+ False, False, True, False, False, False)
+ eyesHandleTransformationContraintEyeballScale = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.eyeballsBoneName, 'MULTIPLY', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.scaleConstraintSuffix,
+ 'SCALE', None, 0.0, 100.0, 1.0, 1.0, 0.0, 100.0,
+ 'SCALE', None, 0.0, 100.0, 1.0, 1.0, 0.0, 100.0)
+ eyesHandleLimitLocationConstraint = koikatsuCommons.addLimitLocationConstraint(metarig, koikatsuCommons.eyesHandleBoneName, koikatsuCommons.headBoneName, 'CUSTOM', koikatsuCommons.limitLocationConstraintBaseName + koikatsuCommons.handleConstraintSuffix,
+ True, -eyesSizeFactor, True, eyesSizeFactor, True, -eyesSizeFactor, True, eyesSizeFactor, False, 0.0, False, 0.0)
+ leftEyeHandleTransformationConstraintEyeballLocation = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeballBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(-180), math.radians(180), math.radians(0), math.radians(0), math.radians(-180), math.radians(180),
+ 'LOCATION', None, eyesSizeFactor / -10, eyesSizeFactor / 10, 0, 0, eyesSizeFactor / -10, eyesSizeFactor / 10)
+ leftEyeHandleTransformationConstraintEyeballLocationCorrectionMin = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeballBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.minConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0),
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ leftEyeHandleTransformationConstraintEyeballLocationCorrectionMax = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeballBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.maxConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0),
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ leftEyeHandleCopyRotationConstraintEyeball = koikatsuCommons.addCopyRotationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeballBoneName, 'AFTER', 'LOCAL', koikatsuCommons.copyRotationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix,
+ False, False, True, False, False, False)
+ leftEyeHandleTransformationConstraintEyeballScale = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.leftEyeballBoneName, 'MULTIPLY', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.scaleConstraintSuffix,
+ 'SCALE', None, 0.0, 100.0, 1.0, 1.0, 0.0, 100.0,
+ 'SCALE', None, 0.0, 100.0, 1.0, 1.0, 0.0, 100.0)
+ leftEyeHandleTransformationConstraintHandleLocationCorrectionMin = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.eyesHandleBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.handleConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.minConstraintSuffix,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ leftEyeHandleTransformationConstraintHandleLocationCorrectionMax = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.eyesHandleBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.handleConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.maxConstraintSuffix,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ leftEyeHandleLimitLocationConstraint = koikatsuCommons.addLimitLocationConstraint(metarig, koikatsuCommons.leftEyeHandleBoneName, koikatsuCommons.headBoneName, 'CUSTOM', koikatsuCommons.limitLocationConstraintBaseName + koikatsuCommons.handleConstraintSuffix,
+ True, -eyesSizeFactor, True, eyesSizeFactor, True, -eyesSizeFactor, True, eyesSizeFactor, False, 0.0, False, 0.0)
+ rightEyeHandleTransformationConstraintEyeballLocation = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeballBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(-180), math.radians(180), math.radians(0), math.radians(0), math.radians(-180), math.radians(180),
+ 'LOCATION', None, eyesSizeFactor / -10, eyesSizeFactor / 10, 0, 0, eyesSizeFactor / -10, eyesSizeFactor / 10)
+ rightEyeHandleTransformationConstraintEyeballLocationCorrectionMin = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeballBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.minConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0),
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ rightEyeHandleTransformationConstraintEyeballLocationCorrectionMax = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeballBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.maxConstraintSuffix,
+ 'ROTATION', 'AUTO', math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0), math.radians(0),
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ rightEyeHandleCopyRotationConstraintEyeball = koikatsuCommons.addCopyRotationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeballBoneName, 'AFTER', 'LOCAL', koikatsuCommons.copyRotationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix,
+ False, False, True, False, False, False)
+ rightEyeHandleTransformationConstraintEyeballScale = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.rightEyeballBoneName, 'MULTIPLY', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.eyeballConstraintSuffix + koikatsuCommons.scaleConstraintSuffix,
+ 'SCALE', None, 0.0, 100.0, 1.0, 1.0, 0.0, 100.0,
+ 'SCALE', None, 0.0, 100.0, 1.0, 1.0, 0.0, 100.0)
+ rightEyeHandleTransformationConstraintHandleLocationCorrectionMin = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.eyesHandleBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.handleConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.minConstraintSuffix,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ rightEyeHandleTransformationConstraintHandleLocationCorrectionMax = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.eyesHandleBoneName, 'ADD', 'LOCAL', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.handleConstraintSuffix + koikatsuCommons.locationConstraintSuffix + koikatsuCommons.correctionConstraintSuffix + koikatsuCommons.maxConstraintSuffix,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0,
+ 'LOCATION', None, 0, 0, 0, 0, 0, 0)
+ rightEyeHandleLimitLocationConstraint = koikatsuCommons.addLimitLocationConstraint(metarig, koikatsuCommons.rightEyeHandleBoneName, koikatsuCommons.headBoneName, 'CUSTOM', koikatsuCommons.limitLocationConstraintBaseName + koikatsuCommons.handleConstraintSuffix,
+ True, -eyesSizeFactor, True, eyesSizeFactor, True, -eyesSizeFactor, True, eyesSizeFactor, False, 0.0, False, 0.0)
+ eyesTrackTargetParentArmatureConstraint = koikatsuCommons.addArmatureConstraint(metarig, koikatsuCommons.eyesTrackTargetParentBoneName, [koikatsuCommons.headBoneName, placeholderTorsoBoneName, placeholderRootBoneName], koikatsuCommons.armatureConstraintBaseName + koikatsuCommons.parentConstraintSuffix)
+ eyeballsCopyRotationConstraintTrack = koikatsuCommons.addCopyRotationConstraint(metarig, koikatsuCommons.eyeballsBoneName, koikatsuCommons.eyeballsTrackBoneName, 'REPLACE', 'LOCAL', koikatsuCommons.copyRotationConstraintBaseName + koikatsuCommons.trackConstraintSuffix,
+ True, False, False, False, True, False)
+ leftEyeballCopyRotationConstraintTrack = koikatsuCommons.addCopyRotationConstraint(metarig, koikatsuCommons.leftEyeballBoneName, koikatsuCommons.leftEyeballTrackCorrectionBoneName, 'REPLACE', 'LOCAL', koikatsuCommons.copyRotationConstraintBaseName + koikatsuCommons.trackConstraintSuffix + koikatsuCommons.correctionConstraintSuffix,
+ True, False, False, False, True, False)
+ rightEyeballCopyRotationConstraintTrack = koikatsuCommons.addCopyRotationConstraint(metarig, koikatsuCommons.rightEyeballBoneName, koikatsuCommons.rightEyeballTrackCorrectionBoneName, 'REPLACE', 'LOCAL', koikatsuCommons.copyRotationConstraintBaseName + koikatsuCommons.trackConstraintSuffix + koikatsuCommons.correctionConstraintSuffix,
+ True, False, False, False, True, False)
+ eyeballsTrackDampedTrackConstraint = koikatsuCommons.addDampedTrackConstraint(metarig, koikatsuCommons.eyeballsTrackBoneName, koikatsuCommons.eyesTrackTargetBoneName, koikatsuCommons.dampedTrackConstraintBaseName + koikatsuCommons.trackConstraintSuffix)
+ leftEyeballTrackDampedTrackConstraint = koikatsuCommons.addDampedTrackConstraint(metarig, koikatsuCommons.leftEyeballTrackBoneName, koikatsuCommons.eyesTrackTargetBoneName, koikatsuCommons.dampedTrackConstraintBaseName + koikatsuCommons.trackConstraintSuffix)
+ rightEyeballTrackDampedTrackConstraint = koikatsuCommons.addDampedTrackConstraint(metarig, koikatsuCommons.rightEyeballTrackBoneName, koikatsuCommons.eyesTrackTargetBoneName, koikatsuCommons.dampedTrackConstraintBaseName + koikatsuCommons.trackConstraintSuffix)
+ headTransformationConstraintHeadRotation = koikatsuCommons.addTransformationConstraint(metarig, koikatsuCommons.headBoneName, koikatsuCommons.headTrackBoneName, 'REPLACE', 'LOCAL_WITH_PARENT', koikatsuCommons.transformationConstraintBaseName + koikatsuCommons.headConstraintSuffix + koikatsuCommons.rotationConstraintSuffix,
+ 'ROTATION', 'QUATERNION', math.radians(-180), math.radians(180), math.radians(-180), math.radians(180), math.radians(-180), math.radians(180),
+ 'ROTATION', 'AUTO', math.radians(180), math.radians(-180), math.radians(-180), math.radians(180), math.radians(-180), math.radians(180),
+ 'X', 'Z', 'Y')
+ headLimitRotationConstraint = koikatsuCommons.addLimitRotationConstraint(metarig, koikatsuCommons.headBoneName, None, 'LOCAL', koikatsuCommons.limitRotationConstraintBaseName + koikatsuCommons.headConstraintSuffix,
+ True, math.radians(-180), math.radians(180), True, math.radians(-180), math.radians(180), True, math.radians(-180), math.radians(180))
+ headTrackTargetParentArmatureConstraint = koikatsuCommons.addArmatureConstraint(metarig, koikatsuCommons.headTrackTargetParentBoneName, [placeholderHeadTweakBoneName, placeholderTorsoBoneName, placeholderRootBoneName], koikatsuCommons.armatureConstraintBaseName + koikatsuCommons.parentConstraintSuffix)
+ headTrackDampedTrackConstraint = koikatsuCommons.addDampedTrackConstraint(metarig, koikatsuCommons.headTrackBoneName, koikatsuCommons.headTrackTargetBoneName, koikatsuCommons.dampedTrackConstraintBaseName + koikatsuCommons.trackConstraintSuffix)
+
+ eyelidsShapeKeyCopy = koikatsuCommons.duplicateShapeKey(koikatsuCommons.bodyName(), koikatsuCommons.eyelidsShapeKeyName, koikatsuCommons.eyelidsShapeKeyCopyName)
+
+ eyesHandleLocationXDriverVariable = koikatsuCommons.DriverVariable("locX", 'TRANSFORMS', metarig, koikatsuCommons.eyesHandleBoneName, 'LOCAL_SPACE', None, None, None, None, 'LOC_X', None)
+ eyesHandleDefaultEyelidsValueDriverVariable = koikatsuCommons.DriverVariable("default", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleDefaultEyelidsValueDataPath, None, None)
+ eyesHandleMinEyelidsValueDriverVariable = koikatsuCommons.DriverVariable("min", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleMinEyelidsValueDataPath, None, None)
+ eyesHandleMaxEyelidsValueDriverVariable = koikatsuCommons.DriverVariable("max", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleMaxEyelidsValueDataPath, None, None)
+ eyesHandleEyelidsSpeedFactorDriverVariable = koikatsuCommons.DriverVariable("speed", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleEyelidsSpeedFactorDataPath, None, None)
+ eyesHandleEyelidsAutomationDriverVariable = koikatsuCommons.DriverVariable("enabled", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleEyelidsAutomationDataPath, None, None)
+ eyesHandleLimitLocationDriverVariable = koikatsuCommons.DriverVariable("limit", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleLimitLocationDataPath, None, None)
+ eyesHandleEyesSizeFactorDriverVariable = koikatsuCommons.DriverVariable("sizeFactor", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesHandleEyesSizeFactorDataPath, None, None)
+ leftEyeHandleLimitLocationDriverVariable = koikatsuCommons.DriverVariable("limit_L", 'SINGLE_PROP', metarig, None, None, None, None, None, leftEyeHandleLimitLocationDataPath, None, None)
+ leftEyeHandleMinXLocationDriverVariable = koikatsuCommons.DriverVariable("minX_L", 'SINGLE_PROP', metarig, None, None, None, None, None, leftEyeHandleMinXLocationDataPath, None, None)
+ leftEyeHandleMaxXLocationDriverVariable = koikatsuCommons.DriverVariable("maxX_L", 'SINGLE_PROP', metarig, None, None, None, None, None, leftEyeHandleMaxXLocationDataPath, None, None)
+ leftEyeHandleMinYLocationDriverVariable = koikatsuCommons.DriverVariable("minY_L", 'SINGLE_PROP', metarig, None, None, None, None, None, leftEyeHandleMinYLocationDataPath, None, None)
+ leftEyeHandleMaxYLocationDriverVariable = koikatsuCommons.DriverVariable("maxY_L", 'SINGLE_PROP', metarig, None, None, None, None, None, leftEyeHandleMaxYLocationDataPath, None, None)
+ leftEyeHandleSpeedCorrectionDriverVariable = koikatsuCommons.DriverVariable("corr_L", 'SINGLE_PROP', metarig, None, None, None, None, None, leftEyeHandleSpeedCorrectionDataPath, None, None)
+ leftEyeHandleEyesHandleDistanceXDriverVariable = koikatsuCommons.DriverVariable("distX_L", 'LOC_DIFF', metarig, koikatsuCommons.eyesHandleMarkerBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.leftEyeHandleMarkerXBoneName, 'WORLD_SPACE', None, None, None)
+ leftEyeHandleEyesHandleDistanceYDriverVariable = koikatsuCommons.DriverVariable("distY_L", 'LOC_DIFF', metarig, koikatsuCommons.eyesHandleMarkerBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.leftEyeHandleMarkerZBoneName, 'WORLD_SPACE', None, None, None)
+ leftEyeHandleHeadDistanceXDriverVariable = koikatsuCommons.DriverVariable("headDistX_L", 'LOC_DIFF', metarig, koikatsuCommons.headBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.leftHeadMarkerXBoneName, 'WORLD_SPACE', None, None, None)
+ leftEyeHandleHeadDistanceYDriverVariable = koikatsuCommons.DriverVariable("headDistY_L", 'LOC_DIFF', metarig, koikatsuCommons.headBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.leftHeadMarkerZBoneName, 'WORLD_SPACE', None, None, None)
+ rightEyeHandleLimitLocationDriverVariable = koikatsuCommons.DriverVariable("limit_R", 'SINGLE_PROP', metarig, None, None, None, None, None, rightEyeHandleLimitLocationDataPath, None, None)
+ rightEyeHandleMinXLocationDriverVariable = koikatsuCommons.DriverVariable("minX_R", 'SINGLE_PROP', metarig, None, None, None, None, None, rightEyeHandleMinXLocationDataPath, None, None)
+ rightEyeHandleMaxXLocationDriverVariable = koikatsuCommons.DriverVariable("maxX_R", 'SINGLE_PROP', metarig, None, None, None, None, None, rightEyeHandleMaxXLocationDataPath, None, None)
+ rightEyeHandleMinYLocationDriverVariable = koikatsuCommons.DriverVariable("minY_R", 'SINGLE_PROP', metarig, None, None, None, None, None, rightEyeHandleMinYLocationDataPath, None, None)
+ rightEyeHandleMaxYLocationDriverVariable = koikatsuCommons.DriverVariable("maxY_R", 'SINGLE_PROP', metarig, None, None, None, None, None, rightEyeHandleMaxYLocationDataPath, None, None)
+ rightEyeHandleSpeedCorrectionDriverVariable = koikatsuCommons.DriverVariable("corr_R", 'SINGLE_PROP', metarig, None, None, None, None, None, rightEyeHandleSpeedCorrectionDataPath, None, None)
+ rightEyeHandleEyesHandleDistanceXDriverVariable = koikatsuCommons.DriverVariable("distX_R", 'LOC_DIFF', metarig, koikatsuCommons.eyesHandleMarkerBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.rightEyeHandleMarkerXBoneName, 'WORLD_SPACE', None, None, None)
+ rightEyeHandleEyesHandleDistanceYDriverVariable = koikatsuCommons.DriverVariable("distY_R", 'LOC_DIFF', metarig, koikatsuCommons.eyesHandleMarkerBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.rightEyeHandleMarkerZBoneName, 'WORLD_SPACE', None, None, None)
+ rightEyeHandleHeadDistanceXDriverVariable = koikatsuCommons.DriverVariable("headdDistX_R", 'LOC_DIFF', metarig, koikatsuCommons.headBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.rightHeadMarkerXBoneName, 'WORLD_SPACE', None, None, None)
+ rightEyeHandleHeadDistanceYDriverVariable = koikatsuCommons.DriverVariable("headDistY_R", 'LOC_DIFF', metarig, koikatsuCommons.headBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.rightHeadMarkerZBoneName, 'WORLD_SPACE', None, None, None)
+ eyesTrackTargetParentToHeadDriverVariable = koikatsuCommons.DriverVariable("parentHead", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesTrackTargetParentToHeadDataPath, None, None)
+ eyesTrackTargetParentToTorsoDriverVariable = koikatsuCommons.DriverVariable("parentTorso", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesTrackTargetParentToTorsoDataPath, None, None)
+ eyesTrackTargetParentToRootDriverVariable = koikatsuCommons.DriverVariable("parentRoot", 'SINGLE_PROP', metarig, None, None, None, None, None, eyesTrackTargetParentToRootDataPath, None, None)
+ eyeballsSpeedFactorDriverVariable = koikatsuCommons.DriverVariable("rotSpeed", 'SINGLE_PROP', metarig, None, None, None, None, None, eyeballsSpeedFactorDataPath, None, None)
+ eyeballsTrackTargetDriverVariable = koikatsuCommons.DriverVariable("track", 'SINGLE_PROP', metarig, None, None, None, None, None, eyeballsTrackTargetDataPath, None, None)
+ eyeballsTrackSpeedFactorDriverVariable = koikatsuCommons.DriverVariable("trackSpeed", 'SINGLE_PROP', metarig, None, None, None, None, None, eyeballsTrackSpeedFactorDataPath, None, None)
+ eyeballsNearbyTargetTrackCorrectionDriverVariable = koikatsuCommons.DriverVariable("trackCorr", 'SINGLE_PROP', metarig, None, None, None, None, None, eyeballsNearbyTargetTrackCorrectionDataPath, None, None)
+ eyeballsNearbyTargetSizeFactorDriverVariable = koikatsuCommons.DriverVariable("factor", 'SINGLE_PROP', metarig, None, None, None, None, None, eyeballsNearbyTargetSizeFactorDataPath, None, None)
+ leftEyeballRightEyeHandleMarkerDistanceDriverVariable = koikatsuCommons.DriverVariable("orgDist_L", 'LOC_DIFF', metarig, koikatsuCommons.leftEyeballTrackCorrectionBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.leftEyeHandleMarkerBoneName, 'WORLD_SPACE', None, None, None)
+ rightEyeballRightEyeHandleMarkerDistanceDriverVariable = koikatsuCommons.DriverVariable("orgDist_R", 'LOC_DIFF', metarig, koikatsuCommons.rightEyeballTrackCorrectionBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.rightEyeHandleMarkerBoneName, 'WORLD_SPACE', None, None, None)
+ leftEyeballEyesTrackTargetDistanceDriverVariable = koikatsuCommons.DriverVariable("currDist_L", 'LOC_DIFF', metarig, koikatsuCommons.leftEyeballTrackCorrectionBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.eyesTrackTargetBoneName, 'WORLD_SPACE', None, None, None)
+ rightEyeballEyesTrackTargetDistanceDriverVariable = koikatsuCommons.DriverVariable("currDist_R", 'LOC_DIFF', metarig, koikatsuCommons.rightEyeballTrackCorrectionBoneName, 'WORLD_SPACE', metarig, koikatsuCommons.eyesTrackTargetBoneName, 'WORLD_SPACE', None, None, None)
+ eyeballsTrackBoneRotationZDriverVariable = koikatsuCommons.DriverVariable("rotZ", 'TRANSFORMS', metarig, koikatsuCommons.eyeballsTrackBoneName, 'LOCAL_SPACE', None, None, None, None, 'ROT_Z', 'QUATERNION')
+ leftEyeballTrackBoneRotationZDriverVariable = koikatsuCommons.DriverVariable("rotZ_L", 'TRANSFORMS', metarig, koikatsuCommons.leftEyeballTrackBoneName, 'LOCAL_SPACE', None, None, None, None, 'ROT_Z', 'QUATERNION')
+ rightEyeballTrackBoneRotationZDriverVariable = koikatsuCommons.DriverVariable("rotZ_R", 'TRANSFORMS', metarig, koikatsuCommons.rightEyeballTrackBoneName, 'LOCAL_SPACE', None, None, None, None, 'ROT_Z', 'QUATERNION')
+ headTrackTargetLimitRotationDriverVariable = koikatsuCommons.DriverVariable("limit", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetLimitRotationDataPath, None, None)
+ headTrackTargetMinXRotationDriverVariable = koikatsuCommons.DriverVariable("minX", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetMinXRotationDataPath, None, None)
+ headTrackTargetMaxXRotationDriverVariable = koikatsuCommons.DriverVariable("maxX", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetMaxXRotationDataPath, None, None)
+ headTrackTargetMinYRotationDriverVariable = koikatsuCommons.DriverVariable("minY", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetMinYRotationDataPath, None, None)
+ headTrackTargetMaxYRotationDriverVariable = koikatsuCommons.DriverVariable("maxY", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetMaxYRotationDataPath, None, None)
+ headTrackTargetMinZRotationDriverVariable = koikatsuCommons.DriverVariable("minZ", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetMinZRotationDataPath, None, None)
+ headTrackTargetMaxZRotationDriverVariable = koikatsuCommons.DriverVariable("maxZ", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetMaxZRotationDataPath, None, None)
+ headTrackTargetParentToHeadDriverVariable = koikatsuCommons.DriverVariable("parentHead", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetParentToHeadDataPath, None, None)
+ headTrackTargetParentToTorsoDriverVariable = koikatsuCommons.DriverVariable("parentTorso", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetParentToTorsoDataPath, None, None)
+ headTrackTargetParentToRootDriverVariable = koikatsuCommons.DriverVariable("parentRoot", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetParentToRootDataPath, None, None)
+ headTrackTargetDriverVariable = koikatsuCommons.DriverVariable("track", 'SINGLE_PROP', metarig, None, None, None, None, None, headTrackTargetTrackTargetDataPath, None, None)
+
+ koikatsuCommons.addDriver(eyelidsShapeKeyCopy, "value", None, 'SCRIPTED', [eyesHandleLocationXDriverVariable, eyesHandleDefaultEyelidsValueDriverVariable, eyesHandleMinEyelidsValueDriverVariable, eyesHandleMaxEyelidsValueDriverVariable, eyesHandleEyelidsSpeedFactorDriverVariable, eyesHandleEyelidsAutomationDriverVariable],
+ eyesHandleEyelidsAutomationDriverVariable.name + " * (" + eyesHandleMinEyelidsValueDriverVariable.name + " if (" + eyesHandleDefaultEyelidsValueDriverVariable.name + " + " + eyesHandleLocationXDriverVariable.name + " * -" + eyesHandleEyelidsSpeedFactorDriverVariable.name + " if " + eyesHandleLocationXDriverVariable.name + " < 0 else " + eyesHandleDefaultEyelidsValueDriverVariable.name + " - " + eyesHandleLocationXDriverVariable.name + " * " + eyesHandleEyelidsSpeedFactorDriverVariable.name + ") < " + eyesHandleMinEyelidsValueDriverVariable.name + " else " + eyesHandleMaxEyelidsValueDriverVariable.name + " if (" + eyesHandleDefaultEyelidsValueDriverVariable.name + " + " + eyesHandleLocationXDriverVariable.name + " * -" + eyesHandleEyelidsSpeedFactorDriverVariable.name + " if " + eyesHandleLocationXDriverVariable.name + " < 0 else " + eyesHandleDefaultEyelidsValueDriverVariable.name + " - " + eyesHandleLocationXDriverVariable.name + " * " + eyesHandleEyelidsSpeedFactorDriverVariable.name + ") > " + eyesHandleMaxEyelidsValueDriverVariable.name + " else (" + eyesHandleDefaultEyelidsValueDriverVariable.name + " + " + eyesHandleLocationXDriverVariable.name + " * -" + eyesHandleEyelidsSpeedFactorDriverVariable.name + " if " + eyesHandleLocationXDriverVariable.name + " < 0 else " + eyesHandleDefaultEyelidsValueDriverVariable.name + " - " + eyesHandleLocationXDriverVariable.name + " * " + eyesHandleEyelidsSpeedFactorDriverVariable.name + "))")
+ koikatsuCommons.addDriver(eyesHandleLimitLocationConstraint, "influence", None, 'AVERAGE', [eyesHandleLimitLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(eyesHandleLimitLocationConstraint, "min_x", None, 'SCRIPTED', [leftEyeHandleMinXLocationDriverVariable, rightEyeHandleMinXLocationDriverVariable, leftEyeHandleEyesHandleDistanceXDriverVariable, rightEyeHandleEyesHandleDistanceXDriverVariable],
+ leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceXDriverVariable.name + " if " + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceXDriverVariable.name + " <= " + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceXDriverVariable.name + " else " + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceXDriverVariable.name)
+ koikatsuCommons.addDriver(eyesHandleLimitLocationConstraint, "max_x", None, 'SCRIPTED', [leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleEyesHandleDistanceXDriverVariable, rightEyeHandleEyesHandleDistanceXDriverVariable],
+ rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceXDriverVariable.name + " if " + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceXDriverVariable.name + " > " + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceXDriverVariable.name + " else " + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceXDriverVariable.name)
+ koikatsuCommons.addDriver(eyesHandleLimitLocationConstraint, "min_y", None, 'SCRIPTED', [leftEyeHandleMinYLocationDriverVariable, rightEyeHandleMinYLocationDriverVariable, leftEyeHandleEyesHandleDistanceYDriverVariable, rightEyeHandleEyesHandleDistanceYDriverVariable],
+ leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceYDriverVariable.name + " if " + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceYDriverVariable.name + " <= " + rightEyeHandleMinYLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceYDriverVariable.name + " else " + rightEyeHandleMinYLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceYDriverVariable.name)
+ koikatsuCommons.addDriver(eyesHandleLimitLocationConstraint, "max_y", None, 'SCRIPTED', [leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleEyesHandleDistanceYDriverVariable, rightEyeHandleEyesHandleDistanceYDriverVariable],
+ rightEyeHandleMaxYLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceYDriverVariable.name + " if " + rightEyeHandleMaxYLocationDriverVariable.name + " + " + rightEyeHandleEyesHandleDistanceYDriverVariable.name + " > " + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceYDriverVariable.name + " else " + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleEyesHandleDistanceYDriverVariable.name)
+ koikatsuCommons.addDriver(eyesHandleTransformationConstraintEyeballLocation, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, eyeballsTrackSpeedFactorDriverVariable],
+ eyeballsSpeedFactorDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") + " + eyeballsTrackSpeedFactorDriverVariable.name + " * " + eyeballsTrackTargetDriverVariable.name)
+ koikatsuCommons.addDriver(eyesHandleTransformationConstraintEyeballLocation, "to_min_x", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ "-" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(eyesHandleTransformationConstraintEyeballLocation, "to_max_x", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(eyesHandleTransformationConstraintEyeballLocation, "to_min_z", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ "-" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(eyesHandleTransformationConstraintEyeballLocation, "to_max_z", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleLimitLocationConstraint, "min_x", None, 'AVERAGE', [leftEyeHandleMinXLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleLimitLocationConstraint, "max_x", None, 'AVERAGE', [leftEyeHandleMaxXLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleLimitLocationConstraint, "min_y", None, 'AVERAGE', [leftEyeHandleMinYLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleLimitLocationConstraint, "max_y", None, 'AVERAGE', [leftEyeHandleMaxYLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMax, "to_max_z", None, 'SCRIPTED', [leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleLimitLocationConstraint, "influence", None, 'AVERAGE', [leftEyeHandleLimitLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMax, "influence", None, 'AVERAGE', [leftEyeHandleSpeedCorrectionDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMax, "from_max_z", None, 'SCRIPTED', [leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMin, "from_min_z", None, 'SCRIPTED', [leftEyeHandleMinXLocationDriverVariable, rightEyeHandleMinXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else -" + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMin, "to_min_z", None, 'SCRIPTED', [leftEyeHandleMinXLocationDriverVariable, rightEyeHandleMinXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMin, "influence", None, 'AVERAGE', [leftEyeHandleSpeedCorrectionDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMax, "from_max_x", None, 'SCRIPTED', [leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMax, "to_max_x", None, 'SCRIPTED', [leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMin, "from_min_x", None, 'SCRIPTED', [leftEyeHandleMinYLocationDriverVariable, rightEyeHandleMinYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else -" + eyesHandleEyesSizeFactorDriverVariable.name + "")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintHandleLocationCorrectionMin, "to_min_x", None, 'SCRIPTED', [leftEyeHandleMinYLocationDriverVariable, rightEyeHandleMinYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocation, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, eyeballsTrackSpeedFactorDriverVariable],
+ eyeballsSpeedFactorDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") + " + eyeballsTrackSpeedFactorDriverVariable.name + " * " + eyeballsTrackTargetDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, leftEyeHandleSpeedCorrectionDriverVariable],
+ leftEyeHandleSpeedCorrectionDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") * " + eyeballsSpeedFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, leftEyeHandleSpeedCorrectionDriverVariable],
+ leftEyeHandleSpeedCorrectionDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") * " + eyeballsSpeedFactorDriverVariable.name)
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "to_max_z", None, 'SCRIPTED', [leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "to_max_x", None, 'SCRIPTED', [leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "from_max_z_rot", None, 'SCRIPTED', [leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(180)")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "from_max_x_rot", None, 'SCRIPTED', [leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(180)")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "to_min_z", None, 'SCRIPTED', [leftEyeHandleMinXLocationDriverVariable, rightEyeHandleMinXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "to_min_x", None, 'SCRIPTED', [leftEyeHandleMinYLocationDriverVariable, rightEyeHandleMinYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "from_min_z_rot", None, 'SCRIPTED', [leftEyeHandleMinXLocationDriverVariable, rightEyeHandleMinXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(-180)")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "from_min_x_rot", None, 'SCRIPTED', [leftEyeHandleMinYLocationDriverVariable, rightEyeHandleMinYLocationDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(-180)")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocation, "to_min_x", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ "-" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocation, "to_max_x", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocation, "to_min_z", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ "-" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(leftEyeHandleTransformationConstraintEyeballLocation, "to_max_z", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleLimitLocationConstraint, "min_x", None, 'AVERAGE', [rightEyeHandleMinXLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleLimitLocationConstraint, "max_x", None, 'AVERAGE', [rightEyeHandleMaxXLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleLimitLocationConstraint, "min_y", None, 'AVERAGE', [rightEyeHandleMinYLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleLimitLocationConstraint, "max_y", None, 'AVERAGE', [rightEyeHandleMaxYLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMax, "to_max_z", None, 'SCRIPTED', [rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleLimitLocationConstraint, "influence", None, 'AVERAGE', [rightEyeHandleLimitLocationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMax, "influence", None, 'AVERAGE', [rightEyeHandleSpeedCorrectionDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMax, "from_max_z", None, 'SCRIPTED', [rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMin, "from_min_z", None, 'SCRIPTED', [rightEyeHandleMinXLocationDriverVariable, leftEyeHandleMinXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else -" + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMin, "to_min_z", None, 'SCRIPTED', [rightEyeHandleMinXLocationDriverVariable, leftEyeHandleMinXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMin, "influence", None, 'AVERAGE', [rightEyeHandleSpeedCorrectionDriverVariable],
+ None)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMax, "from_max_x", None, 'SCRIPTED', [rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMax, "to_max_x", None, 'SCRIPTED', [rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMin, "from_min_x", None, 'SCRIPTED', [rightEyeHandleMinYLocationDriverVariable, leftEyeHandleMinYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else -" + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintHandleLocationCorrectionMin, "to_min_x", None, 'SCRIPTED', [rightEyeHandleMinYLocationDriverVariable, leftEyeHandleMinYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocation, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, eyeballsTrackSpeedFactorDriverVariable],
+ eyeballsSpeedFactorDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") + " + eyeballsTrackSpeedFactorDriverVariable.name + " * " + eyeballsTrackTargetDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, rightEyeHandleSpeedCorrectionDriverVariable],
+ rightEyeHandleSpeedCorrectionDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") * " + eyeballsSpeedFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "influence", None, 'SCRIPTED', [eyeballsSpeedFactorDriverVariable, eyeballsTrackTargetDriverVariable, rightEyeHandleSpeedCorrectionDriverVariable],
+ rightEyeHandleSpeedCorrectionDriverVariable.name + " * (1 - " + eyeballsTrackTargetDriverVariable.name + ") * " + eyeballsSpeedFactorDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "to_max_z", None, 'SCRIPTED', [rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "to_max_x", None, 'SCRIPTED', [rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") * " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") - " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "from_max_z_rot", None, 'SCRIPTED', [rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + rightEyeHandleMaxXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMaxXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(180)")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMax, "from_max_x_rot", None, 'SCRIPTED', [rightEyeHandleMaxYLocationDriverVariable, leftEyeHandleMaxYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + rightEyeHandleMaxYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMaxYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") <= 0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(180)")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "to_min_z", None, 'SCRIPTED', [rightEyeHandleMinXLocationDriverVariable, leftEyeHandleMinXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "to_min_x", None, 'SCRIPTED', [rightEyeHandleMinYLocationDriverVariable, leftEyeHandleMinYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "0 if (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") * -" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10 / (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") + " + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "from_min_z_rot", None, 'SCRIPTED', [rightEyeHandleMinXLocationDriverVariable, leftEyeHandleMinXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + rightEyeHandleMinXLocationDriverVariable.name + " + " + rightEyeHandleHeadDistanceXDriverVariable.name + ") - (" + leftEyeHandleMinXLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceXDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(-180)")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocationCorrectionMin, "from_min_x_rot", None, 'SCRIPTED', [rightEyeHandleMinYLocationDriverVariable, leftEyeHandleMinYLocationDriverVariable, rightEyeHandleHeadDistanceYDriverVariable, leftEyeHandleHeadDistanceYDriverVariable, eyesHandleEyesSizeFactorDriverVariable],
+ "radians(0) if (" + rightEyeHandleMinYLocationDriverVariable.name + " - " + rightEyeHandleHeadDistanceYDriverVariable.name + ") - (" + leftEyeHandleMinYLocationDriverVariable.name + " - " + leftEyeHandleHeadDistanceYDriverVariable.name + ") >= -0.000001 * " + eyesHandleEyesSizeFactorDriverVariable.name + " else radians(-180)")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocation, "to_min_x", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ "-" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocation, "to_max_x", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocation, "to_min_z", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ "-" + eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(rightEyeHandleTransformationConstraintEyeballLocation, "to_max_z", None, 'SCRIPTED', [eyesHandleEyesSizeFactorDriverVariable],
+ eyesHandleEyesSizeFactorDriverVariable.name + " / 10")
+ koikatsuCommons.addDriver(eyesTrackTargetParentArmatureConstraint.targets[0], "weight", None, 'AVERAGE', [eyesTrackTargetParentToHeadDriverVariable],
+ None)
+ koikatsuCommons.addDriver(eyesTrackTargetParentArmatureConstraint.targets[1], "weight", None, 'AVERAGE', [eyesTrackTargetParentToTorsoDriverVariable],
+ None)
+ koikatsuCommons.addDriver(eyesTrackTargetParentArmatureConstraint.targets[2], "weight", None, 'AVERAGE', [eyesTrackTargetParentToRootDriverVariable],
+ None)
+ koikatsuCommons.addDriver(eyeballsCopyRotationConstraintTrack, "influence", None, 'AVERAGE', [eyeballsTrackTargetDriverVariable],
+ None)
+ koikatsuCommons.addDriver(leftEyeballCopyRotationConstraintTrack, "influence", None, 'SCRIPTED', [eyeballsTrackTargetDriverVariable, eyeballsNearbyTargetTrackCorrectionDriverVariable],
+ eyeballsTrackTargetDriverVariable.name + " * " + eyeballsNearbyTargetTrackCorrectionDriverVariable.name)
+ koikatsuCommons.addDriver(rightEyeballCopyRotationConstraintTrack, "influence", None, 'SCRIPTED', [eyeballsTrackTargetDriverVariable, eyeballsNearbyTargetTrackCorrectionDriverVariable],
+ eyeballsTrackTargetDriverVariable.name + " * " + eyeballsNearbyTargetTrackCorrectionDriverVariable.name)
+ """
+ koikatsuCommons.addDriver(metarig.pose.bones[koikatsuCommons.leftEyeballTrackCorrectionBoneName], "rotation_quaternion", 3, 'SCRIPTED', [leftEyeballRightEyeHandleMarkerDistanceDriverVariable, leftEyeballEyesTrackTargetDistanceDriverVariable, leftEyeballTrackBoneRotationZDriverVariable],
+ "radians(0) if " + leftEyeballRightEyeHandleMarkerDistanceDriverVariable.name + " - " + leftEyeballEyesTrackTargetDistanceDriverVariable.name + " <= 0 else (radians(-45) + " + leftEyeballTrackBoneRotationZDriverVariable.name + ") * (" + leftEyeballRightEyeHandleMarkerDistanceDriverVariable.name + " - " + leftEyeballEyesTrackTargetDistanceDriverVariable.name + ") * 15")
+ koikatsuCommons.addDriver(metarig.pose.bones[koikatsuCommons.rightEyeballTrackCorrectionBoneName], "rotation_quaternion", 3, 'SCRIPTED', [rightEyeballRightEyeHandleMarkerDistanceDriverVariable, rightEyeballEyesTrackTargetDistanceDriverVariable, rightEyeballTrackBoneRotationZDriverVariable],
+ "radians(0) if " + rightEyeballRightEyeHandleMarkerDistanceDriverVariable.name + " - " + rightEyeballEyesTrackTargetDistanceDriverVariable.name + " <= 0 else (radians(45) + " + rightEyeballTrackBoneRotationZDriverVariable.name + ") * (" + rightEyeballRightEyeHandleMarkerDistanceDriverVariable.name + " - " + rightEyeballEyesTrackTargetDistanceDriverVariable.name + ") * 15")
+ """
+ koikatsuCommons.addDriver(metarig.pose.bones[koikatsuCommons.leftEyeballTrackCorrectionBoneName], "rotation_quaternion", 3, 'SCRIPTED', [leftEyeballRightEyeHandleMarkerDistanceDriverVariable, leftEyeballEyesTrackTargetDistanceDriverVariable, eyeballsTrackBoneRotationZDriverVariable, leftEyeballTrackBoneRotationZDriverVariable, leftEyeHandleSpeedCorrectionDriverVariable, leftEyeHandleMaxXLocationDriverVariable, rightEyeHandleMaxXLocationDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, eyeballsNearbyTargetSizeFactorDriverVariable],
+ "0 if " + leftEyeballRightEyeHandleMarkerDistanceDriverVariable.name + "-" + leftEyeballEyesTrackTargetDistanceDriverVariable.name + "<=0 else (radians(-45)+" + leftEyeballTrackBoneRotationZDriverVariable.name + ")*(" + leftEyeballRightEyeHandleMarkerDistanceDriverVariable.name + "-" + leftEyeballEyesTrackTargetDistanceDriverVariable.name + ")/" + eyeballsNearbyTargetSizeFactorDriverVariable.name + "*(" + eyeballsTrackBoneRotationZDriverVariable.name + "-" + leftEyeballTrackBoneRotationZDriverVariable.name + ")*20*(1 if (" + leftEyeHandleMaxXLocationDriverVariable.name + "-" + leftEyeHandleHeadDistanceXDriverVariable.name + ")-(" + rightEyeHandleMaxXLocationDriverVariable.name + "+" + rightEyeHandleHeadDistanceXDriverVariable.name + ")<=0.000001 else 1+" + leftEyeHandleSpeedCorrectionDriverVariable.name + "*(" + leftEyeHandleMaxXLocationDriverVariable.name + "-" + leftEyeHandleHeadDistanceXDriverVariable.name + ")/(" + rightEyeHandleMaxXLocationDriverVariable.name + "+" + rightEyeHandleHeadDistanceXDriverVariable.name + "))")
+ koikatsuCommons.addDriver(metarig.pose.bones[koikatsuCommons.rightEyeballTrackCorrectionBoneName], "rotation_quaternion", 3, 'SCRIPTED', [rightEyeballRightEyeHandleMarkerDistanceDriverVariable, rightEyeballEyesTrackTargetDistanceDriverVariable, eyeballsTrackBoneRotationZDriverVariable, rightEyeballTrackBoneRotationZDriverVariable, rightEyeHandleSpeedCorrectionDriverVariable, rightEyeHandleMinXLocationDriverVariable, leftEyeHandleMinXLocationDriverVariable, rightEyeHandleHeadDistanceXDriverVariable, leftEyeHandleHeadDistanceXDriverVariable, eyeballsNearbyTargetSizeFactorDriverVariable],
+ "0 if " + rightEyeballRightEyeHandleMarkerDistanceDriverVariable.name + "-" + rightEyeballEyesTrackTargetDistanceDriverVariable.name + "<=0 else (radians(45)+" + rightEyeballTrackBoneRotationZDriverVariable.name + ")*(" + rightEyeballRightEyeHandleMarkerDistanceDriverVariable.name + "-" + rightEyeballEyesTrackTargetDistanceDriverVariable.name + ")/" + eyeballsNearbyTargetSizeFactorDriverVariable.name + "*(" + rightEyeballTrackBoneRotationZDriverVariable.name + "-" + eyeballsTrackBoneRotationZDriverVariable.name + ")*20*(1 if (" + rightEyeHandleMinXLocationDriverVariable.name + "+" + rightEyeHandleHeadDistanceXDriverVariable.name + ")-(" + leftEyeHandleMinXLocationDriverVariable.name + "-" + leftEyeHandleHeadDistanceXDriverVariable.name + ")>=-0.000001 else 1+" + rightEyeHandleSpeedCorrectionDriverVariable.name + "*(" + rightEyeHandleMinXLocationDriverVariable.name + "+" + rightEyeHandleHeadDistanceXDriverVariable.name + ")/(" + leftEyeHandleMinXLocationDriverVariable.name + "-" + leftEyeHandleHeadDistanceXDriverVariable.name + "))")
+ koikatsuCommons.addDriver(headTransformationConstraintHeadRotation, "influence", None, 'AVERAGE', [headTrackTargetDriverVariable],
+ None)
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "influence", None, 'AVERAGE', [headTrackTargetLimitRotationDriverVariable],
+ None)
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "min_x", None, 'SCRIPTED', [headTrackTargetMinXRotationDriverVariable],
+ "radians(" + headTrackTargetMinXRotationDriverVariable.name + ")")
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "max_x", None, 'SCRIPTED', [headTrackTargetMaxXRotationDriverVariable],
+ "radians(" + headTrackTargetMaxXRotationDriverVariable.name + ")")
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "min_y", None, 'SCRIPTED', [headTrackTargetMinYRotationDriverVariable],
+ "radians(" + headTrackTargetMinYRotationDriverVariable.name + ")")
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "max_y", None, 'SCRIPTED', [headTrackTargetMaxYRotationDriverVariable],
+ "radians(" + headTrackTargetMaxYRotationDriverVariable.name + ")")
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "min_z", None, 'SCRIPTED', [headTrackTargetMinZRotationDriverVariable],
+ "radians(" + headTrackTargetMinZRotationDriverVariable.name + ")")
+ koikatsuCommons.addDriver(headLimitRotationConstraint, "max_z", None, 'SCRIPTED', [headTrackTargetMaxZRotationDriverVariable],
+ "radians(" + headTrackTargetMaxZRotationDriverVariable.name + ")")
+ koikatsuCommons.addDriver(headTrackTargetParentArmatureConstraint.targets[0], "weight", None, 'AVERAGE', [headTrackTargetParentToHeadDriverVariable],
+ None)
+ koikatsuCommons.addDriver(headTrackTargetParentArmatureConstraint.targets[1], "weight", None, 'AVERAGE', [headTrackTargetParentToTorsoDriverVariable],
+ None)
+ koikatsuCommons.addDriver(headTrackTargetParentArmatureConstraint.targets[2], "weight", None, 'AVERAGE', [headTrackTargetParentToRootDriverVariable],
+ None)
+
+ def finalizeEyeballBone(rig, boneName):
+ rig.pose.bones[boneName].lock_location[0] = True
+ rig.pose.bones[boneName].lock_location[1] = True
+ rig.pose.bones[boneName].lock_location[2] = True
+
+ finalizeEyeballBone(metarig, koikatsuCommons.eyeballsBoneName)
+ finalizeEyeballBone(metarig, koikatsuCommons.leftEyeballBoneName)
+ finalizeEyeballBone(metarig, koikatsuCommons.rightEyeballBoneName)
+
+ """
+ Begin Rigifying
+ """
+
+ if bpy.app.version[0] == 3:
+ bpy.ops.pose.rigify_layer_init()
+ bpy.ops.armature.rigify_add_bone_groups()
+ else:
+ bpy.ops.armature.rigify_add_color_sets()
+ bpy.ops.armature.rigify_collection_select(index=2)
+ bpy.ops.armature.rigify_collection_set_ui_row(index=2, row=1)
+
+ for index, rigifyLayer in enumerate(koikatsuCommons.rigifyLayers):
+ koikatsuCommons.setRigifyLayer(metarig, index, rigifyLayer)
+ koikatsuCommons.setRootRigifyLayer(metarig, koikatsuCommons.rootBoneGroupIndex)
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ def removeAllConstraints(rig, boneName):
+ boneToEmpty = rig.pose.bones[boneName]
+ for constraint in boneToEmpty.constraints:
+ koikatsuCommons.removeConstraint(rig, boneName, constraint.name)
+
+ removeAllConstraints(metarig, koikatsuCommons.leftElbowBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.rightElbowBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.leftWristBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.rightWristBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.leftToeBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.rightToeBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.leftAnkleBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.rightAnkleBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.leftKneeBoneName)
+ removeAllConstraints(metarig, koikatsuCommons.rightKneeBoneName)
+
+ def renameAllVertexGroups(vertexGroupNameOld, vertexGroupNameNew):
+ for obj in bpy.context.scene.objects:
+ if obj.type == 'MESH':
+ vertexGroup = bpy.data.objects[obj.name].vertex_groups.get(vertexGroupNameOld)
+ if vertexGroup is not None:
+ vertexGroup.name = vertexGroupNameNew
+
+ renameAllVertexGroups(koikatsuCommons.originalHeadDeformBoneName, koikatsuCommons.headDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalNeckDeformBoneName, koikatsuCommons.neckDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalUpperChestDeformBoneName, koikatsuCommons.upperChestDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalChestDeformBoneName, koikatsuCommons.chestDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalSpineDeformBoneName, koikatsuCommons.spineDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalHipsDeformBoneName, koikatsuCommons.hipsDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalLeftArmDeformBone1Name, koikatsuCommons.leftArmDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightArmDeformBone1Name, koikatsuCommons.rightArmDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftArmDeformBone2Name, koikatsuCommons.leftArmDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightArmDeformBone2Name, koikatsuCommons.rightArmDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftArmDeformBone3Name, koikatsuCommons.leftArmDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightArmDeformBone3Name, koikatsuCommons.rightArmDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftElbowDeformBone1Name, koikatsuCommons.leftElbowDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightElbowDeformBone1Name, koikatsuCommons.rightElbowDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftElbowDeformBone2Name, koikatsuCommons.leftElbowDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightElbowDeformBone2Name, koikatsuCommons.rightElbowDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftElbowDeformBone3Name, koikatsuCommons.leftElbowDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightElbowDeformBone3Name, koikatsuCommons.rightElbowDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftWristDeformBoneName, koikatsuCommons.leftWristDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalRightWristDeformBoneName, koikatsuCommons.rightWristDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.originalLeftThumbDeformBone1Name, koikatsuCommons.leftThumbDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightThumbDeformBone1Name, koikatsuCommons.rightThumbDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftThumbDeformBone2Name, koikatsuCommons.leftThumbDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightThumbDeformBone2Name, koikatsuCommons.rightThumbDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftThumbDeformBone3Name, koikatsuCommons.leftThumbDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightThumbDeformBone3Name, koikatsuCommons.rightThumbDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftIndexFingerDeformBone1Name, koikatsuCommons.leftIndexFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightIndexFingerDeformBone1Name, koikatsuCommons.rightIndexFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftIndexFingerDeformBone2Name, koikatsuCommons.leftIndexFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightIndexFingerDeformBone2Name, koikatsuCommons.rightIndexFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftIndexFingerDeformBone3Name, koikatsuCommons.leftIndexFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightIndexFingerDeformBone3Name, koikatsuCommons.rightIndexFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftMiddleFingerDeformBone1Name, koikatsuCommons.leftMiddleFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightMiddleFingerDeformBone1Name, koikatsuCommons.rightMiddleFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftMiddleFingerDeformBone2Name, koikatsuCommons.leftMiddleFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightMiddleFingerDeformBone2Name, koikatsuCommons.rightMiddleFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftMiddleFingerDeformBone3Name, koikatsuCommons.leftMiddleFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightMiddleFingerDeformBone3Name, koikatsuCommons.rightMiddleFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftRingFingerDeformBone1Name, koikatsuCommons.leftRingFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightRingFingerDeformBone1Name, koikatsuCommons.rightRingFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftRingFingerDeformBone2Name, koikatsuCommons.leftRingFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightRingFingerDeformBone2Name, koikatsuCommons.rightRingFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftRingFingerDeformBone3Name, koikatsuCommons.leftRingFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightRingFingerDeformBone3Name, koikatsuCommons.rightRingFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftLittleFingerDeformBone1Name, koikatsuCommons.leftLittleFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightLittleFingerDeformBone1Name, koikatsuCommons.rightLittleFingerDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftLittleFingerDeformBone2Name, koikatsuCommons.leftLittleFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightLittleFingerDeformBone2Name, koikatsuCommons.rightLittleFingerDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftLittleFingerDeformBone3Name, koikatsuCommons.leftLittleFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightLittleFingerDeformBone3Name, koikatsuCommons.rightLittleFingerDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftThighDeformBone1Name, koikatsuCommons.leftThighDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightThighDeformBone1Name, koikatsuCommons.rightThighDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftThighDeformBone2Name, koikatsuCommons.leftThighDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightThighDeformBone2Name, koikatsuCommons.rightThighDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftThighDeformBone3Name, koikatsuCommons.leftThighDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightThighDeformBone3Name, koikatsuCommons.rightThighDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftLegDeformBone1Name, koikatsuCommons.leftLegDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightLegDeformBone1Name, koikatsuCommons.rightLegDeformBone1Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftLegDeformBone2Name, koikatsuCommons.leftLegDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightLegDeformBone2Name, koikatsuCommons.rightLegDeformBone2Name)
+ renameAllVertexGroups(koikatsuCommons.originalLeftLegDeformBone3Name, koikatsuCommons.leftLegDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.originalRightLegDeformBone3Name, koikatsuCommons.rightLegDeformBone3Name)
+ renameAllVertexGroups(koikatsuCommons.leftAnkleBoneName, koikatsuCommons.leftAnkleDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.rightAnkleBoneName, koikatsuCommons.rightAnkleDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.leftToeBoneName, koikatsuCommons.leftToeDeformBoneName)
+ renameAllVertexGroups(koikatsuCommons.rightToeBoneName, koikatsuCommons.rightToeDeformBoneName)
+
+ if hasSkirt:
+ for primaryIndex in range(8):
+ for secondaryIndex in range(6):
+ renameAllVertexGroups(koikatsuCommons.getSkirtBoneName(False, primaryIndex, secondaryIndex), koikatsuCommons.getSkirtDeformBoneName(primaryIndex, secondaryIndex))
+
+ def fix_bone_orientations(armature):
+ # Connect all bones with their children if they have exactly one
+ for bone in armature.data.edit_bones:
+ if bpy.app.version[0] == 3:
+ if len(bone.children) == 1 and (metarig.data.bones[bone.name].layers[koikatsuCommons.originalAccessoryLayerIndex] == True or metarig.data.bones[bone.name].layers[koikatsuCommons.originalMchLayerIndex] == True):
+ p1 = bone.head
+ p2 = bone.children[0].head
+ dist = ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2) ** (1/2)
+
+ # Only connect them if the other bone is a certain distance away, otherwise blender will delete them
+ if dist > 0.005:
+ bone.tail = bone.children[0].head
+ if len(bone.parent.children) == 1: # if the bone's parent bone only has one child, connect the bones (Don't connect them all because that would mess up hand/finger bones)
+ bone.use_connect = True
+ else:
+ if len(bone.children) == 1 and (metarig.data.bones[bone.name].collections.get(str(koikatsuCommons.originalAccessoryLayerIndex)) or metarig.data.bones[bone.name].collections.get(str(koikatsuCommons.originalMchLayerIndex))):
+ p1 = bone.head
+ p2 = bone.children[0].head
+ dist = ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2) ** (1/2)
+
+ # Only connect them if the other bone is a certain distance away, otherwise blender will delete them
+ if dist > 0.005:
+ bone.tail = bone.children[0].head
+ if len(bone.parent.children) == 1: # if the bone's parent bone only has one child, connect the bones (Don't connect them all because that would mess up hand/finger bones)
+ bone.use_connect = True
+
+ fix_bone_orientations(metarig)
+
+
+ '''Put all Mch bones into an array'''
+
+ accessoryBoneNames = []
+ accessoryMchBoneNames = []
+ faceMchBoneNames = []
+ if bpy.app.version[0] == 3:
+ for bone in metarig.data.edit_bones:
+ if metarig.data.bones[bone.name].layers[koikatsuCommons.originalUpperFaceLayerIndex] == True or metarig.data.bones[bone.name].layers[koikatsuCommons.originalLowerFaceLayerIndex] == True:
+ if metarig.data.bones[bone.parent.name].layers[koikatsuCommons.originalMchLayerIndex] == True and bone.parent.name not in faceMchBoneNames and metarig.data.bones[bone.parent.name].layers[koikatsuCommons.originalUpperFaceLayerIndex] == False and metarig.data.bones[bone.parent.name].layers[koikatsuCommons.originalLowerFaceLayerIndex] == False:
+ faceMchBoneNames.append(bone.parent.name)
+ if metarig.data.bones[bone.name].layers[koikatsuCommons.originalAccessoryLayerIndex] == True:
+ accessoryBoneNames.append(bone.name)
+ if metarig.data.bones[bone.parent.name].layers[koikatsuCommons.originalMchLayerIndex] == True and metarig.data.bones[bone.parent.name].layers[koikatsuCommons.originalAccessoryLayerIndex] == False:
+ accessoryMchBoneNames.append(bone.parent.name)
+ else:
+ for bone in metarig.data.edit_bones:
+ if metarig.data.bones[bone.name].collections.get(str(koikatsuCommons.originalUpperFaceLayerIndex)) or metarig.data.bones[bone.name].collections.get(str(koikatsuCommons.originalLowerFaceLayerIndex)):
+ if metarig.data.bones[bone.parent.name].collections.get(str(koikatsuCommons.originalMchLayerIndex)) and bone.parent.name not in faceMchBoneNames and not metarig.data.bones[bone.parent.name].collections.get(str(koikatsuCommons.originalUpperFaceLayerIndex)) and not metarig.data.bones[bone.parent.name].collections.get(str(koikatsuCommons.originalLowerFaceLayerIndex)):
+ faceMchBoneNames.append(bone.parent.name)
+ if metarig.data.bones[bone.name].collections.get(str(koikatsuCommons.originalAccessoryLayerIndex)):
+ accessoryBoneNames.append(bone.name)
+ if metarig.data.bones[bone.parent.name].collections.get(str(koikatsuCommons.originalMchLayerIndex)) and not metarig.data.bones[bone.parent.name].collections.get(str(koikatsuCommons.originalAccessoryLayerIndex)):
+ accessoryMchBoneNames.append(bone.parent.name)
+
+ def finalizeMchList(rig, mchBoneNames, sourceLayerIndex, excludedLayerIndexes, excludedList = None):
+ for childBoneName in mchBoneNames:
+ childBone = metarig.data.edit_bones[childBoneName]
+ #childBone.length = childBone.length / 4
+ if childBone.name == 'Center':
+ continue
+ if bpy.app.version[0] == 3:
+ if rig.data.bones[childBone.parent.name].layers[sourceLayerIndex] == True and childBone.parent.name not in mchBoneNames:
+ insideExcludedLayer = False
+ for excludedLayerIndex in excludedLayerIndexes:
+ if rig.data.bones[childBone.parent.name].layers[excludedLayerIndex] == True:
+ insideExcludedLayer = True
+ break
+ if not insideExcludedLayer and (excludedList is None or childBone.parent.name not in excludedList):
+ mchBoneNames.append(childBone.parent.name)
+ else:
+ if rig.data.bones[childBone.parent.name].collections.get(str(sourceLayerIndex)) and childBone.parent.name not in mchBoneNames:
+ insideExcludedLayer = False
+ for excludedLayerIndex in excludedLayerIndexes:
+ if rig.data.bones[childBone.parent.name].collections.get(str(excludedLayerIndex)):
+ insideExcludedLayer = True
+ break
+ if not insideExcludedLayer and (excludedList is None or childBone.parent.name not in excludedList):
+ mchBoneNames.append(childBone.parent.name)
+
+ finalizeMchList(metarig, faceMchBoneNames, koikatsuCommons.originalMchLayerIndex, [koikatsuCommons.originalUpperFaceLayerIndex, koikatsuCommons.originalLowerFaceLayerIndex])
+ finalizeMchList(metarig, accessoryMchBoneNames, koikatsuCommons.originalMchLayerIndex, [koikatsuCommons.originalAccessoryLayerIndex], faceMchBoneNames)
+
+ accessoryBoneConnectedChildNames = []
+ accessoryBoneConnectedParentNames = []
+ accessoryMchConnectedParentNames = []
+ for boneName in accessoryBoneNames:
+ renameAllVertexGroups(boneName, koikatsuCommons.deformBonePrefix + boneName)
+ bone = metarig.data.edit_bones[boneName]
+ if bone.use_connect == False or bone.parent.name not in accessoryBoneNames:
+ for childBone in bone.children:
+ if childBone.use_connect == True and childBone.name in accessoryBoneNames:
+ accessoryBoneConnectedParentNames.append(bone.name)
+ break
+ if bone.use_connect == True and bone.parent.name in accessoryMchBoneNames:
+ accessoryMchConnectedParentNames.append(bone.parent.name)
+ elif bone.use_connect == True:
+ accessoryBoneConnectedChildNames.append(bone.name)
+ if bone.parent.name in accessoryMchBoneNames:
+ accessoryMchConnectedParentNames.append(bone.parent.name)
+
+ def connectAndParentBones(rig, childBoneName, parentBoneName, connected):
+ childBone = rig.data.edit_bones[childBoneName]
+ parentBone = rig.data.edit_bones[parentBoneName]
+ parentBone.tail = childBone.head
+ childBone.parent = parentBone
+ childBone.use_connect = connected
+
+ if hasRiggedTongue:
+ connectAndParentBones(metarig, koikatsuCommons.riggedTongueBone5Name, koikatsuCommons.riggedTongueBone4Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.riggedTongueBone4Name, koikatsuCommons.riggedTongueBone3Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.riggedTongueBone3Name, koikatsuCommons.riggedTongueBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.riggedTongueBone2Name, koikatsuCommons.riggedTongueBone1Name, False)
+ connectAndParentBones(metarig, koikatsuCommons.headBoneName, koikatsuCommons.neckBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.neckBoneName, koikatsuCommons.upperChestBoneName, False)
+ connectAndParentBones(metarig, koikatsuCommons.upperChestBoneName, koikatsuCommons.chestBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.chestBoneName, koikatsuCommons.spineBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.spineBoneName, koikatsuCommons.hipsBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftWristBoneName, koikatsuCommons.leftElbowBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightWristBoneName, koikatsuCommons.rightElbowBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftElbowBoneName, koikatsuCommons.leftArmBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightElbowBoneName, koikatsuCommons.rightArmBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftArmBoneName, koikatsuCommons.leftShoulderBoneName, False)
+ connectAndParentBones(metarig, koikatsuCommons.rightArmBoneName, koikatsuCommons.rightShoulderBoneName, False)
+ connectAndParentBones(metarig, koikatsuCommons.leftThumbBone3Name, koikatsuCommons.leftThumbBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightThumbBone3Name, koikatsuCommons.rightThumbBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftThumbBone2Name, koikatsuCommons.leftThumbBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightThumbBone2Name, koikatsuCommons.rightThumbBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftIndexFingerBone3Name, koikatsuCommons.leftIndexFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightIndexFingerBone3Name, koikatsuCommons.rightIndexFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftIndexFingerBone2Name, koikatsuCommons.leftIndexFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightIndexFingerBone2Name, koikatsuCommons.rightIndexFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftMiddleFingerBone3Name, koikatsuCommons.leftMiddleFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightMiddleFingerBone3Name, koikatsuCommons.rightMiddleFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftMiddleFingerBone2Name, koikatsuCommons.leftMiddleFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightMiddleFingerBone2Name, koikatsuCommons.rightMiddleFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftRingFingerBone3Name, koikatsuCommons.leftRingFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightRingFingerBone3Name, koikatsuCommons.rightRingFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftRingFingerBone2Name, koikatsuCommons.leftRingFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightRingFingerBone2Name, koikatsuCommons.rightRingFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftLittleFingerBone3Name, koikatsuCommons.leftLittleFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightLittleFingerBone3Name, koikatsuCommons.rightLittleFingerBone2Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftLittleFingerBone2Name, koikatsuCommons.leftLittleFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightLittleFingerBone2Name, koikatsuCommons.rightLittleFingerBone1Name, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftToeBoneName, koikatsuCommons.leftAnkleBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightToeBoneName, koikatsuCommons.rightAnkleBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftAnkleBoneName, koikatsuCommons.leftKneeBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightAnkleBoneName, koikatsuCommons.rightKneeBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.leftKneeBoneName, koikatsuCommons.leftLegBoneName, True)
+ connectAndParentBones(metarig, koikatsuCommons.rightKneeBoneName, koikatsuCommons.rightLegBoneName, True)
+
+ #using left vertex groups as reference for right bones because of inconsistent right group values
+ def finalizeRiggedTongueSideBones(rig, leftBoneName, rightBoneName, length, factorX, factorY = None, factorZ = None):
+ leftBone = rig.data.edit_bones[leftBoneName]
+ rightBone = rig.data.edit_bones[rightBoneName]
+ riggedTongueLeftBoneVertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(leftBoneName, koikatsuCommons.riggedTongueName())
+ leftBone.head.x = riggedTongueLeftBoneVertexGroupExtremities.minX + math.dist([riggedTongueLeftBoneVertexGroupExtremities.minX], [riggedTongueLeftBoneVertexGroupExtremities.maxX]) * factorX
+ leftBone.tail.x = leftBone.head.x
+ rightBone.head.x = -leftBone.head.x
+ rightBone.tail.x = rightBone.head.x
+ if factorY:
+ leftBone.head.y = riggedTongueLeftBoneVertexGroupExtremities.minY + math.dist([riggedTongueLeftBoneVertexGroupExtremities.minY], [riggedTongueLeftBoneVertexGroupExtremities.maxY]) * factorY
+ leftBone.tail.y = leftBone.head.y
+ rightBone.head.y = leftBone.head.y
+ rightBone.tail.y = rightBone.head.y
+ if factorZ:
+ leftBone.head.z = riggedTongueLeftBoneVertexGroupExtremities.minZ + math.dist([riggedTongueLeftBoneVertexGroupExtremities.minZ], [riggedTongueLeftBoneVertexGroupExtremities.maxZ]) * factorZ
+ rightBone.head.z = leftBone.head.z
+ leftBone.length = length
+ rightBone.length = length
+
+ if hasRiggedTongue:
+ riggedTongueBone2 = metarig.data.edit_bones[koikatsuCommons.riggedTongueBone2Name]
+ finalizeRiggedTongueSideBones(metarig, koikatsuCommons.riggedTongueLeftBone3Name, koikatsuCommons.riggedTongueRightBone3Name, riggedTongueBone2.length, 0.75)
+ finalizeRiggedTongueSideBones(metarig, koikatsuCommons.riggedTongueLeftBone4Name, koikatsuCommons.riggedTongueRightBone4Name, riggedTongueBone2.length, 0.65, 0.5, 0.5)
+ finalizeRiggedTongueSideBones(metarig, koikatsuCommons.riggedTongueLeftBone5Name, koikatsuCommons.riggedTongueRightBone5Name, riggedTongueBone2.length, 0.65, 0.2, 0.3)
+ riggedTongueBone5 = metarig.data.edit_bones[koikatsuCommons.riggedTongueBone5Name]
+ riggedTongueLeftBone5VertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.riggedTongueLeftBone5Name, koikatsuCommons.riggedTongueName())
+ riggedTongueBone5.tail.z = riggedTongueLeftBone5VertexGroupExtremities.minZ + math.dist([riggedTongueLeftBone5VertexGroupExtremities.minZ], [riggedTongueLeftBone5VertexGroupExtremities.maxZ]) * 0.17
+ riggedTongueBone5.tail.y = riggedTongueLeftBone5VertexGroupExtremities.minY
+ if not hasHeadMod:
+ metarig.data.edit_bones[koikatsuCommons.headBoneName].tail.z = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.originalFaceUpDeformBoneName, koikatsuCommons.bodyName()).maxZ
+ else:
+ metarig.data.edit_bones[koikatsuCommons.headBoneName].tail.z = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.originalFaceBaseDeformBoneName, koikatsuCommons.bodyName()).maxZ
+ metarig.data.edit_bones[koikatsuCommons.hipsBoneName].head = metarig.data.edit_bones[koikatsuCommons.waistBoneName].head
+ leftShoulderJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.leftShoulderJointCorrectionBoneName]
+ rightShoulderJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.rightShoulderJointCorrectionBoneName]
+ midLeftElbowJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.midLeftElbowJointCorrectionBoneName]
+ midRightElbowJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.midRightElbowJointCorrectionBoneName]
+ backLeftElbowJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.backLeftElbowJointCorrectionBoneName]
+ backRightElbowJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.backRightElbowJointCorrectionBoneName]
+ frontLeftElbowJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.frontLeftElbowJointCorrectionBoneName]
+ frontRightElbowJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.frontRightElbowJointCorrectionBoneName]
+ leftShoulderDeformBone = metarig.data.edit_bones[koikatsuCommons.leftShoulderDeformBoneName]
+ rightShoulderDeformBone = metarig.data.edit_bones[koikatsuCommons.rightShoulderDeformBoneName]
+ leftArmBone = metarig.data.edit_bones[koikatsuCommons.leftArmBoneName]
+ rightArmBone = metarig.data.edit_bones[koikatsuCommons.rightArmBoneName]
+ leftElbowBone = metarig.data.edit_bones[koikatsuCommons.leftElbowBoneName]
+ rightElbowBone = metarig.data.edit_bones[koikatsuCommons.rightElbowBoneName]
+ leftElbowBone.head.y = midLeftElbowJointCorrectionBone.head.y + math.dist([midLeftElbowJointCorrectionBone.head.y], [frontLeftElbowJointCorrectionBone.head.y]) * 0.33
+ rightElbowBone.head.y = midRightElbowJointCorrectionBone.head.y + math.dist([midRightElbowJointCorrectionBone.head.y], [frontRightElbowJointCorrectionBone.head.y]) * 0.33
+ leftElbowBone.roll = radians(0)
+ rightElbowBone.roll = radians(0)
+ frontLeftElbowJointCorrectionBone.roll = radians(180)
+ #frontRightElbowJointCorrectionBone.roll = radians(180)
+ backLeftElbowJointCorrectionBone.roll = radians(180)
+ #backRightElbowJointCorrectionBone.roll = radians(180)
+ leftShoulderJointCorrectionBone.tail = leftArmBone.tail
+ rightShoulderJointCorrectionBone.tail = rightArmBone.tail
+ leftShoulderJointCorrectionBone.roll = radians(90)
+ rightShoulderJointCorrectionBone.roll = radians(-90)
+ leftShoulderDeformBone.tail = leftArmBone.tail
+ rightShoulderDeformBone.tail = rightArmBone.tail
+ leftShoulderDeformBone.roll = radians(90)
+ rightShoulderDeformBone.roll = radians(-90)
+ leftWristBone = metarig.data.edit_bones[koikatsuCommons.leftWristBoneName]
+ rightWristBone = metarig.data.edit_bones[koikatsuCommons.rightWristBoneName]
+ leftWristBone.head.y = midLeftElbowJointCorrectionBone.head.y - math.dist([midLeftElbowJointCorrectionBone.head.y], [backLeftElbowJointCorrectionBone.head.y]) * 0.15
+ rightWristBone.head.y = midRightElbowJointCorrectionBone.head.y - math.dist([midRightElbowJointCorrectionBone.head.y], [backRightElbowJointCorrectionBone.head.y]) * 0.15
+ if leftWristBone.tail.z != leftWristBone.head.z:
+ leftWristBone.tail.y = leftWristBone.head.y
+ leftWristBone.tail.z = leftWristBone.head.z
+ leftWristGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.leftWristDeformBoneName, koikatsuCommons.bodyName())
+ leftWristBone.tail.x = leftWristGroupExtremities.minX + math.dist([leftWristGroupExtremities.minX], [leftWristGroupExtremities.maxX]) * 0.66
+ if rightWristBone.tail.z != rightWristBone.head.z:
+ rightWristBone.tail.y = rightWristBone.head.y
+ rightWristBone.tail.z = rightWristBone.head.z
+ rightWristGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.rightWristDeformBoneName, koikatsuCommons.bodyName())
+ rightWristBone.tail.x = rightWristGroupExtremities.maxX - math.dist([rightWristGroupExtremities.minX], [rightWristGroupExtremities.maxX]) * 0.66
+ leftToeBone = metarig.data.edit_bones[koikatsuCommons.leftToeBoneName]
+ rightToeBone = metarig.data.edit_bones[koikatsuCommons.rightToeBoneName]
+ leftToeBone.tail.z = leftToeBone.head.z
+ rightToeBone.tail.z = rightToeBone.head.z
+ leftToeBone.tail.y = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.leftToeDeformBoneName, koikatsuCommons.bodyName()).minY
+ rightToeBone.tail.y = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.rightToeDeformBoneName, koikatsuCommons.bodyName()).minY
+ leftAnkleGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.leftAnkleDeformBoneName, koikatsuCommons.bodyName())
+ rightAnkleGroupExtremities = koikatsuCommons.findVertexGroupExtremities(koikatsuCommons.rightAnkleDeformBoneName, koikatsuCommons.bodyName())
+ midLeftKneeJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.midLeftKneeJointCorrectionBoneName]
+ midRightKneeJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.midRightKneeJointCorrectionBoneName]
+ backLeftKneeJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.backLeftKneeJointCorrectionBoneName]
+ backRightKneeJointCorrectionBone = metarig.data.edit_bones[koikatsuCommons.backRightKneeJointCorrectionBoneName]
+ metarig.data.edit_bones[koikatsuCommons.leftLegBoneName].head.y = midLeftKneeJointCorrectionBone.head.y + math.dist([midLeftKneeJointCorrectionBone.head.y], [backLeftKneeJointCorrectionBone.head.y]) * 0.33
+ metarig.data.edit_bones[koikatsuCommons.rightLegBoneName].head.y = midRightKneeJointCorrectionBone.head.y + math.dist([midRightKneeJointCorrectionBone.head.y], [backRightKneeJointCorrectionBone.head.y]) * 0.33
+ leftHeelBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.originalLeftHeelBoneName, koikatsuCommons.leftHeelBoneName)
+ rightHeelBone = koikatsuCommons.copyBone(metarig, koikatsuCommons.originalRightHeelBoneName, koikatsuCommons.rightHeelBoneName)
+ leftHeelBone.head.x = leftAnkleGroupExtremities.minX
+ rightHeelBone.head.x = rightAnkleGroupExtremities.maxX
+ leftHeelBone.head.y = leftAnkleGroupExtremities.maxY
+ rightHeelBone.head.y = rightAnkleGroupExtremities.maxY
+ leftHeelBone.head.z = leftAnkleGroupExtremities.minZ
+ rightHeelBone.head.z = rightAnkleGroupExtremities.minZ
+ leftHeelBone.tail.x = leftAnkleGroupExtremities.maxX
+ rightHeelBone.tail.x = rightAnkleGroupExtremities.minX
+ leftHeelBone.tail.y = leftAnkleGroupExtremities.maxY
+ rightHeelBone.tail.y = rightAnkleGroupExtremities.maxY
+ leftHeelBone.tail.z = leftAnkleGroupExtremities.minZ
+ rightHeelBone.tail.z = rightAnkleGroupExtremities.minZ
+ leftHeelBone.parent = metarig.data.edit_bones[koikatsuCommons.leftAnkleBoneName]
+ rightHeelBone.parent = metarig.data.edit_bones[koikatsuCommons.rightAnkleBoneName]
+ leftAnkleBone = metarig.data.edit_bones[koikatsuCommons.leftAnkleBoneName]
+ rightAnkleBone = metarig.data.edit_bones[koikatsuCommons.rightAnkleBoneName]
+ originalLeftLegDeformBone3 = metarig.data.edit_bones[koikatsuCommons.originalLeftLegDeformBone3Name]
+ originalRightLegDeformBone3 = metarig.data.edit_bones[koikatsuCommons.originalRightLegDeformBone3Name]
+ leftAnkleBone.head.z = leftAnkleBone.tail.z + math.dist([leftAnkleBone.tail.z], [originalLeftLegDeformBone3.head.z]) * 0.7
+ rightAnkleBone.head.z = rightAnkleBone.tail.z + math.dist([rightAnkleBone.tail.z], [originalRightLegDeformBone3.head.z]) * 0.7
+ leftAnkleBone.head.y = originalLeftLegDeformBone3.head.y + math.dist([originalLeftLegDeformBone3.head.y], [leftHeelBone.head.y]) * 0.2
+ rightAnkleBone.head.y = originalRightLegDeformBone3.head.y + math.dist([originalRightLegDeformBone3.head.y], [rightHeelBone.head.y]) * 0.2
+
+ def createPalmVertexGroups(wristVertexGroupName, palmVertexGroupName, rightSide, palmMinX, palmMaxX, palmMidY, lowerPalmMidY, higherPalmMidY):
+ for object in bpy.context.scene.objects:
+ if object.type == 'MESH':
+ wristVertexGroup = object.vertex_groups.get(wristVertexGroupName)
+ if wristVertexGroup is not None:
+ wristVertexGroupExtremities = koikatsuCommons.findVertexGroupExtremities(wristVertexGroupName, object.name)
+ palmVertexGroup = object.vertex_groups.get(palmVertexGroupName)
+ if palmVertexGroup is None:
+ palmVertexGroup = object.vertex_groups.new(name = palmVertexGroupName)
+ for vertex in wristVertexGroupExtremities.vertices:
+ if (not rightSide and wristVertexGroupExtremities.coordinates[vertex.index][0] > palmMinX) or (rightSide and wristVertexGroupExtremities.coordinates[vertex.index][0] < palmMaxX):
+ if lowerPalmMidY is None or math.dist([wristVertexGroupExtremities.coordinates[vertex.index][1]], [palmMidY]) < math.dist([wristVertexGroupExtremities.coordinates[vertex.index][1]], [lowerPalmMidY]):
+ if higherPalmMidY is None or math.dist([wristVertexGroupExtremities.coordinates[vertex.index][1]], [palmMidY]) < math.dist([wristVertexGroupExtremities.coordinates[vertex.index][1]], [higherPalmMidY]):
+ palmVertexGroup.add([vertex.index], wristVertexGroup.weight(vertex.index), 'REPLACE')
+ wristVertexGroup.remove([vertex.index])
+
+ leftIndexFingerBone1 = metarig.data.edit_bones[koikatsuCommons.leftIndexFingerBone1Name]
+ rightIndexFingerBone1 = metarig.data.edit_bones[koikatsuCommons.rightIndexFingerBone1Name]
+ leftMiddleFingerBone1 = metarig.data.edit_bones[koikatsuCommons.leftMiddleFingerBone1Name]
+ rightMiddleFingerBone1 = metarig.data.edit_bones[koikatsuCommons.rightMiddleFingerBone1Name]
+ leftRingFingerBone1 = metarig.data.edit_bones[koikatsuCommons.leftRingFingerBone1Name]
+ rightRingFingerBone1 = metarig.data.edit_bones[koikatsuCommons.rightRingFingerBone1Name]
+ leftLittleFingerBone1 = metarig.data.edit_bones[koikatsuCommons.leftLittleFingerBone1Name]
+ rightLittleFingerBone1 = metarig.data.edit_bones[koikatsuCommons.rightLittleFingerBone1Name]
+ leftPalmMinX = leftWristBone.tail.x - math.dist([leftWristBone.head.x], [leftWristBone.tail.x]) * 0.85
+ rightPalmMaxX = rightWristBone.tail.x + math.dist([rightWristBone.head.x], [rightWristBone.tail.x]) * 0.85
+ createPalmVertexGroups(koikatsuCommons.leftWristDeformBoneName, koikatsuCommons.leftIndexFingerPalmDeformBoneName, False, leftPalmMinX, None, leftIndexFingerBone1.head.y, None, leftMiddleFingerBone1.head.y)
+ createPalmVertexGroups(koikatsuCommons.rightWristDeformBoneName, koikatsuCommons.rightIndexFingerPalmDeformBoneName, True, None, rightPalmMaxX, rightIndexFingerBone1.head.y, None, rightMiddleFingerBone1.head.y)
+ createPalmVertexGroups(koikatsuCommons.leftWristDeformBoneName, koikatsuCommons.leftMiddleFingerPalmDeformBoneName, False, leftPalmMinX, None, leftMiddleFingerBone1.head.y, None, leftRingFingerBone1.head.y)
+ createPalmVertexGroups(koikatsuCommons.rightWristDeformBoneName, koikatsuCommons.rightMiddleFingerPalmDeformBoneName, True, None, rightPalmMaxX, rightMiddleFingerBone1.head.y, None, rightRingFingerBone1.head.y)
+ createPalmVertexGroups(koikatsuCommons.leftWristDeformBoneName, koikatsuCommons.leftRingFingerPalmDeformBoneName, False, leftPalmMinX, None, leftRingFingerBone1.head.y, None, leftLittleFingerBone1.head.y)
+ createPalmVertexGroups(koikatsuCommons.rightWristDeformBoneName, koikatsuCommons.rightRingFingerPalmDeformBoneName, True, None, rightPalmMaxX, rightRingFingerBone1.head.y, None, rightLittleFingerBone1.head.y)
+ createPalmVertexGroups(koikatsuCommons.leftWristDeformBoneName, koikatsuCommons.leftLittleFingerPalmDeformBoneName, False, leftPalmMinX, None, leftLittleFingerBone1.head.y, None, None)
+ createPalmVertexGroups(koikatsuCommons.rightWristDeformBoneName, koikatsuCommons.rightLittleFingerPalmDeformBoneName, True, None, rightPalmMaxX, rightLittleFingerBone1.head.y, None, None)
+
+ def finalizeFingerBones(leftSide, rig, wristBoneName, fingerPalmBoneName, fingerBone1Name, fingerBone2Name, fingerBone3Name, fingerDeformBone1Name, fingerDeformBone2Name, fingerDeformBone3Name, objectName, palmBoneRoll, fingerBone1Roll, fingerBone2Roll, fingerBone3Roll, middle = False, thumb = False):
+ fingerBone1 = rig.data.edit_bones[fingerBone1Name]
+ fingerBone2 = rig.data.edit_bones[fingerBone2Name]
+ fingerBone3 = rig.data.edit_bones[fingerBone3Name]
+ fingerDeformBone1Extremities = koikatsuCommons.findVertexGroupExtremities(fingerDeformBone1Name, objectName)
+ fingerDeformBone2Extremities = koikatsuCommons.findVertexGroupExtremities(fingerDeformBone2Name, objectName)
+ fingerDeformBone3Extremities = koikatsuCommons.findVertexGroupExtremities(fingerDeformBone3Name, objectName)
+ if not thumb:
+ if not middle:
+ fingerBone1.head.z = fingerDeformBone1Extremities.minZ + math.dist([fingerDeformBone1Extremities.minZ], [fingerDeformBone1Extremities.maxZ]) * 0.4
+ else:
+ fingerBone1.head.z = fingerDeformBone1Extremities.minZ + math.dist([fingerDeformBone1Extremities.minZ], [fingerDeformBone1Extremities.maxZ]) * 0.5
+ fingerBone2.head.z = fingerDeformBone2Extremities.minZ + math.dist([fingerDeformBone2Extremities.minZ], [fingerDeformBone2Extremities.maxZ]) * 0.6
+ fingerBone3.head.z = fingerDeformBone3Extremities.minZ + math.dist([fingerDeformBone3Extremities.minZ], [fingerDeformBone3Extremities.maxZ]) * 0.6
+ if leftSide:
+ fingerBone3.tail.x = fingerDeformBone3Extremities.minX + math.dist([fingerDeformBone3Extremities.minX], [fingerDeformBone3Extremities.maxX]) * 0.95
+ else:
+ fingerBone3.tail.x = fingerDeformBone3Extremities.maxX - math.dist([fingerDeformBone3Extremities.minX], [fingerDeformBone3Extremities.maxX]) * 0.95
+ fingerBone3.tail.y = fingerBone3.head.y + (fingerBone2.tail.y - fingerBone2.head.y)
+ fingerBone3.tail.z = fingerDeformBone3Extremities.minZ + math.dist([fingerDeformBone3Extremities.minZ], [fingerDeformBone3Extremities.maxZ]) * 0.4
+ wristBone = rig.data.edit_bones[wristBoneName]
+ fingerPalmBone = koikatsuCommons.copyBone(rig, fingerBone1Name, fingerPalmBoneName)
+ fingerPalmBone.parent = rig.data.edit_bones[wristBoneName]
+ connectAndParentBones(rig, fingerBone1Name, fingerPalmBoneName, False)
+ fingerPalmBone.head.z = fingerPalmBone.tail.z
+ if leftSide:
+ fingerPalmBone.head.x = wristBone.tail.x - math.dist([wristBone.tail.x], [wristBone.head.x]) * 0.6
+ else:
+ fingerPalmBone.head.x = wristBone.tail.x + math.dist([wristBone.tail.x], [wristBone.head.x]) * 0.6
+ fingerPalmBone.head.y = fingerPalmBone.head.y + (fingerBone1.head.y - fingerBone1.tail.y)
+ fingerPalmBone.roll = palmBoneRoll
+ else:
+ if leftSide:
+ fingerBone1.head.x = fingerDeformBone1Extremities.minX + math.dist([fingerDeformBone1Extremities.minX], [fingerDeformBone1Extremities.maxX]) * 0.2
+ fingerBone2.head.x = fingerDeformBone2Extremities.minX + math.dist([fingerDeformBone2Extremities.minX], [fingerDeformBone2Extremities.maxX]) * 0.5
+ fingerBone3.head.x = fingerDeformBone3Extremities.minX + math.dist([fingerDeformBone3Extremities.minX], [fingerDeformBone3Extremities.maxX]) * 0.4
+ fingerBone3.tail.x = fingerDeformBone3Extremities.minX + math.dist([fingerDeformBone3Extremities.minX], [fingerDeformBone3Extremities.maxX]) * 0.95
+ else:
+ fingerBone1.head.x = fingerDeformBone1Extremities.maxX - math.dist([fingerDeformBone1Extremities.minX], [fingerDeformBone1Extremities.maxX]) * 0.2
+ fingerBone2.head.x = fingerDeformBone2Extremities.maxX - math.dist([fingerDeformBone2Extremities.minX], [fingerDeformBone2Extremities.maxX]) * 0.5
+ fingerBone3.head.x = fingerDeformBone3Extremities.maxX - math.dist([fingerDeformBone3Extremities.minX], [fingerDeformBone3Extremities.maxX]) * 0.4
+ fingerBone3.tail.x = fingerDeformBone3Extremities.maxX - math.dist([fingerDeformBone3Extremities.minX], [fingerDeformBone3Extremities.maxX]) * 0.95
+ fingerBone1.head.y = fingerDeformBone1Extremities.minY + math.dist([fingerDeformBone1Extremities.minY], [fingerDeformBone1Extremities.maxY]) * 0.45
+ fingerBone1.head.z = fingerDeformBone1Extremities.minZ + math.dist([fingerDeformBone1Extremities.minZ], [fingerDeformBone1Extremities.maxZ]) * 0.35
+ fingerBone2.head.y = fingerDeformBone2Extremities.minY + math.dist([fingerDeformBone2Extremities.minY], [fingerDeformBone2Extremities.maxY]) * 0.35
+ fingerBone2.head.z = fingerDeformBone2Extremities.minZ + math.dist([fingerDeformBone2Extremities.minZ], [fingerDeformBone2Extremities.maxZ]) * 0.3
+ fingerBone3.head.y = fingerDeformBone3Extremities.minY + math.dist([fingerDeformBone3Extremities.minY], [fingerDeformBone3Extremities.maxY]) * 0.4
+ fingerBone3.head.z = fingerDeformBone3Extremities.minZ + math.dist([fingerDeformBone3Extremities.minZ], [fingerDeformBone3Extremities.maxZ]) * 0.45
+ fingerBone3.tail.y = fingerDeformBone3Extremities.minY + math.dist([fingerDeformBone3Extremities.minY], [fingerDeformBone3Extremities.maxY]) * 0.1
+ fingerBone3.tail.z = fingerDeformBone3Extremities.minZ + math.dist([fingerDeformBone3Extremities.minZ], [fingerDeformBone3Extremities.maxZ]) * 0.25
+ fingerBone1.parent = rig.data.edit_bones[fingerPalmBoneName]
+ fingerBone1.roll = fingerBone1Roll
+ fingerBone2.roll = fingerBone2Roll
+ fingerBone3.roll = fingerBone3Roll
+
+ finalizeFingerBones(True, metarig, koikatsuCommons.leftWristBoneName, koikatsuCommons.leftIndexFingerPalmBoneName, koikatsuCommons.leftIndexFingerBone1Name, koikatsuCommons.leftIndexFingerBone2Name, koikatsuCommons.leftIndexFingerBone3Name, koikatsuCommons.leftIndexFingerDeformBone1Name, koikatsuCommons.leftIndexFingerDeformBone2Name, koikatsuCommons.leftIndexFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(0), radians(5), radians(10))
+ finalizeFingerBones(False, metarig, koikatsuCommons.rightWristBoneName, koikatsuCommons.rightIndexFingerPalmBoneName, koikatsuCommons.rightIndexFingerBone1Name, koikatsuCommons.rightIndexFingerBone2Name, koikatsuCommons.rightIndexFingerBone3Name, koikatsuCommons.rightIndexFingerDeformBone1Name, koikatsuCommons.rightIndexFingerDeformBone2Name, koikatsuCommons.rightIndexFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(0), radians(-5), radians(-10))
+ finalizeFingerBones(True, metarig, koikatsuCommons.leftWristBoneName, koikatsuCommons.leftMiddleFingerPalmBoneName, koikatsuCommons.leftMiddleFingerBone1Name, koikatsuCommons.leftMiddleFingerBone2Name, koikatsuCommons.leftMiddleFingerBone3Name, koikatsuCommons.leftMiddleFingerDeformBone1Name, koikatsuCommons.leftMiddleFingerDeformBone2Name, koikatsuCommons.leftMiddleFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(-10), radians(-2), radians(6), True)
+ finalizeFingerBones(False, metarig, koikatsuCommons.rightWristBoneName, koikatsuCommons.rightMiddleFingerPalmBoneName, koikatsuCommons.rightMiddleFingerBone1Name, koikatsuCommons.rightMiddleFingerBone2Name, koikatsuCommons.rightMiddleFingerBone3Name, koikatsuCommons.rightMiddleFingerDeformBone1Name, koikatsuCommons.rightMiddleFingerDeformBone2Name, koikatsuCommons.rightMiddleFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(10), radians(2), radians(-6), True)
+ finalizeFingerBones(True, metarig, koikatsuCommons.leftWristBoneName, koikatsuCommons.leftRingFingerPalmBoneName, koikatsuCommons.leftRingFingerBone1Name, koikatsuCommons.leftRingFingerBone2Name, koikatsuCommons.leftRingFingerBone3Name, koikatsuCommons.leftRingFingerDeformBone1Name, koikatsuCommons.leftRingFingerDeformBone2Name, koikatsuCommons.leftRingFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(-15), radians(-10), radians(-5))
+ finalizeFingerBones(False, metarig, koikatsuCommons.rightWristBoneName, koikatsuCommons.rightRingFingerPalmBoneName, koikatsuCommons.rightRingFingerBone1Name, koikatsuCommons.rightRingFingerBone2Name, koikatsuCommons.rightRingFingerBone3Name, koikatsuCommons.rightRingFingerDeformBone1Name, koikatsuCommons.rightRingFingerDeformBone2Name, koikatsuCommons.rightRingFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(15), radians(10), radians(5))
+ finalizeFingerBones(True, metarig, koikatsuCommons.leftWristBoneName, koikatsuCommons.leftLittleFingerPalmBoneName, koikatsuCommons.leftLittleFingerBone1Name, koikatsuCommons.leftLittleFingerBone2Name, koikatsuCommons.leftLittleFingerBone3Name, koikatsuCommons.leftLittleFingerDeformBone1Name, koikatsuCommons.leftLittleFingerDeformBone2Name, koikatsuCommons.leftLittleFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(-20), radians(-18), radians(-16))
+ finalizeFingerBones(False, metarig, koikatsuCommons.rightWristBoneName, koikatsuCommons.rightLittleFingerPalmBoneName, koikatsuCommons.rightLittleFingerBone1Name, koikatsuCommons.rightLittleFingerBone2Name, koikatsuCommons.rightLittleFingerBone3Name, koikatsuCommons.rightLittleFingerDeformBone1Name, koikatsuCommons.rightLittleFingerDeformBone2Name, koikatsuCommons.rightLittleFingerDeformBone3Name, koikatsuCommons.bodyName(), radians(0), radians(20), radians(18), radians(16))
+ finalizeFingerBones(True, metarig, koikatsuCommons.leftWristBoneName, koikatsuCommons.leftIndexFingerPalmBoneName, koikatsuCommons.leftThumbBone1Name, koikatsuCommons.leftThumbBone2Name, koikatsuCommons.leftThumbBone3Name, koikatsuCommons.leftThumbDeformBone1Name, koikatsuCommons.leftThumbDeformBone2Name, koikatsuCommons.leftThumbDeformBone3Name, koikatsuCommons.bodyName(), None, radians(-257), radians(-260), radians(-263), False, True)
+ finalizeFingerBones(False, metarig, koikatsuCommons.rightWristBoneName, koikatsuCommons.rightIndexFingerPalmBoneName, koikatsuCommons.rightThumbBone1Name, koikatsuCommons.rightThumbBone2Name, koikatsuCommons.rightThumbBone3Name, koikatsuCommons.rightThumbDeformBone1Name, koikatsuCommons.rightThumbDeformBone2Name, koikatsuCommons.rightThumbDeformBone3Name, koikatsuCommons.bodyName(), None, radians(-103), radians(-106), radians(-109), False, True)
+
+ skirtPalmBoneRatioReferences = [Vector((-0.0000, -0.0083, 0.0287)), Vector((-0.0057, -0.0080, 0.0294)), Vector((-0.0136, -0.0002, 0.0317)), Vector((-0.0094, 0.0151, 0.0360)), Vector((-0.0010, 0.0153, 0.0333)), Vector((0.0094, 0.0151, 0.0360)), Vector((0.0136, -0.0002, 0.0317)), Vector((0.0057, -0.0080, 0.0294))]
+ if hasSkirt and not metarig.data.edit_bones.get(koikatsuCommons.skirtParentBoneCopyName):
+ koikatsuCommons.copyBone(metarig, koikatsuCommons.skirtParentBoneName, koikatsuCommons.skirtParentBoneCopyName)
+ for primaryIndex in range(8):
+ skirtPalmBone = metarig.data.edit_bones[koikatsuCommons.getSkirtBoneName(True, primaryIndex)]
+ skirtBone0 = metarig.data.edit_bones[koikatsuCommons.getSkirtBoneName(False, primaryIndex, 0)]
+ skirtBone4 = metarig.data.edit_bones[koikatsuCommons.getSkirtBoneName(False, primaryIndex, 4)]
+ skirtBone5 = metarig.data.edit_bones[koikatsuCommons.getSkirtBoneName(False, primaryIndex, 5)]
+ skirtPalmBone.tail = skirtPalmBone.head - skirtPalmBoneRatioReferences[primaryIndex]
+ skirtPalmBone.length = skirtBone0.length / 2
+ headPos = skirtPalmBone.head.copy()
+ skirtPalmBone.head = skirtPalmBone.tail
+ skirtPalmBone.tail = headPos
+ skirtBone5.tail = skirtBone5.head - (skirtBone4.head - skirtBone4.tail)
+ skirtPalmBoneLength = skirtPalmBone.length
+ shrinkFactorX = skirtPalmBone.head.x * 0.1
+ shrinkFactorY = skirtPalmBone.head.y * 0.1
+ skirtPalmBone.head = skirtPalmBone.head - Vector((shrinkFactorX, shrinkFactorY, 0))
+ skirtPalmBone.tail = skirtPalmBone.tail - Vector((shrinkFactorX, shrinkFactorY, 0))
+ skirtBoneLengths = []
+ skirtBoneOrientations = []
+ for secondaryIndex in range(6):
+ skirtBone = metarig.data.edit_bones[koikatsuCommons.getSkirtBoneName(False, primaryIndex, secondaryIndex)]
+ skirtBoneLengths.append(skirtBone.length)
+ skirtBoneOrientations.append(skirtBone.tail - skirtBone.head)
+ shrinkFactorX = skirtBone.head.x * 0.1
+ shrinkFactorY = skirtBone.head.y * 0.1
+ skirtBone.head = skirtBone.head - Vector((shrinkFactorX, shrinkFactorY, 0))
+ skirtBone.tail = skirtBone.tail - Vector((shrinkFactorX, shrinkFactorY, 0))
+ skirtPalmBone.length = skirtPalmBoneLength
+ previousBoneOrientation = skirtPalmBone.tail - skirtPalmBone.head
+ for secondaryIndex in range(6):
+ skirtBone = metarig.data.edit_bones[koikatsuCommons.getSkirtBoneName(False, primaryIndex, secondaryIndex)]
+ if secondaryIndex == 0:
+ skirtBone.tail = skirtBone.head + previousBoneOrientation * Vector((0.4, 0.4, 0.4)) + skirtBoneOrientations[secondaryIndex] * Vector((0.6, 0.6, 0.6))
+ else:
+ skirtBone.tail = skirtBone.head + previousBoneOrientation * Vector((0.5, 0.5, 0.5)) + skirtBoneOrientations[secondaryIndex] * Vector((0, 0, 0.5))
+ skirtBone.length = skirtBoneLengths[secondaryIndex]
+ previousBoneOrientation = skirtBone.tail - skirtBone.head
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.leftHeelBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.rightHeelBoneName, 0)
+ koikatsuCommons.assignSingleBoneLayer(metarig, koikatsuCommons.skirtParentBoneCopyName, 0)
+
+ legConstrainedBoneNames = [koikatsuCommons.waistJointCorrectionBoneName, koikatsuCommons.leftButtockJointCorrectionBoneName, koikatsuCommons.rightButtockJointCorrectionBoneName, koikatsuCommons.leftLegJointCorrectionBoneName, koikatsuCommons.rightLegJointCorrectionBoneName]
+ for legConstrainedBoneName in legConstrainedBoneNames:
+ for constraint in metarig.pose.bones[legConstrainedBoneName].constraints:
+ if constraint.type == 'COPY_ROTATION' and (constraint.subtarget == koikatsuCommons.leftLegBoneName or constraint.subtarget == koikatsuCommons.rightLegBoneName):
+ constraint.invert_y = True
+ constraint.invert_z = True
+
+ for driver in metarig.animation_data.drivers:
+ if driver.data_path.startswith("pose.bones"):
+ ownerName = driver.data_path.split('"')[1]
+ property = driver.data_path.rsplit('.', 1)[1]
+ if ownerName == koikatsuCommons.leftWristJointCorrectionBoneName and property == "location":
+ driver.driver.expression = driver.driver.expression.replace(">", "<")
+ elif (ownerName == koikatsuCommons.leftShoulderJointCorrectionBoneName or ownerName == koikatsuCommons.rightShoulderJointCorrectionBoneName) and property == "location":
+ for variable in driver.driver.variables:
+ if len(variable.targets) == 1:
+ if variable.targets[0].bone_target == koikatsuCommons.leftArmBoneName:
+ if driver.array_index == 0:
+ leftShoulderJointCorrectionBoneDriverX = driver
+ elif driver.array_index == 1:
+ leftShoulderJointCorrectionBoneDriverY = driver
+ elif driver.array_index == 2:
+ leftShoulderJointCorrectionBoneDriverZ = driver
+ elif variable.targets[0].bone_target == koikatsuCommons.rightArmBoneName:
+ if driver.array_index == 0:
+ rightShoulderJointCorrectionBoneDriverX = driver
+ elif driver.array_index == 1:
+ rightShoulderJointCorrectionBoneDriverY = driver
+ elif driver.array_index == 2:
+ rightShoulderJointCorrectionBoneDriverZ = driver
+
+ shoulderJointCorrectionBoneDriverExpressionPrefix = "(-1)*"
+ if not leftShoulderJointCorrectionBoneDriverX.driver.expression.startswith(shoulderJointCorrectionBoneDriverExpressionPrefix):
+ originalExpressionX = leftShoulderJointCorrectionBoneDriverX.driver.expression
+ leftShoulderJointCorrectionBoneDriverX.driver.expression = shoulderJointCorrectionBoneDriverExpressionPrefix + leftShoulderJointCorrectionBoneDriverY.driver.expression
+ leftShoulderJointCorrectionBoneDriverX.driver.variables[0].targets[0].transform_type = 'ROT_Z'
+ leftShoulderJointCorrectionBoneDriverY.driver.expression = originalExpressionX
+ leftShoulderJointCorrectionBoneDriverY.driver.variables[0].targets[0].transform_type = 'ROT_X'
+ leftShoulderJointCorrectionBoneDriverZ.driver.expression = shoulderJointCorrectionBoneDriverExpressionPrefix + leftShoulderJointCorrectionBoneDriverZ.driver.expression
+ leftShoulderJointCorrectionBoneDriverZ.driver.variables[0].targets[0].transform_type = 'ROT_X'
+ if not rightShoulderJointCorrectionBoneDriverY.driver.expression.startswith(shoulderJointCorrectionBoneDriverExpressionPrefix):
+ originalExpressionX = rightShoulderJointCorrectionBoneDriverX.driver.expression
+ rightShoulderJointCorrectionBoneDriverX.driver.expression = rightShoulderJointCorrectionBoneDriverY.driver.expression
+ rightShoulderJointCorrectionBoneDriverX.driver.variables[0].targets[0].transform_type = 'ROT_Z'
+ rightShoulderJointCorrectionBoneDriverY.driver.expression = shoulderJointCorrectionBoneDriverExpressionPrefix + originalExpressionX
+ rightShoulderJointCorrectionBoneDriverY.driver.variables[0].targets[0].transform_type = 'ROT_X'
+ rightShoulderJointCorrectionBoneDriverZ.driver.expression = rightShoulderJointCorrectionBoneDriverZ.driver.expression
+ rightShoulderJointCorrectionBoneDriverZ.driver.variables[0].targets[0].transform_type = 'ROT_X'
+
+ '''Apply rigify bone type to all bones'''
+
+ for bone in metarig.pose.bones[:]:
+ bone.rigify_type = "basic.raw_copy"
+
+ def finalizeFingerBoneParameters(rig, fingerBone1Name, fingerBone2Name, fingerBone3Name):
+ metarig.pose.bones[fingerBone1Name].rigify_type = "limbs.super_finger"
+ metarig.pose.bones[fingerBone1Name].rigify_parameters.primary_rotation_axis = "-X"
+ metarig.pose.bones[fingerBone1Name].rigify_parameters.make_extra_ik_control = True
+ metarig.pose.bones[fingerBone1Name].custom_shape = None
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[fingerBone1Name].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[fingerBone1Name].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.fingersLayerName + koikatsuCommons.detailLayerSuffix)] = True
+ metarig.pose.bones[fingerBone2Name].rigify_type = ""
+ metarig.pose.bones[fingerBone2Name].custom_shape = None
+ metarig.pose.bones[fingerBone3Name].rigify_type = ""
+ metarig.pose.bones[fingerBone3Name].custom_shape = None
+
+ metarig.pose.bones[koikatsuCommons.originalRootBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.eyesTrackTargetBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.eyesTrackTargetBoneName].rigify_parameters.optional_widget_type = "pivot_cross"
+ metarig.pose.bones[koikatsuCommons.eyeballsBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.eyeballsBoneName].rigify_parameters.optional_widget_type = "bone"
+ metarig.pose.bones[koikatsuCommons.leftEyeballBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftEyeballBoneName].rigify_parameters.optional_widget_type = "bone"
+ metarig.pose.bones[koikatsuCommons.rightEyeballBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightEyeballBoneName].rigify_parameters.optional_widget_type = "bone"
+ if hasRiggedTongue:
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone1Name].rigify_parameters.optional_widget_type = "jaw"
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone2Name].rigify_type = "limbs.super_finger"
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone2Name].rigify_parameters.primary_rotation_axis = "-X"
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone2Name].rigify_parameters.make_extra_ik_control = True
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone2Name].custom_shape = None
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone2Name].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone2Name].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.eyesLayerName + koikatsuCommons.secondaryLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone3Name].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone3Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.riggedTongueLeftBone3Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.riggedTongueRightBone3Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone4Name].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone4Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.riggedTongueLeftBone4Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.riggedTongueRightBone4Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone5Name].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.riggedTongueBone5Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.riggedTongueLeftBone5Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.riggedTongueRightBone5Name].rigify_parameters.optional_widget_type = "sphere"
+
+ metarig.pose.bones[koikatsuCommons.headBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.headBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.headTrackBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.headTrackBoneName].rigify_parameters.parent_bone = koikatsuCommons.headTweakBoneName
+ metarig.pose.bones[koikatsuCommons.headTrackTargetBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.headTrackTargetBoneName].rigify_parameters.optional_widget_type = "pivot_cross"
+ metarig.pose.bones[koikatsuCommons.neckBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.neckBoneName].rigify_type = "spines.super_head"
+ metarig.pose.bones[koikatsuCommons.neckBoneName].rigify_parameters.connect_chain = True
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[koikatsuCommons.neckBoneName].rigify_parameters.tweak_layers[1] = False
+ debug = int(koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.torsoLayerName + koikatsuCommons.detailLayerSuffix))
+ metarig.pose.bones[koikatsuCommons.neckBoneName].rigify_parameters.tweak_layers[debug] = True
+ metarig.pose.bones[koikatsuCommons.upperChestBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.upperChestBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.chestBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.chestBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.spineBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.spineBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].rigify_type = "spines.basic_spine"
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].rigify_parameters.pivot_pos = 1
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.torsoLayerName + koikatsuCommons.detailLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].rigify_parameters.fk_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.hipsBoneName].rigify_parameters.fk_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.torsoLayerName + koikatsuCommons.detailLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.waistBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.waistBoneName].rigify_parameters.optional_widget_type = "diamond"
+ metarig.pose.bones[koikatsuCommons.crotchBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.crotchBoneName].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.anusBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.anusBoneName].rigify_parameters.optional_widget_type = "sphere"
+ if hasBetterPenetrationMod:
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRootCrotchBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRootCrotchBoneName].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationFrontCrotchBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationFrontCrotchBoneName].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone1Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone1Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone1Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone1Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone2Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone2Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone2Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone2Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone3Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone3Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone3Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone3Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone4Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone4Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone4Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone4Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone5Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationLeftCrotchBone5Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone5Name].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationRightCrotchBone5Name].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.betterPenetrationBackCrotchBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.betterPenetrationBackCrotchBoneName].rigify_parameters.optional_widget_type = "circle"
+ metarig.pose.bones[koikatsuCommons.leftBreastDeformBone1Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.rightBreastDeformBone1Name].rigify_parameters.optional_widget_type = "sphere"
+ widgetFace = bpy.data.objects[koikatsuCommons.widgetFaceName]
+ metarig.pose.bones[koikatsuCommons.leftBreastBone2Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.rightBreastBone2Name].custom_shape = widgetFace
+ def set_layer(bone_name, show_layer):
+ if metarig.data.bones.get(str(bone_name)):
+ if metarig.data.collections.get(str(show_layer)):
+ metarig.data.collections[str(show_layer)].assign(metarig.data.bones.get(bone_name))
+ else:
+ metarig.data.collections.new(str(show_layer))
+ metarig.data.collections[str(show_layer)].assign(metarig.data.bones.get(bone_name))
+ if bpy.app.version[0] == 3:
+ metarig.data.bones[koikatsuCommons.leftBreastBone2Name].layers[koikatsuCommons.originalFkLayerIndex] = True
+ metarig.data.bones[koikatsuCommons.rightBreastBone2Name].layers[koikatsuCommons.originalFkLayerIndex] = True
+ else:
+ set_layer(koikatsuCommons.leftBreastBone2Name, koikatsuCommons.originalFkLayerIndex)
+ set_layer(koikatsuCommons.rightBreastBone2Name, koikatsuCommons.originalFkLayerIndex)
+ metarig.pose.bones[koikatsuCommons.leftBreastDeformBone2Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.rightBreastDeformBone2Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.leftBreastBone3Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.rightBreastBone3Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.leftBreastDeformBone3Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.rightBreastDeformBone3Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.leftNippleBone1Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.rightNippleBone1Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.leftNippleDeformBone1Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.rightNippleDeformBone1Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.leftNippleBone2Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.rightNippleBone2Name].custom_shape = widgetFace
+ metarig.pose.bones[koikatsuCommons.leftNippleDeformBone2Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.rightNippleDeformBone2Name].rigify_parameters.optional_widget_type = "sphere"
+ metarig.pose.bones[koikatsuCommons.leftShoulderBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightShoulderBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftShoulderBoneName].rigify_parameters.optional_widget_type = "shoulder"
+ metarig.pose.bones[koikatsuCommons.rightShoulderBoneName].rigify_parameters.optional_widget_type = "shoulder"
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_type = "limbs.arm"
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_type = "limbs.arm"
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.segments = 3
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.segments = 3
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.rotation_axis = "automatic"
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.rotation_axis = "automatic"
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.limb_uniform_scale = True
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.limb_uniform_scale = True
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.auto_align_extremity = True
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.auto_align_extremity = True
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.make_ik_wrist_pivot = True
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.make_ik_wrist_pivot = True
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.leftArmLayerName + koikatsuCommons.tweakLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.rightArmLayerName + koikatsuCommons.tweakLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.fk_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.fk_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.leftArmBoneName].rigify_parameters.fk_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.leftArmLayerName + koikatsuCommons.fkLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.rightArmBoneName].rigify_parameters.fk_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.rightArmLayerName + koikatsuCommons.fkLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.leftElbowBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightElbowBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftElbowBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightElbowBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.originalLeftElbowPoleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightElbowPoleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalLeftWristBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightWristBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightElbowPoleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftWristBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightWristBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.frontLeftElbowJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.frontRightElbowJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.frontLeftElbowJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.leftElbowDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.frontRightElbowJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.rightElbowDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.midLeftElbowJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.midRightElbowJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.midLeftElbowJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.leftElbowDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.midRightElbowJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.rightElbowDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.leftIndexFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightIndexFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftIndexFingerPalmBoneName].rigify_type = "limbs.super_palm"
+ metarig.pose.bones[koikatsuCommons.rightIndexFingerPalmBoneName].rigify_type = "limbs.super_palm"
+ metarig.pose.bones[koikatsuCommons.leftIndexFingerPalmBoneName].rigify_parameters.palm_both_sides = True
+ metarig.pose.bones[koikatsuCommons.rightIndexFingerPalmBoneName].rigify_parameters.palm_both_sides = True
+ metarig.pose.bones[koikatsuCommons.leftIndexFingerPalmBoneName].rigify_parameters.palm_rotation_axis = 'X'
+ metarig.pose.bones[koikatsuCommons.rightIndexFingerPalmBoneName].rigify_parameters.palm_rotation_axis = 'X'
+ metarig.pose.bones[koikatsuCommons.leftIndexFingerPalmBoneName].rigify_parameters.make_extra_control = True
+ metarig.pose.bones[koikatsuCommons.rightIndexFingerPalmBoneName].rigify_parameters.make_extra_control = True
+ metarig.pose.bones[koikatsuCommons.leftMiddleFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightMiddleFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftMiddleFingerPalmBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightMiddleFingerPalmBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.leftRingFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightRingFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftRingFingerPalmBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightRingFingerPalmBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.leftLittleFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightLittleFingerPalmBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftLittleFingerPalmBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightLittleFingerPalmBoneName].rigify_type = ""
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.leftThumbBone1Name, koikatsuCommons.leftThumbBone2Name, koikatsuCommons.leftThumbBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.rightThumbBone1Name, koikatsuCommons.rightThumbBone2Name, koikatsuCommons.rightThumbBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.leftIndexFingerBone1Name, koikatsuCommons.leftIndexFingerBone2Name, koikatsuCommons.leftIndexFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.rightIndexFingerBone1Name, koikatsuCommons.rightIndexFingerBone2Name, koikatsuCommons.rightIndexFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.leftMiddleFingerBone1Name, koikatsuCommons.leftMiddleFingerBone2Name, koikatsuCommons.leftMiddleFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.rightMiddleFingerBone1Name, koikatsuCommons.rightMiddleFingerBone2Name, koikatsuCommons.rightMiddleFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.leftRingFingerBone1Name, koikatsuCommons.leftRingFingerBone2Name, koikatsuCommons.leftRingFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.rightRingFingerBone1Name, koikatsuCommons.rightRingFingerBone2Name, koikatsuCommons.rightRingFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.leftLittleFingerBone1Name, koikatsuCommons.leftLittleFingerBone2Name, koikatsuCommons.leftLittleFingerBone3Name)
+ finalizeFingerBoneParameters(metarig, koikatsuCommons.rightLittleFingerBone1Name, koikatsuCommons.rightLittleFingerBone2Name, koikatsuCommons.rightLittleFingerBone3Name)
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_type = "limbs.leg"
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_type = "limbs.leg"
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.extra_ik_toe = True
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.extra_ik_toe = True
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.segments = 3
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.segments = 3
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.rotation_axis = "automatic"
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.rotation_axis = "automatic"
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.limb_uniform_scale = True
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.limb_uniform_scale = True
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.leftLegLayerName + koikatsuCommons.tweakLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.rightLegLayerName + koikatsuCommons.tweakLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.fk_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.fk_layers[1] = False
+ metarig.pose.bones[koikatsuCommons.leftLegBoneName].rigify_parameters.fk_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.leftLegLayerName + koikatsuCommons.fkLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.rightLegBoneName].rigify_parameters.fk_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.rightLegLayerName + koikatsuCommons.fkLayerSuffix)] = True
+ metarig.pose.bones[koikatsuCommons.leftKneeBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightKneeBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftKneeBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightKneeBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.originalLeftKneePoleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightKneePoleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalLeftAnkleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightAnkleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftAnkleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightAnkleBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftAnkleBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightAnkleBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.originalLeftToeBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightToeBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftToeBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightToeBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftToeBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightToeBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.originalLeftHeelIkBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightHeelIkBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalLeftHeelBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.originalRightHeelBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftHeelBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.rightHeelBoneName].custom_shape = None
+ metarig.pose.bones[koikatsuCommons.leftHeelBoneName].rigify_type = ""
+ metarig.pose.bones[koikatsuCommons.rightHeelBoneName].rigify_type = ""
+ if bpy.app.version[0] == 3:
+ metarig.data.bones[koikatsuCommons.leftHeelBoneName].layers[koikatsuCommons.originalFkLayerIndex] = True
+ metarig.data.bones[koikatsuCommons.rightHeelBoneName].layers[koikatsuCommons.originalFkLayerIndex] = True
+ else:
+ set_layer(koikatsuCommons.leftHeelBoneName, koikatsuCommons.originalFkLayerIndex)
+ set_layer(koikatsuCommons.rightHeelBoneName, koikatsuCommons.originalFkLayerIndex)
+ metarig.pose.bones[koikatsuCommons.frontLeftKneeJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.frontRightKneeJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.frontLeftKneeJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.leftLegDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.frontRightKneeJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.rightLegDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.midLeftKneeJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.midRightKneeJointCorrectionBoneName].rigify_parameters.relink_constraints = True
+ metarig.pose.bones[koikatsuCommons.midLeftKneeJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.leftLegDeformBone1Name
+ metarig.pose.bones[koikatsuCommons.midRightKneeJointCorrectionBoneName].rigify_parameters.parent_bone = koikatsuCommons.rightLegDeformBone1Name
+
+ if isMale:
+ koikatsuCommons.torsoLayerBoneNames.remove(koikatsuCommons.breastsHandleBoneName)
+ koikatsuCommons.torsoLayerBoneNames.remove(koikatsuCommons.leftBreastHandleBoneName)
+ koikatsuCommons.torsoLayerBoneNames.remove(koikatsuCommons.rightBreastHandleBoneName)
+
+ if not hasRiggedTongue:
+ try:
+ koikatsuCommons.eyesPrimaryLayerBoneNames.remove(koikatsuCommons.riggedTongueBone1Name)
+ koikatsuCommons.eyesPrimaryLayerBoneNames.remove(koikatsuCommons.riggedTongueBone2Name)
+ koikatsuCommons.eyesPrimaryLayerBoneNames.remove(koikatsuCommons.riggedTongueBone3Name)
+ koikatsuCommons.eyesPrimaryLayerBoneNames.remove(koikatsuCommons.riggedTongueBone4Name)
+ koikatsuCommons.eyesPrimaryLayerBoneNames.remove(koikatsuCommons.riggedTongueBone5Name)
+ koikatsuCommons.eyesSecondaryLayerBoneNames.remove(koikatsuCommons.riggedTongueLeftBone3Name)
+ koikatsuCommons.eyesSecondaryLayerBoneNames.remove(koikatsuCommons.riggedTongueRightBone3Name)
+ koikatsuCommons.eyesSecondaryLayerBoneNames.remove(koikatsuCommons.riggedTongueLeftBone4Name)
+ koikatsuCommons.eyesSecondaryLayerBoneNames.remove(koikatsuCommons.riggedTongueRightBone4Name)
+ koikatsuCommons.eyesSecondaryLayerBoneNames.remove(koikatsuCommons.riggedTongueLeftBone5Name)
+ koikatsuCommons.eyesSecondaryLayerBoneNames.remove(koikatsuCommons.riggedTongueRightBone5Name)
+ except:
+ pass
+
+ if not hasBetterPenetrationMod:
+ try:
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationRootCrotchBoneName)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationFrontCrotchBoneName)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationLeftCrotchBone1Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationRightCrotchBone1Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationLeftCrotchBone2Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationRightCrotchBone2Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationLeftCrotchBone3Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationRightCrotchBone3Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationLeftCrotchBone4Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationRightCrotchBone4Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationLeftCrotchBone5Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationRightCrotchBone5Name)
+ koikatsuCommons.torsoTweakLayerBoneNames.remove(koikatsuCommons.betterPenetrationBackCrotchBoneName)
+ except:
+ pass
+
+ ctrlBoneNames = []
+ ctrlBoneNames.extend(koikatsuCommons.eyesPrimaryLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.eyesSecondaryLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.torsoLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.torsoTweakLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.leftArmIkLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.rightArmIkLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.fingersLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.leftLegIkLayerBoneNames)
+ ctrlBoneNames.extend(koikatsuCommons.rightLegIkLayerBoneNames)
+
+ for bone in metarig.data.bones:
+ if bpy.app.version[0] == 3:
+ if bone.layers[koikatsuCommons.originalUpperFaceLayerIndex] == True or bone.layers[koikatsuCommons.originalLowerFaceLayerIndex] == True:
+ if bone.name == koikatsuCommons.originalRootUpperBoneName:
+ continue
+ if bone.name not in ctrlBoneNames:
+ ctrlBoneNames.append(bone.name)
+ koikatsuCommons.assignSingleBoneLayer(metarig, bone.name, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.faceLayerName))
+ else:
+ if bone.collections.get(str(koikatsuCommons.originalUpperFaceLayerIndex)) or bone.collections.get(str(koikatsuCommons.originalLowerFaceLayerIndex)):
+ if bone.name == koikatsuCommons.originalRootUpperBoneName:
+ continue
+ if bone.name not in ctrlBoneNames:
+ ctrlBoneNames.append(bone.name)
+ koikatsuCommons.assignSingleBoneLayer(metarig, bone.name, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.faceLayerName))
+
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.eyesPrimaryLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.eyesLayerName + koikatsuCommons.primaryLayerSuffix))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.eyesSecondaryLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.eyesLayerName + koikatsuCommons.secondaryLayerSuffix))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.torsoLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.torsoLayerName))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.torsoTweakLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.torsoLayerName + koikatsuCommons.detailLayerSuffix))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.leftArmIkLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.leftArmLayerName + koikatsuCommons.ikLayerSuffix))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.rightArmIkLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.rightArmLayerName + koikatsuCommons.ikLayerSuffix))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.fingersLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.fingersLayerName))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.leftLegIkLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.leftLegLayerName + koikatsuCommons.ikLayerSuffix))
+ koikatsuCommons.assignSingleBoneLayerToList(metarig, koikatsuCommons.rightLegIkLayerBoneNames, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.rightLegLayerName + koikatsuCommons.ikLayerSuffix))
+
+ usefulBoneNames = [metarigIdBoneName, koikatsuCommons.eyesXBoneName, koikatsuCommons.eyesBoneName, koikatsuCommons.leftEyeBoneName, koikatsuCommons.rightEyeBoneName, koikatsuCommons.eyesTrackTargetParentBoneName,
+ koikatsuCommons.eyesHandleMarkerBoneName, koikatsuCommons.leftEyeHandleMarkerBoneName, koikatsuCommons.rightEyeHandleMarkerBoneName, koikatsuCommons.leftEyeHandleMarkerXBoneName, koikatsuCommons.rightEyeHandleMarkerXBoneName,
+ koikatsuCommons.leftEyeHandleMarkerZBoneName, koikatsuCommons.rightEyeHandleMarkerZBoneName, koikatsuCommons.eyeballsTrackBoneName, koikatsuCommons.leftEyeballTrackBoneName, koikatsuCommons.rightEyeballTrackBoneName,
+ koikatsuCommons.leftEyeballTrackCorrectionBoneName, koikatsuCommons.rightEyeballTrackCorrectionBoneName, koikatsuCommons.leftHeadMarkerXBoneName, koikatsuCommons.rightHeadMarkerXBoneName, koikatsuCommons.leftHeadMarkerZBoneName,
+ koikatsuCommons.rightHeadMarkerZBoneName, koikatsuCommons.headTrackBoneName, koikatsuCommons.headTrackTargetParentBoneName]
+ usefulBoneNames.extend(ctrlBoneNames)
+
+ defBoneNames = koikatsuCommons.getDeformBoneNames(metarig)
+
+ for name in defBoneNames:
+ if name not in usefulBoneNames:
+ usefulBoneNames.append(name)
+
+ '''Setup rigify type and layers for all skirt bones'''
+
+ if hasSkirt:
+ for primaryIndex in range(8):
+ skirtPalmBoneName = koikatsuCommons.getSkirtBoneName(True, primaryIndex)
+ if skirtPalmBoneName not in ctrlBoneNames:
+ ctrlBoneNames.append(skirtPalmBoneName)
+ if skirtPalmBoneName not in usefulBoneNames:
+ usefulBoneNames.append(skirtPalmBoneName)
+ metarig.pose.bones[skirtPalmBoneName].custom_shape = None
+ koikatsuCommons.assignSingleBoneLayer(metarig, skirtPalmBoneName, 23)
+ if primaryIndex == 2:
+ metarig.pose.bones[skirtPalmBoneName].rigify_type = "limbs.super_palm"
+ metarig.pose.bones[skirtPalmBoneName].rigify_parameters.palm_both_sides = True
+ metarig.pose.bones[skirtPalmBoneName].rigify_parameters.palm_rotation_axis = 'X'
+ metarig.pose.bones[skirtPalmBoneName].rigify_parameters.make_extra_control = True
+ else:
+ metarig.pose.bones[skirtPalmBoneName].rigify_type = ""
+ for secondaryIndex in range(6):
+ skirtBoneName = koikatsuCommons.getSkirtBoneName(False, primaryIndex, secondaryIndex)
+ if skirtBoneName not in ctrlBoneNames:
+ ctrlBoneNames.append(skirtBoneName)
+ if skirtBoneName not in usefulBoneNames:
+ usefulBoneNames.append(skirtBoneName)
+ metarig.pose.bones[skirtBoneName].custom_shape = None
+ koikatsuCommons.assignSingleBoneLayer(metarig, skirtBoneName, 23 if (('master' in skirtBoneName) or ('_ik' in skirtBoneName)) else 24)
+ if secondaryIndex == 0:
+ metarig.pose.bones[skirtBoneName].rigify_type = "limbs.super_finger"
+ metarig.pose.bones[skirtBoneName].rigify_parameters.make_extra_ik_control = True
+ if bpy.app.version[0] == 3:
+ metarig.pose.bones[skirtBoneName].rigify_parameters.tweak_layers[1] = False
+ metarig.pose.bones[skirtBoneName].rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.skirtLayerName + koikatsuCommons.detailLayerSuffix)] = True
+ else:
+ metarig.pose.bones[skirtBoneName].rigify_type = ""
+
+ for boneName in accessoryBoneNames:
+ if boneName not in usefulBoneNames:
+ usefulBoneNames.append(boneName)
+ if boneName == koikatsuCommons.originalRootUpperBoneName:
+ continue
+ if boneName not in ctrlBoneNames:
+ ctrlBoneNames.append(boneName)
+ bone = metarig.pose.bones[boneName]
+ bone.custom_shape = None
+ koikatsuCommons.assignSingleBoneLayer(metarig, boneName, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.hairLayerName))
+ if boneName in accessoryBoneConnectedParentNames:
+ bone.rigify_type = "limbs.super_finger"
+ bone.rigify_parameters.make_extra_ik_control = True
+ koikatsuCommons.assignSingleBoneLayer(metarig, boneName, 1)
+ if bpy.app.version[0] == 3:
+ bone.rigify_parameters.tweak_layers[1] = False
+ bone.rigify_parameters.tweak_layers[koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.hairLayerName + koikatsuCommons.detailLayerSuffix)] = True
+ elif boneName in accessoryBoneConnectedChildNames:
+ bone.rigify_type = ""
+ else:
+ bone.rigify_type = "basic.super_copy"
+ bone.rigify_parameters.make_control = True
+ bone.rigify_parameters.make_widget = True
+ bone.rigify_parameters.super_copy_widget_type = "limb"
+ bone.rigify_parameters.make_deform = True
+
+ accessoryMchPalmBoneNames = []
+ for boneName in accessoryMchBoneNames:
+ #metarig.data.bones[boneName].layers[koikatsuCommons.temporaryAccessoryMchLayerIndex] = True
+ if boneName not in ctrlBoneNames:
+ ctrlBoneNames.append(boneName)
+ if boneName not in usefulBoneNames:
+ usefulBoneNames.append(boneName)
+ bone = metarig.pose.bones[boneName]
+ bone.custom_shape = None
+ koikatsuCommons.assignSingleBoneLayer(metarig, boneName, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.hairLayerName + koikatsuCommons.mchLayerSuffix))
+ if boneName in accessoryMchPalmBoneNames:
+ continue
+ if boneName in accessoryMchConnectedParentNames:
+ accessoryMchPalmBones = []
+ for connectedParentBoneName in accessoryMchConnectedParentNames:
+ if boneName == connectedParentBoneName:
+ continue
+ connectedParentBone = metarig.pose.bones[connectedParentBoneName]
+ if bone.parent == connectedParentBone.parent:
+ accessoryMchPalmBoneNames.append(connectedParentBone.name)
+ accessoryMchPalmBones.append(connectedParentBone)
+ connectedParentBone.rigify_type = ""
+ if accessoryMchPalmBones:
+ accessoryMchPalmBoneNames.append(bone.name)
+ accessoryMchPalmBones.append(bone)
+ bone.rigify_type = ""
+ maxHeadDistance = 0
+ for palmBone1 in accessoryMchPalmBones:
+ for palmBone2 in accessoryMchPalmBones:
+ headDistance = (palmBone1.head - palmBone2.head).length
+ if headDistance > maxHeadDistance:
+ bone = palmBone1
+ maxHeadDistance = headDistance
+ bone.rigify_type = "limbs.super_palm"
+ bone.rigify_parameters.palm_both_sides = True
+ bone.rigify_parameters.make_extra_control = True
+ continue
+ bone.rigify_parameters.optional_widget_type = "limb"
+ else:
+ bone.rigify_parameters.optional_widget_type = "limb"
+
+ for boneName in faceMchBoneNames:
+ #metarig.data.bones[boneName].layers[koikatsuCommons.temporaryFaceMchLayerIndex] = True
+ #metarig.data.bones[boneName].layers[koikatsuCommons.originalMchLayerIndex] = False
+ if boneName not in ctrlBoneNames:
+ ctrlBoneNames.append(boneName)
+ if boneName not in usefulBoneNames:
+ usefulBoneNames.append(boneName)
+ bone = metarig.pose.bones[boneName]
+ bone.custom_shape = None
+ bone.rigify_parameters.optional_widget_type = "pivot"
+ koikatsuCommons.assignSingleBoneLayer(metarig, boneName, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.faceLayerName + koikatsuCommons.mchLayerSuffix))
+
+ for boneName in usefulBoneNames:
+ if boneName in defBoneNames:
+ if boneName in ctrlBoneNames:
+ if bpy.app.version[0] == 3:
+ metarig.data.bones[boneName].layers[koikatsuCommons.defLayerIndex] = True
+ else:
+ set_layer(boneName, koikatsuCommons.defLayerIndex)
+ else:
+ koikatsuCommons.assignSingleBoneLayer(metarig, boneName, koikatsuCommons.defLayerIndex)
+ koikatsuCommons.lockAllPoseTransforms(metarig, boneName)
+ else:
+ if boneName not in ctrlBoneNames:
+ koikatsuCommons.assignSingleBoneLayer(metarig, boneName, koikatsuCommons.mchLayerIndex)
+ koikatsuCommons.lockAllPoseTransforms(metarig, boneName)
+ relatedBoneNames = koikatsuCommons.getRelatedBoneNames(metarig, boneName)
+ for relatedBoneName in relatedBoneNames:
+ if relatedBoneName not in usefulBoneNames:
+ usefulBoneNames.append(relatedBoneName)
+
+ for bone in metarig.pose.bones:
+ if bpy.app.version[0] == 3:
+ try:
+ bone['mmd_bone'] = None
+ except:
+ #oh well
+ pass
+ if bone.name not in usefulBoneNames:
+ koikatsuCommons.assignSingleBoneLayer(metarig, bone.name, koikatsuCommons.getRigifyLayerIndexByName(koikatsuCommons.junkLayerName))
+ continue
+ if bone.rigify_type == "basic.raw_copy" or bone.rigify_type == "basic.super_copy":
+ for targetToChange in koikatsuCommons.targetsToChange:
+ if bone.parent == targetToChange:
+ bone.rigify_parameters.relink_constraints = True
+ bone.rigify_parameters.parent_bone = koikatsuCommons.deformBonePrefix + targetToChange
+ for constraint in bone.constraints:
+ for targetToChange in koikatsuCommons.targetsToChange:
+ try:
+ if constraint.subtarget == targetToChange:
+ bone.rigify_parameters.relink_constraints = True
+ try:
+ index = constraint.name.index("@")
+ constraint.name = constraint.name[:index] + "@" + koikatsuCommons.deformBonePrefix + targetToChange
+ except ValueError as ex:
+ constraint.name = constraint.name + "@" + koikatsuCommons.deformBonePrefix + targetToChange
+ except AttributeError as ex:
+ pass
+
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ for bone in metarig.data.edit_bones:
+ if bone.name in defBoneNames:
+ bone.use_deform = True
+ else:
+ bone.use_deform = False
+ if bone.name not in usefulBoneNames and bone.parent and bone.parent.name in usefulBoneNames:
+ bone.use_connect = False #may otherwise cause Rigify generation failure if part of a finger chain
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ for i in range(32):
+ index = 31 - i
+ if bpy.app.version[0] == 3:
+ if index in selectedLayers:
+ metarig.data.layers[index] = True
+ else:
+ metarig.data.layers[index] = False
+ else:
+ if index in selectedLayers:
+ if metarig.data.collections.get(str(index)):
+ metarig.data.collections[str(index)].is_visible = True
+ else:
+ metarig.data.collections.new(str(index))
+ metarig.data.collections[str(index)].is_visible = True
+ else:
+ if metarig.data.collections.get(str(index)):
+ metarig.data.collections[str(index)].is_visible = False
+ else:
+ metarig.data.collections.new(str(index))
+ metarig.data.collections[str(index)].is_visible = False
+
+ if bpy.app.version[0] != 3:
+ #clean up missing rigify layers because I edited the script
+ def get_rigify_index(collection_name):
+ return metarig.data.collections_all.get(str(collection_name)).index
+
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(1), row=2) #move hair detail to same layer
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(2), row=2) #move hair mch to same layer
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(3), row=3) #move eyes primary
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(4), row=3) #move eyes secondary
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(5), row=4) #move face
+ metarig.data.collections_all['5'].rigify_ui_title_name = 'Face'
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(6), row=5) #move face mch
+ metarig.data.collections_all['6'].rigify_ui_title_name = 'Face (MCH)'
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(7), row=6) #move torso
+ metarig.data.collections_all['7'].rigify_ui_title_name = 'Torso'
+ try:
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index('None'), row=7) #move torso detail
+ metarig.data.collections_all['None'].rigify_ui_title_name = 'Torso (Detail)'
+ except:
+ pass
+ bpy.ops.armature.rigify_collection_set_ui_row(index = get_rigify_index(8), row=7) #move torso tweak
+ metarig.data.collections_all['8'].rigify_ui_title_name = 'Torso (Tweak)'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(9), row=8) #move arm L IK
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(12), row=8) #move arm R IK
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(10), row=9) #arm L fk
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(13), row=9) #arm R FK
+ metarig.data.collections_all['13'].rigify_ui_title_name = 'Arm.R FK'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(11), row=10) #arm L tweak
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(14), row=10) #arm R tweak
+ metarig.data.collections_all['14'].rigify_ui_title_name = 'Arm.R (Tweak)'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(15), row=11)
+ metarig.data.collections_all['15'].rigify_ui_title_name = 'Fingers'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(16), row=12) #mvoe fingers detail
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(17), row=13) #move leg L IK
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(20), row=13) #move leg R IK
+ metarig.data.collections_all['20'].rigify_ui_title_name = 'Leg.R IK'
+ metarig.data.collections_all['21'].rigify_ui_title_name = 'Leg.R FK'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(18), row=14) #move leg L FK
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(21), row=14) #move leg R FK
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(19), row=15) #move leg L tweak
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(22), row=15) #move leg R tweak
+ metarig.data.collections_all['22'].rigify_ui_title_name = 'Leg.R (Tweak)'
+ metarig.data.collections_all['19'].rigify_ui_title_name = 'Leg.L (Tweak)'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(23), row=16) #move skirt
+ metarig.data.collections_all['23'].rigify_ui_title_name = 'Skirt'
+ bpy.ops.armature.rigify_collection_set_ui_row(index= get_rigify_index(24), row=17) #move skirt detail
+ metarig.data.collections_all['24'].rigify_ui_title_name = 'Skirt (Detail)'
+ metarig.data.collections_all['27'].rigify_ui_title_name = 'Junk'
+
+ #there is some kind of issue with the L/R positions of some groups.
+ #this can be fixed by changing the order of the bone collections
+ #move the arm L IK up ten times
+ metarig.data.collections.active_index = get_rigify_index(9)
+ for i in range(10):
+ bpy.ops.armature.collection_move(direction='UP')
+ #move the leg R tweak down ten times
+ metarig.data.collections.active_index = get_rigify_index(22)
+ for i in range(10):
+ bpy.ops.armature.collection_move(direction='DOWN')
+
+ #set new checkbox for the toe lock
+ # metarig.pose.bones['Left leg'].rigify_parameters.extra_toe_roll = True
+ # metarig.pose.bones['Right leg'].rigify_parameters.extra_toe_roll = True
+
+ #add missing color groups
+ bpy.context.object.data.collections_all["5"].rigify_color_set_name = "Tweak"
+ bpy.context.object.data.collections_all["6"].rigify_color_set_name = "Root"
+ bpy.context.object.data.collections_all["7"].rigify_color_set_name = "Special"
+ bpy.context.object.data.collections_all["8"].rigify_color_set_name = "Tweak"
+ bpy.context.object.data.collections_all["13"].rigify_color_set_name = "FK"
+ bpy.context.object.data.collections_all["14"].rigify_color_set_name = "Tweak"
+ bpy.context.object.data.collections_all["15"].rigify_color_set_name = "Extra"
+ bpy.context.object.data.collections_all["19"].rigify_color_set_name = "Tweak"
+ bpy.context.object.data.collections_all["20"].rigify_color_set_name = "IK"
+ bpy.context.object.data.collections_all["21"].rigify_color_set_name = "FK"
+ bpy.context.object.data.collections_all["22"].rigify_color_set_name = "Tweak"
+ bpy.context.object.data.collections_all["23"].rigify_color_set_name = "Extra"
+ bpy.context.object.data.collections_all["24"].rigify_color_set_name = "FK"
+ try:
+ bpy.context.object.data.collections_all["None"].rigify_color_set_name = "FK"
+ except:
+ pass
+
+ #bpy.ops.bone_layer_man.get_rigify_layers()
+ #koikatsuCommons.setBoneManagerLayersFromRigifyLayers(metarig)
+
+class rigify_before(bpy.types.Operator):
+ bl_idname = "kkbp.rigbefore"
+ bl_label = "Before First Rigify Generate - Public"
+ bl_description = 'Converts the KKBP armature to a Rigify metarig'
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ main()
+ return {'FINISHED'}
+
diff --git a/extras/rigifywrapper.py b/extras/rigifywrapper.py
new file mode 100644
index 0000000..f54fa35
--- /dev/null
+++ b/extras/rigifywrapper.py
@@ -0,0 +1,27 @@
+'''
+Wrapper for the rigifyscripts folder
+'''
+import bpy, traceback
+from .. import common as c
+from ..importing.postoperations import post_operations
+from ..interface.dictionary_en import t
+
+class rigify_convert(bpy.types.Operator):
+ bl_idname = "kkbp.rigifyconvert"
+ bl_label = "Convert for rigify"
+ bl_description = t('rigify_convert_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ try:
+ bpy.context.scene.kkbp.armature_dropdown = 'B'
+ post_operations.retreive_stored_tags()
+ post_operations.apply_rigify()
+ return {'FINISHED'}
+ except:
+ c.kklog('Unknown python error occurred', type = 'error')
+ c.kklog(traceback.format_exc())
+ self.report({'ERROR'}, traceback.format_exc())
+ return {"CANCELLED"}
+
+
diff --git a/extras/updatebones.py b/extras/updatebones.py
new file mode 100644
index 0000000..55024e3
--- /dev/null
+++ b/extras/updatebones.py
@@ -0,0 +1,35 @@
+import bpy
+from ..interface.dictionary_en import t
+from .. import common as c
+
+class update_bones(bpy.types.Operator):
+ bl_idname = "kkbp.updatebones"
+ bl_label = "Update bones"
+ bl_description = t('bone_visibility_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+
+ arm = c.get_rig() if c.get_rig() else c.get_armature()
+ arm = bpy.data.armatures[arm.data.name]
+
+ #check if the outfit linked to accessory bones on the armature is visible or not, then update the bone visibility
+ for bone in arm.bones:
+ if bone.get('id'):
+ hide_this_bone = True
+ for outfit_number in bone['id']:
+ matching_outfit = bpy.data.objects.get('Outfit ' + outfit_number + ' ' + c.get_name())
+ if matching_outfit:
+ print(matching_outfit.users_collection[0].name)
+ #also check if the collection this outfit belongs to is enabled in the viewlayer
+ outfit_collection = c.get_layer_collection_state(matching_outfit.users_collection[0].name)
+ if not outfit_collection and not matching_outfit.hide_get():
+ print("Enabling bone {} for outfit {}".format(bone.name, bone['id']))
+ hide_this_bone = False
+ break
+ bone.hide = hide_this_bone
+
+ return {'FINISHED'}
+
+if __name__ == "__main__":
+ bpy.utils.register_class(update_bones)
\ No newline at end of file
diff --git a/importing/Lut_TimeDay.png b/importing/Lut_TimeDay.png
new file mode 100644
index 0000000..a44ac06
Binary files /dev/null and b/importing/Lut_TimeDay.png differ
diff --git a/importing/__pycache__/importbuttons.cpython-311.pyc b/importing/__pycache__/importbuttons.cpython-311.pyc
new file mode 100644
index 0000000..84671cc
Binary files /dev/null and b/importing/__pycache__/importbuttons.cpython-311.pyc differ
diff --git a/importing/__pycache__/modifyarmature.cpython-311.pyc b/importing/__pycache__/modifyarmature.cpython-311.pyc
new file mode 100644
index 0000000..47bc4c7
Binary files /dev/null and b/importing/__pycache__/modifyarmature.cpython-311.pyc differ
diff --git a/importing/__pycache__/modifymaterial.cpython-311.pyc b/importing/__pycache__/modifymaterial.cpython-311.pyc
new file mode 100644
index 0000000..58c0829
Binary files /dev/null and b/importing/__pycache__/modifymaterial.cpython-311.pyc differ
diff --git a/importing/__pycache__/modifymesh.cpython-311.pyc b/importing/__pycache__/modifymesh.cpython-311.pyc
new file mode 100644
index 0000000..c5f489c
Binary files /dev/null and b/importing/__pycache__/modifymesh.cpython-311.pyc differ
diff --git a/importing/__pycache__/postoperations.cpython-311.pyc b/importing/__pycache__/postoperations.cpython-311.pyc
new file mode 100644
index 0000000..b21060c
Binary files /dev/null and b/importing/__pycache__/postoperations.cpython-311.pyc differ
diff --git a/importing/importbuttons.py b/importing/importbuttons.py
new file mode 100644
index 0000000..15fbca1
--- /dev/null
+++ b/importing/importbuttons.py
@@ -0,0 +1,165 @@
+'''
+This file performs the following operations
+· Delete default scene
+· Set view transform to Standard
+· Create KK log in the scripting tab
+· Save the import folder path and character name for later
+· Import all pmx files from folder path, then tag them for later
+. Invokes the other import operations based on what options were chosen on the panel
+'''
+
+import bpy, os, datetime
+
+from ..interface.dictionary_en import t
+from .. import common as c
+
+class kkbp_import(bpy.types.Operator):
+ bl_idname = "kkbp.kkbpimport"
+ bl_label = "Import .pmx file"
+ bl_description = t('kkbp_import_tt')
+ bl_options = {'REGISTER', 'UNDO'}
+
+ filepath : bpy.props.StringProperty(maxlen=1024, default='', options={'HIDDEN'})
+ filter_glob : bpy.props.StringProperty(default='*.pmx', options={'HIDDEN'})
+
+ def execute(self, context):
+ #do this thing because cats does it
+ if hasattr(bpy.context.scene, 'layers'):
+ bpy.context.scene.layers[0] = True
+
+ #delete the default scene if present
+ if len(bpy.data.objects) == 3:
+ for obj in ['Camera', 'Light', 'Cube']:
+ if bpy.data.objects.get(obj):
+ bpy.data.objects.remove(bpy.data.objects[obj])
+ #if the default scene was not present, make sure the default collection is at least there
+ if not bpy.data.collections.get('Collection'):
+ new_col = bpy.data.collections.new('Collection')
+ bpy.context.scene.collection.children.link(new_col)
+ bpy.context.scene.view_layers[0].active_layer_collection = bpy.context.view_layer.layer_collection.children[new_col.name]
+
+ #Set the view transform
+ bpy.data.scenes[0].display_settings.display_device = 'sRGB'
+ bpy.context.scene.view_settings.view_transform = 'Standard'
+ bpy.data.scenes[0].view_settings.look = 'None'
+
+ #save filepath for later
+ bpy.context.scene.kkbp.import_dir = str(self.filepath)[:-9] if self.filepath else bpy.context.scene.kkbp.import_dir
+
+ #delete the cached files if the option is enabled
+ if bpy.context.scene.kkbp.delete_cache and c.get_import_path():
+ c.kklog('Clearing the cache folder...')
+ for cache_folder in ['atlas_files', 'baked_files', 'dark_files', 'saturated_files']:
+ try:
+ for f in os.listdir(os.path.join(c.get_import_path(), cache_folder)):
+ try:
+ os.remove(os.path.join(c.get_import_path(), cache_folder, f))
+ except:
+ pass
+ except:
+ #that cache folder did not exist
+ pass
+
+ #check if there is at least one "Outfit ##" folder inside of this directory
+ # if there isn't, then the user incorrectly chose the .pmx file inside of the outfit directory
+ # correct to the .pmx file inside of the root directory
+ subdirs = [i[1] for i in os.walk(c.get_import_path())][0]
+ outfit_subdirs = [i for i in subdirs if 'Outfit ' in i]
+ if not outfit_subdirs:
+ bpy.context.scene.kkbp.import_dir = os.path.dirname(os.path.dirname(c.get_import_path()))
+ c.kklog('User chose wrong pmx file. Defaulting to pmx file located at ' + str(c.get_import_path()), 'warn')
+
+ try:
+ #get the character name and use it for some things later on
+ bpy.context.scene.kkbp.character_name = c.get_import_path().replace(os.path.dirname(os.path.dirname(c.get_import_path())), '').split('_', maxsplit = 1)[1][:-1]
+ except:
+ #the user renamed the export folder, so there was no underscore. Just use the folder name instead (is the name not saved to the json files?)
+ bpy.context.scene.kkbp.character_name = c.get_import_path().replace(os.path.dirname(os.path.dirname(c.get_import_path())), '')[1:-1]
+ #remove any dots from the character name or blender will get confused when creating an atlas
+ bpy.context.scene.kkbp.character_name = bpy.context.scene.kkbp.character_name.replace('.', '')
+ #but if the name is longer than 64 characters, blender will cut off the name, leading to some issues later on.
+ #The longest material I've encountered is "KK acs_M_nose_tama_00 1290 " and the longest suffix will always be " light.png" at 37 total characters
+ #so I'll arbitrarily cut off the name at 24 characters to be safe (needs to be an even number). The .encode() is used to handle multibyte characters like japanese / korean names
+ if len(bpy.context.scene.kkbp.character_name.encode()) >= 24:
+ bpy.context.scene.kkbp.character_name = bpy.context.scene.kkbp.character_name.encode()[:24].decode()
+
+ c.json_file_manager.init()
+
+ #force pmx armature selection if exportCurrentPose in the Exporter Config json is true
+ force_current_pose = c.json_file_manager.get_json_file('KK_KKBPExporterConfig.json')['exportCurrentPose']
+ if force_current_pose:
+ bpy.context.scene.kkbp.armature_dropdown = 'C'
+
+ #force no dark colors if Cycles classic is chosen as the shader (this mode does not use dark colors at all)
+ if bpy.context.scene.kkbp.shader_dropdown == 'D':
+ bpy.context.scene.kkbp.colors_dropdown = False
+
+ functions = [
+ lambda:bpy.ops.kkbp.modifymesh('INVOKE_DEFAULT'),
+ lambda:bpy.ops.kkbp.modifyarmature('INVOKE_DEFAULT'),
+ lambda:bpy.ops.kkbp.modifymaterial('INVOKE_DEFAULT'),
+ lambda:bpy.ops.kkbp.postoperations('INVOKE_DEFAULT'),
+ ]
+
+
+ #run functions
+ c.toggle_console()
+ self.import_pmx_models()
+ for index, function in enumerate(functions):
+ print('Import function {} running'.format(index))
+ function()
+ c.toggle_console()
+ bpy.context.scene.kkbp.plugin_state = 'imported'
+ c.kklog('KKBP import finished in {} minutes'.format(round(((datetime.datetime.now().minute * 60 + datetime.datetime.now().second + datetime.datetime.now().microsecond / 1e6) - bpy.context.scene.kkbp.total_timer) / 60, 2)))
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ context.window_manager.fileselect_add(self)
+ return {'RUNNING_MODAL'}
+
+ def import_pmx_models(self):
+ c.kklog('Importing pmx files with mmdtools...')
+
+ for subdir, dirs, files in os.walk(c.get_import_path()):
+ for file in [f for f in files if f == 'model.pmx']:
+ pmx_path = os.path.join(subdir, file)
+ outfit = 'Outfit' in subdir
+
+ #import the pmx file with mmd_tools
+ if bpy.app.version[0] == 3:
+ bpy.ops.mmd_tools.import_model('EXEC_DEFAULT',
+ files=[{'name': pmx_path}],
+ directory=pmx_path,
+ scale=1,
+ clean_model = False,
+ types={'MESH', 'ARMATURE', 'MORPHS'} if not outfit else {'MESH'},
+ log_level='WARNING')
+ else:
+ bpy.ops.mmd_tools.import_model('EXEC_DEFAULT',
+ filepath=pmx_path,
+ scale=1,
+ clean_model = False,
+ types={'MESH', 'ARMATURE', 'MORPHS'} if not outfit else {'MESH', 'ARMATURE'})
+
+ #tag the newly import object after pmx import. The active object is the empty, so apply it to the armature and the mesh
+ bpy.context.view_layer.objects.active['name'] = c.get_name()
+ bpy.context.view_layer.objects.active.children[0]['name'] = c.get_name()
+ bpy.context.view_layer.objects.active.children[0].children[0]['name'] = c.get_name()
+ #keep track of the outfit ID if this is an outfit
+ if outfit:
+ bpy.context.view_layer.objects.active.children[0].children[0]['id'] = str(subdir[-2:])
+ bpy.context.view_layer.objects.active.children[0].children[0]['outfit'] = True
+ bpy.context.view_layer.objects.active.children[0].children[0].name = 'Outfit ' + str(subdir[-2:]) + ' ' + c.get_name()
+ else:
+ bpy.context.view_layer.objects.active.children[0].children[0].name = 'Body ' + c.get_name()
+ bpy.context.view_layer.objects.active.children[0].children[0]['body'] = True
+ bpy.context.view_layer.objects.active.children[0].name = 'Armature ' + c.get_name()
+ bpy.context.view_layer.objects.active.children[0]['armature'] = True
+ #get rid of the text files the mmd tools addon generates
+ if bpy.data.texts.get('Model'):
+ bpy.data.texts.remove(bpy.data.texts['Model'])
+ bpy.data.texts.remove(bpy.data.texts['Model_e'])
+ #rename the collection to the character name
+ bpy.data.collections['Collection'].name = c.get_name()
+ c.initialize_timer()
+ c.print_timer('Import PMX')
diff --git a/importing/modifyarmature.py b/importing/modifyarmature.py
new file mode 100644
index 0000000..1275188
--- /dev/null
+++ b/importing/modifyarmature.py
@@ -0,0 +1,2280 @@
+
+# This file performs the following operations
+
+# Removes empties and parents all objects to the Body armature
+# Removes mmd bone constraints and bone drivers, unlocks all bones
+# Scales armature bones down by a factor of 12
+# Reparents the leg03 bone and p_cf_body_bone to match koikatsu armature
+# Deletes all bones not parented to the cf_n_height bone
+
+# Move and rotate finger bones to match koikatsu in game armature
+# Set bone roll data to match koikatsu in game armature
+# Slightly bend some bones outward to better support IKs
+
+# Delete empty vertex groups on the body as long as it's not a bone on the armature
+# Places each bone type (core bones, skirt bones, cf_s_ bones, etc) onto different armature layers
+# Identifies accessory bones and moves them to their own armature layer
+# Also sets the outfit ID for each accessory bone (not all accessory bones need to be visible at all times,
+# so only the current outfit bones will be shown at the end)
+
+# (KKBP armature) Visually connects all toe bones
+# (KKBP armature) Scales all skirt / face / eye / BP bones, connects all skirt bones
+
+# (KKBP Armature) Creates a reference bone for the eye uv warp modifier (Eyesx)
+# (KKBP armature) Creates an eye controller bone for the eye uv warp modifier (Eye Controller)
+# (KKBP armature) shortens kokan bone
+# (KKBP armature) resizes skirt and face bones
+
+# (KKBP armature) Repurposes pv bones for IK functionality
+# (KKBP armature) Creates a foot IK, hand IK, heel controller
+# (KKBP armature) Setup bone drivers for correction bones
+# (KKBP armature) Moves new bones for IK / eyes to correct armature layers
+
+# (KKBP armature) Adds bones to bone groups to give them colors
+# (KKBP armature) Renames some core bones to be more readable
+# Set mmd bone names for each bone
+
+# (KKBP armature) Load custom bone widgets from the KK Shader file to apply to the armature
+# (KKBP armature) Hide everything but the core bones
+# (KKBP armature) Hide bone widgets collection
+
+# Survey code was taken from MediaMoots here https://github.com/FlailingFog/KK-Blender-Shader-Pack/issues/29
+# Majority of the joint driver corrections were taken from a blend file by johnbbob_la_petite on the koikatsu discord
+
+import bpy, math
+from .. import common as c
+from mathutils import Vector
+
+# Better FBX 骨骼尾部偏移数据 - 用于设置更自然的骨骼方向
+# 这些数据从 better_fbx 导入的模型中提取,可以让骨骼沿着肢体方向延伸
+# 而不是所有骨骼都朝上的小线段
+# 包含所有 181 个骨骼的完整数据
+better_fbx_bone_tails = {
+ 'p_cf_body_00': (0.0000000000, 0.0000000000, 0.5717499852),
+ 'cf_o_root': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'n_body': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'o_body_a': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'cf_j_root': (0.0000000000, 0.0000000000, 0.0099999998),
+ 'cf_n_height': (0.0000000000, 0.0000000000, 1.1434999704),
+ 'cf_j_hips': (0.0000000000, 0.0000000000, 0.0214350224),
+ 'cf_j_waist01': (0.0000000050, 0.0082969246, -0.0676312447),
+ 'cf_j_waist02': (0.0000061035, 0.0022072941, -0.0269574523),
+ 'cf_d_ana': (0.0000000011, -0.0037057437, 0.0183483958),
+ 'cf_j_ana': (0.0000000000, 0.0004356094, -0.0009342432),
+ 'cf_s_ana': (0.0000000000, 0.0004356094, -0.0009342432),
+ 'cf_d_kokan': (0.0000000003, -0.0000340615, 0.0193923712),
+ 'cf_j_kokan': (0.0000000003, -0.0000340852, 0.0193923712),
+ 'cf_d_siri_L': (0.0006995648, 0.0001653936, 0.0200324059),
+ 'cf_d_siri01_L': (-0.0000022426, 0.0499999970, 0.0000028610),
+ 'cf_j_siri_L': (0.0006995425, 0.0006653816, 0.0200324059),
+ 'cf_s_siri_L': (0.0006995425, 0.0006653816, 0.0200324059),
+ 'cf_d_siri_R': (-0.0007000044, 0.0001655202, 0.0200324059),
+ 'cf_d_siri01_R': (0.0000000000, 0.0499999858, 0.0000020266),
+ 'cf_j_siri_R': (-0.0007000044, 0.0006655231, 0.0200324059),
+ 'cf_s_siri_R': (-0.0007000044, 0.0006655082, 0.0200324059),
+ 'cf_j_thigh00_L': (0.0000000000, 0.0045367014, -0.2599085569),
+ 'cf_d_thigh01_L': (0.0008304268, -0.0000085849, 0.0197808743),
+ 'cf_s_thigh01_L': (0.0008304268, -0.0000085849, 0.0197808743),
+ 'cf_d_thigh02_L': (0.0008304268, 0.0000311676, 0.0175034404),
+ 'cf_s_thigh02_L': (0.0008304268, 0.0000311676, 0.0175034404),
+ 'cf_d_thigh03_L': (0.0008304268, 0.0000528507, 0.0162612200),
+ 'cf_s_thigh03_L': (0.0008304268, 0.0000528507, 0.0162612200),
+ 'cf_j_leg01_L': (-0.0000001565, -0.0001882389, -0.2238392234),
+ 'cf_d_kneeF_L': (-0.0000010356, -0.0399756283, -0.0013961792),
+ 'cf_d_leg02_L': (0.0008304194, -0.0000160560, 0.0126936734),
+ 'cf_s_leg02_L': (0.0004141256, 0.0006808266, -0.0072855055),
+ 'cf_d_leg03_L': (0.0008304194, 0.0000490360, 0.0108296350),
+ 'cf_s_leg03_L': (0.0008304194, 0.0000490360, 0.0108296350),
+ 'cf_j_leg03_L': (0.0000000000, 0.0117738210, -0.0496065579),
+ 'cf_j_foot_L': (-0.0000000075, -0.1070634499, -0.0073633678),
+ 'cf_j_toes_L': (-0.0000000075, -0.1070634536, -0.0073633678),
+ 'cf_s_leg01_L': (0.0000015497, 0.0599634517, 0.0020940304),
+ 'cf_s_kneeB_L': (0.0000015497, 0.0599634424, 0.0020940304),
+ 'cf_j_thigh00_R': (0.0000000149, 0.0045371708, -0.2599344850),
+ 'cf_d_thigh01_R': (-0.0008299947, -0.0000085682, 0.0197798610),
+ 'cf_s_thigh01_R': (-0.0008299947, -0.0000085682, 0.0197798610),
+ 'cf_d_thigh02_R': (-0.0008299947, 0.0000311676, 0.0175034404),
+ 'cf_s_thigh02_R': (-0.0008299947, 0.0000311676, 0.0175034404),
+ 'cf_d_thigh03_R': (-0.0008299947, 0.0000528507, 0.0162612200),
+ 'cf_s_thigh03_R': (-0.0008299947, 0.0000528507, 0.0162612200),
+ 'cf_j_leg01_R': (0.0000000000, -0.0001882352, -0.2238395214),
+ 'cf_d_kneeF_R': (-0.0000000894, -0.0399756199, -0.0013961196),
+ 'cf_d_leg02_R': (-0.0008300021, -0.0000160560, 0.0126936436),
+ 'cf_s_leg02_R': (-0.0008300021, -0.0000160560, 0.0126936436),
+ 'cf_d_leg03_R': (-0.0008300021, 0.0000490360, 0.0108296126),
+ 'cf_s_leg03_R': (-0.0008300021, 0.0000490360, 0.0108296126),
+ 'cf_j_leg03_R': (0.0000000745, 0.0117738321, -0.0495891199),
+ 'cf_j_foot_R': (0.0000000447, -0.1070637517, -0.0073810909),
+ 'cf_j_toes_R': (0.0000000447, -0.1070637591, -0.0073810909),
+ 'cf_s_leg01_R': (0.0000001490, 0.0599634480, 0.0020939708),
+ 'cf_s_kneeB_R': (0.0000001490, 0.0599634498, 0.0020939708),
+ 'cf_s_waist02': (0.0000041698, 0.0000045616, 0.0999919176),
+ 'cf_s_leg_L': (0.1000121087, 0.0000034906, 0.0999964476),
+ 'cf_s_leg_R': (-0.1000037491, 0.0000056308, 0.0999873877),
+ 'cf_s_waist01': (0.0000000001, 0.0000000000, 0.0213849545),
+ 'cf_j_spine01': (0.0000000000, 0.0032969250, 0.0450000763),
+ 'cf_s_spine01': (0.0000000000, 0.0000000000, 0.0214849710),
+ 'cf_j_spine02': (0.0000000000, 0.0050841947, 0.0449999571),
+ 'cf_s_spine02': (0.0000000000, 0.0000659386, 0.0223850012),
+ 'cf_j_spine03': (0.0000064438, 0.0028000008, 0.0566434860),
+ 'cf_j_neck': (0.0000000000, 0.0000000000, 0.0325000286),
+ 'cf_j_head': (0.0000000000, 0.0001676232, 0.0251541138),
+ 'cf_s_head': (0.0000000000, 0.0001676232, 0.0251541138),
+ 'cf_s_neck': (0.0000000000, 0.0001676232, 0.0245041847),
+ 'cf_s_spine03': (0.0000000000, 0.0001676232, 0.0232850313),
+ 'cf_d_shoulder_L': (0.0001563374, 0.0002376232, 0.0241320133),
+ 'cf_j_shoulder_L': (0.0940132719, -0.0000001993, 0.0000000000),
+ 'cf_d_shoulder02_L': (0.0010964721, 0.0002376176, 0.0241320133),
+ 'cf_s_shoulder02_L': (0.0010964721, 0.0002376176, 0.0241320133),
+ 'cf_j_arm00_L': (0.1489044800, 0.0066715311, 0.0000000000),
+ 'cf_d_arm01_L': (0.0012971163, 0.0002268590, 0.0241320133),
+ 'cf_s_arm01_L': (0.0012968481, 0.0002330560, 0.0241320133),
+ 'cf_d_arm02_L': (0.0022885203, 0.0002723690, 0.0241320133),
+ 'cf_s_arm02_L': (0.0022885203, 0.0002723690, 0.0241320133),
+ 'cf_d_arm03_L': (0.0031349957, 0.0003089495, 0.0241320133),
+ 'cf_s_arm03_L': (0.0031350553, 0.0003072731, 0.0241320133),
+ 'cf_j_forearm01_L': (0.1090970039, 0.0050626770, 0.0000000000),
+ 'cf_d_forearm02_L': (0.0046593249, 0.0003493950, 0.0241320133),
+ 'cf_s_forearm02_L': (0.0046591461, 0.0003536306, 0.0241320133),
+ 'cf_d_wrist_L': (0.0055440068, 0.0003493391, 0.0241320133),
+ 'cf_s_wrist_L': (0.0250180364, -0.0004227310, 0.0000002384),
+ 'cf_d_hand_L': (0.0250180364, -0.0004227310, 0.0000002384),
+ 'cf_j_hand_L': (0.0061240196, 0.0003494397, 0.0241320133),
+ 'cf_s_hand_L': (0.0434948206, -0.0036587194, -0.0018885136),
+ 'cf_j_index01_L': (0.0308244824, -0.0027071098, -0.0026963949),
+ 'cf_j_index02_L': (0.0205492973, -0.0018050103, -0.0018019676),
+ 'cf_j_index03_L': (0.0205492973, -0.0018050103, -0.0018019676),
+ 'cf_j_little01_L': (0.0280875564, 0.0039625615, -0.0024572611),
+ 'cf_j_little02_L': (0.0152931809, 0.0021575317, -0.0013381243),
+ 'cf_j_little03_L': (0.0152931809, 0.0021575317, -0.0013381243),
+ 'cf_j_middle01_L': (0.0340363383, 0.0000000112, -0.0029777288),
+ 'cf_j_middle02_L': (0.0226911902, 0.0000002403, -0.0019830465),
+ 'cf_j_middle03_L': (0.0226911902, 0.0000002403, -0.0019830465),
+ 'cf_j_ring01_L': (0.0324101448, 0.0022749901, -0.0028355122),
+ 'cf_j_ring02_L': (0.0216068029, 0.0015166700, -0.0018904209),
+ 'cf_j_ring03_L': (0.0216068029, 0.0015166700, -0.0018904209),
+ 'cf_j_thumb01_L': (0.0274280310, -0.0195015902, -0.0048363209),
+ 'cf_j_thumb02_L': (0.0201677084, -0.0143394098, -0.0035561323),
+ 'cf_j_thumb03_L': (0.0201677084, -0.0143394079, -0.0035561323),
+ 'cf_s_elbo_L': (-0.0000313818, 0.0248819850, 0.0000001192),
+ 'cf_s_forearm01_L': (0.0004531145, -0.0307315015, 0.0000000000),
+ 'cf_s_elboback_L': (0.0004531145, -0.0307315006, 0.0000000000),
+ 'cf_d_bust00': (0.0000000000, 0.0001676232, 0.0232039690),
+ 'cf_s_bust00_R': (-0.0575999990, -0.0454922598, 0.0026999712),
+ 'cf_d_bust01_R': (-0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_j_bust01_R': (-0.0051772669, -0.0134591460, 0.0015155077),
+ 'cf_d_bust02_R': (-0.0003042668, 0.0004191138, 0.0232065916),
+ 'cf_j_bust02_R': (-0.0046416745, -0.0120668225, 0.0013588667),
+ 'cf_d_bust03_R': (-0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_j_bust03_R': (-0.0017852709, -0.0046411008, 0.0005227327),
+ 'cf_d_bnip01_R': (-0.0006545857, -0.0017017275, 0.0001918077),
+ 'cf_j_bnip02root_R': (-0.0048966482, -0.0061617792, 0.0016967058),
+ 'cf_s_bnip02_R': (-0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_j_bnip02_R': (-0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_s_bnip025_R': (-0.0083650872, -0.0086106881, 0.0029753447),
+ 'cf_s_bnip01_R': (-0.0004328042, 0.0000849515, 0.0232441425),
+ 'cf_s_bnip015_R': (-0.0008926094, -0.0023205206, 0.0002615452),
+ 'cf_s_bust03_R': (-0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_s_bust02_R': (-0.0003042668, 0.0004191138, 0.0232064724),
+ 'cf_s_bust01_R': (-0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_s_bust00_L': (0.0575999990, -0.0454922598, 0.0026999712),
+ 'cf_d_bust01_L': (0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_j_bust01_L': (0.0051772669, -0.0134591460, 0.0015155077),
+ 'cf_d_bust02_L': (0.0003042668, 0.0004191138, 0.0232065916),
+ 'cf_j_bust02_L': (0.0046416819, -0.0120668299, 0.0013588667),
+ 'cf_d_bust03_L': (0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_j_bust03_L': (0.0017852709, -0.0046411008, 0.0005227327),
+ 'cf_d_bnip01_L': (0.0006546006, -0.0017017350, 0.0001916885),
+ 'cf_j_bnip02root_L': (0.0049032941, -0.0061767399, 0.0016988516),
+ 'cf_s_bnip02_L': (0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_j_bnip02_L': (0.0004578009, 0.0000199825, 0.0232515335),
+ 'cf_s_bnip025_L': (0.0083783790, -0.0086406097, 0.0029795170),
+ 'cf_s_bnip01_L': (0.0004328042, 0.0000849515, 0.0232441425),
+ 'cf_s_bnip015_L': (0.0008926317, -0.0023205578, 0.0002613068),
+ 'cf_s_bust03_L': (0.0003971010, 0.0001777783, 0.0232336521),
+ 'cf_s_bust02_L': (0.0003042668, 0.0004191138, 0.0232064724),
+ 'cf_s_bust01_L': (0.0002007224, 0.0006882958, 0.0231761932),
+ 'cf_d_shoulder_R': (-0.0001560142, 0.0002376232, 0.0241320133),
+ 'cf_j_shoulder_R': (-0.0939531336, -0.0000126194, 0.0000000000),
+ 'cf_d_shoulder02_R': (-0.0010954589, 0.0002373699, 0.0241320133),
+ 'cf_s_shoulder02_R': (-0.0010954589, 0.0002373699, 0.0241320133),
+ 'cf_j_arm00_R': (-0.1495516151, 0.0065441933, 0.0000001192),
+ 'cf_d_arm01_R': (-0.0012957752, 0.0002367441, 0.0241320133),
+ 'cf_s_arm01_R': (-0.0012955368, 0.0002426561, 0.0241320133),
+ 'cf_d_arm02_R': (-0.0022943318, 0.0002824105, 0.0241320133),
+ 'cf_s_arm02_R': (-0.0022938997, 0.0002823994, 0.0241320133),
+ 'cf_d_arm03_R': (-0.0031512678, 0.0003276281, 0.0241320133),
+ 'cf_s_arm03_R': (-0.0031512082, 0.0003276169, 0.0241320133),
+ 'cf_j_forearm01_R': (-0.1111739874, 0.0051015355, -0.0000002384),
+ 'cf_s_elbo_R': (0.0000000298, 0.0251077451, 0.0000021458),
+ 'cf_s_forearm01_R': (-0.0000276864, -0.0304288915, -0.0000027418),
+ 'cf_s_elboback_R': (-0.0000276864, -0.0304288925, -0.0000027418),
+ 'cf_d_forearm02_R': (-0.0047625899, 0.0003447086, 0.0241320133),
+ 'cf_s_forearm02_R': (-0.0047624111, 0.0003491975, 0.0241320133),
+ 'cf_d_wrist_R': (-0.0055437088, 0.0003451817, 0.0241320133),
+ 'cf_s_wrist_R': (-0.0250178576, -0.0004224181, 0.0000003576),
+ 'cf_d_hand_R': (-0.0250178576, -0.0004224181, 0.0000003576),
+ 'cf_j_hand_R': (-0.0061238408, 0.0003451258, 0.0241320133),
+ 'cf_s_hand_R': (-0.0434947014, -0.0036587510, -0.0018894672),
+ 'cf_j_little01_R': (-0.0281152725, 0.0039664730, -0.0024597645),
+ 'cf_j_little02_R': (-0.0152906775, 0.0021577775, -0.0013365746),
+ 'cf_j_little03_R': (-0.0152906775, 0.0021577775, -0.0013365746),
+ 'cf_j_middle01_R': (-0.0340698957, -0.0000000205, -0.0029808283),
+ 'cf_j_middle02_R': (-0.0227140188, 0.0000003595, -0.0019767284),
+ 'cf_j_middle03_R': (-0.0227140188, 0.0000003595, -0.0019767284),
+ 'cf_j_ring01_R': (-0.0323968530, 0.0022740699, -0.0028343201),
+ 'cf_j_ring02_R': (-0.0215651989, 0.0015144981, -0.0018799305),
+ 'cf_j_ring03_R': (-0.0215651989, 0.0015144981, -0.0018799305),
+ 'cf_j_thumb01_R': (-0.0274280310, -0.0195016302, -0.0048363209),
+ 'cf_j_thumb02_R': (-0.0201677680, -0.0143393399, -0.0035561323),
+ 'cf_j_thumb03_R': (-0.0201677680, -0.0143393390, -0.0035561323),
+ 'cf_j_index01_R': (-0.0308635831, -0.0027105501, -0.0027000904),
+ 'cf_j_index02_R': (-0.0205416679, -0.0018021995, -0.0018149614),
+ 'cf_j_index03_R': (-0.0205416679, -0.0018021995, -0.0018149614),
+}
+
+class modify_armature(bpy.types.Operator):
+ bl_idname = "kkbp.modifyarmature"
+ bl_label = bl_idname
+ bl_description = bl_idname
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ try:
+
+ self.reparent_all_objects()
+ self.remove_bone_locks_and_modifiers()
+ self.scale_armature_bones_down()
+ self.reparent_leg_and_body_bone()
+ self.delete_non_height_bones()
+
+ self.modify_finger_bone_orientations()
+ self.set_bone_roll_data()
+ self.bend_bones_for_iks()
+
+ self.remove_empty_vertex_groups()
+ self.reorganize_armature_layers()
+ self.move_accessory_bones_to_layer10()
+
+ self.create_eye_reference_bone()
+ self.create_eye_controller_bone()
+ self.shorten_kokan_bone()
+ self.scale_skirt_and_face_bones()
+
+ self.prepare_ik_bones()
+ self.create_ik_bones()
+ self.create_joint_drivers()
+
+ self.categorize_bones()
+ self.rename_bones_for_clarity()
+ self.rename_mmd_bones()
+
+ # 最后再次应用 better_fbx 骨骼方向,确保不被其他步骤覆盖
+ self.apply_better_fbx_bone_orientations()
+
+ self.apply_bone_widgets()
+ self.hide_widgets()
+
+ return {'FINISHED'}
+ except Exception as error:
+ c.handle_error(self, error)
+ return {"CANCELLED"}
+
+ # %% Main functions
+ def reparent_all_objects(self):
+ '''Reparents all objects to the main armature'''
+ armature = c.get_armature()
+ outfits = c.get_outfits()
+ body = c.get_body()
+ c.switch(armature, 'object')
+ armature.parent = None
+ armature.name = 'Armature'
+
+ #edit armature modifier on body
+ body.modifiers[0].show_in_editmode = True
+ body.modifiers[0].show_on_cage = True
+ body.modifiers[0].show_expanded = False
+ body.modifiers[0].name = 'Armature modifier'
+
+ #reparent the outfit meshes as well
+ for outfit in outfits:
+ outfit_armature = outfit.parent.name
+ outfit.parent = armature
+ outfit.modifiers[0].object = armature
+ outfit.modifiers[0].show_in_editmode = True
+ outfit.modifiers[0].show_on_cage = True
+ outfit.modifiers[0].show_expanded = False
+ outfit.modifiers[0].name = 'Armature modifier'
+ bpy.data.objects.remove(bpy.data.objects[outfit_armature])
+ #remove the empties
+ empties = c.get_empties()
+ for empty in empties:
+ bpy.data.objects.remove(empty)
+ #reparent the alts and hairs to the main outfit object
+ for alt in c.get_alts():
+ alt.parent = armature
+ alt.modifiers[0].object = armature
+ alt.modifiers[0].show_in_editmode = True
+ alt.modifiers[0].show_on_cage = True
+ alt.modifiers[0].show_expanded = False
+ alt.modifiers[0].name = 'Armature modifier'
+
+ for hair in c.get_hairs():
+ hair.parent = armature
+ hair.modifiers[0].object = armature
+ hair.modifiers[0].show_in_editmode = True
+ hair.modifiers[0].show_on_cage = True
+ hair.modifiers[0].show_expanded = False
+ hair.modifiers[0].name = 'Armature modifier'
+
+ #reparent the tongue, tears and gag eyes if they exist
+ objects = []
+ if c.get_tongue():
+ objects.append(c.get_tongue())
+ if c.get_tears():
+ objects.append(c.get_tears())
+ if c.get_gags():
+ objects.append(c.get_gags())
+ for object in objects:
+ object.parent = body
+ #reparent hitboxes if they exist
+ for hb in c.get_hitboxes():
+ hb.parent = armature
+ c.print_timer('reparent_all_objects')
+
+ def scale_armature_bones_down(self):
+ '''scale all bone sizes down by a factor of 12. (all armature bones must be sticking upwards)'''
+ c.switch(c.get_armature(), 'edit')
+ for bone in c.get_armature().data.edit_bones:
+ bone.tail.z = bone.head.z + (bone.tail.z - bone.head.z)/12
+ c.print_timer('scale_armature_bones_down')
+
+ def remove_bone_locks_and_modifiers(self):
+ '''Removes mmd bone constraints and bone drivers, unlocks all bones'''
+ #remove all constraints from all bones
+ armature = c.get_armature()
+ c.switch(armature, 'pose')
+ for bone in armature.pose.bones:
+ for constraint in bone.constraints:
+ bone.constraints.remove(constraint)
+
+ #remove all drivers from all armature bones
+ #animation_data is nonetype if no drivers have been created yet
+ if armature.animation_data:
+ drivers_data = armature.animation_data.drivers
+ for driver in drivers_data:
+ armature.driver_remove(driver.data_path, -1)
+
+ #unlock the armature and all bones
+ armature.lock_location = [False, False, False]
+ armature.lock_rotation = [False, False, False]
+ armature.lock_scale = [False, False, False]
+
+ for bone in armature.pose.bones:
+ bone.lock_location = [False, False, False]
+ c.print_timer('remove_bone_locks_and_modifiers')
+
+ def reparent_leg_and_body_bone(self):
+ '''Reparent the leg bone to match the koikatsu armature. Unparent the body_bone bone to match koikatsu armature'''
+ if bpy.context.scene.kkbp.armature_dropdown != 'D':
+ armature = c.get_armature()
+ #reparent foot to leg03
+ armature.data.edit_bones['cf_j_foot_R'].parent = armature.data.edit_bones['cf_j_leg03_R']
+ armature.data.edit_bones['cf_j_foot_L'].parent = armature.data.edit_bones['cf_j_leg03_L']
+ #unparent body bone to match KK
+ armature.data.edit_bones['p_cf_body_bone'].parent = None
+ c.print_timer('reparent_leg_and_body_bone')
+
+ def delete_non_height_bones(self):
+ '''delete bones not under the cf_n_height bone. Deletes bones not under the BodyTop bone if PMX armature was selected'''
+ armature = c.get_armature()
+ def select_children(parent):
+ try:
+ parent.select = True
+ parent.select_head = True
+ parent.select_tail = True
+ for child in parent.children:
+ select_children(child)
+ except:
+ #This is the last bone in the chain
+ return
+ if bpy.context.scene.kkbp.armature_dropdown == 'D':
+ select_children(armature.data.edit_bones['BodyTop'])
+ else:
+ select_children(armature.data.edit_bones['cf_n_height'])
+ #make sure these bones aren't deleted
+ for preserve_bone in ['cf_j_root', 'p_cf_body_bone', 'cf_n_height']:
+ armature.data.edit_bones[preserve_bone].select = True
+ armature.data.edit_bones[preserve_bone].select_head = True
+ armature.data.edit_bones[preserve_bone].select_tail = True
+ bpy.ops.armature.select_all(action='INVERT')
+ bpy.ops.armature.delete()
+
+ # 删除无用的配饰骨骼(头发、衣服等),只保留核心人体骨骼
+ # self.delete_accessory_bones()
+
+ c.print_timer('delete_non_height_bones')
+
+ def delete_accessory_bones(self):
+ '''删除不在 better_fbx_bone_tails 字典中的骨骼,只保留核心人体骨骼'''
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+
+ # 定义必须保留的骨骼(即使不在 better_fbx_bone_tails 中)
+ # 这些是 KKBP 后续步骤需要的特殊骨骼
+ must_keep_bones = [
+ 'cf_pv_root', # IK 根骨骼
+ 'cf_pv_hand_L', 'cf_pv_hand_R', # 手部 IK
+ 'cf_pv_elbo_L', 'cf_pv_elbo_R', # 肘部 IK
+ 'cf_pv_foot_L', 'cf_pv_foot_R', # 脚部 IK
+ 'cf_pv_knee_L', 'cf_pv_knee_R', # 膝盖 IK
+ 'cf_hit_head', # 头部碰撞
+ 'p_cf_body_bone', # 身体骨骼
+ 'p_cf_head_bone', # 头部骨骼
+ 'a_n_back', # 背部锚点
+ 'a_n_headside', # 头部侧面锚点
+ ]
+
+ # 收集所有需要删除的骨骼(不在 better_fbx_bone_tails 中且不在必须保留列表中的)
+ bones_to_delete = []
+ for bone in armature.data.edit_bones:
+ bone_name = bone.name
+ # 如果骨骼不在 better_fbx_bone_tails 字典中,且不在必须保留列表中,标记为删除
+ if bone_name not in better_fbx_bone_tails and bone_name not in must_keep_bones:
+ bones_to_delete.append(bone_name)
+
+ # 删除标记的骨骼
+ deleted_count = 0
+ for bone_name in bones_to_delete:
+ if bone_name in armature.data.edit_bones:
+ armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
+ deleted_count += 1
+
+ print(f"[KKBP] 已删除 {deleted_count} 个配饰骨骼")
+ print(f"[KKBP] 保留了 {len(better_fbx_bone_tails)} 个核心骨骼 + {len(must_keep_bones)} 个必需骨骼")
+
+ c.switch(armature, 'object')
+
+ def modify_finger_bone_orientations(self):
+ '''Reorient the finger bones to match the in game koikatsu armature'''
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ height_adjust = Vector((0,0,0.1))
+
+ #all finger bones need to be rotated a specific direction
+ def rotate_thumb(bone):
+ bpy.ops.armature.select_all(action='DESELECT')
+ armature.data.edit_bones[bone].select = True
+ armature.data.edit_bones[bone].select_head = True
+ armature.data.edit_bones[bone].select_tail = True
+ parent = armature.data.edit_bones[bone].parent
+ armature.data.edit_bones[bone].parent = None
+
+ #right thumbs face towards hand center
+ #left thumbs face away from hand center
+ angle = -math.pi/2
+ s = math.sin(angle)
+ c = math.cos(angle)
+
+ # translate point to origin:
+ armature.data.edit_bones[bone].tail.x -= armature.data.edit_bones[bone].head.x
+ armature.data.edit_bones[bone].tail.y -= armature.data.edit_bones[bone].head.y
+
+ # rotate point around origin
+ xnew = armature.data.edit_bones[bone].tail.x * c - armature.data.edit_bones[bone].tail.y * s
+ ynew = armature.data.edit_bones[bone].tail.x * s + armature.data.edit_bones[bone].tail.y * c
+
+ # translate point back to original position:
+ armature.data.edit_bones[bone].tail.x = xnew + armature.data.edit_bones[bone].head.x
+ armature.data.edit_bones[bone].tail.y = ynew + armature.data.edit_bones[bone].head.y
+ armature.data.edit_bones[bone].roll = 0
+ armature.data.edit_bones[bone].parent = parent
+
+ rotate_thumb('cf_j_thumb03_L')
+ rotate_thumb('cf_j_thumb02_L')
+ rotate_thumb('cf_j_thumb01_L')
+ rotate_thumb('cf_j_thumb03_R')
+ rotate_thumb('cf_j_thumb02_R')
+ rotate_thumb('cf_j_thumb01_R')
+
+ height_adjust = Vector((0,0,0.05))
+ def flip_finger(bone):
+ parent = armature.data.edit_bones[bone].parent
+ armature.data.edit_bones[bone].parent = None
+ armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head - height_adjust
+ armature.data.edit_bones[bone].parent = parent
+
+ finger_list = (
+ 'cf_j_index03_R', 'cf_j_index02_R', 'cf_j_index01_R',
+ 'cf_j_middle03_R', 'cf_j_middle02_R', 'cf_j_middle01_R',
+ 'cf_j_ring03_R', 'cf_j_ring02_R', 'cf_j_ring01_R',
+ 'cf_j_little03_R', 'cf_j_little02_R', 'cf_j_little01_R'
+ )
+
+ for finger in finger_list:
+ flip_finger(finger)
+
+ height_adjust = Vector((0,0,0.05))
+ def resize_finger(bone):
+ parent = armature.data.edit_bones[bone].parent
+ armature.data.edit_bones[bone].parent = None
+ armature.data.edit_bones[bone].tail = armature.data.edit_bones[bone].head + height_adjust
+ armature.data.edit_bones[bone].parent = parent
+
+ finger_list = (
+ 'cf_j_index03_L', 'cf_j_index02_L', 'cf_j_index01_L',
+ 'cf_j_middle03_L', 'cf_j_middle02_L', 'cf_j_middle01_L',
+ 'cf_j_ring03_L', 'cf_j_ring02_L', 'cf_j_ring01_L',
+ 'cf_j_little03_L', 'cf_j_little02_L', 'cf_j_little01_L'
+ )
+
+ for finger in finger_list:
+ resize_finger(finger)
+
+ c.print_timer('modify_finger_bone_orientations')
+
+ def set_bone_roll_data(self):
+ '''Use roll data from a reference armature dump to set the roll for each bone'''
+ reroll_data = {
+ 'BodyTop':0.0,
+ 'p_cf_body_bone':0.0,
+ 'cf_j_root':0.0,
+ 'cf_n_height':0.0,
+ 'cf_j_hips':0.0,
+ 'cf_j_spine01':0.0,
+ 'cf_j_spine02':0.0,
+ 'cf_j_spine03':0.0,
+ 'cf_d_backsk_00':0.0,
+ 'cf_j_backsk_C_01':-1.810556946517264e-23,
+ 'cf_j_backsk_C_02':-1.1667208633608472e-15,
+ 'cf_j_backsk_L_01':-0.001851903973147273,
+ 'cf_j_backsk_L_02':-0.0034122250508517027,
+ 'cf_j_backsk_R_01':0.0018519698642194271,
+ 'cf_j_backsk_R_02':0.003412271151319146,
+ 'cf_d_bust00':0.0,
+ 'cf_s_bust00_L':0.0,
+ 'cf_d_bust01_L':0.4159948229789734,
+ 'cf_j_bust01_L':0.4159948229789734,
+ 'cf_d_bust02_L':0.4159948229789734,
+ 'cf_j_bust02_L':0.4151105582714081,
+ 'cf_d_bust03_L':0.4151104986667633,
+ 'cf_j_bust03_L':0.4151104986667633,
+ 'cf_d_bnip01_L':0.4151104986667633,
+ 'cf_j_bnip02root_L':0.4151104986667633,
+ 'cf_s_bnip02_L':0.4151104986667633,
+ 'cf_j_bnip02_L':0.4154767096042633,
+ 'cf_s_bnip025_L':0.4154742360115051,
+ 'cf_s_bnip01_L':0.41547420620918274,
+ 'cf_s_bnip015_L':0.41547420620918274,
+ 'cf_s_bust03_L':0.4153861403465271,
+ 'cf_s_bust02_L':0.38631150126457214,
+ 'cf_s_bust01_L':0.4159947633743286,
+ 'cf_s_bust00_R':0.0,
+ 'cf_d_bust01_R':-0.4159948527812958,
+ 'cf_j_bust01_R':-0.4159948229789734,
+ 'cf_d_bust02_R':-0.41599488258361816,
+ 'cf_j_bust02_R':-0.41511064767837524,
+ 'cf_d_bust03_R':-0.41511061787605286,
+ 'cf_j_bust03_R':-0.41511064767837524,
+ 'cf_d_bnip01_R':-0.41511061787605286,
+ 'cf_j_bnip02root_R':-0.41511061787605286,
+ 'cf_s_bnip02_R':-0.41511061787605286,
+ 'cf_j_bnip02_R':-0.41547682881355286,
+ 'cf_s_bnip025_R':-0.4154742360115051,
+ 'cf_s_bnip01_R':-0.4154742956161499,
+ 'cf_s_bnip015_R':-0.4154742956161499,
+ 'cf_s_bust03_R':-0.41538622975349426,
+ 'cf_s_bust02_R':-0.38631150126457214,
+ 'cf_s_bust01_R':-0.4159948229789734,
+ 'cf_d_shoulder_L':0.0,
+ 'cf_j_shoulder_L':0.0,
+ 'cf_d_shoulder02_L':4.2021918488899246e-05,
+ 'cf_s_shoulder02_L':4.202192212687805e-05,
+ 'cf_j_arm00_L':0.0,
+ 'cf_d_arm01_L':-0.0012054670369252563,
+ 'cf_s_arm01_L':0.009222406893968582,
+ 'cf_d_arm02_L':-0.0012054670369252563,
+ 'cf_s_arm02_L':0.004008470103144646,
+ 'cf_d_arm03_L':-0.0012054670369252563,
+ 'cf_s_arm03_L':-0.0012097591534256935,
+ 'cf_j_forearm01_L':0.0,
+ 'cf_d_forearm02_L':0.0,
+ 'cf_s_forearm02_L':0.0,
+ 'cf_d_wrist_L':0.0,
+ 'cf_s_wrist_L':0.0,
+ 'cf_d_hand_L':0.0,
+ 'cf_j_hand_L':-6.776263578034403e-21,
+ 'cf_s_hand_L':-6.776263578034403e-21,
+ 'cf_j_index01_L':math.radians(-11),
+ 'cf_j_index02_L':math.radians(-5),
+ 'cf_j_index03_L':math.radians(0),
+ 'cf_j_little01_L':math.radians(30),
+ 'cf_j_little02_L':math.radians(11),
+ 'cf_j_little03_L':math.radians(30),
+ 'cf_j_middle01_L':math.radians(3),
+ 'cf_j_middle02_L':math.radians(3),
+ 'cf_j_middle03_L':math.radians(3),
+ 'cf_j_ring01_L':math.radians(15),
+ 'cf_j_ring02_L':math.radians(7),
+ 'cf_j_ring03_L':math.radians(15),
+ 'cf_j_thumb01_L':math.pi,
+ 'cf_j_thumb02_L':math.pi,
+ 'cf_j_thumb03_L':math.pi,
+ 'cf_s_elbo_L':0.0,
+ 'cf_s_forearm01_L':0.0,
+ 'cf_s_elboback_L':0.0,
+ 'cf_d_shoulder_R':0.0,
+ 'cf_j_shoulder_R':0.0,
+ 'cf_d_shoulder02_R':-4.355472628958523e-05,
+ 'cf_s_shoulder02_R':-4.355472628958523e-05,
+ 'cf_j_arm00_R':0.0,
+ 'cf_d_arm01_R':0.0009736516512930393,
+ 'cf_s_arm01_R':0.0009736517095007002,
+ 'cf_d_arm02_R':0.0009736516512930393,
+ 'cf_s_arm02_R':-0.004238337744027376,
+ 'cf_d_arm03_R':0.0009736516512930393,
+ 'cf_s_arm03_R':0.0009736517095007002,
+ 'cf_j_forearm01_R':0.0,
+ 'cf_d_forearm02_R':2.6637668270268477e-05,
+ 'cf_s_forearm02_R':2.6637668270268477e-05,
+ 'cf_d_wrist_R':1.9139706637361087e-05,
+ 'cf_s_wrist_R':1.9139704818371683e-05,
+ 'cf_d_hand_R':1.9139704818371683e-05,
+ 'o_brac':0.0,
+ 'cf_j_hand_R':-6.776470373187541e-21,
+ 'cf_s_hand_R':-6.776470373187541e-21,
+ 'cf_j_index01_R':math.radians(-11),
+ 'cf_j_index02_R':math.radians(-5),
+ 'cf_j_index03_R':math.radians(0),
+ 'cf_j_little01_R':math.radians(30),
+ 'cf_j_little02_R':math.radians(11),
+ 'cf_j_little03_R':math.radians(30),
+ 'cf_j_middle01_R':math.radians(3),
+ 'cf_j_middle02_R':math.radians(3),
+ 'cf_j_middle03_R':math.radians(3),
+ 'cf_j_ring01_R':math.radians(15),
+ 'cf_j_ring02_R':math.radians(7),
+ 'cf_j_ring03_R':math.radians(15),
+ 'cf_j_thumb01_R':math.radians(0),
+ 'cf_j_thumb02_R':math.radians(0),
+ 'cf_j_thumb03_R':math.radians(0),
+ 'cf_s_elbo_R':0.0,
+ 'cf_s_forearm01_R':0.0,
+ 'cf_s_elboback_R':4.203895392974451e-44,
+ 'cf_d_spinesk_00':0.0,
+ 'cf_j_spinesk_00':5.2774636787104646e-23,
+ 'cf_j_spinesk_01':-0.01019450556486845,
+ 'cf_j_spinesk_02':0.0032659571152180433,
+ 'cf_j_spinesk_03':-0.001969193108379841,
+ 'cf_j_spinesk_04':-0.001969192875549197,
+ 'cf_j_spinesk_05':-0.00196919240988791,
+ 'cf_j_neck':0.0,
+ 'cf_j_head':0.0,
+ 'cf_s_head':0.0,
+ 'p_cf_head_bone':0.0,
+ 'cf_J_N_FaceRoot':2.1210576051089447e-07,
+ 'cf_J_FaceRoot':2.1210573208918504e-07,
+ 'cf_J_FaceBase':2.1210573208918504e-07,
+ 'cf_J_FaceLow_tz':2.1210573208918504e-07,
+ 'cf_J_FaceLow_sx':2.1210573208918504e-07,
+ 'cf_J_CheekUpBase':2.1210573208918504e-07,
+ 'cf_J_CheekUp_s_L':2.1210573208918504e-07,
+ 'cf_J_CheekUp_s_R':2.1210573208918504e-07,
+ 'cf_J_Chin_Base':2.1210574630003975e-07,
+ 'cf_J_CheekLow_s_L':2.1210573208918504e-07,
+ 'cf_J_CheekLow_s_R':2.1210573208918504e-07,
+ 'cf_J_Chin_s':2.1210573208918504e-07,
+ 'cf_J_ChinTip_Base':2.1210574630003975e-07,
+ 'cf_J_ChinLow':2.1210573208918504e-07,
+ 'cf_J_MouthBase_ty':2.1210573208918504e-07,
+ 'cf_J_MouthBase_rx':2.1210573208918504e-07,
+ 'cf_J_MouthCavity':2.1210571787833032e-07,
+ 'cf_J_MouthMove':2.1210571787833032e-07,
+ 'cf_J_Mouth_L':2.1210573208918504e-07,
+ 'cf_J_Mouth_R':2.1210573208918504e-07,
+ 'cf_J_MouthLow':2.1210573208918504e-07,
+ 'cf_J_Mouthup':2.1210573208918504e-07,
+ 'cf_J_FaceUp_ty':2.1210573208918504e-07,
+ 'a_n_headside':2.1210574630003975e-07,
+ 'cf_J_EarBase_ry_L':-0.1504509449005127,
+ 'cf_J_EarLow_L':-0.1504509449005127,
+ 'cf_J_EarUp_L':-0.1504509449005127,
+ 'cf_J_EarBase_ry_R':0.15762563049793243,
+ 'cf_J_EarLow_R':0.15762564539909363,
+ 'cf_J_EarUp_R':0.15762564539909363,
+ 'cf_J_FaceUp_tz':2.1210574630003975e-07,
+ 'cf_J_Eye_tz':2.1210574630003975e-07,
+ 'cf_J_Eye_txdam_L':2.1210574630003975e-07,
+ 'cf_J_Eye_tx_L':2.1210573208918504e-07,
+ 'cf_J_Eye_rz_L':0.20466913282871246,
+ 'cf_J_CheekUp2_L':0.004773593973368406,
+ 'cf_J_Eye01_s_L':0.20466917753219604,
+ 'cf_J_Eye02_s_L':0.20466917753219604,
+ 'cf_J_Eye03_s_L':0.20466917753219604,
+ 'cf_J_Eye04_s_L':0.20466917753219604,
+ 'cf_J_Eye05_s_L':0.20466917753219604,
+ 'cf_J_Eye06_s_L':0.20466917753219604,
+ 'cf_J_Eye07_s_L':0.20466917753219604,
+ 'cf_J_Eye08_s_L':0.20466917753219604,
+ 'cf_J_hitomi_tx_L':0.18981075286865234,
+ 'cf_J_Eye_txdam_R':2.1210574630003975e-07,
+ 'cf_J_Eye_tx_R':2.1210576051089447e-07,
+ 'cf_J_Eye_rz_R':-0.2046687752008438,
+ 'cf_J_CheekUp2_R':-0.0047732163220644,
+ 'cf_J_Eye01_s_R':-0.2046687752008438,
+ 'cf_J_Eye02_s_R':-0.2046687752008438,
+ 'cf_J_Eye03_s_R':-0.2046687752008438,
+ 'cf_J_Eye04_s_R':-0.2046687752008438,
+ 'cf_J_Eye05_s_R':-0.2046687752008438,
+ 'cf_J_Eye06_s_R':-0.2046687752008438,
+ 'cf_J_Eye07_s_R':-0.2046687752008438,
+ 'cf_J_Eye08_s_R':-0.2046687752008438,
+ 'cf_J_hitomi_tx_R':-0.2046687752008438,
+ 'cf_J_Mayu_ty':2.1210574630003975e-07,
+ 'cf_J_Mayumoto_L':0.3458269536495209,
+ 'cf_J_Mayu_L':0.34616410732269287,
+ 'cf_J_MayuMid_s_L':0.34626930952072144,
+ 'cf_J_MayuTip_s_L':0.3462893068790436,
+ 'cf_J_Mayumoto_R':-0.34582653641700745,
+ 'cf_J_Mayu_R':-0.34616369009017944,
+ 'cf_J_MayuMid_s_R':-0.3462689220905304,
+ 'cf_J_MayuTip_s_R':-0.34628885984420776,
+ 'cf_J_NoseBase':2.1210573208918504e-07,
+ 'cf_J_NoseBase_rx':2.1210573208918504e-07,
+ 'cf_J_Nose_rx':2.1210573208918504e-07,
+ 'cf_J_Nose_tip':2.1210571787833032e-07,
+ 'cf_J_NoseBridge_ty':2.1210573208918504e-07,
+ 'cf_J_NoseBridge_rx':2.1210573208918504e-07,
+ 'cf_s_neck':0.0,
+ 'cf_s_spine03':0.0,
+ 'a_n_back':-3.1415927410125732,
+ 'cf_s_spine02':0.0,
+ 'cf_s_spine01':0.0,
+ 'cf_j_waist01':0.0,
+ 'cf_d_sk_top':0.0,
+ 'cf_d_sk_00_00':-3.1415927410125732,
+ 'cf_j_sk_00_00':-3.1415927410125732,
+ 'cf_j_sk_00_01':-3.1415927410125732,
+ 'cf_j_sk_00_02':3.1415293216705322,
+ 'cf_j_sk_00_03':3.1415293216705322,
+ 'cf_j_sk_00_04':3.1415293216705322,
+ 'cf_j_sk_00_05':3.1415293216705322,
+ 'cf_d_sk_01_00':2.3621666431427,
+ 'cf_j_sk_01_00':2.3621666431427,
+ 'cf_j_sk_01_01':2.364142656326294,
+ 'cf_j_sk_01_02':2.3684122562408447,
+ 'cf_j_sk_01_03':2.3684122562408447,
+ 'cf_j_sk_01_04':2.3684122562408447,
+ 'cf_j_sk_01_05':2.3684122562408447,
+ 'cf_d_sk_02_00':1.5806118249893188,
+ 'cf_j_sk_02_00':1.5808137655258179,
+ 'cf_j_sk_02_01':1.5806832313537598,
+ 'cf_j_sk_02_02':1.5820348262786865,
+ 'cf_j_sk_02_03':1.5820348262786865,
+ 'cf_j_sk_02_04':1.5820348262786865,
+ 'cf_j_sk_02_05':1.5820348262786865,
+ 'cf_d_sk_03_00':0.7112568616867065,
+ 'cf_j_sk_03_00':0.7112571597099304,
+ 'cf_j_sk_03_01':0.7112568020820618,
+ 'cf_j_sk_03_02':0.709623396396637,
+ 'cf_j_sk_03_03':0.7096233367919922,
+ 'cf_j_sk_03_04':0.7096233367919922,
+ 'cf_j_sk_03_05':0.7096234560012817,
+ 'cf_d_sk_04_00':3.4498308600352867e-17,
+ 'cf_j_sk_04_00':0.00037256989162415266,
+ 'cf_j_sk_04_01':0.00012998198508284986,
+ 'cf_j_sk_04_02':0.0001299990399274975,
+ 'cf_j_sk_04_03':0.0001299990399274975,
+ 'cf_j_sk_04_04':0.0001299990399274975,
+ 'cf_j_sk_04_05':0.0001299990399274975,
+ 'cf_d_sk_05_00':-0.7112577557563782,
+ 'cf_j_sk_05_00':-0.7112579345703125,
+ 'cf_j_sk_05_01':-0.7112577557563782,
+ 'cf_j_sk_05_02':-0.7096185088157654,
+ 'cf_j_sk_05_03':-0.7096185088157654,
+ 'cf_j_sk_05_04':-0.7096185088157654,
+ 'cf_j_sk_05_05':-0.7096185088157654,
+ 'cf_d_sk_06_00':-1.5806118249893188,
+ 'cf_j_sk_06_00':-1.5808138847351074,
+ 'cf_j_sk_06_01':-1.5806833505630493,
+ 'cf_j_sk_06_02':-1.5820401906967163,
+ 'cf_j_sk_06_03':-1.5820401906967163,
+ 'cf_j_sk_06_04':-1.5820401906967163,
+ 'cf_j_sk_06_05':-1.5820401906967163,
+ 'cf_d_sk_07_00':-2.3621666431427,
+ 'cf_j_sk_07_00':-2.3621666431427,
+ 'cf_j_sk_07_01':-2.3649401664733887,
+ 'cf_j_sk_07_02':-2.3683762550354004,
+ 'cf_j_sk_07_03':-2.3683762550354004,
+ 'cf_j_sk_07_04':-2.3683762550354004,
+ 'cf_j_sk_07_05':-2.3683762550354004,
+ 'cf_j_waist02':0.0,
+ 'cf_d_siri_L':4.435180380824022e-05,
+ 'cf_d_siri01_L':4.484502278501168e-05,
+ 'cf_j_siri_L':4.435180744621903e-05,
+ 'cf_s_siri_L':4.607543087331578e-05,
+ 'cf_d_ana':4.053832753925235e-07,
+ 'cf_j_ana':4.053832753925235e-07,
+ 'cf_s_ana':4.0538333223594236e-07,
+ 'cf_d_kokan':0.0,
+ 'cf_j_kokan':7.531064056820469e-07,
+ 'cf_d_siri_R':-5.766015220842746e-08,
+ 'cf_d_siri01_R':-5.766015220842746e-08,
+ 'cf_j_siri_R':-5.766015220842746e-08,
+ 'cf_s_siri_R':-5.766015576114114e-08,
+ 'cf_j_thigh00_L':0.0,
+ 'cf_d_thigh01_L':0.0,
+ 'cf_s_thigh01_L':0.0,
+ 'cf_d_thigh02_L':0.0,
+ 'cf_s_thigh02_L':0.0,
+ 'cf_d_thigh03_L':0.0,
+ 'cf_s_thigh03_L':0.0,
+ 'cf_j_leg01_L':0.0,
+ 'cf_d_kneeF_L':0.0,
+ 'cf_d_leg02_L':-8.435114585980674e-12,
+ 'cf_s_leg02_L':0.0019489085534587502,
+ 'cf_d_leg03_L':2.6021072699222714e-05,
+ 'cf_s_leg03_L':2.6021054509328678e-05,
+ 'cf_j_leg03_L':-1.7005811689396744e-11,
+ 'cf_j_foot_L':-1.6870217028897017e-11,
+ 'cf_j_toes_L':-1.6870217028897017e-11,
+ 'cf_s_leg01_L':1.5783663344534925e-25,
+ 'cf_s_kneeB_L':1.5783659646749431e-25,
+ 'cf_j_thigh00_R':0.0,
+ 'cf_d_thigh01_R':-2.9915531455925155e-27,
+ 'cf_s_thigh01_R':-2.3654518046693106e-22,
+ 'cf_d_thigh02_R':0.0,
+ 'cf_s_thigh02_R':0.0,
+ 'cf_d_thigh03_R':0.0,
+ 'cf_s_thigh03_R':0.0,
+ 'cf_j_leg01_R':0.0,
+ 'cf_d_kneeF_R':0.0,
+ 'cf_d_leg02_R':-3.092561655648751e-07,
+ 'cf_s_leg02_R':-3.092561655648751e-07,
+ 'cf_d_leg03_R':-4.125216790384911e-08,
+ 'cf_s_leg03_R':-4.125216790384911e-08,
+ 'cf_j_leg03_R':-6.69778160045098e-07,
+ 'cf_j_foot_R':-6.185123311297502e-07,
+ 'cf_j_toes_R':-6.185122742863314e-07,
+ 'cf_s_leg01_R':-8.901179133911008e-16,
+ 'cf_s_kneeB_R':-8.901180192702192e-16,
+ 'cf_s_waist02':0.0,
+ 'cf_s_leg_L':-0.004678195342421532,
+ 'cf_s_leg_R':0.004779986571520567,
+ 'cf_s_waist01':0.0,
+ }
+
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ for bone in reroll_data:
+ if armature.data.edit_bones.get(bone):
+ armature.data.edit_bones[bone].roll = reroll_data[bone]
+ c.print_timer('set_bone_roll_data')
+
+ def bend_bones_for_iks(self):
+ '''slightly modify the armature to support IKs'''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A', 'B']:
+ return
+
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ armature.data.edit_bones['cf_n_height'].parent = None
+ armature.data.edit_bones['cf_j_root'].parent = armature.data.edit_bones['cf_pv_root']
+ armature.data.edit_bones['p_cf_body_bone'].parent = armature.data.edit_bones['cf_pv_root']
+ #relocate the tail of some bones to make IKs easier
+ def relocate_tail(bone1, bone2, direction):
+ if direction == 'leg':
+ # 使用 better_fbx 数据而不是简单地对齐到目标骨骼
+ if bone1 in better_fbx_bone_tails:
+ print(f"[KKBP] 保留 {bone1} 的 better_fbx 骨骼方向")
+ # 不修改尾部,保留之前设置的方向
+ pass
+ else:
+ armature.data.edit_bones[bone1].tail.z = armature.data.edit_bones[bone2].head.z
+ armature.data.edit_bones[bone1].roll = 0
+ #move the bone forward a bit or the ik bones might not bend correctly
+ armature.data.edit_bones[bone1].head.y += -0.01
+ elif direction == 'arm':
+ armature.data.edit_bones[bone1].tail.x = armature.data.edit_bones[bone2].head.x
+ armature.data.edit_bones[bone1].tail.z = armature.data.edit_bones[bone2].head.z
+ armature.data.edit_bones[bone1].roll = -math.pi/2
+ elif direction == 'hand':
+ armature.data.edit_bones[bone1].tail = armature.data.edit_bones[bone2].tail
+ #make hand bone shorter so you can easily click the hand and the pv bone
+ armature.data.edit_bones[bone1].tail.z += .01
+ armature.data.edit_bones[bone1].head = armature.data.edit_bones[bone2].head
+ else:
+ # 使用 better_fbx 数据而不是简单地对齐到目标骨骼
+ if bone1 in better_fbx_bone_tails:
+ print(f"[KKBP] 保留 {bone1} 的 better_fbx 骨骼方向")
+ # 不修改尾部,保留之前设置的方向
+ pass
+ else:
+ armature.data.edit_bones[bone1].tail.y = armature.data.edit_bones[bone2].head.y
+ armature.data.edit_bones[bone1].tail.z = armature.data.edit_bones[bone2].head.z
+ armature.data.edit_bones[bone1].roll = 0
+ relocate_tail('cf_j_leg01_R', 'cf_j_foot_R', 'leg')
+ relocate_tail('cf_j_leg01_L', 'cf_j_foot_L', 'leg')
+ relocate_tail('cf_j_forearm01_R', 'cf_j_hand_R', 'arm')
+ relocate_tail('cf_j_forearm01_L', 'cf_j_hand_L', 'arm')
+ relocate_tail('cf_pv_hand_R', 'cf_j_hand_R', 'hand')
+ relocate_tail('cf_pv_hand_L', 'cf_j_hand_L', 'hand')
+ relocate_tail('cf_j_foot_R', 'cf_j_toes_R', 'foot')
+ relocate_tail('cf_j_foot_L', 'cf_j_toes_L', 'foot')
+ c.print_timer('bend_bones_for_iks')
+
+ def remove_empty_vertex_groups(self):
+ '''check body for groups with no vertexes. Delete if the group is not a bone on the armature'''
+ body = c.get_body()
+ vertexWeightMap = self.survey_vertexes(body)
+ bones_in_armature = [bone.name for bone in c.get_armature().data.bones]
+ for group in vertexWeightMap:
+ if group not in bones_in_armature and vertexWeightMap[group] == False and 'cf_J_Vagina' not in group:
+ body.vertex_groups.remove(body.vertex_groups[group])
+ c.print_timer('remove_empty_vertex_groups')
+
+ def reorganize_armature_layers(self):
+ '''Moves all bones to different armature layers'''
+ armature = c.get_armature()
+ if bpy.app.version[0] == 3:
+ c.switch(armature, 'pose')
+ else:
+ c.switch(armature, 'object')
+
+ core_list = self.get_bone_list('core_list')
+ non_ik = self.get_bone_list('non_ik')
+ toe_list = self.get_bone_list('toe_list')
+ bp_list = self.get_bone_list('bp_list')
+ eye_list = self.get_bone_list('eye_list')
+ mouth_list = self.get_bone_list('mouth_list')
+ skirt_list = self.get_bone_list('skirt_list')
+ tongue_list = self.get_bone_list('tongue_list')
+
+ #throw all bones to armature layer 11
+ for bone in bpy.data.armatures[0].bones:
+ self.set_armature_layer(bone.name, show_layer = 10)
+ #reshow cf_hit_ bones on layer 12
+ for bone in [bones for bones in bpy.data.armatures[0].bones if 'cf_hit_' in bones.name]:
+ self.set_armature_layer(bone.name, show_layer = 11)
+ #reshow k_f_ bones on layer 13
+ for bone in [bones for bones in bpy.data.armatures[0].bones if 'k_f_' in bones.name]:
+ self.set_armature_layer(bone.name, show_layer = 12)
+ #reshow core bones on layer 1
+ for bone in core_list:
+ self.set_armature_layer(bone, show_layer = 0)
+ #reshow non_ik bones on layer 2
+ for bone in non_ik:
+ self.set_armature_layer(bone, show_layer = 1)
+ #Put the charamaker bones on layer 3
+ for bone in [bones for bones in bpy.data.armatures[0].bones if 'cf_s_' in bones.name]:
+ self.set_armature_layer(bone.name, show_layer = 2)
+ #Put the deform bones on layer 4
+ for bone in [bones for bones in bpy.data.armatures[0].bones if 'cf_d_' in bones.name]:
+ self.set_armature_layer(bone.name, show_layer = 3)
+ try:
+ #Put the better penetration bones on layer 5
+ for bone in bp_list:
+ self.set_armature_layer(bone, show_layer = 4)
+ #rename the bones so you can mirror them over the x axis in pose mode
+ if 'Vagina_L_' in bone or 'Vagina_R_' in bone:
+ bpy.data.armatures[0].bones[bone].name = 'Vagina' + bone[8:] + '_' + bone[7]
+ #Put the toe bones on layer 5
+ for bone in toe_list:
+ self.set_armature_layer(bone, show_layer = 4)
+ except:
+ #this armature isn't a BP armature
+ pass
+ #Put the upper eye bones on layer 17
+ for bone in eye_list:
+ self.set_armature_layer(bone, show_layer = 16)
+ #Put the lower mouth bones on layer 18
+ for bone in mouth_list:
+ self.set_armature_layer(bone, show_layer = 17)
+ #Put the tongue rig bones on layer 19
+ for bone in tongue_list:
+ self.set_armature_layer(bone, show_layer = 18)
+ #Put the skirt bones on layer 9
+ for bone in skirt_list:
+ self.set_armature_layer(bone, show_layer = 8)
+ #put accessory bones on layer 10 during reshow_accessory_bones() later on
+ #Make all bone layers visible for now
+ all_layers = [
+ True, True, True, True, True, False, False, False, #body
+ True, True, True, False, False, False, False, False, #clothes
+ True, True, False, False, False, False, False, False, #face
+ False, False, False, False, False, False, False, False]
+ if bpy.app.version[0] == 3:
+ bpy.ops.armature.armature_layers(layers=all_layers)
+ else:
+ for index, show_layer in enumerate(all_layers):
+ if armature.data.collections.get(str(index)):
+ armature.data.collections.get(str(index)).is_visible = show_layer
+ armature.data.display_type = 'STICK'
+ c.switch(armature, 'object')
+ c.print_timer('reorganize_armature_layers')
+
+ def move_accessory_bones_to_layer10(self):
+ '''Moves the accessory bones that have weight to them to armature layer 10'''
+ armature = c.get_armature()
+ c.switch(armature, 'object')
+ #go through each outfit and move ALL accessory bones to layer 10
+ dont_move_these = [
+ 'cf_pv', 'Eyesx',
+ 'cf_J_hitomi_tx_', 'cf_J_FaceRoot', 'cf_J_FaceUp_t',
+ 'n_cam', 'EyesLookTar', 'N_move', 'a_n_', 'cf_hit',
+ 'cf_j_bnip02', 'cf_j_kokan', 'cf_j_ana']
+ outfits = c.get_outfits()
+ outfits.extend(c.get_alts())
+ outfits.extend(c.get_hairs())
+ for outfit_or_hair in outfits:
+ # Find empty vertex groups
+ vertexWeightMap = self.survey_vertexes(outfit_or_hair)
+ #add outfit id to all accessory bones used by that outfit in an array
+ if bpy.app.version[0] == 3:
+ for bone in [bone for bone in armature.data.bones if bone.layers[10]]:
+ no_move_bone = False
+ for this_prefix in dont_move_these:
+ if this_prefix in bone.name:
+ no_move_bone = True
+ if not no_move_bone and vertexWeightMap.get(bone.name):
+ try:
+ outfit_id_array = bone['id'].to_list()
+ outfit_id_array.append(outfit_or_hair['id'])
+ bone['id'] = outfit_id_array
+ except:
+ bone['id'] = [outfit_or_hair['id']]
+ else:
+ for bone in [bone for bone in armature.data.bones if bone.collections.get('10')]:
+ no_move_bone = False
+ for this_prefix in dont_move_these:
+ if this_prefix in bone.name:
+ no_move_bone = True
+ if not no_move_bone and vertexWeightMap.get(bone.name):
+ try:
+ outfit_id_array = bone['id'].to_list()
+ outfit_id_array.append(outfit_or_hair['id'])
+ bone['id'] = outfit_id_array
+ except:
+ bone['id'] = [outfit_or_hair['id']]
+ #move accessory bones to armature layer 10
+ for bone in [bone for bone in armature.data.bones if bone.get('id')]:
+ self.set_armature_layer(bone.name, show_layer = 9)
+ c.print_timer('move_accessory_bones_to_layer10')
+
+ def rename_mmd_bones(self):
+ '''renames japanese name field for importing vmds via mmd tools
+ these names are separate from Blender's bone names'''
+ pmx_rename_dict = {
+ '全ての親':'p_cf_body_bone',
+ 'センター':'cf_j_hips',
+ '上半身':'cf_j_spine01',
+ '上半身2':'cf_j_spine02',
+ '首':'cf_j_neck',
+ '頭':'cf_j_head',
+ '両目':'Eyesx',
+ '左目':'cf_J_hitomi_tx_L',
+ '右目':'cf_J_hitomi_tx_R',
+ '左腕':'cf_j_arm00_L',
+ '右腕':'cf_j_arm00_R',
+ '左ひじ':'cf_j_forearm01_L',
+ '右ひじ':'cf_j_forearm01_R',
+ '左肩':'cf_j_shoulder_L',
+ '右肩':'cf_j_shoulder_R',
+ '左手首':'cf_j_hand_L',
+ '右手首':'cf_j_hand_R',
+ '左親指0':'cf_j_thumb01_L',
+ '左親指1':'cf_j_thumb02_L',
+ '左親指2':'cf_j_thumb03_L',
+ '左薬指1':'cf_j_ring01_L',
+ '左薬指2':'cf_j_ring02_L',
+ '左薬指3':'cf_j_ring03_L',
+ '左中指1':'cf_j_middle01_L',
+ '左中指2':'cf_j_middle02_L',
+ '左中指3':'cf_j_middle03_L',
+ '左小指1':'cf_j_little01_L',
+ '左小指2':'cf_j_little02_L',
+ '左小指3':'cf_j_little03_L',
+ '左人指1':'cf_j_index01_L',
+ '左人指2':'cf_j_index02_L',
+ '左人指3':'cf_j_index03_L',
+ '右親指0':'cf_j_thumb01_R',
+ '右親指1':'cf_j_thumb02_R',
+ '右親指2':'cf_j_thumb03_R',
+ '右薬指1':'cf_j_ring01_R',
+ '右薬指2':'cf_j_ring02_R',
+ '右薬指3':'cf_j_ring03_R',
+ '右中指1':'cf_j_middle01_R',
+ '右中指2':'cf_j_middle02_R',
+ '右中指3':'cf_j_middle03_R',
+ '右小指1':'cf_j_little01_R',
+ '右小指2':'cf_j_little02_R',
+ '右小指3':'cf_j_little03_R',
+ '右人指1':'cf_j_index01_R',
+ '右人指2':'cf_j_index02_R',
+ '右人指3':'cf_j_index03_R',
+ '下半身':'cf_j_waist01',
+ '左足':'cf_j_thigh00_L',
+ '右足':'cf_j_thigh00_R',
+ '左ひざ':'cf_j_leg01_L',
+ '右ひざ':'cf_j_leg01_R',
+ '左足首':'cf_j_leg03_L',
+ '右足首':'cf_j_leg03_R',
+ }
+ armature = c.get_armature()
+ for bone in pmx_rename_dict:
+ if armature.pose.bones.get(pmx_rename_dict[bone]):
+ armature.pose.bones[pmx_rename_dict[bone]].mmd_bone.name_j = bone
+ c.print_timer('rename_mmd_bones')
+
+ def apply_better_fbx_bone_orientations(self):
+ '''统一应用 better_fbx 骨骼方向,确保不被其他步骤覆盖'''
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+
+ applied_count = 0
+ skipped_count = 0
+
+ print("[KKBP] 应用 better_fbx 骨骼方向...")
+
+ # 遍历字典中的所有骨骼并应用方向
+ for bone_name, offset in better_fbx_bone_tails.items():
+ if bone_name in armature.data.edit_bones:
+ bone = armature.data.edit_bones[bone_name]
+ bone.tail = bone.head + Vector(offset)
+ applied_count += 1
+ else:
+ skipped_count += 1
+
+ print(f"[KKBP] 骨骼方向应用完成: {applied_count} 个已应用, {skipped_count} 个跳过")
+ print(f"[KKBP] 骨架总骨骼数: {len(armature.data.edit_bones)}")
+
+ c.switch(armature, 'object')
+ c.print_timer('apply_better_fbx_bone_orientations')
+
+ def visually_connect_bones(self):
+ '''make sure certain bones are visually connected'''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A', 'B']:
+ return
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ # Make sure all toe bones are visually correct if using the better penetration armature
+ try:
+ armature.data.edit_bones['Toes4_L'].tail.y = armature.data.edit_bones['Toes30_L'].head.y
+ armature.data.edit_bones['Toes4_L'].tail.z = armature.data.edit_bones['Toes30_L'].head.z*.8
+ armature.data.edit_bones['Toes0_L'].tail.y = armature.data.edit_bones['Toes10_L'].head.y
+ armature.data.edit_bones['Toes0_L'].tail.z = armature.data.edit_bones['Toes30_L'].head.z*.9
+
+ armature.data.edit_bones['Toes30_L'].tail.z = armature.data.edit_bones['Toes30_L'].head.z*0.8
+ armature.data.edit_bones['Toes30_L'].tail.y = armature.data.edit_bones['Toes30_L'].head.y*1.2
+ armature.data.edit_bones['Toes20_L'].tail.z = armature.data.edit_bones['Toes20_L'].head.z*0.8
+ armature.data.edit_bones['Toes20_L'].tail.y = armature.data.edit_bones['Toes20_L'].head.y*1.2
+ armature.data.edit_bones['Toes10_L'].tail.z = armature.data.edit_bones['Toes10_L'].head.z*0.8
+ armature.data.edit_bones['Toes10_L'].tail.y = armature.data.edit_bones['Toes10_L'].head.y*1.2
+
+ armature.data.edit_bones['Toes4_R'].tail.y = armature.data.edit_bones['Toes30_R'].head.y
+ armature.data.edit_bones['Toes4_R'].tail.z = armature.data.edit_bones['Toes30_R'].head.z*.8
+ armature.data.edit_bones['Toes0_R'].tail.y = armature.data.edit_bones['Toes10_R'].head.y
+ armature.data.edit_bones['Toes0_R'].tail.z = armature.data.edit_bones['Toes30_R'].head.z*.9
+
+ armature.data.edit_bones['Toes30_R'].tail.z = armature.data.edit_bones['Toes30_R'].head.z*0.8
+ armature.data.edit_bones['Toes30_R'].tail.y = armature.data.edit_bones['Toes30_R'].head.y*1.2
+ armature.data.edit_bones['Toes20_R'].tail.z = armature.data.edit_bones['Toes20_R'].head.z*0.8
+ armature.data.edit_bones['Toes20_R'].tail.y = armature.data.edit_bones['Toes20_R'].head.y*1.2
+ armature.data.edit_bones['Toes10_R'].tail.z = armature.data.edit_bones['Toes10_R'].head.z*0.8
+ armature.data.edit_bones['Toes10_R'].tail.y = armature.data.edit_bones['Toes10_R'].head.y*1.2
+ except:
+ #this character isn't using the BP/toe control armature
+ c.kklog('No toe bones detected. Skipping...', type = 'warn')
+ pass
+ c.switch(armature, 'object')
+ c.print_timer('visually_connect_bones')
+
+ def shorten_kokan_bone(self):
+ '''make the kokan bone shorter if it's on the armature'''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A', 'B']:
+ return
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ if armature.data.edit_bones.get('cf_j_kokan'):
+ armature.data.edit_bones['cf_j_kokan'].tail.z = armature.data.edit_bones['cf_s_waist02'].head.z
+ c.print_timer('shorten_kokan_bone')
+
+ def scale_skirt_and_face_bones(self):
+ '''scales skirt bones and face bones down. Scales BP bones down if exists'''
+
+ #skip this operation if this is the pmx or koikatsu armature
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A', 'B']:
+ return
+
+ armature = c.get_armature()
+ c.switch(armature, 'pose')
+
+ def shorten_bone(bone, scale):
+ c.switch(armature, 'edit')
+ armature.data.edit_bones[bone].select_head = True
+ armature.data.edit_bones[bone].select_tail = True
+ previous_roll = armature.data.edit_bones[bone].roll + 1 #roll doesn't save if you don't add a number at the end
+ armature.data.edit_bones[bone].tail = (armature.data.edit_bones[bone].tail+armature.data.edit_bones[bone].head)/2
+ armature.data.edit_bones[bone].tail = (armature.data.edit_bones[bone].tail+armature.data.edit_bones[bone].head)/2
+ armature.data.edit_bones[bone].tail = (armature.data.edit_bones[bone].tail+armature.data.edit_bones[bone].head)/2
+ armature.data.edit_bones[bone].select_head = False
+ armature.data.edit_bones[bone].select_tail = False
+ armature.data.edit_bones[bone].roll = previous_roll - 1 #subtract the number at the end to save roll
+ c.switch(armature, 'pose')
+
+ def connect_bone(root, chain):
+ bone = 'cf_j_sk_0'+str(root)+'_0'+str(chain)
+ child_bone = 'cf_j_sk_0'+str(root)+'_0'+str(chain+1)
+ #first connect tail to child bone to keep head in place during connection
+ c.switch(armature, 'edit')
+ if armature.data.edit_bones.get(bone) and armature.data.edit_bones.get(child_bone) and chain <= 4:
+ armature.data.edit_bones[bone].tail = armature.data.edit_bones[child_bone].head
+ #then connect child head to parent tail (both are at the same position, so head doesn't move)
+ armature.data.edit_bones[child_bone].use_connect = True
+
+ skirtchain = [0,1,2,3,4,5,6,7]
+ skirtchild = [0,1,2,3,4]
+ try:
+ for root in skirtchain:
+ for chain in skirtchild:
+ connect_bone(root, chain)
+ except:
+ c.kklog('No skirt bones detected. Skipping...', type = 'warn')
+
+ #scale eye bones, mouth bones, eyebrow bones
+ c.switch(armature, 'pose')
+
+ eyebones = [1,2,3,4,5,6,7,8]
+
+ for piece in eyebones:
+ bpy.ops.pose.select_all(action='DESELECT')
+ left = 'cf_J_Eye0'+str(piece)+'_s_L'
+ right = 'cf_J_Eye0'+str(piece)+'_s_R'
+
+ shorten_bone(left, 0.1)
+ shorten_bone(right, 0.1)
+
+ restOfFace = [
+ 'cf_J_Mayu_R', 'cf_J_MayuMid_s_R', 'cf_J_MayuTip_s_R',
+ 'cf_J_Mayu_L', 'cf_J_MayuMid_s_L', 'cf_J_MayuTip_s_L',
+ 'cf_J_Mouth_R', 'cf_J_Mouth_L',
+ 'cf_J_Mouthup', 'cf_J_MouthLow', 'cf_J_MouthMove', 'cf_J_MouthCavity']
+
+ for bone in restOfFace:
+ bpy.ops.pose.select_all(action='DESELECT')
+ shorten_bone(bone, 0.1)
+
+ #move eye bone location
+ c.switch(armature, 'edit')
+
+ for eyebone in ['Eyesx', 'Eye Controller']:
+ armature.data.edit_bones[eyebone].head.y = armature.data.edit_bones['cf_d_bust02_R'].tail.y
+ armature.data.edit_bones[eyebone].tail.y = armature.data.edit_bones['cf_d_bust02_R'].tail.y*1.5
+ armature.data.edit_bones[eyebone].tail.z = armature.data.edit_bones['cf_J_Nose_tip'].tail.z
+ armature.data.edit_bones[eyebone].head.z = armature.data.edit_bones['cf_J_Nose_tip'].tail.z
+
+ #scale BP bones if they exist
+ BPList = ['cf_j_kokan', 'cf_j_ana', 'Vagina_Root', 'Vagina_B', 'Vagina_F', 'Vagina_001_L', 'Vagina_002_L', 'Vagina_003_L', 'Vagina_004_L', 'Vagina_005_L', 'Vagina_001_R', 'Vagina_002_R', 'Vagina_003_R', 'Vagina_004_R', 'Vagina_005_R']
+ for bone in BPList:
+ if armature.data.edit_bones.get(bone):
+ armature.data.edit_bones[bone].tail.z = armature.data.edit_bones[bone].tail.z*.95
+ c.print_timer('scale_skirt_and_face_bones')
+
+ def create_eye_reference_bone(self):
+ '''Create a bone called "Eyesx that will act as a fixed reference bone for the Eye controller" '''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A', 'B']:
+ return
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ new_bone = armature.data.edit_bones.new('Eyesx')
+ new_bone.head = armature.data.edit_bones['cf_hit_head'].tail
+ new_bone.head.y = new_bone.head.y + 0.05
+ new_bone.tail = armature.data.edit_bones['cf_J_Mayu_R'].tail
+ new_bone.tail.x = new_bone.head.x
+ new_bone.tail.y = new_bone.head.y
+ new_bone.parent = armature.data.edit_bones['cf_j_head']
+ c.print_timer('create_eye_reference_bone')
+
+ def create_eye_controller_bone(self):
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A', 'B']:
+ return
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+
+ #roll the eye bone based on armature, create a copy and name it eye controller
+ armature_data = armature.data
+ armature_data.edit_bones['Eyesx'].roll = -math.pi/2
+ copy = self.new_bone('Eye Controller')
+ copy.head = armature_data.edit_bones['Eyesx'].head/2
+ copy.tail = armature_data.edit_bones['Eyesx'].tail/2
+ copy.matrix = armature_data.edit_bones['Eyesx'].matrix
+ copy.parent = armature_data.edit_bones['cf_j_head']
+ armature_data.edit_bones['Eye Controller'].roll = -math.pi/2
+
+ c.switch(armature, 'pose')
+ #Lock y location at zero
+ armature.pose.bones['Eye Controller'].lock_location[1] = True
+ #Hide the original Eyesx bone
+ armature.data.bones['Eyesx'].hide = True
+ self.set_armature_layer('Eye Controller', 0)
+ c.switch(armature, 'object')
+
+ #Create a UV warp modifier for the eyes. Controlled by the Eye controller bone
+ def eyeUV(modifiername, eyevertexgroup):
+ mod = c.get_body().modifiers.new(modifiername, 'UV_WARP')
+ mod.axis_u = 'Z'
+ mod.axis_v = 'X'
+ mod.object_from = armature
+ mod.bone_from = armature.data.bones['Eyesx'].name
+ mod.object_to = armature
+ mod.bone_to = armature.data.bones['Eye Controller'].name
+ mod.vertex_group = eyevertexgroup
+ mod.uv_layer = 'UVMap'
+ mod.show_expanded = False
+
+ eyeUV("Left Eye UV warp", 'Left Eye')
+ eyeUV("Right Eye UV warp", 'Right Eye')
+ c.print_timer('create_eye_controller_bone')
+
+ def prepare_ik_bones(self):
+ '''reparents some bones to work for IK'''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ return
+ #Select the armature and make it active
+ armature = c.get_armature()
+ c.switch(armature, 'edit')
+ bpy.context.scene.cursor.location = (0.0, 0.0, 0.0)
+ #separate the PV bones, so the elbow IKs rotate with the spine
+ pvrootupper = self.new_bone('cf_pv_root_upper')
+ pvrootupper.tail = armature.data.edit_bones['cf_pv_root'].tail
+ pvrootupper.head = armature.data.edit_bones['cf_pv_root'].head
+ #reparent things
+ def reparent(bone,newparent):
+ #refresh armature by going to object mode then back to edit mode?
+ c.switch(armature, 'edit')
+ armature.data.edit_bones[bone].parent = armature.data.edit_bones[newparent]
+ reparent('cf_pv_root_upper', 'cf_j_spine01')
+ reparent('cf_pv_elbo_R', 'cf_pv_root_upper')
+ reparent('cf_pv_elbo_L', 'cf_pv_root_upper')
+ c.print_timer('prepare_ik_bones')
+
+ def create_ik_bones(self):
+ '''give the leg a foot IK, the foot a heel controller, and the arm a hand IK'''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ return
+
+ def legIK(legbone, IKtarget, IKpole, IKpoleangle, footIK, kneebone, toebone, footbone):
+ bone = c.get_armature().pose.bones[legbone]
+
+ #Make IK
+ ik = bone.constraints.new("IK")
+ ik.name = "IK"
+
+ #Set target and subtarget
+ bone.constraints["IK"].target = c.get_armature()
+ bone.constraints["IK"].subtarget = c.get_armature().data.bones[IKtarget].name
+
+ #Set pole and subpole and pole angle
+ bone.constraints["IK"].pole_target = c.get_armature()
+ bone.constraints["IK"].pole_subtarget = c.get_armature().data.bones[IKpole].name
+ bone.constraints["IK"].pole_angle = IKpoleangle
+
+ #Set chain length
+
+ bone.constraints["IK"].chain_count=2
+
+ #Flip foot IK to match foot bone
+ bone = c.get_armature().data.edit_bones[footIK]
+
+ bone = c.get_armature().data.edit_bones[footIK]
+ bone.head.y = c.get_armature().data.edit_bones[kneebone].tail.y
+ bone = c.get_armature().data.edit_bones[footIK]
+ bone.tail.z = c.get_armature().data.edit_bones[toebone].head.z
+ bone = c.get_armature().data.edit_bones[footIK]
+ bone.head.z = c.get_armature().data.edit_bones[footbone].head.z
+
+ bone = c.get_armature().data.edit_bones[footIK]
+ bone.head.x = c.get_armature().data.edit_bones[kneebone].tail.x
+ bone = c.get_armature().data.edit_bones[footIK]
+ bone.tail.x = bone.head.x
+
+ #unparent the bone
+ center_bone = c.get_armature().data.edit_bones['cf_n_height']
+ bone = c.get_armature().data.edit_bones[footIK]
+ bone.parent = center_bone
+
+ #Run for each side
+ legIK('cf_j_leg01_R', 'cf_pv_foot_R', 'cf_pv_knee_R', math.pi/2, 'cf_pv_foot_R', 'cf_j_leg01_R', 'cf_j_toes_R', 'cf_j_foot_R')
+ legIK('cf_j_leg01_L', 'cf_pv_foot_L', 'cf_pv_knee_L', math.pi/2, 'cf_pv_foot_L', 'cf_j_leg01_L', 'cf_j_toes_L', 'cf_j_foot_L')
+
+ #adds an IK for the toe bone, moves the knee IKs a little closer to the body
+ def footIK(footbone, toebone, footIK, kneebone, legbone):
+ bone = c.get_armature().pose.bones[footbone]
+
+ #Make Copy rotation
+ bone.constraints.new("COPY_ROTATION")
+
+ #Set target and subtarget
+ bone.constraints[0].target=c.get_armature()
+ bone.constraints[0].subtarget = c.get_armature().data.bones[footIK].name
+
+ #Set the rotation to local space
+ bone.constraints[0].target_space = 'LOCAL_WITH_PARENT'
+ bone.constraints[0].owner_space = 'LOCAL_WITH_PARENT'
+
+ # move knee IKs closer to body
+ kneedist = round((c.get_armature().pose.bones[footbone].head - c.get_armature().pose.bones[footbone].tail).length,2)
+ c.get_armature().data.edit_bones[kneebone].head.y = kneedist * -5
+ c.get_armature().data.edit_bones[kneebone].tail.y = kneedist * -5
+
+ # make toe bone shorter
+ c.get_armature().data.edit_bones[toebone].tail.z = c.get_armature().data.edit_bones[legbone].head.z * 0.2
+
+ #Run for each side
+ footIK('cf_j_foot_R', 'cf_j_toes_R', 'cf_pv_foot_R', 'cf_pv_knee_R', 'cf_j_leg01_R')
+ footIK('cf_j_foot_L', 'cf_j_toes_L', 'cf_pv_foot_L', 'cf_pv_knee_L', 'cf_j_leg01_L')
+
+ #Add a heel controller to the foot
+ #this fucking thing keeps crashing so retreive_stored_tags is called after most operations
+ def heelController(footbone, footIK, toebone):
+ #duplicate the foot IK. This is the new master bone
+ c.switch(c.get_armature(), 'edit')
+ masterbone = self.new_bone('MasterFootIK.' + footbone[-1])
+ masterbone = c.get_armature().data.edit_bones['MasterFootIK.' + footbone[-1]]
+ masterbone.head = c.get_armature().data.edit_bones[footbone].head
+ masterbone = c.get_armature().data.edit_bones['MasterFootIK.' + footbone[-1]]
+ masterbone.tail = c.get_armature().data.edit_bones[footbone].tail
+ masterbone = c.get_armature().data.edit_bones['MasterFootIK.' + footbone[-1]]
+ masterbone.matrix = c.get_armature().data.edit_bones[footbone].matrix
+ masterbone = c.get_armature().data.edit_bones['MasterFootIK.' + footbone[-1]]
+ masterbone.parent = c.get_armature().data.edit_bones['cf_n_height']
+
+ #Create the heel controller
+ heelIK = self.new_bone('HeelIK.' + footbone[-1])
+ heelIK = c.get_armature().data.edit_bones['HeelIK.' + footbone[-1]]
+ heelIK.head = c.get_armature().data.edit_bones[footbone].tail
+ heelIK = c.get_armature().data.edit_bones['HeelIK.' + footbone[-1]]
+ heelIK.tail = c.get_armature().data.edit_bones[footbone].head
+ heelIK = c.get_armature().data.edit_bones['HeelIK.' + footbone[-1]]
+ heelIK.parent = masterbone
+ heelIK = c.get_armature().data.edit_bones['HeelIK.' + footbone[-1]]
+ heelIK = c.get_armature().data.edit_bones['HeelIK.' + footbone[-1]]
+ heelIK.tail.y *= .5
+
+ #parent footIK to heel controller
+ c.get_armature().data.edit_bones[footIK].parent = heelIK
+
+ #make a bone to pin the foot
+ footPin = self.new_bone('FootPin.' + footbone[-1])
+ footPin = c.get_armature().data.edit_bones['FootPin.' + footbone[-1]]
+ footPin.head = c.get_armature().data.edit_bones[toebone].head
+ footPin = c.get_armature().data.edit_bones['FootPin.' + footbone[-1]]
+ footPin.tail = c.get_armature().data.edit_bones[toebone].tail
+ footPin = c.get_armature().data.edit_bones['FootPin.' + footbone[-1]]
+ footPin.parent = masterbone
+ footPin = c.get_armature().data.edit_bones['FootPin.' + footbone[-1]]
+ footPin.tail.z*=.8
+
+ #make a bone to allow rotation of the toe along an arc
+ toeRotator = self.new_bone('ToeRotator.' + footbone[-1])
+ toeRotator = c.get_armature().data.edit_bones['ToeRotator.' + footbone[-1]]
+ toeRotator.head = c.get_armature().data.edit_bones[toebone].head
+ toeRotator = c.get_armature().data.edit_bones['ToeRotator.' + footbone[-1]]
+ toeRotator.tail = c.get_armature().data.edit_bones[toebone].tail
+ toeRotator = c.get_armature().data.edit_bones['ToeRotator.' + footbone[-1]]
+ toeRotator.parent = masterbone
+
+ #make a bone to pin the toe
+ toePin = self.new_bone('ToePin.' + footbone[-1])
+ toePin = c.get_armature().data.edit_bones['ToePin.' + footbone[-1]]
+ toePin.head = c.get_armature().data.edit_bones[toebone].tail
+ toePin = c.get_armature().data.edit_bones['ToePin.' + footbone[-1]]
+ toePin.tail = c.get_armature().data.edit_bones[toebone].tail
+ toePin = c.get_armature().data.edit_bones['ToePin.' + footbone[-1]]
+ toePin.parent = toeRotator
+
+ toePin = c.get_armature().data.edit_bones['ToePin.' + footbone[-1]]
+ toePin.tail.z *=1.2
+
+ #pin the foot
+ c.switch(c.get_armature(), 'pose')
+ bone = c.get_armature().pose.bones[footbone]
+ ik = bone.constraints.new("IK")
+ ik.name = "IK"
+ bone = c.get_armature().pose.bones[footbone]
+ bone.constraints["IK"].target = c.get_armature()
+ bone = c.get_armature().pose.bones[footbone]
+ bone.constraints["IK"].subtarget = c.get_armature().data.bones['FootPin.' + footbone[-1]].name
+ bone = c.get_armature().pose.bones[footbone]
+ bone.constraints["IK"].chain_count=1
+
+ #pin the toe
+ bone = c.get_armature().pose.bones[toebone]
+ ik = bone.constraints.new("IK")
+ ik.name = "IK"
+ bone = c.get_armature().pose.bones[toebone]
+ bone.constraints["IK"].target = c.get_armature()
+ bone = c.get_armature().pose.bones[toebone]
+ bone.constraints["IK"].subtarget = c.get_armature().data.bones['ToePin.' + footbone[-1]].name
+ bone = c.get_armature().pose.bones[toebone]
+ bone.constraints["IK"].chain_count=1
+
+ #move these bones to armature layer 2
+ bpy.ops.object.mode_set(mode='POSE') #use this instead of c.switch to prevent crashing
+ layer2 = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+ bpy.ops.pose.select_all(action='DESELECT')
+ if bpy.app.version[0] == 3:
+ c.get_armature().data.bones['FootPin.' + footbone[-1]].select = True
+ c.get_armature().data.bones['ToePin.' + footbone[-1]].select = True
+ c.get_armature().data.bones[toebone].select = True
+ bpy.ops.pose.bone_layers(layers=layer2)
+ c.get_armature().data.bones[footIK].select = True
+ else:
+ c.get_armature().data.bones['FootPin.' + footbone[-1]].collections.clear()
+ self.set_armature_layer('FootPin.' + footbone[-1], 2)
+ c.get_armature().data.bones['ToePin.' + footbone[-1]].collections.clear()
+ self.set_armature_layer('ToePin.' + footbone[-1], 2)
+ c.get_armature().data.bones[toebone].collections.clear()
+ self.set_armature_layer(toebone, 2)
+ c.get_armature().data.bones[footIK].collections.clear()
+ self.set_armature_layer(footIK, 2)
+
+ heelController('cf_j_foot_L', 'cf_pv_foot_L', 'cf_j_toes_L')
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ heelController('cf_j_foot_R', 'cf_pv_foot_R', 'cf_j_toes_R')
+
+ #Give the new foot IKs an mmd bone name
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ c.get_armature().pose.bones['MasterFootIK.L'].mmd_bone.name_j = '左足IK'
+ c.get_armature().pose.bones['MasterFootIK.R'].mmd_bone.name_j = '右足IK'
+
+ #add an IK to the arm, makes the wrist bone copy the hand IK's rotation, moves elbow IKs a little closer to the body
+ def armhandIK(elbowbone, handcontroller, elbowcontroller, IKangle, wristbone):
+ #Set IK bone
+ bone = c.get_armature().pose.bones[elbowbone]
+
+ #Add IK
+ bone.constraints.new("IK")
+
+ #Set target and subtarget
+ bone.constraints["IK"].target = c.get_armature()
+ bone.constraints["IK"].subtarget = c.get_armature().data.bones[handcontroller].name
+
+ #Set pole and subpole and pole angle
+ bone.constraints["IK"].pole_target = c.get_armature()
+ bone.constraints["IK"].pole_subtarget = c.get_armature().data.bones[elbowcontroller].name
+ bone.constraints["IK"].pole_angle= IKangle
+
+ #Set chain length
+ bone.constraints["IK"].chain_count=2
+
+ #unparent the bone
+ c.switch(c.get_armature(), 'edit')
+
+ bone = c.get_armature().data.edit_bones[handcontroller]
+ bone.parent = c.get_armature().data.edit_bones['cf_n_height']
+ c.get_armature().data.bones[wristbone].hide = True
+
+ # move elbow IKs closer to body
+ elbowdist = round((c.get_armature().data.edit_bones[elbowbone].head - c.get_armature().data.edit_bones[elbowbone].tail).length,2)
+ c.get_armature().data.edit_bones[elbowcontroller].head.y = elbowdist*2
+ c.get_armature().data.edit_bones[elbowcontroller].tail.y = elbowdist*2
+ c.switch(c.get_armature(), 'pose')
+
+ # Set hand rotation then hide it
+ bone = c.get_armature().pose.bones[wristbone]
+ bone.constraints.new("COPY_ROTATION")
+ bone.constraints[0].target = c.get_armature()
+ bone.constraints[0].subtarget = c.get_armature().data.bones[handcontroller].name
+
+ #Run for each side
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ armhandIK('cf_j_forearm01_R', 'cf_pv_hand_R', 'cf_pv_elbo_R', 0, 'cf_j_hand_R')
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ armhandIK('cf_j_forearm01_L', 'cf_pv_hand_L', 'cf_pv_elbo_L', 180, 'cf_j_hand_L')
+
+ #move newly created bones to correct armature layers
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ self.set_armature_layer('MasterFootIK.L', 0)
+ self.set_armature_layer('MasterFootIK.R', 0)
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ self.set_armature_layer('HeelIK.L', 0)
+ self.set_armature_layer('HeelIK.R', 0)
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ self.set_armature_layer('ToeRotator.L', 0)
+ self.set_armature_layer('ToeRotator.R', 0)
+ self.set_armature_layer('cf_d_bust00', 0)
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ c.get_armature().data.bones['cf_pv_root_upper'].hide = True
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+ c.switch(c.get_armature(), 'object')
+
+ def create_joint_drivers(self):
+ '''There are several joint corrections that use the cf_d_ and cf_s_ bones on the armature. This function attempts to replicate them using blender drivers and bone constraints'''
+ if not bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ return
+ armature = c.get_armature()
+ c.switch(armature, 'pose')
+ #generic function to set a copy rotation modifier
+ def set_copy(bone, bonetarget, influence, axis = 'all', mix = 'replace', space = 'LOCAL'):
+ constraint = armature.pose.bones[bone].constraints.new("COPY_ROTATION")
+ constraint.target = armature
+ constraint.subtarget = bonetarget
+ constraint.influence = influence
+ constraint.target_space = space
+ constraint.owner_space = space
+
+ if axis == 'X':
+ constraint.use_y = False
+ constraint.use_z = False
+
+ elif axis == 'Y':
+ constraint.use_x = False
+ constraint.use_z = False
+
+ elif axis == 'antiX':
+ constraint.use_y = False
+ constraint.use_z = False
+ constraint.invert_x = True
+
+ elif axis == 'Z':
+ constraint.use_x = False
+ constraint.use_y = False
+
+ if mix == 'add':
+ constraint.mix_mode = 'ADD'
+
+ #setup most of the drivers with this
+ set_copy('cf_d_shoulder02_L', 'cf_j_arm00_L', 0.5)
+ set_copy('cf_d_arm01_L', 'cf_j_arm00_L', 0.75, axis = 'X')
+ set_copy('cf_d_arm02_L', 'cf_j_arm00_L', 0.5, axis = 'X')
+ set_copy('cf_d_arm03_L', 'cf_j_arm00_L', 0.25, axis = 'X')
+ set_copy('cf_d_forearm02_L', 'cf_j_hand_L', 0.33, axis = 'X')
+ set_copy('cf_d_wrist_L', 'cf_j_hand_L', 0.33, axis = 'X', )
+ set_copy('cf_d_kneeF_L', 'cf_j_leg01_L', 0.5, axis = 'antiX', mix = 'add')
+ set_copy('cf_d_siri_L', 'cf_j_thigh00_L', 0.33)
+ set_copy('cf_d_thigh02_L', 'cf_j_thigh00_L', 0.25, axis='Y')
+ set_copy('cf_d_thigh03_L', 'cf_j_thigh00_L', 0.25, axis='Y')
+ set_copy('cf_d_leg02_L', 'cf_j_leg01_L', 0.33, axis='Y')
+ set_copy('cf_d_leg03_L', 'cf_j_leg01_L', 0.66, axis='Y')
+
+ set_copy('cf_d_shoulder02_R', 'cf_j_arm00_R', 0.5)
+ set_copy('cf_d_arm01_R', 'cf_j_arm00_R', 0.75, axis = 'X')
+ set_copy('cf_d_arm02_R', 'cf_j_arm00_R', 0.5, axis = 'X')
+ set_copy('cf_d_arm03_R', 'cf_j_arm00_R', 0.25, axis = 'X')
+ set_copy('cf_d_forearm02_R', 'cf_j_hand_R', 0.33, axis = 'X')
+ set_copy('cf_d_wrist_R', 'cf_j_hand_R', 0.33, axis = 'X')
+ set_copy('cf_d_kneeF_R', 'cf_j_leg01_R', 0.5, axis = 'antiX', mix = 'add')
+ set_copy('cf_d_siri_R', 'cf_j_thigh00_R', 0.33)
+ set_copy('cf_d_thigh02_R', 'cf_j_thigh00_R', 0.25, axis='Y')
+ set_copy('cf_d_thigh03_R', 'cf_j_thigh00_R', 0.25, axis='Y')
+ set_copy('cf_d_leg02_R', 'cf_j_leg01_R', 0.33, axis='Y')
+ set_copy('cf_d_leg03_R', 'cf_j_leg01_R', 0.66, axis='Y')
+
+ #move the waist some if only one leg is rotated
+ set_copy('cf_s_waist02', 'cf_j_thigh00_L', 0.1, mix = 'add')
+ set_copy('cf_s_waist02', 'cf_j_thigh00_R', 0.1, mix = 'add')
+ #set_copy('cf_s_waist02', 'cf_j_thigh00_R', 0.1, mix = 'add')
+ #set_copy('cf_s_waist02', 'cf_j_thigh00_L', 0.1, mix = 'add')
+
+ set_copy('cf_s_waist02', 'cf_j_waist02', 0.5, axis = 'antiX')
+
+ #this rotation helps when doing a split
+ set_copy('cf_s_leg_L', 'cf_j_thigh00_L', .9, axis = 'Z', mix = 'add')
+ set_copy('cf_s_leg_R', 'cf_j_thigh00_R', .9, axis = 'Z', mix = 'add')
+
+ #generic function for creating a driver
+ def setDriver (bone, drivertype, drivertypeselect, drivertarget, drivertt, drivermult, expresstype = 'move'):
+
+ #add driver to first component
+ #drivertype is the kind of driver you want to be applied to the bone and can be location/rotation
+ #drivertypeselect is the component of the bone you want the driver to be applied to
+ # for location it's (0 is x component, y is 1, z is 2)
+ # for rotation it's (0 is w, 1 is x, etc)
+ # for scale it's (0 is x, 1 is y, 2 is z)
+ driver = armature.pose.bones[bone].driver_add(drivertype, drivertypeselect)
+
+ #add driver variable
+ vari = driver.driver.variables.new()
+ vari.name = 'var'
+ vari.type = 'TRANSFORMS'
+
+ #set the target and subtarget
+ target = vari.targets[0]
+ target.id = armature
+ target.bone_target = armature.pose.bones[drivertarget].name
+
+ #set the transforms for the target. this can be rotation or location
+ target.transform_type = drivertt
+
+ #set the transform space. can be world space too
+ target.transform_space = 'LOCAL_SPACE'
+ target.rotation_mode = 'QUATERNION' if expresstype in ['scale', 'quat'] else 'AUTO'
+
+ #use the distance to the target bone's parent to make results consistent for different sized bones
+ targetbonelength = str(round((armature.pose.bones[drivertarget].head - armature.pose.bones[drivertarget].parent.head).length,3))
+
+ #driver expression is the rotation value of the target bone multiplied by a percentage of the driver target bone's length
+ if expresstype in ['move', 'quat']:
+ driver.driver.expression = vari.name + '*' + targetbonelength + '*' + drivermult
+
+ #move but only during positive rotations
+ elif expresstype == 'movePos':
+ driver.driver.expression = vari.name + '*' + targetbonelength + '*' + drivermult + ' if ' + vari.name + ' > 0 else 0'
+
+ #move but only during negative rotations
+ elif expresstype == 'moveNeg':
+ driver.driver.expression = vari.name + '*' + targetbonelength + '*' + drivermult + ' if ' + vari.name + ' < 0 else 0'
+
+ #move but the ABS value
+ elif expresstype == 'moveABS':
+ driver.driver.expression = 'abs(' + vari.name + '*' + targetbonelength + '*' + drivermult +')'
+
+ #move but the negative ABS value
+ elif expresstype == 'moveABSNeg':
+ driver.driver.expression = '-abs(' + vari.name + '*' + targetbonelength + '*' + drivermult +')'
+
+ #move but exponentially
+ elif expresstype == 'moveexp':
+ driver.driver.expression = vari.name + '*' + vari.name + '*' + targetbonelength + '*' + drivermult
+
+ elif expresstype == 'scale':
+ driver.driver.expression = '1 + ' + vari.name + '*' + targetbonelength + '*' + drivermult
+
+ elif expresstype == 'rotation':
+ driver.driver.expression = vari.name + '*' + targetbonelength + '*' + drivermult
+
+ #Set the remaining joint correction drivers
+ #set knee joint corrections. These go in toward the body and down toward the foot at an exponential rate
+ setDriver('cf_s_kneeB_R', 'location', 1, 'cf_j_leg01_R', 'ROT_X', '-0.2', expresstype = 'moveexp')
+ setDriver('cf_s_kneeB_R', 'location', 2, 'cf_j_leg01_R', 'ROT_X', '-0.08')
+
+ setDriver('cf_s_kneeB_L', 'location', 1, 'cf_j_leg01_L', 'ROT_X', '-0.2', expresstype = 'moveexp')
+ setDriver('cf_s_kneeB_L', 'location', 2, 'cf_j_leg01_L', 'ROT_X', '-0.08')
+
+ #knee correction to thicken the knee in a kneeling pose if the rigify armature is being used
+ # if bpy.context.scene.kkbp.armature_dropdown == 'B' and bpy.context.scene.kkbp.categorize_dropdown in ['A', 'B', 'C']:
+ # setDriver('cf_s_leg01_R', 'scale', 2, 'cf_j_leg01_R', 'ROT_X', '1', expresstype = 'scale')
+ # setDriver('cf_s_leg01_R', 'scale', 0, 'cf_j_leg01_R', 'ROT_X', '-2', expresstype = 'scale')
+ # setDriver('cf_s_leg01_R', 'location', 2, 'cf_j_leg01_R', 'ROT_X', '0.05', expresstype='quat')
+ # setDriver('cf_d_thigh03_R', 'location', 2, 'cf_j_leg01_R', 'ROT_X', '.015')
+
+ # setDriver('cf_s_leg01_L', 'scale', 2, 'cf_j_leg01_L', 'ROT_X', '1', expresstype = 'scale')
+ # setDriver('cf_s_leg01_L', 'scale', 0, 'cf_j_leg01_L', 'ROT_X', '-2', expresstype = 'scale')
+ # setDriver('cf_s_leg01_L', 'location', 2, 'cf_j_leg01_L', 'ROT_X', '0.05', expresstype='quat')
+ # setDriver('cf_d_thigh03_L', 'location', 2, 'cf_j_leg01_L', 'ROT_X', '-.015')
+
+ #knee tip corrections go up toward the waist and in toward the body, also rotate a bit
+ setDriver('cf_d_kneeF_R', 'location', 1, 'cf_j_leg01_R', 'ROT_X', '0.02')
+ setDriver('cf_d_kneeF_R', 'location', 2, 'cf_j_leg01_R', 'ROT_X', '-0.04')
+
+ setDriver('cf_d_kneeF_L', 'location', 1, 'cf_j_leg01_L', 'ROT_X', '0.02')
+ setDriver('cf_d_kneeF_L', 'location', 2, 'cf_j_leg01_L', 'ROT_X', '-0.04')
+
+ #butt corrections go slightly up to the spine and in to the waist
+ setDriver('cf_d_siri_R', 'location', 1, 'cf_j_thigh00_R', 'ROT_X', '0.02')
+ setDriver('cf_d_siri_R', 'location', 2, 'cf_j_thigh00_R', 'ROT_X', '0.02')
+
+ setDriver('cf_d_siri_L', 'location', 1, 'cf_j_thigh00_L', 'ROT_X', '0.02')
+ setDriver('cf_d_siri_L', 'location', 2, 'cf_j_thigh00_L', 'ROT_X', '0.02')
+
+ #hand corrections go up to the head and in towards the elbow
+ setDriver('cf_d_hand_R', 'location', 0, 'cf_j_hand_R', 'ROT_Z', '-0.4', expresstype = 'moveNeg')
+ setDriver('cf_d_hand_R', 'location', 1, 'cf_j_hand_R', 'ROT_Z', '-0.4', expresstype = 'moveNeg')
+
+ setDriver('cf_d_hand_L', 'location', 0, 'cf_j_hand_L', 'ROT_Z', '-0.4', expresstype = 'movePos')
+ setDriver('cf_d_hand_L', 'location', 1, 'cf_j_hand_L', 'ROT_Z', '0.4', expresstype = 'movePos')
+
+ #elboback goes out to the chest and into the shoulder
+ #elbo goes does the opposite
+ setDriver('cf_s_elboback_R', 'location', 0, 'cf_j_forearm01_R', 'ROT_X', '-0.7')
+ setDriver('cf_s_elboback_R', 'location', 2, 'cf_j_forearm01_R', 'ROT_X', '0.6')
+ setDriver('cf_s_elbo_R', 'location', 0, 'cf_j_forearm01_R', 'ROT_X', '0.025')
+ setDriver('cf_s_elbo_R', 'location', 2, 'cf_j_forearm01_R', 'ROT_X', '0.025')
+
+ setDriver('cf_s_elboback_L', 'location', 0, 'cf_j_forearm01_L', 'ROT_X', '-0.7')
+ setDriver('cf_s_elboback_L', 'location', 2, 'cf_j_forearm01_L', 'ROT_X', '-0.6')
+ setDriver('cf_s_elbo_L', 'location', 0, 'cf_j_forearm01_L', 'ROT_X', '0.025')
+ setDriver('cf_s_elbo_L', 'location', 2, 'cf_j_forearm01_L', 'ROT_X', '-0.025')
+
+ #shoulder bones have a few corrections as well
+ setDriver('cf_d_shoulder02_R', 'location', 1, 'cf_j_arm00_R', 'ROT_Z', '-0.1', expresstype = 'moveNeg')
+ setDriver('cf_d_shoulder02_R', 'location', 0, 'cf_j_arm00_R', 'ROT_Y', '0.1', expresstype = 'moveABSNeg')
+ setDriver('cf_d_shoulder02_R', 'location', 2, 'cf_j_arm00_R', 'ROT_Y', '-0.1')
+
+ setDriver('cf_d_shoulder02_L', 'location', 1, 'cf_j_arm00_L', 'ROT_Z', '0.1', expresstype = 'movePos')
+ setDriver('cf_d_shoulder02_L', 'location', 0, 'cf_j_arm00_L', 'ROT_Y', '-0.1', expresstype = 'moveABS')
+ setDriver('cf_d_shoulder02_L', 'location', 2, 'cf_j_arm00_L', 'ROT_Y', '0.1')
+
+ #leg corrections go up to the head and slightly forwards/backwards
+ setDriver('cf_s_leg_R', 'location', 1, 'cf_j_thigh00_R', 'ROT_X', '1', expresstype = 'moveexp')
+ setDriver('cf_s_leg_R', 'location', 2, 'cf_j_thigh00_R', 'ROT_X', '-1.5')
+
+ setDriver('cf_s_leg_L', 'location', 1, 'cf_j_thigh00_L', 'ROT_X', '1', expresstype = 'moveexp')
+ setDriver('cf_s_leg_L', 'location', 2, 'cf_j_thigh00_L', 'ROT_X', '-1.5')
+
+ #waist correction slightly moves out to chest when lower waist rotates
+ setDriver('cf_s_waist02', 'location', 2, 'cf_j_waist02', 'ROT_X', '0.2', expresstype='moveABS')
+
+ def categorize_bones(self):
+ '''Add some bones to bone groups to give them colors'''
+ if bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ armature = c.get_armature()
+ c.switch(armature, 'pose')
+ if bpy.app.version[0] == 3:
+ bpy.ops.pose.group_add()
+ group_index = len(armature.pose.bone_groups)-1
+ group = armature.pose.bone_groups[group_index]
+ group.name = 'IK controllers'
+ armature.data.bones['cf_pv_hand_L'].select = True
+ armature.data.bones['cf_pv_hand_R'].select = True
+ armature.data.bones['MasterFootIK.L'].select = True
+ armature.data.bones['MasterFootIK.R'].select = True
+ bpy.ops.pose.group_assign(type=group_index+1)
+ group.color_set = 'THEME01'
+
+ c.switch(armature, 'pose')
+ bpy.ops.pose.group_add()
+ group_index = len(armature.pose.bone_groups)-1
+ group = armature.pose.bone_groups[group_index]
+ group.name = 'IK poles'
+ armature.pose.bone_groups.active_index = 1
+ armature.data.bones['cf_pv_elbo_R'].select = True
+ armature.data.bones['cf_pv_elbo_L'].select = True
+ armature.data.bones['cf_pv_knee_R'].select = True
+ armature.data.bones['cf_pv_knee_L'].select = True
+ bpy.ops.pose.group_assign(type=group_index+1)
+ group.color_set = 'THEME09'
+ else:
+ group_name = 'IK controllers'
+ for bone in ['cf_pv_hand_L', 'cf_pv_hand_R', 'MasterFootIK.L', 'MasterFootIK.R']:
+ self.set_armature_layer(bone, group_name)
+ armature.data.bones[bone].color.palette = 'THEME01'
+
+ group_name = 'IK poles'
+ for bone in ['cf_pv_elbo_R', 'cf_pv_elbo_L', 'cf_pv_knee_R', 'cf_pv_knee_L']:
+ self.set_armature_layer(bone, group_name)
+ armature.data.bones[bone].color.palette = 'THEME09'
+
+ def rename_bones_for_clarity(self):
+ '''rename core bones for easier identification. Also allows Unity to automatically detect each bone in a humanoid armature'''
+ if bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ unity_rename_dict = {
+ 'cf_n_height':'Center',
+ 'cf_j_hips':'Hips',
+ 'cf_j_waist01':'Pelvis',
+ 'cf_j_spine01':'Spine',
+ 'cf_j_spine02':'Chest',
+ 'cf_j_spine03':'Upper Chest',
+ 'cf_j_neck':'Neck',
+ 'cf_j_head':'Head',
+ 'cf_j_shoulder_L':'Left shoulder',
+ 'cf_j_shoulder_R':'Right shoulder',
+ 'cf_j_arm00_L':'Left arm',
+ 'cf_j_arm00_R':'Right arm',
+ 'cf_j_forearm01_L':'Left elbow',
+ 'cf_j_forearm01_R':'Right elbow',
+ 'cf_j_hand_R':'Right wrist',
+ 'cf_j_hand_L':'Left wrist',
+ 'cf_J_hitomi_tx_L':'Left Eye',
+ 'cf_J_hitomi_tx_R':'Right Eye',
+
+ 'cf_j_thumb01_L':'Thumb0_L',
+ 'cf_j_thumb02_L':'Thumb1_L',
+ 'cf_j_thumb03_L':'Thumb2_L',
+ 'cf_j_ring01_L':'RingFinger1_L',
+ 'cf_j_ring02_L':'RingFinger2_L',
+ 'cf_j_ring03_L':'RingFinger3_L',
+ 'cf_j_middle01_L':'MiddleFinger1_L',
+ 'cf_j_middle02_L':'MiddleFinger2_L',
+ 'cf_j_middle03_L':'MiddleFinger3_L',
+ 'cf_j_little01_L':'LittleFinger1_L',
+ 'cf_j_little02_L':'LittleFinger2_L',
+ 'cf_j_little03_L':'LittleFinger3_L',
+ 'cf_j_index01_L':'IndexFinger1_L',
+ 'cf_j_index02_L':'IndexFinger2_L',
+ 'cf_j_index03_L':'IndexFinger3_L',
+
+ 'cf_j_thumb01_R':'Thumb0_R',
+ 'cf_j_thumb02_R':'Thumb1_R',
+ 'cf_j_thumb03_R':'Thumb2_R',
+ 'cf_j_ring01_R':'RingFinger1_R',
+ 'cf_j_ring02_R':'RingFinger2_R',
+ 'cf_j_ring03_R':'RingFinger3_R',
+ 'cf_j_middle01_R':'MiddleFinger1_R',
+ 'cf_j_middle02_R':'MiddleFinger2_R',
+ 'cf_j_middle03_R':'MiddleFinger3_R',
+ 'cf_j_little01_R':'LittleFinger1_R',
+ 'cf_j_little02_R':'LittleFinger2_R',
+ 'cf_j_little03_R':'LittleFinger3_R',
+ 'cf_j_index01_R':'IndexFinger1_R',
+ 'cf_j_index02_R':'IndexFinger2_R',
+ 'cf_j_index03_R':'IndexFinger3_R',
+
+ 'cf_j_thigh00_L':'Left leg',
+ 'cf_j_thigh00_R':'Right leg',
+ 'cf_j_leg01_L':'Left knee',
+ 'cf_j_leg01_R':'Right knee',
+ 'cf_j_foot_L':'Left ankle',
+ 'cf_j_foot_R':'Right ankle',
+ 'cf_j_toes_L':'Left toe',
+ 'cf_j_toes_R':'Right toe'
+ }
+ for bone in unity_rename_dict:
+ if c.get_armature().data.bones.get(bone):
+ c.get_armature().data.bones[bone].name = unity_rename_dict[bone]
+
+ #reset the eye vertex groups after renaming the bones
+ mod = c.get_body().modifiers[1]
+ mod.vertex_group = 'Left Eye'
+ mod = c.get_body().modifiers[2]
+ mod.vertex_group = 'Right Eye'
+
+ def apply_bone_widgets(self):
+ '''apply custom bone shapes from library file'''
+ if bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ #Import custom bone shapes
+ c.import_from_library_file('Collection', ['Bone Widgets'], use_fake_user=False)
+
+ #Add custom shapes to the armature
+ armature = c.get_armature()
+ armature.data.show_bone_custom_shapes = True
+ c.switch(armature, 'pose')
+
+ armature.pose.bones["Spine"].custom_shape = bpy.data.objects["WidgetChest"]
+ armature.pose.bones["Chest"].custom_shape = bpy.data.objects["WidgetChest"]
+ armature.pose.bones["Upper Chest"].custom_shape = bpy.data.objects["WidgetChest"]
+
+ armature.pose.bones["cf_d_bust00"].custom_shape = bpy.data.objects["WidgetBust"]
+ armature.pose.bones["cf_d_bust00"].use_custom_shape_bone_size = False
+ armature.pose.bones["cf_j_bust01_L"].custom_shape = bpy.data.objects["WidgetBreastL"]
+ armature.pose.bones["cf_j_bust01_L"].use_custom_shape_bone_size = False
+ armature.pose.bones["cf_j_bust01_R"].custom_shape = bpy.data.objects["WidgetBreastR"]
+ armature.pose.bones["cf_j_bust01_R"].use_custom_shape_bone_size = False
+
+ armature.pose.bones["Left shoulder"].custom_shape = bpy.data.objects["WidgetShoulderL"]
+ armature.pose.bones["Right shoulder"].custom_shape = bpy.data.objects["WidgetShoulderR"]
+ armature.pose.bones["cf_pv_hand_R"].custom_shape = bpy.data.objects["WidgetHandR"]
+ armature.pose.bones["cf_pv_hand_L"].custom_shape = bpy.data.objects["WidgetHandL"]
+
+ armature.pose.bones["Head"].custom_shape = bpy.data.objects["WidgetHead"]
+ armature.pose.bones["Eye Controller"].custom_shape = bpy.data.objects["WidgetEye"]
+ armature.pose.bones["Neck"].custom_shape = bpy.data.objects["WidgetNeck"]
+
+ armature.pose.bones["Hips"].custom_shape = bpy.data.objects["WidgetHips"]
+ armature.pose.bones["Pelvis"].custom_shape = bpy.data.objects["WidgetPelvis"]
+
+ armature.pose.bones["MasterFootIK.R"].custom_shape = bpy.data.objects["WidgetFoot"]
+ armature.pose.bones["MasterFootIK.L"].custom_shape = bpy.data.objects["WidgetFoot"]
+ armature.pose.bones["ToeRotator.R"].custom_shape = bpy.data.objects["WidgetToe"]
+ armature.pose.bones["ToeRotator.L"].custom_shape = bpy.data.objects["WidgetToe"]
+ armature.pose.bones["HeelIK.R"].custom_shape = bpy.data.objects["WidgetHeel"]
+ armature.pose.bones["HeelIK.L"].custom_shape = bpy.data.objects["WidgetHeel"]
+
+ armature.pose.bones["cf_pv_knee_R"].custom_shape = bpy.data.objects["WidgetKnee"]
+ armature.pose.bones["cf_pv_knee_L"].custom_shape = bpy.data.objects["WidgetKnee"]
+ armature.pose.bones["cf_pv_elbo_R"].custom_shape = bpy.data.objects["WidgetKnee"]
+ armature.pose.bones["cf_pv_elbo_L"].custom_shape = bpy.data.objects["WidgetKnee"]
+
+ armature.pose.bones["Center"].custom_shape = bpy.data.objects["WidgetRoot"]
+
+ try:
+ bpy.context.space_data.overlay.show_relationship_lines = False
+ except:
+ #the script was run in the text editor or console, so this won't work
+ pass
+
+ # apply eye bones, mouth bones, eyebrow bones
+ eyebones = [1,2,3,4,5,6,7,8]
+ for piece in eyebones:
+ left = 'cf_J_Eye0'+str(piece)+'_s_L'
+ right = 'cf_J_Eye0'+str(piece)+'_s_R'
+ armature.pose.bones[left].custom_shape = bpy.data.objects['WidgetFace']
+ armature.pose.bones[right].custom_shape = bpy.data.objects['WidgetFace']
+
+ restOfFace = [
+ 'cf_J_Mayu_R', 'cf_J_MayuMid_s_R', 'cf_J_MayuTip_s_R',
+ 'cf_J_Mayu_L', 'cf_J_MayuMid_s_L', 'cf_J_MayuTip_s_L',
+ 'cf_J_Mouth_R', 'cf_J_Mouth_L',
+ 'cf_J_Mouthup', 'cf_J_MouthLow', 'cf_J_MouthMove', 'cf_J_MouthCavity']
+ for bone in restOfFace:
+ armature.pose.bones[bone].custom_shape = bpy.data.objects['WidgetFace']
+
+ evenMoreOfFace = [
+ 'cf_J_EarUp_L', 'cf_J_EarBase_ry_L', 'cf_J_EarLow_L',
+ 'cf_J_CheekUp2_L', 'cf_J_Eye_rz_L', 'cf_J_Eye_rz_L',
+ 'cf_J_CheekUp_s_L', 'cf_J_CheekLow_s_L',
+
+ 'cf_J_EarUp_R', 'cf_J_EarBase_ry_R', 'cf_J_EarLow_R',
+ 'cf_J_CheekUp2_R', 'cf_J_Eye_rz_R', 'cf_J_Eye_rz_R',
+ 'cf_J_CheekUp_s_R', 'cf_J_CheekLow_s_R',
+
+ 'cf_J_ChinLow', 'cf_J_Chin_s', 'cf_J_ChinTip_Base',
+ 'cf_J_NoseBase', 'cf_J_NoseBridge_rx', 'cf_J_Nose_tip']
+
+ for bone in evenMoreOfFace:
+ armature.pose.bones[bone].custom_shape = bpy.data.objects['WidgetSpine']
+
+ fingerList = [
+ 'IndexFinger1_L', 'IndexFinger2_L', 'IndexFinger3_L',
+ 'MiddleFinger1_L', 'MiddleFinger2_L', 'MiddleFinger3_L',
+ 'RingFinger1_L', 'RingFinger2_L', 'RingFinger3_L',
+ 'LittleFinger1_L', 'LittleFinger2_L', 'LittleFinger3_L',
+ 'Thumb0_L', 'Thumb1_L', 'Thumb2_L',
+
+ 'IndexFinger1_R', 'IndexFinger2_R', 'IndexFinger3_R',
+ 'MiddleFinger1_R', 'MiddleFinger2_R', 'MiddleFinger3_R',
+ 'RingFinger1_R', 'RingFinger2_R', 'RingFinger3_R',
+ 'LittleFinger1_R', 'LittleFinger2_R', 'LittleFinger3_R',
+ 'Thumb0_R', 'Thumb1_R', 'Thumb2_R']
+
+ for finger in fingerList:
+ if 'Thumb' in finger:
+ armature.pose.bones[finger].custom_shape = bpy.data.objects['WidgetFingerThumb']
+ else:
+ armature.pose.bones[finger].custom_shape = bpy.data.objects['WidgetFinger']
+
+ bp_list = self.get_bone_list('bp_list')
+ toe_list = self.get_bone_list('toe_list')
+ for bone in bp_list:
+ if armature.pose.bones.get(bone):
+ armature.pose.bones[bone].custom_shape = bpy.data.objects['WidgetSpine']
+ armature.pose.bones[bone].custom_shape_scale_xyz = Vector((1.8, 1.8, 1.8))
+ for bone in toe_list:
+ if armature.pose.bones.get(bone):
+ armature.pose.bones[bone].custom_shape = bpy.data.objects['WidgetSpine']
+
+ #Make the body and clothes layers visible
+ all_layers = [
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False]
+ all_layers[0] = True
+ all_layers[8] = True
+ all_layers[9] = True
+
+ if bpy.app.version[0] == 3:
+ bpy.ops.armature.armature_layers(layers=all_layers)
+ else:
+ for index, show_layer in enumerate(all_layers):
+ if armature.data.collections.get(str(index)):
+ armature.data.collections.get(str(index)).is_visible = show_layer
+ armature.data.display_type = 'STICK'
+
+ def hide_widgets(self):
+ '''automatically hide bone widgets collection if it's visible'''
+ if bpy.context.scene.kkbp.armature_dropdown in ['A','B']:
+ c.show_layer_collection('Bone Widgets', False)
+
+ def only_show_core_armature_bones(self):
+ #Make only core, skirt and accessory bones visible
+ armature = c.get_armature()
+ c.switch(armature, 'object')
+ core_layers = [
+ True, False, False, False, False, False, False, False, #body
+ True, True, False, False, False, False, False, False, #clothes
+ False, False, False, False, False, False, False, False, #face
+ False, False, False, False, False, False, False, False]
+ if bpy.app.version[0] == 3:
+ bpy.ops.armature.armature_layers(layers=core_layers)
+ else:
+ for index, show_layer in enumerate(core_layers):
+ if armature.data.collections.get(str(index)):
+ armature.data.collections.get(str(index)).is_visible = show_layer
+ armature.data.display_type = 'STICK'
+ c.switch(armature, 'object')
+
+ # %% Supporting functions
+ @staticmethod
+ def survey(obj):
+ '''Function to check for empty vertex groups of an object
+ returns a dictionary in the form {vertex_group1: maxweight1, vertex_group2: maxweight2, etc}'''
+ maxWeight = {}
+ #prefill vertex group list with zeroes
+ for i in obj.vertex_groups:
+ maxWeight[i.name] = 0
+ #preserve the indexes
+ keylist = list(maxWeight)
+ #then fill in the real value using the indexes
+ for v in obj.data.vertices:
+ for g in v.groups:
+ gn = g.group
+ w = obj.vertex_groups[g.group].weight(v.index)
+ if (maxWeight.get(keylist[gn]) is None or w>maxWeight[keylist[gn]]):
+ maxWeight[keylist[gn]] = w
+ return maxWeight
+
+ @staticmethod
+ def survey_vertexes(obj):
+ has_vertexes = {}
+ for i in obj.vertex_groups:
+ has_vertexes[i.name] = False
+ #preserve the indexes
+ keylist = list(has_vertexes)
+ #then fill in the real value using the indexes
+ for v in obj.data.vertices:
+ for g in v.groups:
+ gn = g.group
+ w = obj.vertex_groups[g.group].weight(v.index)
+ if (has_vertexes.get(keylist[gn]) is None or w>has_vertexes[keylist[gn]]):
+ has_vertexes[keylist[gn]] = True
+ return has_vertexes
+
+ def set_armature_layer(self, bone_name, show_layer, hidden = False):
+ '''Assigns a bone to a bone collection.'''
+ armature = c.get_armature()
+ bone = armature.data.bones.get(bone_name)
+ if bone:
+ if bpy.app.version[0] == 3:
+ if armature.data.bones.get(bone_name):
+ armature.data.bones[bone_name].layers = (
+ True, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False
+ )
+ #have to show the bone on both layer 1 and chosen layer before setting it to just chosen layer
+ armature.data.bones[bone_name].layers[show_layer] = True
+ armature.data.bones[bone_name].layers[0] = False
+ armature.data.bones[bone_name].hide = hidden
+ else:
+ original_mode = bpy.context.object.mode
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ show_layer = str(show_layer)
+ bone.collections.clear()
+ if armature.data.bones.get(bone_name):
+ if armature.data.collections.get(show_layer):
+ armature.data.collections[show_layer].assign(armature.data.bones.get(bone_name))
+ else:
+ armature.data.collections.new(show_layer)
+ armature.data.collections[show_layer].assign(armature.data.bones.get(bone_name))
+ armature.data.bones[bone_name].hide = hidden
+ bpy.ops.object.mode_set(mode = original_mode)
+
+ @staticmethod
+ def get_bone_list(kind):
+ '''returns a list of a certain category of bones'''
+ if kind == 'core_list':
+ #main bone list
+ return [
+ 'cf_n_height', 'cf_j_hips', 'cf_j_waist01', 'cf_j_waist02',
+ 'cf_j_spine01', 'cf_j_spine02', 'cf_j_spine03',
+ 'cf_j_neck', 'cf_j_head',
+ 'cf_d_bust00', 'cf_j_bust01_L', 'cf_j_bust01_R', 'Eyesx',
+
+ 'cf_j_shoulder_L', 'cf_j_shoulder_R', 'cf_j_arm00_L', 'cf_j_arm00_R',
+ 'cf_j_forearm01_L', 'cf_j_forearm01_R', 'cf_j_hand_R', 'cf_j_hand_L',
+
+ 'cf_j_thumb01_L','cf_j_thumb02_L', 'cf_j_thumb03_L',
+ 'cf_j_ring01_L', 'cf_j_ring02_L', 'cf_j_ring03_L',
+ 'cf_j_middle01_L','cf_j_middle02_L', 'cf_j_middle03_L',
+ 'cf_j_little01_L','cf_j_little02_L', 'cf_j_little03_L',
+ 'cf_j_index01_L','cf_j_index02_L', 'cf_j_index03_L',
+
+ 'cf_j_thumb01_R','cf_j_thumb02_R', 'cf_j_thumb03_R',
+ 'cf_j_ring01_R','cf_j_ring02_R', 'cf_j_ring03_R',
+ 'cf_j_middle01_R','cf_j_middle02_R', 'cf_j_middle03_R',
+ 'cf_j_little01_R','cf_j_little02_R', 'cf_j_little03_R',
+ 'cf_j_index01_R', 'cf_j_index02_R', 'cf_j_index03_R',
+
+ 'cf_j_thigh00_L', 'cf_j_thigh00_R', 'cf_j_leg01_L', 'cf_j_leg01_R',
+ 'cf_j_foot_L', 'cf_j_foot_R', 'cf_j_toes_L', 'cf_j_toes_R',
+
+ 'cf_j_siri_L', 'cf_j_siri_R',
+
+ 'cf_pv_knee_L', 'cf_pv_knee_R',
+ 'cf_pv_elbo_L', 'cf_pv_elbo_R',
+ 'cf_pv_hand_L', 'cf_pv_hand_R',
+ 'cf_pv_foot_L', 'cf_pv_foot_R'
+ ]
+
+ elif kind == 'non_ik':
+ #IK bone list
+ return [
+ 'cf_j_forearm01_L', 'cf_j_forearm01_R',
+ 'cf_j_arm00_L', 'cf_j_arm00_R',
+ 'cf_j_thigh00_L', 'cf_j_thigh00_R',
+ 'cf_j_leg01_L', 'cf_j_leg01_R',
+ 'cf_j_leg03_L', 'cf_j_leg03_R',
+ 'cf_j_foot_L', 'cf_j_foot_R',
+ 'cf_j_hand_L', 'cf_j_hand_R',
+ 'cf_j_bust03_L', 'cf_j_bnip02root_L', 'cf_j_bnip02_L',
+ 'cf_j_bust03_R', 'cf_j_bnip02root_R', 'cf_j_bnip02_R']
+
+ elif kind == 'eye_list':
+ return [
+ 'cf_J_Eye01_s_L', 'cf_J_Eye01_s_R',
+ 'cf_J_Eye02_s_L', 'cf_J_Eye02_s_R',
+ 'cf_J_Eye03_s_L', 'cf_J_Eye03_s_R',
+ 'cf_J_Eye04_s_L', 'cf_J_Eye04_s_R',
+ 'cf_J_Eye05_s_L', 'cf_J_Eye05_s_R',
+ 'cf_J_Eye06_s_L', 'cf_J_Eye06_s_R',
+ 'cf_J_Eye07_s_L', 'cf_J_Eye07_s_R',
+ 'cf_J_Eye08_s_L', 'cf_J_Eye08_s_R',
+
+ 'cf_J_Mayu_R', 'cf_J_MayuMid_s_R', 'cf_J_MayuTip_s_R',
+ 'cf_J_Mayu_L', 'cf_J_MayuMid_s_L', 'cf_J_MayuTip_s_L']
+
+ elif kind == 'mouth_list':
+ return [
+ 'cf_J_Mouth_R', 'cf_J_Mouth_L',
+ 'cf_J_Mouthup', 'cf_J_MouthLow', 'cf_J_MouthMove', 'cf_J_MouthCavity',
+
+ 'cf_J_EarUp_L', 'cf_J_EarBase_ry_L', 'cf_J_EarLow_L',
+ 'cf_J_CheekUp2_L', 'cf_J_Eye_rz_L', 'cf_J_Eye_rz_L',
+ 'cf_J_CheekUp_s_L', 'cf_J_CheekLow_s_L',
+
+ 'cf_J_EarUp_R', 'cf_J_EarBase_ry_R', 'cf_J_EarLow_R',
+ 'cf_J_CheekUp2_R', 'cf_J_Eye_rz_R', 'cf_J_Eye_rz_R',
+ 'cf_J_CheekUp_s_R', 'cf_J_CheekLow_s_R',
+
+ 'cf_J_ChinLow', 'cf_J_Chin_s', 'cf_J_ChinTip_Base',
+ 'cf_J_NoseBase', 'cf_J_NoseBridge_rx', 'cf_J_Nose_tip']
+
+ elif kind == 'toe_list':
+ #bones that appear on the Better Penetration armature
+ return [
+ 'cf_j_toes0_L', 'cf_j_toes1_L', 'cf_j_toes10_L',
+ 'cf_j_toes2_L', 'cf_j_toes20_L',
+ 'cf_j_toes3_L', 'cf_j_toes30_L', 'cf_j_toes4_L',
+
+ 'cf_j_toes0_R', 'cf_j_toes1_R', 'cf_j_toes10_R',
+ 'cf_j_toes2_R', 'cf_j_toes20_R',
+ 'cf_j_toes3_R', 'cf_j_toes30_R', 'cf_j_toes4_R']
+
+ elif kind == 'bp_list':
+ #more bones that appear on the Better Penetration armature
+ return [
+ 'cf_j_kokan', 'cf_j_ana', 'cf_J_Vagina_root', 'cf_J_Vagina_B', 'cf_J_Vagina_F',
+ 'cf_J_Vagina_L.001', 'cf_J_Vagina_L.002', 'cf_J_Vagina_L.003', 'cf_J_Vagina_L.004', 'cf_J_Vagina_L.005',
+ 'cf_J_Vagina_R.001', 'cf_J_Vagina_R.002', 'cf_J_Vagina_R.003', 'cf_J_Vagina_R.004', 'cf_J_Vagina_R.005']
+
+ elif kind == 'skirt_list':
+ return [
+ 'cf_j_sk_00_00', 'cf_j_sk_00_01', 'cf_j_sk_00_02', 'cf_j_sk_00_03', 'cf_j_sk_00_04',
+ 'cf_j_sk_01_00', 'cf_j_sk_01_01', 'cf_j_sk_01_02', 'cf_j_sk_01_03', 'cf_j_sk_01_04',
+ 'cf_j_sk_02_00', 'cf_j_sk_02_01', 'cf_j_sk_02_02', 'cf_j_sk_02_03', 'cf_j_sk_02_04',
+ 'cf_j_sk_03_00', 'cf_j_sk_03_01', 'cf_j_sk_03_02', 'cf_j_sk_03_03', 'cf_j_sk_03_04',
+ 'cf_j_sk_04_00', 'cf_j_sk_04_01', 'cf_j_sk_04_02', 'cf_j_sk_04_03', 'cf_j_sk_04_04',
+ 'cf_j_sk_05_00', 'cf_j_sk_05_01', 'cf_j_sk_05_02', 'cf_j_sk_05_03', 'cf_j_sk_05_04',
+ 'cf_j_sk_06_00', 'cf_j_sk_06_01', 'cf_j_sk_06_02', 'cf_j_sk_06_03', 'cf_j_sk_06_04',
+ 'cf_j_sk_07_00', 'cf_j_sk_07_01', 'cf_j_sk_07_02', 'cf_j_sk_07_03', 'cf_j_sk_07_04']
+
+ elif kind == 'tongue_list':
+ return [
+ 'cf_j_tang_01', 'cf_j_tang_02', 'cf_j_tang_03', 'cf_j_tang_04', 'cf_j_tang_05',
+ 'cf_j_tang_L_03', 'cf_j_tang_L_04', 'cf_j_tang_L_05',
+ 'cf_j_tang_R_03', 'cf_j_tang_R_04', 'cf_j_tang_R_05',
+ ]
+
+ def new_bone(self, new_bone_name):
+ '''Creates a new bone on the armature with the specified name and returns the blender bone'''
+ if bpy.app.version[0] == 3:
+ bpy.ops.armature.bone_primitive_add()
+ bone = c.get_armature().data.edit_bones['Bone']
+ bone.name = new_bone_name
+ else:
+ bone = c.get_armature().data.edit_bones.new(new_bone_name)
+ return bone
+
diff --git a/importing/modifymaterial.md b/importing/modifymaterial.md
new file mode 100644
index 0000000..e3577b5
--- /dev/null
+++ b/importing/modifymaterial.md
@@ -0,0 +1,117 @@
+### Texture Postfix Legend:
+ _MT_CT -> _MainTex_ColorTexture
+ _MT_DT -> _MainTex_DarkenedColorTexture (aka DarkTex)
+ _MT -> _MainTex (aka Plain MainTex)
+ _ST_CT -> _Saturated_MainTex_ColorTexture
+ _ST_DT -> _Saturated_MainTex_DarkenedColorTexture (aka Saturated DarkTex)
+ _ST -> _Saturated_MainTex (aka Plain Saturated MainTex)
+ _AM -> _AlphaMask
+ _CM -> _ColorMask
+ _DM -> _DetailMask
+ _LM -> _LineMask
+ _NM -> _NormalMask
+ _NMP -> _NormalMap
+ _NMPD -> _NormalMapDetail
+ _ot1 -> _overtex1
+ _ot2 -> _overtex2
+ _ot3 -> _overtex3
+ _lqdm -> _liquidmask
+ _HGLS -> _HairGloss
+ _T2 -> _Texture2
+ _T3 -> _Texture3
+ _T4 -> _Texture4
+ _T5 -> _Texture5
+ _T6 -> _Texture6
+ _T7 -> _Texture7
+ _PM1 -> _PatternMask1
+ _PM1 -> _PatternMask2
+ _PM1 -> _PatternMask3
+
+### stripped down clothes source for dark colors
+ float3 diffuse = mainTex * color;
+
+ float3 shadingAdjustment = ShadeAdjustItem(diffuse);
+
+ float3 diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
+ diffuseShaded = -diffuseShaded * 2 + 1;
+
+ bool3 compTest = 0.555555582 < shadingAdjustment;
+ shadingAdjustment *= 1.79999995;
+ diffuseShaded = -diffuseShaded * 0.7225 + 1;
+ {
+ float3 hlslcc_movcTemp = shadingAdjustment;
+ hlslcc_movcTemp.x = (compTest.x) ? diffuseShaded.x : shadingAdjustment.x;
+ hlslcc_movcTemp.y = (compTest.y) ? diffuseShaded.y : shadingAdjustment.y;
+ hlslcc_movcTemp.z = (compTest.z) ? diffuseShaded.z : shadingAdjustment.z;
+ shadingAdjustment = saturate(hlslcc_movcTemp);
+ }
+
+ float3 diffuseShadow = diffuse * shadingAdjustment;
+
+ float3 lightCol = float3(1.0656, 1.0656, 1.0656); // constant calculated from custom ambient .666 and light color .666
+ float3 ambientCol = max(lightCol, .15);
+ diffuseShadow = diffuseShadow * ambientCol;
+
+ return float4(diffuseShadow, 1);
+
+### stripped down skin source for dark colors
+ //Diffuse and color maps KK uses for shading I assume
+ float3 diffuse = GetDiffuse(i);
+ float3 specularAdjustment; //Adjustments for specular from detailmap
+ float3 shadingAdjustment; //Adjustments for shading
+ MapValuesMain(diffuse, specularAdjustment, shadingAdjustment);
+
+ //Shading
+ float3 diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
+ diffuseShaded = -diffuseShaded * 2 + 1;
+
+ bool3 compTest = 0.555555582 < shadingAdjustment;
+ shadingAdjustment *= 1.79999995;
+ diffuseShaded = -diffuseShaded * 0.7225 + 1;
+ {
+ float3 hlslcc_movcTemp = shadingAdjustment;
+ hlslcc_movcTemp.x = (compTest.x) ? diffuseShaded.x : shadingAdjustment.x;
+ hlslcc_movcTemp.y = (compTest.y) ? diffuseShaded.y : shadingAdjustment.y;
+ hlslcc_movcTemp.z = (compTest.z) ? diffuseShaded.z : shadingAdjustment.z;
+ shadingAdjustment = saturate(hlslcc_movcTemp);
+ }
+ float3 finalDiffuse = diffuse * shadingAdjustment;
+
+ float3 bodyShine = float3(1.0656, 1.0656, 1.0656);
+ finalDiffuse *= bodyShine;
+
+ return float4(finalDiffuse, 1);
+
+### stripped down hair source for dark colors
+ float3 diffuse = GetDiffuse(i.uv0) * mainTex.rgb;
+
+ float3 finalAmbientShadow = 0.7225;
+ finalAmbientShadow = saturate(finalAmbientShadow);
+ float3 invertFinalAmbientShadow = 0.7225;
+
+ finalAmbientShadow = finalAmbientShadow * _ShadowColor.xyz;
+ finalAmbientShadow += finalAmbientShadow;
+ float3 shadowCol = _ShadowColor - 0.5;
+ shadowCol = -shadowCol * 2 + 1;
+
+ invertFinalAmbientShadow = -shadowCol * invertFinalAmbientShadow + 1;
+ bool3 shadeCheck = 0.5 < _ShadowColor.xyz;
+ {
+ float3 hlslcc_movcTemp = finalAmbientShadow;
+ hlslcc_movcTemp.x = (shadeCheck.x) ? invertFinalAmbientShadow.x : finalAmbientShadow.x;
+ hlslcc_movcTemp.y = (shadeCheck.y) ? invertFinalAmbientShadow.y : finalAmbientShadow.y;
+ hlslcc_movcTemp.z = (shadeCheck.z) ? invertFinalAmbientShadow.z : finalAmbientShadow.z;
+ finalAmbientShadow = hlslcc_movcTemp;
+ }
+ finalAmbientShadow = saturate(finalAmbientShadow);
+ diffuse *= finalAmbientShadow;
+
+ float3 finalDiffuse = saturate(diffuse);
+
+ float3 shading = 1 - finalAmbientShadow;
+ shading = 1 * shading + finalAmbientShadow;
+ finalDiffuse *= shading;
+ shading = 1.0656;
+ finalDiffuse *= shading;
+
+ return float4(finalDiffuse, alpha);
\ No newline at end of file
diff --git a/importing/modifymaterial.py b/importing/modifymaterial.py
new file mode 100644
index 0000000..493917e
--- /dev/null
+++ b/importing/modifymaterial.py
@@ -0,0 +1,1579 @@
+
+# This file performs the following operations
+
+# Remove unused material slots on all objects
+# Remap duplicate material slots on all objects
+
+# Replace all materials with templates from the KK Shader file
+# Remove all duplicate node groups after importing everything
+
+# Import all textures from .pmx directory
+# Saturates all main textures and creates dark versions of all main textures
+# Load all textures to correct spot on all materials
+# Sets up the normal smoothing geometry nodes group
+# Sets up drivers to make the gag eye shapekeys work correctly
+
+# Load all colors from KK_MaterialDataComplete.json to correct spot on all materials
+# Adds an outline modifier and outline materials to the face, body, hair and outfit meshes
+
+# Color and image saturation code taken from MediaMoots https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/ecad6a136e86aaf6c51194705157200797f91e5f/importing/importcolors.py
+# Dark color conversion code taken from Xukmi https://github.com/xukmi/KKShadersPlus/tree/main/Shaders
+
+
+import bpy, os, numpy, math, time, concurrent.futures, threading, queue
+from pathlib import Path
+from .. import common as c
+
+class modify_material(bpy.types.Operator):
+ bl_idname = "kkbp.modifymaterial"
+ bl_label = bl_idname
+ bl_description = bl_idname
+ bl_options = {'REGISTER', 'UNDO'}
+
+ # numpy's precision
+ np_number_precision = numpy.float32
+
+ # these three parameters should be exposed in the gui and decided by user
+
+ # This is how many cpu cores you want to use to saturate the images.
+ # If you have a better CPU, you can set it higher.
+ kkbp_package_name = __package__[:__package__.rindex('.')]
+ max_thread_num = bpy.context.preferences.addons[kkbp_package_name].preferences.max_thread_num
+
+ # this is related to memory usage.
+ # Actually it's not perfect because the size of each image varies.
+ # If loading four 4096 * 4096, the peak memory usage could reach 16000MB.
+ # If the user doesn't have this much available memory, the program will crash.
+ # In that case, the user should lower the value
+ max_image_num = bpy.context.preferences.addons[kkbp_package_name].preferences.max_image_num
+
+ # this is related to cpu and memory usage.
+ # This is the number of rows of pixels to process in one batch (images are saturated in batches).
+ # Simply separate images in rows, ignoring that the num of column usually increase as num of rows increasing
+ # For a 1024 * 1024, a batch is 512 * 1024.But for 2048 * 2048, a batch is 512 * 2048
+ batch_rows = bpy.context.preferences.addons[kkbp_package_name].preferences.batch_rows
+
+ # constants for later
+ lut_pixels = None
+ coord_scale = None
+ coord_offset = None
+ texel_height_X0 = None
+ # used to protect the data_queue
+ queue_lock = threading.Lock()
+ data_queue = queue.Queue()
+
+ def execute(self, context):
+ try:
+
+ self.remove_unused_material_slots()
+ self.remap_duplicate_material_slots()
+
+ self.replace_materials_for_body()
+ self.replace_materials_for_hair()
+ self.replace_materials_for_outfits()
+ self.replace_materials_for_tears_tongue_gageye()
+ self.remove_duplicate_node_groups()
+
+ self.load_images()
+ self.link_textures_for_face_body()
+ self.link_textures_for_hair()
+ self.link_textures_for_clothes()
+ self.link_textures_for_tongue_tear_gag()
+ self.create_dark_textures()
+
+ self.import_and_setup_smooth_normals()
+ self.setup_gag_eye_material_drivers()
+
+ self.add_outlines_to_body()
+ self.add_outlines_to_hair()
+ self.add_outlines_to_clothes()
+
+ self.load_luts()
+ self.load_json_colors()
+ self.set_color_management()
+
+ c.clean_orphaned_data()
+ return {'FINISHED'}
+ except Exception as error:
+ c.handle_error(self, error)
+ return {"CANCELLED"}
+
+ def init_prefab_data(self):
+ '''Initialize constants for saturating textures'''
+ modify_material.lut_pixels = numpy.array(bpy.data.images['Lut_TimeDay.png'].pixels[:],dtype=modify_material.np_number_precision).reshape(bpy.data.images['Lut_TimeDay.png'].size[1], bpy.data.images['Lut_TimeDay.png'].size[0], 4)
+ #constants to ensure bot and top are within the 32 x 1024 dimensions of the lut
+ modify_material.coord_scale = numpy.array([0.0302734375, 0.96875, 31.0],dtype=modify_material.np_number_precision)
+ modify_material.coord_offset = numpy.array([0.5 / 1024, 0.5 / 32, 0.0], dtype=modify_material.np_number_precision)
+ modify_material.texel_height_X0 = numpy.array([1 / 32, 0], dtype=modify_material.np_number_precision)
+ pass
+
+ # %% Main functions
+ def remove_unused_material_slots(self):
+ '''Remove unused mat slots on all visible objects'''
+ objects = c.get_outfits()
+ objects.extend(c.get_alts())
+ objects.append(c.get_body)
+ objects.extend(c.get_hairs())
+ for object in objects:
+ try:
+ c.switch(object, 'object')
+ bpy.ops.object.material_slot_remove_unused()
+
+ #If this is an outfit, and there's only one material slot, check if it's a duplicate material.
+ #If it's a duplicate the object actually had no materials to begin with, but the material_slot_remove_unused function left a slot behind
+ #This can happen if the outfit does not contain any clothes
+ if len(object.material_slots) == 1 and object.get('outfit') and bpy.data.objects.get('Hair ' + object.name):
+ if object.material_slots[0].material.get('id') == bpy.data.objects.get(object.name.replace('Ouftit', 'Hair Outfit')).material_slots[0].material.get('id'):
+ c.kklog(f'No materials detected in outfit "{object.name}". Deleting...', 'warn')
+ bpy.data.objects.remove(object)
+ except:
+ pass
+ c.print_timer('remove_unused_material_slots')
+
+ def remap_duplicate_material_slots(self):
+ c.switch(c.get_body(), 'object')
+ objects = c.get_outfits()
+ objects.extend(c.get_alts())
+ objects.append(c.get_body())
+ objects.extend(c.get_hairs())
+
+
+ for obj in objects:
+ c.switch(obj, 'object')
+ bpy.ops.object.material_slot_remove_unused()
+ c.switch(obj, 'edit')
+ #combine duplicated material slots
+ for mat_name in [o.name for o in obj.data.materials]:
+ index = 1
+ base_material = bpy.data.materials.get(mat_name)
+ while redundant_material := bpy.data.materials.get(f'{mat_name}.{index:03d}'):
+ index += 1
+ redundant_material.user_remap(base_material)
+ bpy.data.materials.remove(redundant_material)
+
+ #then clean material slots by going through each slot and reassigning the slots that are repeated
+ repeats = {}
+ material_list = obj.data.materials
+ for index, mat in enumerate(material_list):
+ if mat.name not in repeats:
+ repeats[mat.name] = [index]
+ else:
+ repeats[mat.name].append(index)
+
+ for material_name in repeats.keys():
+ if len(repeats[material_name]) > 1:
+ for repeated_slot in repeats[material_name]:
+ #don't touch the first slot
+ if repeated_slot == repeats[material_name][0]:
+ continue
+ c.kklog("Moving duplicate material {} in slot {} to the original slot {}".format(material_name, repeated_slot, repeats[material_name][0]))
+ obj.active_material_index = repeated_slot
+ bpy.ops.object.material_slot_select()
+ obj.active_material_index = repeats[material_name][0]
+ bpy.ops.object.material_slot_assign()
+ bpy.ops.mesh.select_all(action='DESELECT')
+ c.switch(obj, 'object')
+ bpy.ops.object.material_slot_remove_unused()
+ c.print_timer('remap_duplicate_material_slots')
+
+ def replace_materials_for_body(self):
+ c.switch(c.get_body(), 'object')
+ if bpy.app.version[0] != 3:
+ c.get_body().visible_shadow = False
+ templateList = [
+ 'KK Body',
+ 'KK Tears',
+ 'KK Gag00',
+ 'KK Gag01',
+ 'KK Gag02',
+ 'KK EyeR (hitomi)',
+ 'KK EyeL (hitomi)',
+ 'KK Eyebrows (mayuge)',
+ 'KK Eyeline down',
+ 'KK Eyeline kage',
+ 'KK Eyeline up',
+ 'KK Eyewhites (sirome)',
+ 'KK Face',
+ 'KK General',
+ 'KK Hair',
+ 'KK Nose',
+ 'KK Teeth (tooth)',
+ 'KK Simple',
+ 'KK Glasses',
+ 'Outline General',
+ 'Outline Body',
+ ]
+ c.import_from_library_file(category='Material', list_of_items=templateList, use_fake_user=True)
+
+ #Replace all materials on the body with templates
+ def swap_body_material(original_materials: list[str], template_name: str):
+ c_name = c.get_name()
+ #remove dupes and check the material slot actually exists
+ original_materials = list(set(original_materials))
+ for original_material in original_materials:
+ if c.get_body().material_slots.get(original_material):
+ template = bpy.data.materials[template_name].copy()
+ template['body'] = True
+
+ template['name'] = c_name
+ template['id'] = original_material
+ template['bake'] = True
+ template.name = bpy.data.materials[template_name].name + ' ' + c_name
+ c.get_body().material_slots[original_material].material = template
+ template_group = template.node_tree.nodes['textures'].node_tree.copy()
+ template_group.name = 'Tex ' + original_material + ' ' + c_name
+ template.node_tree.nodes['textures'].node_tree = template_group
+ else:
+ c.kklog(f'material or template wasn\'t found when replacing body materials: {str(original_material)} / {str(template_name)}', 'warn')
+
+ swap_body_material(c.get_material_names('cf_O_face'),'KK Face') # However, some model has multiple textures on face, like nose material. Simply converting all of them to KK Face gets a white face(could be fixed by removing those material slots manually).Meanwhile, face's material name could change, getting face's material by its general name(cf_m_face_00) may fail under some circumstances
+ swap_body_material(c.get_material_names('cf_O_mayuge'),'KK Eyebrows (mayuge)')
+ swap_body_material(c.get_material_names('cf_O_noseline'),'KK Nose')
+ swap_body_material(c.get_material_names('cf_O_eyeline_low'),'KK Eyeline down')
+ swap_body_material(c.get_material_names('cf_Ohitomi_L'),'KK Eyewhites (sirome)')
+ # swap_body_material(c.get_material_names('cf_Ohitomi_R'),'KK Eyewhites (sirome)')
+ swap_body_material(c.get_material_names('cf_Ohitomi_L02'),'KK EyeL (hitomi)')
+ swap_body_material(c.get_material_names('cf_Ohitomi_R02'),'KK EyeR (hitomi)')
+ swap_body_material(c.get_material_names('o_body_a'),'KK Body')
+ swap_body_material(c.get_material_names('cf_O_tooth'),'KK Teeth (tooth)')
+ swap_body_material(c.get_material_names('o_tang'),'KK General')
+
+ #Replace the eyeline materials separately
+ if c.get_material_names('cf_O_eyeline'):
+ swap_body_material([c.get_material_names('cf_O_eyeline')[0]],'KK Eyeline up')
+ if len(c.get_material_names('cf_O_eyeline')) > 1:
+ swap_body_material([c.get_material_names('cf_O_eyeline')[1]],'KK Eyeline kage')
+
+ c.print_timer('replace_materials_for_body')
+
+ def replace_materials_for_hair(self):
+ '''Replace all of the Hair materials with hair templates and name accordingly'''
+ c_name = c.get_name()
+ for hair in c.get_hairs():
+ if bpy.app.version[0] != 3:
+ hair.visible_shadow = False
+ for material_slot in hair.material_slots:
+ original_name = material_slot.material.name
+ template = bpy.data.materials['KK Hair'].copy()
+ template['hair'] = True
+
+ template['name'] = c_name
+ template['bake'] = True
+ #Some hair materials are repeated. The order goes 'hair_material', 'hair_material 00', 'hair_material 01', etc.
+ #If this happens use the name without numbers or the color information from the json will not be loaded correctly
+ if original_name[-2:].isnumeric() and original_name[-3] == ' ':
+ template['id'] = original_name[:-3]
+ else:
+ template['id'] = original_name
+ template.name = 'KK ' + original_name + ' ' + c_name
+ material_slot.material = bpy.data.materials[template.name]
+
+ template_group = template.node_tree.nodes['textures'].node_tree.copy()
+ template_group.name = 'Tex ' + original_name + ' ' + c_name
+ template.node_tree.nodes['textures'].node_tree = template_group
+
+ template_group_pos = template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree.copy()
+ template_group_pos.name = 'Pos ' + original_name + ' ' + c_name
+ template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree = template_group_pos
+
+ c.print_timer('replace_materials_for_hair')
+
+ def replace_materials_for_outfits(self):
+ #Replace all other materials with the general template and name accordingly
+ outfits = c.get_outfits()
+ outfits.extend(c.get_alts())
+ c_name = c.get_name()
+ for ob in outfits:
+ if bpy.app.version[0] != 3:
+ ob.visible_shadow = False
+ for material_slot in ob.material_slots:
+ original_name = material_slot.material.name
+ template = bpy.data.materials['KK General'].copy()
+ template['outfit'] = True
+
+ template['name'] = c_name
+ template['bake'] = True
+ #Some outfit materials are repeated. The order goes 'outfit_material', 'outfit_material 00', 'outfit_material 01', etc.
+ #If this happens use the name without numbers or the color information from the json will not be loaded correctly
+
+ if original_name[-2:].isnumeric() and original_name[-3] == ' ':
+ template['id'] = original_name[:-3]
+ else:
+ template['id'] = original_name
+ template.name = 'KK ' + original_name + ' ' + c_name
+ material_slot.material = bpy.data.materials[template.name]
+
+ template_group = template.node_tree.nodes['textures'].node_tree.copy()
+ template_group.name = 'Tex ' + original_name + ' ' + c_name
+ template.node_tree.nodes['textures'].node_tree = template_group
+
+ template_group_pos = template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree.copy()
+ template_group_pos.name = 'Pos ' + original_name + ' ' + c_name
+ template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree = template_group_pos
+
+ c.print_timer('replace_materials_for_outfits')
+
+ def replace_materials_for_tears_tongue_gageye(self):
+
+ #give the tears a material template
+ c_name = c.get_name()
+ if (tears := c.get_tears()):
+ template = bpy.data.materials['KK Tears'].copy()
+ template.name = 'KK Tears ' + c_name
+ template['tears'] = True
+ template['id'] = c.get_material_names('cf_O_namida_L')[0]
+ tears.material_slots[0].material = bpy.data.materials[template.name]
+ template_group = template.node_tree.nodes['textures'].node_tree.copy()
+ template.node_tree.nodes['textures'].node_tree = template_group
+ template_group.name += ' ' + c_name
+
+ #replace tongue material if it exists
+ if c.get_body().material_slots.get('KK General ' + c_name):
+ #Make the tongue material unique so parts of the General Template aren't overwritten
+ template = bpy.data.materials['KK General'].copy()
+ template.name = 'KK Tongue ' + c_name
+ template['tongue'] = True
+ template['bake'] = True
+ # template['id'] = c.get_material_names('o_tang')[0]
+ for material_name in c.get_material_names('o_tang'): # avoid getting the deleted material
+ if bpy.data.materials.get(material_name):
+ template['id'] = material_name
+ break
+ if template.get('id') is None:
+ c.kklog('Failed to replace tongue material', 'warn')
+
+ c.get_body().material_slots['KK General ' + c_name].material = template
+ template_group = template.node_tree.nodes['textures'].node_tree.copy()
+ template.node_tree.nodes['textures'].node_tree = template_group
+ template_group.name = 'Tex Tongue ' + c_name
+ template_group_pos = template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree.copy()
+ template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree = template_group_pos
+ template_group_pos.name = 'Position Tongue ' + c_name
+
+ #give the rigged tongue the existing material template
+ if c.get_tongue():
+ c.get_tongue().material_slots[0].material = template
+
+ #give the gag eyes a material template if they exist
+ if c.get_gags():
+ gag = c.get_gags()
+ for num in ['00', '01', '02']:
+ template = bpy.data.materials['KK Gag'+num].copy()
+ template['gag'] = True
+ template['id'] = c.get_material_names('cf_O_gag_eye_'+num)[0]
+ gag.material_slots['cf_m_gageye_'+num].material = template
+ template.name = 'KK Gag' + num + ' ' + c_name
+ template_group = template.node_tree.nodes['textures'].node_tree.copy()
+ template.node_tree.nodes['textures'].node_tree = template_group
+ template_group.name = 'Tex Gag' + num + ' ' + c_name
+ c.print_timer('replace_materials_for_tears_tongue_gageye')
+
+ def remove_duplicate_node_groups(self):
+ # Get rid of the duplicate node groups cause there's a lot
+ #stolen from somewhere
+ def eliminate(node):
+ node_groups = bpy.data.node_groups
+ # Get the node group name as 3-tuple (base, separator, extension)
+ (base, sep, ext) = node.node_tree.name.rpartition('.')
+ # Replace the numeric duplicate
+ if ext.isnumeric():
+ if base in node_groups:
+ node.node_tree.use_fake_user = False
+ node.node_tree = node_groups.get(base)
+ #--- Search for duplicates in actual node groups
+ node_groups = bpy.data.node_groups
+ for group in node_groups:
+ for node in group.nodes:
+ if node.type == 'GROUP':
+ eliminate(node)
+ #--- Search for duplicates in materials
+ mats = list(bpy.data.materials)
+ worlds = list(bpy.data.worlds)
+ for mat in mats + worlds:
+ if mat.use_nodes:
+ for node in mat.node_tree.nodes:
+ if node.type == 'GROUP':
+ eliminate(node)
+ c.print_timer('remove_duplicate_node_groups')
+
+ def load_images(self):
+ c.switch(c.get_body(), 'object')
+
+ file_dir = os.path.dirname(__file__)
+ lut_image = os.path.join(file_dir, 'Lut_TimeDay.png')
+ lut_image = bpy.data.images.load(str(lut_image))
+ self.init_prefab_data()
+
+ fileList = Path(bpy.context.scene.kkbp.import_dir).rglob('*.png')
+ files = [file for file in fileList if file.is_file() and "_MT" in file.name]
+ unloaded = unprocessed = len(files) # unloaded: unloaded images to saturate, unprocessed: unfinished images that still need to be saturated
+ last_miss_time = time.time() # last time of accessing queue
+ current_image_num = 0 # current num of concurrent processing images
+ futures = []
+ record = {} # as each image is separated to several batches, this is to record each image's base info
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_thread_num) as executor:
+ # the to-output data occupies a lot of memory, so we should save them timely before loading more images
+ while unloaded > 0 or unprocessed > 0:
+ if (current_time := time.time()) - last_miss_time > 0.3: # accessing queue every 0.3s, because queue is empty most of the time, so is no need to access it every loop
+ with self.queue_lock:
+ try:
+ while True: # fetching all data from queue if not empty
+ result = self.data_queue.get(timeout=0.1)
+ (result := record[result])[0] -= 1 # data in tuple: (current_image's batch num, name that the image to be saved with, image, image_data in numpy array, start time to process this image)
+ # record[result][0] -= 1
+ if result[0] == 0:
+ image = result[2]
+ image_pixels = result[3]
+ image.pixels.foreach_set(image_pixels.ravel())
+ image.save_render(os.path.join(bpy.context.scene.kkbp.import_dir, "saturated_files", result[1]))
+
+ del image_pixels
+ unprocessed -= 1
+ current_image_num -= 1
+ c.kklog(f'Saturating image {result[1]} takes {round(time.time() - result[4], 1)}s')
+ except queue.Empty:
+ last_miss_time = current_time
+ # if there has unloaded images and current num of image in processing is smaller than the max value, then load a image and submit it.This prevent loading too many images, which pushes memory usage to a high level
+ if unloaded > 0 and current_image_num < self.max_image_num:
+ unloaded -= 1
+ # skip this file if it has already been converted
+ if os.path.isfile(os.path.join(bpy.context.scene.kkbp.import_dir, 'saturated_files', (save_file_name := files[unloaded].name.replace('_MT', '_ST')))):
+ c.kklog('File already saturated. Skipping {}'.format(files[unloaded].name))
+ unprocessed -= 1
+ continue
+
+ current_image_num += 1
+ # Load image
+ start_time = time.time()
+ image = bpy.data.images.load(str(files[unloaded]))
+
+ # Submit task
+ width, height = image.size
+ image_pixels = numpy.array(image.pixels[:], dtype=modify_material.np_number_precision).reshape(height, width, 4)
+
+ # separating an image to several batches to make full use of CPU
+ start_row = 0
+ while start_row < height:
+ end_row = start_row + self.batch_rows
+ if end_row > height:
+ end_row = height
+
+ future = executor.submit(
+ self.saturate_texture,
+ unloaded,
+ image_pixels[start_row:end_row],
+ start_row == 0
+ )
+ start_row = end_row - 1
+ if start_row == height - 1:
+ break
+
+ futures.append(future)
+
+ record[unloaded] = [math.ceil(height / self.batch_rows), save_file_name, image, image_pixels, start_time]
+
+
+ bpy.data.use_autopack = True # enable autopack on file save
+
+ # Load all textures
+ fileList = Path(bpy.context.scene.kkbp.import_dir).rglob('*.png')
+ files = [file for file in fileList if file.is_file()]
+ for image_file in files:
+ bpy.ops.image.open(filepath=str(image_file), use_udim_detecting=False)
+ try:
+ bpy.data.images[image_file.name].pack()
+ except:
+ c.kklog('This image was not automatically loaded in because its filename exceeds 64 characters: ' + image_file.name, type = 'error')
+
+ # Monitor completion
+ for future in concurrent.futures.as_completed(futures):
+ try:
+ future.result()
+ except Exception as e:
+ c.kklog(f'Processing failed: {str(e)}')
+
+ c.print_timer('load_images')
+
+ def saturate_texture(self, index, slice_image, is_first_batch):
+ '''The Secret Sauce. Accepts a bpy image and saturates it to match the in-game look.'''
+ # Find the XY coordinates of the LUT image needed to saturate each pixel
+ coord = slice_image[:, :, :3] * self.coord_scale + self.coord_offset
+ coord_frac, coord_floor = numpy.modf(coord)
+ coord_frac_z = coord_frac[:, :, 2:3]
+ del coord_frac # free temporary variables after they're used
+ coord_bot = coord[:, :, :2] + coord_floor[:, :, 2:3] * self.texel_height_X0
+ del coord
+ del coord_floor
+
+ #use those XY coordinates to find the saturated version of the color from the LUT image
+ lutcol_bot = self.__bilinear_interpolation__(self.lut_pixels, coord_bot)
+
+ lut_colors = lutcol_bot * (1 - coord_frac_z)
+ del lutcol_bot
+ coord_top = numpy.clip(coord_bot + self.texel_height_X0, 0, 1)
+ lutcol_top = self.__bilinear_interpolation__(self.lut_pixels, coord_top)
+ lut_colors += lutcol_top * coord_frac_z
+ del lutcol_top
+ del coord_top
+ # slice_image[:, :, :3] = lut_colors[:,:,:3]
+ if is_first_batch:
+ slice_image[:, :, :3] = lut_colors[:,:,:3]
+ else:
+ slice_image[1:, :, :3] = lut_colors[1:, :, :3]
+
+ with self.queue_lock:
+ self.data_queue.put(index)
+
+ def link_textures_for_face_body(self):
+ '''Load all body textures into their texture slots'''
+ self.image_load('Body', '_ST_CT.png')
+ self.image_load('Body', '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available later on
+ #default to colors if there's no maintex
+ if c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image.name == 'Template: Placeholder':
+ c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes['dark' ].inputs['Use main texture instead?'].default_value = 0
+ c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes['light'].inputs['Use main texture instead?'].default_value = 0
+ self.image_load('Body', '_CM.png') #color mask
+ self.image_load('Body', '_DM.png') #cfm female
+ self.image_load('Body', '_LM.png') #line mask for lips
+ self.image_load('Body', '_NMP_CNV.png')
+ self.image_load('Body', '_NMPD_CNV.png')
+ self.image_load('Body', '_ST.png', group_override='texturesnsfw') #chara main texture
+ self.image_load('Body', '_ot2.png', group_override='texturesnsfw') #pubic hair
+ self.image_load('Body', '_ot1.png', group_override='texturesnsfw') #cfm female
+ self.image_load('Body', '_ot1.png', group_override='texturesnsfw', node_override='_ot1.pngleft')
+ self.image_load('Body', '_T3.png') #body overlays
+ self.image_load('Body', '_T6.png')
+ self.set_uv_type('Body', 'nippleuv', 'uv_nipple_and_shine', group= 'texturesnsfw')
+ self.set_uv_type('Body', 'underuv', 'uv_underhair', group= 'texturesnsfw')
+ #find the appropriate alpha mask
+ alpha_mask = None
+ if bpy.data.images.get('_AM.png'):
+ alpha_mask = bpy.data.images.get('_AM.png')
+ elif bpy.data.images.get('_AM_00.png'):
+ alpha_mask = bpy.data.images.get('_AM_00.png')
+ else:
+ #check the other alpha mask numbers
+ for image in bpy.data.images:
+ if '_m_body_AM_' in image.name and image.name[-6:-4].isnumeric():
+ alpha_mask = image
+ break
+ #if there was an alpha mask detected, load it in
+ if alpha_mask:
+ self.image_load('Body', image_override = alpha_mask.name, node_override='_AM.png')
+
+ #load in face textures
+ if c.get_material_names('cf_O_face'):
+ self.image_load('Face', '_ST_CT.png')
+ self.image_load('Face', '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available
+ #default to colors if there's no maintex
+ if c.get_body().material_slots['KK Face ' + c.get_name()].material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image.name == 'Template: Placeholder':
+ c.get_body().material_slots['KK Face ' + c.get_name()].material.node_tree.nodes['light'].inputs['Use main texture instead?'].default_value = 0
+ c.get_body().material_slots['KK Face ' + c.get_name()].material.node_tree.nodes['dark' ].inputs['Use main texture instead?'].default_value = 0
+ self.image_load('Face', '_CM.png')
+ self.image_load('Face', '_DM.png')
+ self.image_load('Face', '_T4.png') #blush
+ self.image_load('Face', '_ST.png') #mouth interior
+ self.image_load('Face', '_LM.png')
+ self.image_load('Face', '_T5.png') #lower lip mask
+ self.image_load('Face', '_ot1.png') #lipstick
+ self.image_load('Face', '_ot2.png') #flush
+ self.image_load('Face', '_T3.png')
+ self.image_load('Face', '_T7.png')
+ self.image_load('Face', '_ot3.png') #eyeshadow
+ self.set_uv_type('Face', 'eyeshadowuv', 'uv_eyeshadow')
+
+ #load in the remaining face materials if they exist
+ if c.get_material_names('cf_O_mayuge'):
+ self.image_load('Eyebrows (mayuge)', '_ST_CT.png')
+ self.image_load('Eyebrows (mayuge)', '_ST_CT.png')
+
+ if c.get_material_names('cf_O_noseline'):
+ self.image_load('Nose', '_ST_CT.png')
+ if c.get_material_names('cf_O_tooth'):
+ self.image_load('Teeth (tooth)', '_ST_CT.png')
+ if c.get_material_names('cf_Ohitomi_R'):
+ self.image_load('Eyewhites (sirome)', image_override = c.get_material_names('cf_Ohitomi_L')[0] + '_ST_CT.png', node_override = '_ST_CT.png')
+ self.image_load('Eyewhites (sirome)', image_override = c.get_material_names('cf_Ohitomi_R')[0] + '_ST_CT.png', node_override = '_ST_CT.png')
+
+ if c.get_material_names('cf_O_eyeline'):
+ self.image_load('Eyeline up', image_override= c.get_material_names('cf_O_eyeline')[0] + '_ST_CT.png', node_override='_ST_CT.png')
+ if len(c.get_material_names('cf_O_eyeline')) > 1:
+ self.image_load('Eyeline up', image_override=c.get_material_names('cf_O_eyeline')[1] + '_ST_CT.png', node_override='_ST_CT.pngkage')
+ if c.get_material_names('cf_O_eyeline_low'):
+ self.image_load('Eyeline up', image_override=c.get_material_names('cf_O_eyeline_low')[0] + '_ST_CT.png', node_override='_ST_CT.pngdown')
+
+ #eyes
+ for side in ['L', 'R']:
+ if c.get_material_names(f'cf_Ohitomi_{side}02'):
+ eye_mat = c.get_material_names(f'cf_Ohitomi_{side}02')[0]
+ self.image_load(f'Eye{side} (hitomi)', '_ST_CT.png')
+ self.image_load(f'Eye{side} (hitomi)', '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available
+ self.image_load(f'Eye{side} (hitomi)', '_ot1.png')
+ self.image_load(f'Eye{side} (hitomi)', '_ot2.png')
+ self.image_load(f'Eye{side} (hitomi)', image_override = eye_mat[:-15] + '_cf_t_expression_00_EXPR.png', node_override= '_cf_t_expression_00_EXPR.png')
+ self.image_load(f'Eye{side} (hitomi)', image_override = eye_mat[:-15] + '_cf_t_expression_01_EXPR.png', node_override= '_cf_t_expression_01_EXPR.png')
+
+ #correct the eye scaling using info from the KK_ChaFileCustomFace.json
+ face_data = c.json_file_manager.get_json_file('KK_ChaFileCustomFace.json')
+ bpy.data.node_groups['.Eye Textures positioning'].nodes['eye_scale'].inputs[1].default_value = 1/(float(face_data[18]['Value']) + 0.0001)
+ bpy.data.node_groups['.Eye Textures positioning'].nodes['eye_scale'].inputs[2].default_value = 1/(float(face_data[19]['Value']) + 0.0001)
+
+ c.print_timer('link_textures_for_face_body')
+
+ def link_textures_for_hair(self):
+ '''Load all hair textures into their texture slots'''
+ for current_obj in c.get_hairs():
+ for hairMat in current_obj.material_slots:
+ #use the material name instead of hairMat.material['id'] to catch any instances of 00 01 02 materials
+ hairType = hairMat.name.replace('KK ','').replace(' ' + c.get_name(), '')
+
+ self.image_load( hairType, '_ST_CT.png')
+ self.image_load( hairType, '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available
+ self.image_load( hairType, '_DM.png')
+ self.image_load( hairType, '_CM.png')
+ self.image_load( hairType, '_HGLS.png')
+ self.image_load( hairType, '_AM.png')
+ self.set_uv_type(hairType, 'hairuv', 'uv_nipple_and_shine')
+
+ c.print_timer('link_textures_for_hair')
+
+ def link_textures_for_clothes(self):
+ '''Load all clothes textures into their texture slots'''
+ outfits = c.get_outfits()
+ outfits.extend(c.get_alts())
+ for outfit in outfits:
+ for genMat in outfit.material_slots:
+ #use the material name instead of genMat.material['id'] to catch any instances of 00 01 02 materials
+ genType = genMat.name.replace('KK ','').replace(' ' + c.get_name(), '')
+
+ #load these textures if they are present
+ self.image_load(genType, '_ST.png')
+ self.image_load(genType, '_ST_CT.png')
+ self.image_load(genType, '_AM.png')
+ self.image_load(genType, '_CM.png')
+ self.image_load(genType, '_DM.png')
+ self.image_load(genType, '_NMP.png')
+ self.image_load(genType, '_NMPD_CNV.png')
+ self.image_load(genType, '_PM1.png')
+ self.image_load(genType, '_PM2.png')
+ self.image_load(genType, '_PM3.png')
+
+ #If there's a plain maintex loaded, but no colored maintex loaded, make the shader use the plain maintex
+ plain_but_no_main = (
+ genMat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image.name == 'Template: Placeholder' and
+ genMat.material.node_tree.nodes['textures'].node_tree.nodes['_ST.png'].image.name != 'Template: Placeholder'
+ )
+ if plain_but_no_main:
+ genMat.material.node_tree.nodes['combine'].inputs['Use plain main texture?'].default_value = 1
+
+ #If there's an AnotherRamp (AR) texture present, the material is likely supposed to be metallic on the red parts of the detail mask
+ #I don't have a template for this, so the material will just look pure white. Turn off the shine intensity to avoid this
+ image_name = genMat.material['id'] + '_AR.png'
+ if bpy.data.images.get(image_name):
+ genMat.material.node_tree.nodes['light'].inputs['Detail intensity (shine)'].default_value = 0
+ genMat.material.node_tree.nodes['dark' ].inputs['Detail intensity (shine)'].default_value = 0
+
+ shader_name = c.get_shader_name(genMat.material['id'])
+
+ #If the shader of this material is set to "main opaque" then there is NOT supposed to be a color mask, but the kkbp exporter exports one anyway
+ #Move the colormask to the opaque slot if one was loaded in. This way it can still be used by the plain main texture
+ if genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.png'].image:
+ shaders = ['Koikano/main_clothes_opaque', 'Shader Forge/main_opaque', 'xukmi/MainOpaquePlus', 'xukmi/MainOpaquePlusTess', 'Shader Forge/main_opaque2', 'Shader Forge/main_opaque_low']
+ if shader_name in shaders:
+ c.kklog('Detected opaque shader. Moving color mask to color mask (plain) slot: {}'.format(genMat.material['id']))
+ genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.pngopaque'].image = genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.png'].image
+ genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.png'].image = None
+
+ #If the shader of this material is set to "main alpha", set the material to "blended" in blender
+ shaders = ['Shader Forge/main_alpha', 'Koikano/main_clothes_alpha', 'xukmi/MainAlphaPlus', 'xukmi/MainAlphaPlusTess', 'xukmi/MainItemAlphaPlus', 'IBL_Shader_alpha', ]
+ #find this material in the MaterialDataComplete.json and see if it's an alpha shader
+ if shader_name in shaders:
+ c.kklog('Detected alpha shader. Setting render method to blended: {}'.format(genMat.material['id']))
+ if bpy.app.version[0] == 3:
+ genMat.material.blend_method = 'BLEND'
+ else:
+ genMat.material.surface_render_method = 'BLENDED'
+
+ #If the shader of this material is set to "glasses", replace the entire shader with
+ shaders = ['Shader Forge/toon_glasses_lod0', 'Koikano/main_clothes_item_glasses',]
+ #find this material in the MaterialDataComplete.json and see if it's a glasses shader
+ if shader_name in shaders:
+ c.kklog('Detected glasses shader. Replacing material with KK Glasses: {}'.format(genMat.material['id']))
+
+ original_textures_group = genMat.material.node_tree.nodes['textures'].node_tree
+ template = bpy.data.materials['KK Glasses'].copy()
+ template.node_tree.nodes['textures'].node_tree = original_textures_group
+ bpy.data.materials.remove(genMat.material)
+ template['bake'] = True
+ template['glasses'] = True
+ template.name = 'KK ' + genType + ' ' + c.get_name()
+ genMat.material = template
+
+ #special exception to clip the emblem image because I am tired of seeing it repeat at the edges
+ if 'KK cf_m_emblem ' in genMat.material.name:
+ genMat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].extension = 'CLIP'
+
+ c.print_timer('link_textures_for_clothes')
+
+ def link_textures_for_tongue_tear_gag(self):
+ if c.get_tongue():
+ tongue_mat = c.get_material_names('o_tang')
+ tongue_mat = tongue_mat if tongue_mat else ['cf_m_tang'] #check for bugged/missing SMR Tongue data
+ self.image_load('Tongue', '_CM.png', node_override='_ST_CT.png') #done on purpose
+ self.image_load('Tongue', '_CM.png', node_override='_ST_DT.png') #still done on purpose
+ self.image_load('Tongue', '_CM.png')
+ self.image_load('Tongue', '_DM.png')
+ self.image_load('Tongue', '_NMP.png')
+ self.image_load('Tongue', '_NMP_CNV.png', node_override = '_NMPD_CNV.png') #load regular map by default
+ self.image_load('Tongue', '_NMPD_CNV.png') #then the detail map if it's there
+
+ #load all gag eye textures if it exists
+ if c.get_gags():
+ self.image_load('Gag00', '_cf_t_gageye_00_ST_CT.png')
+ self.image_load('Gag00', '_cf_t_gageye_02_ST_CT.png')
+ self.image_load('Gag00', '_cf_t_gageye_04_ST_CT.png')
+ self.image_load('Gag00', '_cf_t_gageye_05_ST_CT.png')
+ self.image_load('Gag00', '_cf_t_gageye_06_ST_CT.png')
+ self.image_load('Gag01', '_cf_t_gageye_03_ST_CT.png')
+ self.image_load('Gag01', '_cf_t_gageye_01_ST_CT.png')
+ self.image_load('Gag02', '_cf_t_gageye_07_ST_CT.png')
+ self.image_load('Gag02', '_cf_t_gageye_08_ST_CT.png')
+ self.image_load('Gag02', '_cf_t_gageye_09_ST_CT.png')
+
+ #load the tears texture in
+ if c.get_tears():
+ self.image_load('Tears', '_ST_CT.png')
+
+ c.print_timer('link_textures_for_tongue_tear_gag')
+
+ def create_dark_textures(self):
+ """
+ Creates dark versions of textures for body, hair, and outfit materials.
+
+ This method retrieves all body, hair, and outfit materials, and for each material,
+ it checks if the material has a 'textures' node and if it contains a '_ST_DT.png' texture.
+ If the texture is not a placeholder, it creates a dark version of the texture using the
+ shadow color specific to the material and assigns it to the '_ST_DT.png' texture node.
+ """
+ materials = c.get_body_materials()
+ materials.extend(c.get_hair_materials())
+ materials.extend(c.get_outfit_materials())
+ for material in materials:
+ if material.node_tree.nodes.get('textures'):
+ if material.node_tree.nodes['textures'].node_tree.nodes.get('_ST_DT.png'):
+ maintex = material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image
+ #if this isn't a placeholder image, create a dark version of it
+ if maintex.name != 'Template: Placeholder' and maintex.name != 'cf_m_tang_CM.png':
+ shadow_color = c.json_file_manager.get_shadow_color(material.name)
+ darktex = self.create_darktex(maintex, shadow_color)
+ material.node_tree.nodes['textures'].node_tree.nodes['_ST_DT.png'].image = darktex
+ c.print_timer('create_dark_textures')
+
+ def import_and_setup_smooth_normals(self):
+ '''Sets up the Smooth Normals geo nodes setup for smoother face, body, hair and clothes normals'''
+ try:
+ #import all the node groups
+ body = c.get_body()
+ c.import_from_library_file('NodeTree', ['.Raw Shading (smooth normals)', '.Raw Shading (smooth body normals)', '.Smooth Normals', '.Other Smooth Normals'], bpy.context.scene.kkbp.use_material_fake_user)
+ c.switch(body, 'object')
+ geo_nodes = body.modifiers.new(name = 'Normal Smoothing', type = 'NODES')
+ geo_nodes.node_group = bpy.data.node_groups['.Smooth Normals']
+ geo_nodes.show_viewport = False
+ geo_nodes.show_render = False
+ for ob in c.get_hairs():
+ geo_nodes = ob.modifiers.new(name = 'Normal Smoothing', type = 'NODES')
+ geo_nodes.node_group = bpy.data.node_groups['.Other Smooth Normals']
+ geo_nodes.show_viewport = False
+ geo_nodes.show_render = False
+ outfits = c.get_outfits()
+ outfits.extend(c.get_alts())
+ for ob in outfits:
+ geo_nodes = ob.modifiers.new(name = 'Normal Smoothing', type = 'NODES')
+ geo_nodes.node_group = bpy.data.node_groups['.Other Smooth Normals']
+ geo_nodes.show_viewport = False
+ geo_nodes.show_render = False
+ except:
+ #i don't feel like dealing with any errors related to this
+ c.kklog('The normal smoothing wasnt setup correctly. Oh well.', 'warn')
+ c.print_timer('import_and_setup_smooth_normals')
+
+ def setup_gag_eye_material_drivers(self):
+ '''setup gag eye drivers'''
+ if c.get_gags():
+ body = c.get_body()
+ gag_keys = [
+ 'Circle Eyes 1',
+ 'Circle Eyes 2',
+ 'Spiral Eyes',
+ 'Heart Eyes',
+ 'Fiery Eyes',
+ 'Cartoony Wink',
+ 'Vertical Line',
+ 'Cartoony Closed',
+ 'Horizontal Line',
+ 'Cartoony Crying'
+ ]
+
+ def create_driver(material, expression1, expression2):
+ skey_driver = bpy.data.materials[material].node_tree.nodes['Parser'].inputs[0].driver_add('default_value')
+ skey_driver.driver.type = 'SCRIPTED'
+ for key in gag_keys:
+ newVar = skey_driver.driver.variables.new()
+ newVar.name = key.replace(' ','')
+ newVar.type = 'SINGLE_PROP'
+ newVar.targets[0].id_type = 'KEY'
+ newVar.targets[0].id = body.data.shape_keys
+ newVar.targets[0].data_path = 'key_blocks["' + key + '"].value'
+ skey_driver.driver.expression = expression1
+ skey_driver = bpy.data.materials[material].node_tree.nodes['hider'].inputs[0].driver_add('default_value')
+ skey_driver.driver.type = 'SCRIPTED'
+ for key in gag_keys:
+ newVar = skey_driver.driver.variables.new()
+ newVar.name = key.replace(' ','')
+ newVar.type = 'SINGLE_PROP'
+ newVar.targets[0].id_type = 'KEY'
+ newVar.targets[0].id = body.data.shape_keys
+ newVar.targets[0].data_path = 'key_blocks["' + key + '"].value'
+ skey_driver.driver.expression = expression2
+
+ create_driver (
+ 'KK Gag00 ' + c.get_name(),
+ '0 if CircleEyes1 else 1 if CircleEyes2 else 2 if CartoonyClosed else 3 if VerticalLine else 4',
+ 'CircleEyes1 or CircleEyes2 or CartoonyClosed or VerticalLine or HorizontalLine'
+ )
+
+ create_driver (
+ 'KK Gag01 ' + c.get_name(),
+ '0 if HeartEyes else 1',
+ 'HeartEyes or SpiralEyes'
+ )
+
+ create_driver (
+ 'KK Gag02 ' + c.get_name(),
+ '0 if CartoonyCrying else 1 if CartoonyWink else 2',
+ 'CartoonyCrying or CartoonyWink or FieryEyes'
+ )
+ c.print_timer('setup_gag_eye_material_drivers')
+
+ def add_outlines_to_body(self):
+ #Add face and body outlines, then load in the clothes transparency mask to body outline
+ body = c.get_body()
+ c.switch(body, 'object')
+ mod = body.modifiers.new(type='SOLIDIFY', name='Outline Modifier')
+ mod.thickness = 0.0005
+ mod.offset = 0
+ mod.material_offset = len(body.material_slots)
+ mod.use_flip_normals = True
+ mod.use_rim = False
+ mod.name = 'Outline Modifier'
+ mod.show_expanded = False
+ #face first
+ faceOutlineMat = bpy.data.materials['Outline General'].copy()
+ faceOutlineMat.name = 'Outline Face ' + c.get_name()
+ body.data.materials.append(faceOutlineMat)
+ faceOutlineMat.blend_method = 'CLIP'
+ body_outline_mat = bpy.data.materials['Outline Body'].copy()
+ body_outline_mat.name = 'Outline Body ' + c.get_name()
+ body_outline_mat.node_tree.nodes['textures'].node_tree = bpy.data.materials['KK Body ' + c.get_name()].node_tree.nodes['textures'].node_tree
+ body.data.materials.append(body_outline_mat)
+ c.print_timer('add_outlines_to_body')
+
+ def add_outlines_to_hair(self):
+ #Give each piece of hair with an alphamask on each hair object it's own outline group
+ if not bpy.context.scene.kkbp.use_single_outline:
+ for ob in c.get_hairs():
+ #Get the length of the material list before starting
+ outlineStart = len(ob.material_slots)
+ #link all polygons to material name
+ mats_to_gons = {}
+ for slot in ob.material_slots:
+ mats_to_gons[slot.material.name] = []
+ for gon in ob.data.polygons:
+ mats_to_gons[ob.material_slots[gon.material_index].material.name].append(gon)
+ #find all materials that use an alpha mask or maintex
+ alpha_users = []
+ for mat in ob.material_slots:
+ AlphaImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_AM.png'].image
+ MainImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image
+ if AlphaImage or MainImage:
+ alpha_users.append(mat.material.name)
+ #reorder material_list to place alpha/maintex users first
+ new_mat_list_order = [mat_slot.material.name for mat_slot in ob.material_slots if mat_slot.material.name not in alpha_users]
+ new_mat_list_order = alpha_users + new_mat_list_order
+ #reorder mat slot list
+ for index, mat_slot in enumerate(ob.material_slots):
+ mat_slot.material = bpy.data.materials[new_mat_list_order[index]]
+ #create empty slots for new alpha user outlines
+ for mat in alpha_users:
+ ob.data.materials.append(None)
+ #fill alpha user outline materials, and fill image node
+ for index, mat in enumerate(alpha_users):
+ OutlineMat = bpy.data.materials['Outline General'].copy()
+ OutlineMat.name = mat.replace('KK ', 'Outline ')
+ OutlineMat.node_tree.nodes['textures'].node_tree = bpy.data.materials[mat].node_tree.nodes['textures'].node_tree
+ ob.material_slots[index + outlineStart].material = OutlineMat
+ #update polygon material indexes
+ for mat in mats_to_gons:
+ for gon in mats_to_gons[mat]:
+ gon.material_index = new_mat_list_order.index(mat)
+
+ #Add a general outline that covers the rest of the materials on the hair object that don't need transparency
+ for ob in c.get_hairs():
+ bpy.context.view_layer.objects.active = ob
+ mod = ob.modifiers.new(
+ type='SOLIDIFY',
+ name='Outline Modifier')
+ mod.thickness = 0.0005
+ mod.offset = 1
+ mod.material_offset = outlineStart if not bpy.context.scene.kkbp.use_single_outline else 200
+ mod.use_flip_normals = True
+ mod.use_rim = False
+ mod.show_expanded = False
+ hairOutlineMat = bpy.data.materials['Outline General'].copy()
+ hairOutlineMat.name = 'Outline Hair ' + c.get_name()
+ hairOutlineMat.node_tree.nodes['combine'].inputs['Force visibility'].default_value = 1
+ ob.data.materials.append(hairOutlineMat)
+ c.print_timer('add_outlines_to_hair')
+
+ def add_outlines_to_clothes(self):
+ #Add a standard outline to all other objects
+ #keep a dictionary of the material length list for the next loop
+ outlineStart = {}
+ body = c.get_body()
+ c.switch(body, 'object')
+ outfits = c.get_outfits()
+ outfits.extend(c.get_alts())
+ if not bpy.context.scene.kkbp.use_single_outline:
+ #If the material has a maintex or alphamask then give it it's own outline
+ for ob in outfits:
+ #Get the length of the material list before starting
+ outlineStart[ob.name] = len(ob.material_slots)
+ #link all polygons to material name
+ mats_to_gons = {}
+ for slot in ob.material_slots:
+ mats_to_gons[slot.material.name] = []
+ for gon in ob.data.polygons:
+ mats_to_gons[ob.material_slots[gon.material_index].material.name].append(gon)
+ #find all materials that use an alpha mask or maintex
+ alpha_users = []
+ for mat in ob.material_slots:
+ AlphaImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_AM.png'].image
+ MainImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image
+ if AlphaImage or MainImage:
+ alpha_users.append(mat.material.name)
+ #reorder material_list to place alpha/maintex users first
+ new_mat_list_order = [mat_slot.material.name for mat_slot in ob.material_slots if mat_slot.material.name not in alpha_users]
+ new_mat_list_order = alpha_users + new_mat_list_order
+ #reorder mat slot list
+ for index, mat_slot in enumerate(ob.material_slots):
+ mat_slot.material = bpy.data.materials[new_mat_list_order[index]]
+ #create empty slots for new alpha user outlines
+ for mat in alpha_users:
+ ob.data.materials.append(None)
+ #fill alpha user outline materials, and fill image node
+ for index, mat in enumerate(alpha_users):
+ OutlineMat = bpy.data.materials['Outline General'].copy()
+ OutlineMat.name = mat.replace('KK ', 'Outline ')
+ OutlineMat.node_tree.nodes['textures'].node_tree = bpy.data.materials[mat].node_tree.nodes['textures'].node_tree
+ ob.material_slots[index + outlineStart[ob.name]].material = OutlineMat
+
+ #if the outline material is for a glasses material, disable it
+ if bpy.data.materials[mat].get('glasses'):
+ nodes = OutlineMat.node_tree.nodes
+ links = OutlineMat.node_tree.links
+ links.remove(nodes['combine'].inputs['Main texture (alpha)'].links[0])
+ #update polygon material indexes
+ for mat in mats_to_gons:
+ for gon in mats_to_gons[mat]:
+ gon.material_index = new_mat_list_order.index(mat)
+
+ for ob in outfits:
+ #Add a general outline that covers the rest of the materials on the object that don't need transparency
+ mod = ob.modifiers.new(
+ type='SOLIDIFY',
+ name='Outline Modifier')
+ mod.thickness = 0.0005
+ mod.offset = 1
+ mod.material_offset = outlineStart[ob.name] if not bpy.context.scene.kkbp.use_single_outline else 200
+ mod.use_flip_normals = True
+ mod.use_rim = False
+ mod.show_expanded = False
+ outline_mat = bpy.data.materials['Outline General'].copy()
+ outline_mat.name = 'Outline ' + ob.name
+ outline_mat.node_tree.nodes['combine'].inputs['Force visibility'].default_value = 1
+ ob.data.materials.append(outline_mat)
+ c.print_timer('add_outlines_to_clothes')
+
+ @classmethod
+ def load_luts(cls):
+ self = cls
+ self.lut_selection = bpy.context.scene.kkbp.colors_dropdown
+ self.lut_light = 'Lut_TimeDay.png'
+
+ self.lut_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.lut_light)
+ day_lut = bpy.data.images.load(self.lut_path, check_existing=True)
+ day_lut.use_fake_user = True
+
+
+ def load_json_colors(self):
+ self.update_shaders('light') # Set light colors
+ self.update_shaders('dark') # Set dark colors
+ c.print_timer('load_json_colors')
+
+ def set_color_management(self):
+ if bpy.app.version[0] != 3:
+ #disable shadows in the scene. The toon shading in 4.2 is fucking broken but the broken-ness can be hidden with this setting
+ bpy.data.scenes[0].eevee.use_shadows = False
+ c.print_timer('set_color_management')
+
+ # %% Supporting functions
+ @staticmethod
+ def apply_texture_data_to_image(mat: str, image: str, node:str, group = 'textures'):
+ '''Sets offset and scale of an image node using the TextureData.json '''
+ json_tex_data = c.json_file_manager.get_json_file('KK_TextureData.json')
+ texture_data = [t for t in json_tex_data if t["textureName"] == image]
+ if texture_data and bpy.data.materials.get(mat):
+ #Apply Offset and Scale
+ bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.translation[0] = texture_data[0]["offset"]["x"]
+ bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.translation[1] = texture_data[0]["offset"]["y"]
+ bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.scale[0] = texture_data[0]["scale"]["x"]
+ bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.scale[1] = texture_data[0]["scale"]["y"]
+
+ def image_load(self, material_name: str, image_suffix = '', image_override = None, node_override = None, group_override = None):
+ '''Automatically load image into mat's texture slot'''
+ #get the id from the material
+ material_name = 'KK ' + material_name + ' ' + c.get_name()
+ material = bpy.data.materials[material_name]
+ #get the image name using the id and the suffix
+ image_name = image_override if image_override else material['id'] + image_suffix
+ #then load the image into the texture slot
+ if bpy.data.images.get(image_name):
+ node = node_override if node_override else image_name.replace(material['id'], '')
+ group = group_override if group_override else 'textures'
+ bpy.data.materials[material_name].node_tree.nodes[group].node_tree.nodes[node].image = bpy.data.images[image_name]
+ #also apply scaling and offset data to the image
+ self.apply_texture_data_to_image(material_name, image_name, node, group)
+ else:
+ c.kklog('File wasnt found, skipping: ' + image_name)
+
+ @staticmethod
+ def set_uv_type(mat: str, uvnode: str, uv_name: str, group = 'textures'):
+ bpy.data.materials['KK ' + mat + ' ' + c.get_name()].node_tree.nodes[group].node_tree.nodes['pos'].node_tree.nodes[uvnode].uv_map = uv_name
+
+ @staticmethod
+ def __bilinear_interpolation__(lut_pixels, coords):
+ h, w, _ = lut_pixels.shape
+ x = coords[:, :, 0] * (w - 1)
+ # Fudge x coordinates based on x position. subtract -0.5 if at x position 0 and add 0.5 if at x position 1024 of the LUT.
+ # this helps with some kind of overflow / underflow issue where it reads from the next LUT square when it's not supposed to
+ x = x + (x / 1024 - 0.5)
+ y = coords[:, :, 1] * (h - 1)
+ # Get integer and fractional parts of each coordinate.
+ # Also make sure each coordinate is clipped to the LUT image bounds
+ x0 = numpy.clip(numpy.floor(x).astype(int), 0, w - 1)
+ x1 = numpy.clip(x0 + 1, 0, w - 1)
+ y0 = numpy.clip(numpy.floor(y).astype(int), 0, h - 1)
+ y1 = numpy.clip(y0 + 1, 0, h - 1)
+ x_frac = x - x0
+ y_frac = y - y0
+ # Get the pixel values at four corners of this coordinate
+ f00 = lut_pixels[y0, x0]
+ f01 = lut_pixels[y1, x0]
+ f10 = lut_pixels[y0, x1]
+ f11 = lut_pixels[y1, x1]
+ del x0
+ del x1
+ del y0
+ del y1
+ # Perform the bilinear interpolation using the fractional part of each coordinate
+ # This will ensure the LUT can provide the correct color every single time, even if that color isn't found in the LUT itself
+ # If this isn't performed, the resulting image will look very blocky because it will snap to colors only found in the LUT.
+ lut_col_bot = f00 * (1 - y_frac)[:, :, numpy.newaxis] + f01 * y_frac[:, :, numpy.newaxis]
+ lut_col_top = f10 * (1 - y_frac)[:, :, numpy.newaxis] + f11 * y_frac[:, :, numpy.newaxis]
+ interpolated_colors = lut_col_bot * (1 - x_frac)[:, :, numpy.newaxis] + lut_col_top * x_frac[:, :, numpy.newaxis]
+ return interpolated_colors
+
+ def saturate_color(self, color: float, light_pass = 'light', shadow_color = {'r':0.764, 'g':0.880, 'b':1}) -> dict[str, float]:
+ '''The Secret Sauce. Accepts a 0-1 float rgba color dict, saturates it to match the in-game look
+ and returns it in the form of a 0-1 float rgba array'''
+
+ #fix the color if it does not have an alpha
+ color['a'] = color.get('a', 1)
+
+ #make the color a dark color if the light_pass is set to dark
+ color = color if light_pass == 'light' else self.clothes_dark_color(color, shadow_color)
+ width, height = 1,1
+
+ # Load image and LUT image pixels into array
+ image_pixels = numpy.array([color['r'], color['g'], color['b'], 1],dtype=modify_material.np_number_precision).reshape(height, width, 4)
+
+ # Find the XY coordinates of the LUT image needed to saturate each pixel
+ coord = image_pixels[:, :, :3] * self.coord_scale + self.coord_offset
+ coord_frac, coord_floor = numpy.modf(coord)
+ coord_bot = coord[:, :, :2] + numpy.tile(coord_floor[:, :, 2].reshape(height, width, 1), (1, 1, 2)) * self.texel_height_X0
+ coord_top = numpy.clip(coord_bot + self.texel_height_X0, 0, 1)
+
+ lutcol_bot = self.__bilinear_interpolation__(self.lut_pixels, coord_bot)
+ lutcol_top = self.__bilinear_interpolation__(self.lut_pixels, coord_top)
+ #After the older gpu code uses the texture lookup the colorspace is converted from srgb to linear,
+ # so replicate that behavior here.
+ def srgb_to_linear(srgb):
+ linear_rgb = numpy.where(
+ srgb <= 0.04045,
+ srgb / 12.92,
+ numpy.power((srgb + 0.055) / 1.055, 2.4))
+ return linear_rgb
+ lutcol_bot = srgb_to_linear(lutcol_bot)
+ lutcol_top = srgb_to_linear(lutcol_top)
+ lut_colors = lutcol_bot * (1 - coord_frac[:, :, 2].reshape(height, width, 1)) + lutcol_top * coord_frac[:, :, 2].reshape(height, width, 1)
+ image_pixels[:, :, :3] = lut_colors[:,:,:3]
+
+ return image_pixels.flatten().tolist()[0:4]
+
+
+ def update_shaders(self, light_pass: str):
+ '''Set the colors for everything. This is run once for the light colors and again for the dark colors'''
+ #set the tongue colors if it exists
+ #if c.get_material_names('o_tang') and (tongue := c.get_tongue()):
+ if c.get_material_names('o_tang') and (tongue := c.get_tongue()):
+ shader_inputs = tongue.material_slots[0].material.node_tree.nodes[light_pass].inputs
+ shader_inputs['Maintex Saturation'].default_value = 0.6
+ shader_inputs['Detail intensity (green)'].default_value = 0.01
+ shader_inputs['Color mask (base)'].default_value = [1, 1, 1, 1]
+ mat_name = tongue.material_slots[0].name
+ shader_inputs['Color mask (red)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+ shader_inputs['Color mask (green)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+ shader_inputs['Color mask (blue)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color3 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+
+ #set all of the hair colors
+ hair_materials = [m for m in bpy.data.materials if m.get('hair') == True and m.get('name') == c.get_name()]
+ for hair_material in hair_materials:
+ shader_inputs = hair_material.node_tree.nodes[light_pass].inputs
+ shader_inputs['Hair color'].default_value = self.saturate_color(c.json_file_manager.get_color(hair_material.name, "_Color " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(hair_material.name))
+ shader_inputs['Color mask (root)'].default_value = self.saturate_color(c.json_file_manager.get_color(hair_material.name, "_Color2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(hair_material.name))
+ shader_inputs['Color mask (tip)'].default_value = self.saturate_color(c.json_file_manager.get_color(hair_material.name, "_Color3 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(hair_material.name))
+
+ #set body colors
+ if c.get_body():
+ if c.get_material_names('o_body_a'):
+ shader_inputs = c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes[light_pass].inputs
+ mat_name = 'KK Body ' + c.get_name()
+ if light_pass == 'light':
+ shader_inputs['Skin color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), light_pass = 'light')
+ else:
+ shader_inputs['Skin color'].default_value = self.saturate_color(self.skin_dark_color(c.json_file_manager.get_color(mat_name, "_Color ")), light_pass = 'light')
+ shader_inputs['Detail color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+ shader_inputs['Line mask color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) #use same color for both detail and line
+ shader_inputs['Nail color (multiplied)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color5 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+ if not bpy.context.scene.kkbp.sfw_mode:
+ shader_inputs['Underhair color'].default_value = [0, 0, 0, 1]
+ # shader_inputs['Nipple base'].default_value = [1.0, 0.48, 0.48, 1.0] #these don't seem to be the correct colors. Just use hardcoded colors in .blend file
+ # shader_inputs['Nipple base 2'].default_value = [0.9, 0.0, 0.1, 1.0]
+ # shader_inputs['Nipple shine'].default_value = [1.0, 0.8, 0.8, 1.0]
+ # shader_inputs['Nipple rim'].default_value = [1.0, 0.08, 0.09, 1.0]
+
+ #face
+ #Note that some headmods have multiple face materials. This will only replace the first one
+ if c.get_material_names('cf_O_face'):
+ #setup the face material
+ mat_name = 'KK Face ' + c.get_name()
+ shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes[light_pass].inputs
+ shader_inputs['Skin color'].default_value = c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes[light_pass].inputs['Skin color'].default_value
+ shader_inputs['Detail color'].default_value = self.saturate_color(c.json_file_manager.get_color('KK Body ' + c.get_name(), "_Color2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color('KK Body ' + c.get_name()))
+ shader_inputs['Light blush color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_overcolor2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+ shader_inputs['Lipstick multiplier'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_overcolor1 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+
+ #eyebrows
+ if c.get_material_names('cf_O_mayuge'):
+ mat_name = 'KK Eyebrows (mayuge) ' + c.get_name()
+ shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes['light'].inputs
+ shader_inputs['Eyebrow color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "))
+ shader_inputs['Eyebrow color dark'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), 'dark' , shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+
+ #eyeline
+ if c.get_material_names('cf_O_eyeline'):
+ mat_name = 'KK Eyeline up ' + c.get_name()
+ shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes['light'].inputs
+ shader_inputs['Eyeline fade color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name))
+ #the below doesn't seem to be the correct color. Use the hardcoded one in the blend file for now
+ # if len(c.get_material_names('cf_O_eyeline')) > 1:
+ # shader_inputs['Kage color'].default_value = self.saturate_color(c.json_file_manager.get_color('KK Eyeline kage ' + c.get_name(), "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color('KK Eyeline kage ' + c.get_name()))
+ if c.get_material_names('cf_O_eyeline_low'):
+ shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes['light'].inputs
+ shader_inputs['Eyeline down fade color'].default_value = self.saturate_color(c.json_file_manager.get_color('KK Eyeline down ' + c.get_name(), "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color('KK Eyeline down ' + c.get_name()))
+
+ #set the clothes colors
+ materials = [m for m in bpy.data.materials if m.get('outfit') == True and m.get('name') == c.get_name()]
+ for material in materials:
+ shader_inputs = material.node_tree.nodes[light_pass].inputs
+ shader_inputs['Color mask (red)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name))
+ shader_inputs['Color mask (green)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name))
+ shader_inputs['Color mask (blue)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color3 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name))
+ shader_inputs['Pattern color (red)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color1_2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name))
+ shader_inputs['Pattern color (green)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color2_2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name))
+ shader_inputs['Pattern color (blue)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color3_2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name))
+
+ #something is wrong with this one, currently unused
+ # def hair_dark_color(self, color, shadow_color):
+ # diffuse = float4(color[0], color[1], color[2], 1)
+ # _ShadowColor = float4(shadow_color['r'], shadow_color['g'], shadow_color['b'], 1)
+
+ # finalAmbientShadow = 0.7225; #constant
+ # invertFinalAmbientShadow = finalAmbientShadow #this shouldn't be equal to this but it works so whatever
+
+ # finalAmbientShadow = finalAmbientShadow * _ShadowColor
+ # finalAmbientShadow += finalAmbientShadow;
+ # shadowCol = _ShadowColor - 0.5;
+ # shadowCol = -shadowCol * 2 + 1;
+
+ # invertFinalAmbientShadow = -shadowCol * invertFinalAmbientShadow + 1;
+ # shadeCheck = 0.5 < _ShadowColor;
+ # hlslcc_movcTemp = finalAmbientShadow;
+ # hlslcc_movcTemp.x = invertFinalAmbientShadow.x if (shadeCheck.x) else finalAmbientShadow.x;
+ # hlslcc_movcTemp.y = invertFinalAmbientShadow.y if (shadeCheck.y) else finalAmbientShadow.y;
+ # hlslcc_movcTemp.z = invertFinalAmbientShadow.z if (shadeCheck.z) else finalAmbientShadow.z;
+ # finalAmbientShadow = (hlslcc_movcTemp).saturate();
+ # diffuse *= finalAmbientShadow;
+
+ # finalDiffuse = diffuse.saturate();
+
+ # shading = 1 - finalAmbientShadow;
+ # shading = 1 * shading + finalAmbientShadow;
+ # finalDiffuse *= shading;
+ # shading = 1.0656;
+ # finalDiffuse *= shading;
+
+ # return [finalDiffuse.x, finalDiffuse.y, finalDiffuse.z];
+
+ def MapValuesMain(self, color): #-> float4
+ '''mapvaluesmain function is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Skin/KKPDiffuse.cginc'''
+ t0 = color;
+ tb30 = t0.y>=t0.z;
+ t30 = 1 if tb30 else float(0.0);
+ t1 = float4(t0.z, t0.y, t0.z, t0.w);
+ t2 = float4(t0.y - t1.x, t0.z - t1.y);
+ t1.z = float(-1.0);
+ t1.w = float(0.666666687);
+ t2.z = float(1.0);
+ t2.w = float(-1.0);
+ t1 = float4(t30, t30, t30, t30) * float4(t2.x, t2.y, t2.w, t2.z) + float4(t1.x, t1.y, t1.w, t1.z);
+ tb30 = t0.x>=t1.x;
+ t30 = 1 if tb30 else 0.0;
+ t2.z = t1.w;
+ t1.w = t0.x;
+ t2 = float4(t1.w, t1.y, t2.z, t1.x)
+ t2 = (-t1) + t2;
+ t1 = float4(t30, t30, t30, t30) * t2 + t1;
+ t30 = min(t1.y, t1.w);
+ t30 = (-t30) + t1.x;
+ t2.x = t30 * 6.0 + 1.00000001e-10;
+ t11 = (-t1.y) + t1.w;
+ t11 = t11 / t2.x;
+ t11 = t11 + t1.z;
+ t1.x = t1.x + 1.00000001e-10;
+ t30 = t30 / t1.x;
+ t30 = t30 * 0.660000026;
+ #w component isn't used anymore so ignore
+ t2 = float4(t11, t11, t11).abs() + float4(-0.0799999982, -0.413333356, 0.25333333)
+ t2 = t2.frac()
+ t2 = (-t2) * float4(2.0, 2.0, 2.0) + float4(1.0, 1.0, 1.0);
+ t2 = t2.abs() * float4(3.0, 3.0, 3.0) + float4(-1.0, -1.0, -1.0);
+ t2 = t2.clamp()
+ t2 = t2 + float4(-1.0, -1.0, -1.0);
+ t2 = float4(t30, t30, t30) * t2 + float4(1.0, 1.0, 1.0);
+ return float4(t2.x, t2.y, t2.z, 1);
+
+ def skin_dark_color(self, color) -> dict[str, float]:
+ '''Takes a 1.0 max rgba dict and returns a 1.0 max rgba dict. skin is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Skin/KKPSkinFrag.cginc '''
+ diffuse = float4(color['r'], color['g'], color['b'], 1)
+ shadingAdjustment = self.MapValuesMain(diffuse);
+
+ diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
+ diffuseShaded = -diffuseShaded * 2 + 1;
+
+ compTest = 0.555555582 < shadingAdjustment;
+ shadingAdjustment *= 1.79999995;
+ diffuseShaded = -diffuseShaded * 0.7225 + 1;
+ hlslcc_movcTemp = shadingAdjustment;
+ hlslcc_movcTemp.x = diffuseShaded.x if (compTest.x) else shadingAdjustment.x; #370
+ hlslcc_movcTemp.y = diffuseShaded.y if (compTest.y) else shadingAdjustment.y; #371
+ hlslcc_movcTemp.z = diffuseShaded.z if (compTest.z) else shadingAdjustment.z; #372
+ shadingAdjustment = (hlslcc_movcTemp).saturate(); #374 the lerp result (and shadowCol) is going to be this because shadowColor's alpha is always 1 making shadowCol 1
+
+ finalDiffuse = diffuse * shadingAdjustment;
+
+ bodyShine = float4(1.0656, 1.0656, 1.0656, 1);
+ finalDiffuse *= bodyShine;
+ fudge_factor = float4(0.02, 0.05, 0, 0) #result is slightly off but it looks consistently off so add a fudge factor
+ finalDiffuse += fudge_factor
+
+ return {'r':finalDiffuse.x, 'g':finalDiffuse.y, 'b':finalDiffuse.z, 'a':1}
+
+ def ShadeAdjustItem(self, col, _ShadowColor): #-> float4
+ '''#shadeadjust function is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Item/KKPItemDiffuse.cginc .
+ lines with comments at the end have been translated from C# to python. lines without comments at the end have been copied verbatim from the C# source'''
+ #start at line 63
+ t0 = col
+ t1 = float4(t0.y, t0.z, None, t0.x) * float4(_ShadowColor.y, _ShadowColor.z, None, _ShadowColor.x) #line 65
+ t2 = float4(t1.y, t1.x) #66
+ t3 = float4(t0.y, t0.z) * float4(_ShadowColor.y, _ShadowColor.z) + (-float4(t2.x, t2.y)); #67
+ tb30 = t2.y >= t1.y;
+ t30 = 1 if tb30 else 0;
+ t2 = float4(t2.x, t2.y, -1.0, 0.666666687); #70-71
+ t3 = float4(t3.x, t3.y, 1.0, -1); #72-73
+ t2 = (t30) * t3 + t2;
+ tb30 = t1.w >= t2.x;
+ t30 = 1 if tb30 else float(0.0);
+ t1 = float4(t2.x, t2.y, t2.w, t1.w) #77
+ t2 = float4(t1.w, t1.y, t2.z, t1.x) #78
+ t2 = (-t1) + t2;
+ t1 = (t30) * t2 + t1;
+ t30 = min(t1.y, t1.w);
+ t30 = (-t30) + t1.x;
+ t2.x = t30 * 6.0 + 1.00000001e-10;
+ t11 = (-t1.y) + t1.w;
+ t11 = t11 / t2.x;
+ t11 = t11 + t1.z;
+ t1.x = t1.x + 1.00000001e-10;
+ t30 = t30 / t1.x;
+ t30 = t30 * 0.5;
+ #the w component of t1 is no longer used, so ignore it
+ t1 = abs((t11)) + float4(0.0, -0.333333343, 0.333333343, 1); #90
+ t1 = t1.frac(); #91
+ t1 = -t1 * 2 + 1; #92
+ t1 = t1.abs() * 3 + (-1) #93
+ t1 = t1.clamp() #94
+ t1 = t1 + (-1); #95
+ t1 = (t30) * t1 + 1; #96
+ return float4(t1.x, t1.y, t1.z, 1) #97
+
+ def clothes_dark_color(self, color: dict, shadow_color: dict) -> dict[str, float]:
+ '''Takes a 1.0 max rgba dict and returns a 1.0 max rgba dict.
+ clothes is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Item/MainItemPlus.shader
+ This was stripped down to just the shadow portion, and to remove all constants'''
+ ################### variable setup
+ _ambientshadowG = float4(0.15, 0.15, 0.15, 0.15) #constant from experimentation
+ diffuse = float4(color['r'],color['g'],color['b'],1) #maintex color
+ _ShadowColor = float4(shadow_color['r'],shadow_color['g'],shadow_color['b'],1) #the shadow color from material editor
+ ##########################
+
+ #start at line 344 because the other one is for outlines
+ shadingAdjustment = self.ShadeAdjustItem(diffuse, _ShadowColor)
+
+ #skip to line 352
+ diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
+ diffuseShaded = -diffuseShaded * 2 + 1;
+
+ compTest = 0.555555582 < shadingAdjustment;
+ shadingAdjustment *= 1.79999995;
+ diffuseShaded = -diffuseShaded * 0.7225 + 1; #invertfinalambient shadow is a constant 0.7225, so don't calc it
+
+ hlslcc_movcTemp = shadingAdjustment;
+ hlslcc_movcTemp.x = diffuseShaded.x if (compTest.x) else shadingAdjustment.x; #370
+ hlslcc_movcTemp.y = diffuseShaded.y if (compTest.y) else shadingAdjustment.y; #371
+ hlslcc_movcTemp.z = diffuseShaded.z if (compTest.z) else shadingAdjustment.z; #372
+ shadingAdjustment = (hlslcc_movcTemp).saturate(); #374 the lerp result (and shadowCol) is going to be this because shadowColor's alpha is always 1 making shadowCol 1
+
+ diffuseShadow = diffuse * shadingAdjustment;
+
+ # lightCol is constant [1.0656, 1.0656, 1.0656, 1] calculated from the custom ambient of [0.666, 0.666, 0.666, 1] and sun light color [0.666, 0.666, 0.666, 1],
+ # so ambientCol always results in lightCol after the max function
+ ambientCol = float4(1.0656, 1.0656, 1.0656, 1);
+ diffuseShadow = diffuseShadow * ambientCol;
+
+ return {'r':diffuseShadow.x, 'g':diffuseShadow.y, 'b':diffuseShadow.z, 'a':1}
+
+ @staticmethod
+ def create_darktex(maintex: bpy.types.Image, shadow_color: float) -> bpy.types.Image:
+ '''#accepts a bpy image and creates a dark alternate using a modified version of the darkening code above. Returns a new bpy image'''
+ if not os.path.isfile(bpy.context.scene.kkbp.import_dir + '/dark_files/' + maintex.name[:-6] + 'DT.png'):
+ ok = time.time()
+ image_array = numpy.asarray(maintex.pixels,dtype=modify_material.np_number_precision)
+ image_length = len(image_array)
+ image_row_length = int(image_length/4)
+ image_array = image_array.reshape((image_row_length, 4))
+
+ ################### variable setup
+ _ambientshadowG = numpy.asarray([0.15, 0.15, 0.15, 0.15],dtype=modify_material.np_number_precision) #constant from experimentation
+ diffuse = image_array #maintex color
+ _ShadowColor = numpy.asarray([shadow_color['r'],shadow_color['g'],shadow_color['b'], 1],dtype=modify_material.np_number_precision) #the shadow color from material editor
+ ##########################
+
+ #start at line 344 because the other one is for outlines
+ #shadingAdjustment = ShadeAdjustItemNumpy(diffuse, _ShadowColor)
+ #start at line 63
+ x=0;y=1;z=2;w=3;
+ t0 = diffuse
+ t1 = t0[:, [y, z, z, x]] * _ShadowColor[[y,z,z,x]]
+ t2 = t1[:, [y,x]]
+ t3 = t0[:, [y,z]] * _ShadowColor[[y,z]] + (-t2)
+ tb30 = t2[:, [y]] >= t1[:, [y]]
+ t30 = tb30.astype(int)
+ t2 = numpy.hstack((t2[:, [x,y]], numpy.full((t2.shape[0], 1), -1, t2.dtype), numpy.full((t2.shape[0], 1), 0.666666687, t2.dtype)))
+ t3 = numpy.hstack((t3[:, [x,y]], numpy.full((t3.shape[0], 1), 1, t3.dtype), numpy.full((t3.shape[0], 1), -1, t3.dtype)))
+ t2 = t30 * t3 + t2
+ tb30 = t1[:, [w]] >= t1[:, [x]]
+ t30 = tb30.astype(int)
+ t1 = numpy.hstack((t2[:, [x, y, w]], t1[:, [w]]))
+ t2 = numpy.hstack((t1[:, [w, y]], t2[:, [z]], t1[:, [x]]))
+ t2 = -t1 + t2
+ t1 = t30 * t2 + t1
+ t30 = numpy.minimum(t1[:, [y]], t1[:, [w]])
+ t30 = -t30 + t1[:, [x]]
+ t2[:, [x]] = t30 * 6 + 1.00000001e-10
+ t11 = -t1[:, [y]] + t1[:, [w]]
+ t11 = t11 / t2[:, [x]];
+ t11 = t11 + t1[:, [z]];
+ t1[:, [x]] = t1[:, [x]] + 1.00000001e-10;
+ t30 = t30 / t1[:, [x]];
+ t30 = t30 * 0.5;
+ #the w component of t1 is no longer used, so ignore it
+ t1 = numpy.absolute(t11) + numpy.asarray([0.0, -0.333333343, 0.333333343, 1]); #90
+ t1 = t1 - numpy.floor(t1)
+ t1 = -t1 * 2 + 1
+ t1 = numpy.absolute(t1) * 3 + (-1)
+ t1 = numpy.clip(t1, 0, 1)
+ t1 = t1 + (-1); #95
+ t1 = (t30) * t1 + 1; #96
+
+ shadingAdjustment = t1
+
+ #skip to line 352
+ diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
+ diffuseShaded = -diffuseShaded * 2 + 1;
+
+ compTest = 0.555555582 < shadingAdjustment;
+ shadingAdjustment *= 1.79999995;
+ diffuseShaded = -diffuseShaded * 0.7225 + 1; #invertfinalambient shadow is a constant 0.7225, so don't calc it
+
+ hlslcc_movcTemp = shadingAdjustment;
+ #reframe ifs as selects
+ hlslcc_movcTemp[:, [x]] = numpy.select(condlist=[compTest[:, [x]], numpy.invert(compTest[:, [x]])], choicelist=[diffuseShaded[:, [x]], shadingAdjustment[:, [x]]])
+ hlslcc_movcTemp[:, [y]] = numpy.select(condlist=[compTest[:, [y]], numpy.invert(compTest[:, [y]])], choicelist=[diffuseShaded[:, [y]], shadingAdjustment[:, [y]]])
+ hlslcc_movcTemp[:, [z]] = numpy.select(condlist=[compTest[:, [z]], numpy.invert(compTest[:, [z]])], choicelist=[diffuseShaded[:, [z]], shadingAdjustment[:, [z]]])
+ shadingAdjustment = numpy.clip(hlslcc_movcTemp, 0, 1) #374 the lerp result (and shadowCol) is going to be this because shadowColor's alpha is always 1 making shadowCol 1
+
+ diffuseShadow = diffuse * shadingAdjustment;
+
+ # lightCol is constant [1.0656, 1.0656, 1.0656, 1] calculated from the custom ambient of [0.666, 0.666, 0.666, 1] and sun light color [0.666, 0.666, 0.666, 1],
+ # so ambientCol always results in lightCol after the max function
+ ambientCol = numpy.asarray([1.0656, 1.0656, 1.0656, 1],dtype=modify_material.np_number_precision);
+ diffuseShadow = diffuseShadow * ambientCol;
+
+ #make a new image and place the dark pixels into it
+ dark_array = diffuseShadow
+ darktex = bpy.data.images.new(maintex.name[:-7] + '_DT.png', width=maintex.size[0], height=maintex.size[1], alpha = True)
+ darktex.file_format = 'PNG'
+ darktex.pixels = dark_array.ravel()
+ darktex.use_fake_user = True
+ darktex_filename = maintex.filepath_raw[maintex.filepath_raw.find(maintex.name):][:-7]+ '_DT.png'
+ darktex_filepath = bpy.context.scene.kkbp.import_dir + '/dark_files/' + darktex_filename
+ darktex.filepath_raw = darktex_filepath
+ darktex.pack()
+ darktex.save()
+ c.kklog('Created dark version of {} in {} seconds'.format(darktex.name, time.time() - ok))
+ return darktex
+ else:
+ if bpy.app.version[0] == 3:
+ bpy.ops.image.open(filepath=str(bpy.context.scene.kkbp.import_dir + '/dark_files/' + maintex.name[:-6] + 'DT.png'), use_udim_detecting=False)
+ else:
+ bpy.data.images.load(filepath=str(bpy.context.scene.kkbp.import_dir + '/dark_files/' + maintex.name[:-6] + 'DT.png'))
+ darktex = bpy.data.images[maintex.name[:-6] + 'DT.png']
+ c.kklog('Loading in existing dark version of {}'.format(darktex.name))
+ try:
+ darktex.pack()
+ darktex.save()
+ except:
+ c.kklog('This image was not automatically loaded in because its name exceeds 64 characters: ' + darktex.name, type = 'error')
+ return darktex
+
+class float4:
+ '''class to mimic part of float4 class in Unity
+ multiplying things per element according to https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/float4.gen.cs#L330
+ returning things like float.XZW as [Xposition = X, Yposition = Z, Zposition = W] according to https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/float4.gen.cs#L3056
+ using the variable order x, y, z, w according to https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/float4.gen.cs#L42'''
+ def __init__(self, x = None, y = None, z = None, w = None):
+ self.x = x
+ self.y = y
+ self.z = z
+ self.w = w
+ def __mul__ (self, vector):
+ #if a float4, multiply piece by piece, else multiply full vector
+ if type(vector) in [float, int]:
+ vector = float4(vector, vector, vector, vector)
+ x = self.x * vector.x if self.get('x') != None else None
+ y = self.y * vector.y if self.get('y') != None else None
+ z = self.z * vector.z if self.get('z') != None else None
+ w = self.w * vector.w if self.get('w') != None else None
+ return float4(x,y,z,w)
+ __rmul__ = __mul__
+ def __add__ (self, vector):
+ #if a float4, add piece by piece, else add full vector
+ if type(vector) in [float, int]:
+ vector = float4(vector, vector, vector, vector)
+ x = self.x + vector.x if self.get('x') != None else None
+ y = self.y + vector.y if self.get('y') != None else None
+ z = self.z + vector.z if self.get('z') != None else None
+ w = self.w + vector.w if self.get('w') != None else None
+ return float4(x,y,z,w)
+ __radd__ = __add__
+ def __sub__ (self, vector):
+ #if a float4, subtract piece by piece, else subtract full vector
+ if type(vector) in [float, int]:
+ vector = float4(vector, vector, vector, vector)
+ x = self.x - vector.x if self.get('x') != None else None
+ y = self.y - vector.y if self.get('y') != None else None
+ z = self.z - vector.z if self.get('z') != None else None
+ w = self.w - vector.w if self.get('w') != None else None
+ return float4(x,y,z,w)
+ __rsub__ = __sub__
+ def __gt__ (self, vector):
+ #if a float4, compare piece by piece, else compare full vector
+ if type(vector) in [float, int]:
+ vector = float4(vector, vector, vector, vector)
+ x = self.x > vector.x if self.get('x') != None else None
+ y = self.y > vector.y if self.get('y') != None else None
+ z = self.z > vector.z if self.get('z') != None else None
+ w = self.w > vector.w if self.get('w') != None else None
+ return float4(x,y,z,w)
+ def __neg__ (self):
+ x = -self.x if self.get('x') != None else None
+ y = -self.y if self.get('y') != None else None
+ z = -self.z if self.get('z') != None else None
+ w = -self.w if self.get('w') != None else None
+ return float4(x,y,z,w)
+ def frac(self):
+ x = self.x - math.floor (self.x) if self.get('x') != None else None
+ y = self.y - math.floor (self.y) if self.get('y') != None else None
+ z = self.z - math.floor (self.z) if self.get('z') != None else None
+ w = self.w - math.floor (self.w) if self.get('w') != None else None
+ return float4(x,y,z,w)
+ def abs(self):
+ x = abs(self.x) if self.get('x') != None else None
+ y = abs(self.y) if self.get('y') != None else None
+ z = abs(self.z) if self.get('z') != None else None
+ w = abs(self.w) if self.get('w') != None else None
+ return float4(x,y,z,w)
+ def clamp(self):
+ x = (0 if self.x < 0 else 1 if self.x > 1 else self.x) if self.get('x') != None else None
+ y = (0 if self.y < 0 else 1 if self.y > 1 else self.y) if self.get('y') != None else None
+ z = (0 if self.z < 0 else 1 if self.z > 1 else self.z) if self.get('z') != None else None
+ w = (0 if self.w < 0 else 1 if self.w > 1 else self.w) if self.get('w') != None else None
+ return float4(x,y,z,w)
+ saturate = clamp
+ def clamphalf(self):
+ x = (0 if self.x < 0 else .5 if self.x > .5 else self.x) if self.get('x') != None else None
+ y = (0 if self.y < 0 else .5 if self.y > .5 else self.y) if self.get('y') != None else None
+ z = (0 if self.z < 0 else .5 if self.z > .5 else self.z) if self.get('z') != None else None
+ w = (0 if self.w < 0 else .5 if self.w > .5 else self.w) if self.get('w') != None else None
+ return float4(x,y,z,w)
+ def get(self, var):
+ if hasattr(self, var):
+ return getattr(self, var)
+ else:
+ return None
+ def __str__(self):
+ return str([self.x, self.y, self.z, self.w])
+ __repr__ = __str__
+
diff --git a/importing/modifymesh.md b/importing/modifymesh.md
new file mode 100644
index 0000000..feaac07
--- /dev/null
+++ b/importing/modifymesh.md
@@ -0,0 +1,138 @@
+### ENUM order that corresponds to ChaReference_RefObjKey value in KK_ReferenceInfoData.json
+0. HeadParent,
+1. HairParent,
+1. a_n_hair_pony,
+1. a_n_hair_twin_L,
+1. a_n_hair_twin_R,
+1. a_n_hair_pin,
+1. a_n_hair_pin_R,
+1. a_n_headtop,
+1. a_n_headflont,
+1. a_n_head,
+1. a_n_headside,
+1. a_n_megane,
+1. a_n_earrings_L,
+1. a_n_earrings_R,
+1. a_n_nose,
+1. a_n_mouth,
+1. a_n_neck,
+1. a_n_bust_f,
+1. a_n_bust,
+1. a_n_nip_L,
+1. a_n_nip_R,
+1. a_n_back,
+1. a_n_back_L,
+1. a_n_back_R,
+1. a_n_waist,
+1. a_n_waist_f,
+1. a_n_waist_b,
+1. a_n_waist_L,
+1. a_n_waist_R,
+1. a_n_leg_L,
+1. a_n_leg_R,
+1. a_n_knee_L,
+1. a_n_knee_R,
+1. a_n_ankle_L,
+1. a_n_ankle_R,
+1. a_n_heel_L,
+1. a_n_heel_R,
+1. a_n_shoulder_L,
+1. a_n_shoulder_R,
+1. a_n_elbo_L,
+1. a_n_elbo_R,
+1. a_n_arm_L,
+1. a_n_arm_R,
+1. a_n_wrist_L,
+1. a_n_wrist_R,
+1. a_n_hand_L,
+1. a_n_hand_R,
+1. a_n_ind_L,
+1. a_n_ind_R,
+1. a_n_mid_L,
+1. a_n_mid_R,
+1. a_n_ring_L,
+1. a_n_ring_R,
+1. a_n_dan,
+1. a_n_kokan,
+1. a_n_ana,
+1. k_f_handL_00,
+1. k_f_handR_00,
+1. k_f_shoulderL_00,
+1. k_f_shoulderR_00,
+1. ObjEyeline,
+1. ObjEyelineLow,
+1. ObjEyebrow,
+1. ObjNoseline,
+1. ObjEyeL,
+1. ObjEyeR,
+1. ObjEyeWL,
+1. ObjEyeWR,
+1. ObjFace,
+1. ObjDoubleTooth,
+1. ObjBody,
+1. ObjNip,
+1. N_FaceSpecial,
+1. CORRECT_ARM_L,
+1. CORRECT_ARM_R,
+1. CORRECT_HAND_L,
+1. CORRECT_HAND_R,
+1. CORRECT_TONGUE_TOP,
+1. CORRECT_MOUTH_TARGET,
+1. CORRECT_MOUTH_TARGET02,
+1. CORRECT_HEAD_DBCOL,
+1. S_ANA,
+1. S_TongueF,
+1. S_TongueB,
+1. S_Son,
+1. S_SimpleTop,
+1. S_SimpleBody,
+1. S_SimpleTongue,
+1. S_MNPA,
+1. S_MNPB,
+1. S_MOZ_ALL,
+1. S_GOMU,
+1. S_CTOP_T_DEF,
+1. S_CTOP_T_NUGE,
+1. S_CTOP_B_DEF,
+1. S_CTOP_B_NUGE,
+1. S_CBOT_T_DEF,
+1. S_CBOT_T_NUGE,
+1. S_CBOT_B_DEF,
+1. S_CBOT_B_NUGE,
+1. S_UWT_T_DEF,
+1. S_UWT_T_NUGE,
+1. S_UWT_B_DEF,
+1. S_UWT_B_NUGE,
+1. S_UWB_T_DEF,
+1. S_UWB_T_NUGE,
+1. S_UWB_B_DEF,
+1. S_UWB_B_NUGE,
+1. S_UWB_B_NUGE2,
+1. S_PANST_DEF,
+1. S_PANST_NUGE,
+1. S_TPARTS_00_DEF,
+1. S_TPARTS_00_NUGE,
+1. S_TPARTS_01_DEF,
+1. S_TPARTS_01_NUGE,
+1. S_TPARTS_02_DEF,
+1. S_TPARTS_02_NUGE,
+1. ObjBraDef,
+1. ObjBraNuge,
+1. ObjInnerDef,
+1. ObjInnerNuge,
+1. S_TEARS_01,
+1. S_TEARS_02,
+1. S_TEARS_03,
+1. N_EyeBase,
+1. N_Hitomi,
+1. N_Gag00,
+1. N_Gag01,
+1. N_Gag02,
+1. DB_SKIRT_TOP,
+1. DB_SKIRT_TOPA,
+1. DB_SKIRT_TOPB,
+1. DB_SKIRT_BOT,
+1. F_ADJUSTWIDTHSCALE,
+1. A_ROOTBONE,
+1. BUSTUP_TARGET,
+1. NECK_LOOK_TARGET
\ No newline at end of file
diff --git a/importing/modifymesh.py b/importing/modifymesh.py
new file mode 100644
index 0000000..2241df4
--- /dev/null
+++ b/importing/modifymesh.py
@@ -0,0 +1,896 @@
+'''
+This file performs the following operations
+
+. Separates the rigged tongue, hair, shift/hang state clothing,
+ hitboxes, and puts them into their own collections
+· Delete mask material, shadowcast mesh, and bonelyfans mesh if present
+
+· Remove shapekeys on all objects except body / tears / gag eyes
+· Rename UV maps on body object and outfit objects
+· Translates all shapekey names to english
+· Combines shapekeys based on face part prefix and emotion suffix
+· Creates tear shapekeys
+· Creates gag eye shapekeys and drivers for shapekeys
+
+. Removes doubles on body object to prevent seams (if selected)
+
+· Mark certain body materials as freestyle faces for freestyle exclusion
+'''
+import re
+
+import bpy
+from .. import common as c
+from ..extras.linkshapekeys import link_keys
+
+class modify_mesh(bpy.types.Operator):
+ bl_idname = "kkbp.modifymesh"
+ bl_label = bl_idname
+ bl_description = bl_idname
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ try:
+ self.rename_uv_maps()
+
+ self.clean_up_duplicates()
+ self.separate_rigged_tongue()
+ self.separate_hair()
+ self.separate_alternate_clothing()
+ self.delete_shad_bone()
+ self.separate_hitboxes()
+ self.delete_mask_quad()
+
+ self.remove_unused_shapekeys()
+ self.translate_shapekeys()
+ self.combine_shapekeys()
+ self.create_tear_shapekeys()
+ self.create_gag_eye_shapekeys()
+
+ self.remove_body_seams()
+ self.mark_body_freestyle_faces()
+ c.clean_orphaned_data()
+
+ return {'FINISHED'}
+ except Exception as error:
+ c.handle_error(self, error)
+ return {"CANCELLED"}
+
+ def clean_up_duplicates(self):
+ '''Removes duplicate materials on the body object (there should only be one each)'''
+ c.clean_orphaned_data()
+ pattern = re.compile(r'\.\d{3}$')
+ for material in c.get_body().data.materials:
+ if pattern.search(material.name):
+ new_name = material.name[:-4]
+ c.kklog(f'Renamed duplicate body material {material.name} to {new_name}')
+ material.name = new_name
+ material['id'] = new_name
+ material['name'] = new_name
+
+ # %% Main functions
+ def separate_rigged_tongue(self):
+ """
+ Separates the rigged tongue object from the main body mesh.
+ If no rigged tongue, create one if general tongue exists
+ """
+
+ rigged_tongue_material = None
+ general_tongue_material = None
+ tongue_datas = c.json_file_manager.get_material_info_by_smr('o_tang')
+
+ if tongue_datas is None:
+ c.kklog('No tongue', 'warn')
+ c.print_timer('Skipped')
+ return
+
+ for item in tongue_datas:
+ if item['SMRPath'].endswith('N_cf_haed/o_tang'):
+ general_tongue_material = item['MaterialInformation'][0]['MaterialName']
+ else:
+ rigged_tongue_material = item['MaterialInformation'][0]['MaterialName']
+
+ if rigged_tongue_material == general_tongue_material:
+ rigged_tongue_material += '.001'
+
+ # if rigged tongue doesn't exist,
+ # rename tongue material to .001, duplicate tongue material and rename to general name, separate mesh by .001,
+ # duplicate tongue mesh as general tongue and join back to body
+ if rigged_tongue_material is None or general_tongue_material is None: # Some model only have N_cf_haed/o_tang or n_tang/o_tang
+ if general_tongue_material:
+ base_name = general_tongue_material
+
+ else:
+ base_name = rigged_tongue_material
+ general_tongue_material = base_name
+ rigged_tongue_material = base_name + '.001'
+
+ ori_material = bpy.data.materials[general_tongue_material]
+
+ # rename original material to .001, so we do not need to change the faces' s material to new one
+ ori_material['name'] = rigged_tongue_material
+ ori_material['id'] = rigged_tongue_material
+ ori_material.name = rigged_tongue_material
+
+ new_material = ori_material.copy()
+
+ new_material['name'] = general_tongue_material
+ new_material['id'] = general_tongue_material
+ new_material.name = general_tongue_material
+
+ tongue = self.separate_materials(c.get_body(), [rigged_tongue_material], 'Tongue (rigged) ' + c.get_name())
+
+ c.get_body().data.materials.append(new_material)
+ bpy.ops.object.material_slot_move(direction='DOWN')
+
+ # copy the tongue mesh and join it back to body
+ bpy.ops.object.mode_set(mode='OBJECT')
+ tongue_copy = tongue.copy()
+ tongue_copy.data = tongue.data.copy()
+ bpy.context.collection.objects.link(tongue_copy)
+ bpy.ops.object.select_all(action='DESELECT')
+ tongue_copy.select_set(True)
+ c.get_body().select_set(True)
+ bpy.context.view_layer.objects.active = c.get_body()
+ bpy.ops.object.join()
+
+ tongue['tongue'] = True
+ else:
+ tongue = self.separate_materials(c.get_body(), [rigged_tongue_material], 'Tongue (rigged) ' + c.get_name())
+ tongue['tongue'] = True
+
+ # Now remap the rigged tongue material with the original to allow the rigged tongue and the tongue on the body to share the same material
+ if bpy.data.materials.get(rigged_tongue_material):
+ bpy.data.materials[rigged_tongue_material].user_remap(
+ bpy.data.materials[general_tongue_material])
+ bpy.data.materials.remove(bpy.data.materials[rigged_tongue_material],do_unlink=True) # forcing to delete
+ c.print_timer('separate_rigged_tongue')
+
+ def separate_hair(self):
+ '''Separates the hair from the clothes object'''
+ outfits = c.get_outfits()
+
+ #Separate the hair from each outfit
+ # material_data = c.json_file_manager.get_json_file('KK_MaterialDataComplete.json')
+ material_data = c.json_file_manager.get_materials_info()
+ hair_materials = [
+ material['MaterialName']
+ for obj in material_data.values()
+ for sub_obj in obj
+ for material in sub_obj['MaterialInformation']
+ if material['isHair']
+ ]
+ for outfit in outfits:
+ #find all the hair mats for this outfit
+ cur_hair_mat_list = []
+ outfit_materials = [mat_slot.material.name for mat_slot in outfit.material_slots]
+
+ for material in hair_materials:
+ # some hair materials are repeated. The order goes 'hair_material', 'hair_material 00', 'hair_material 01', etc. Check for those too.
+ cur_hair_mat_list.extend([m for m in outfit_materials if material in m])
+
+ if cur_hair_mat_list:
+ hair_object = self.separate_materials(outfit, cur_hair_mat_list, 'Hair ' + outfit.name)
+ hair_object['hair'] = True
+ hair_object['outfit'] = False
+ c.print_timer('separate_hair')
+
+ def separate_alternate_clothing(self):
+ '''Separates the alternate clothing pieces then hides them'''
+
+ #These are the enum indexes that need to be separated
+ clothes_labels = {
+ 999: 'Indoor shoes',
+ 93: 'Top shift',
+ 97: 'Top shift',
+ 112: 'Top shift',
+ 114: 'Top shift',
+ 116: 'Top shift',
+ 120: 'Top shift',
+ 95: 'Bottom shift',
+ 99: 'Bottom shift',
+ 101: 'Bra shift',
+ 118: 'Bra shift',
+ 107: 'Underwear shift',
+ 108: 'Underwear hang',
+ 110: 'Pantyhose shift',
+ }
+
+ material_data = c.json_file_manager.get_materials_info()
+ for outfit in c.get_outfits():
+ for label in clothes_labels:
+ materials_to_separate = []
+ for smr_name, smr_items in material_data.items():
+ for smr_item in smr_items:
+ if label == smr_item['EnumIndex']:
+ materials_to_separate.extend(c.get_material_names(smr_name))
+
+ if materials_to_separate:
+ alt_clothes = self.separate_materials(outfit, materials_to_separate, clothes_labels[label] + ' ' + outfit['id'] + ' ' + c.get_name())
+ if alt_clothes:
+ alt_clothes['alt'] = True
+ alt_clothes['outfit'] = False
+ c.kklog('Separated {} alternate clothing {} automatically'.format(materials_to_separate, clothes_labels[label]))
+ c.print_timer('separate_alternate_clothing')
+
+ def delete_shad_bone(self):
+ '''Delete the shadowcast and bonelyfans meshes, if present'''
+ mat_list = ['c_m_shadowcast', 'Standard']
+ shadowcast = self.separate_materials(c.get_body(), mat_list, 'shadowcast', search_type = 'fuzzy')
+ if shadowcast:
+ bpy.data.objects.remove(shadowcast)
+
+ #Delete the bonelyfans mesh if any
+ # mat_list = ['Bonelyfans', 'Bonelyfans.001']
+ # some model have .002, even .003, .004
+ mat_list = c.get_material_names('Highlight_o_body_a_rend')
+ mat_list.extend(c.get_material_names('Highlight_cf_O_face_rend'))
+ mat_list = list(set(mat_list))
+ extended = []
+ for mat in mat_list:
+ index = 1
+ while bpy.data.materials.get((name := f'{mat}.{index:03d}')):
+ index += 1
+ extended.append(name)
+
+ mat_list.extend(extended)
+ bonely = self.separate_materials(c.get_body(), mat_list, 'bonelyfans')
+ if bonely:
+ bpy.data.objects.remove(bonely)
+ c.print_timer('delete_shad_bone')
+
+ def separate_hitboxes(self):
+ '''Separate the hitbox mesh, if present'''
+ material_data = c.json_file_manager.get_materials_info()
+ hitbox_names = []
+ for smr_name, smr_infos in material_data.items():
+ if smr_name.startswith('o_hit'):
+ hitbox_names.extend([
+ item['MaterialName']
+ for smr_info in smr_infos
+ for item in smr_info['MaterialInformation']
+ ])
+
+ hitbox_names = list(set(hitbox_names))
+ # first remap all of the duplicate hitbox materials to share the same material name, or some separations will be missed
+ for hitbox_name in hitbox_names:
+ index = 1
+ while bpy.data.materials.get((hitbox := f'{hitbox_name}.{index:03d}')):
+ bpy.data.materials[hitbox].user_remap(bpy.data.materials[hitbox_name])
+ bpy.data.materials.remove(bpy.data.materials[hitbox])
+ index += 1
+ hitbox = self.separate_materials(c.get_body(), hitbox_names, 'Hitboxes Body ' + c.get_name())
+ if hitbox:
+ hitbox['hitbox'] = True
+ hitbox['body'] = False
+ for outfit in c.get_outfits():
+ hitbox = self.separate_materials(outfit, hitbox_names, 'Hitboxes ' + outfit['id'] + ' ' + c.get_name())
+ if hitbox:
+ hitbox['hitbox'] = True
+ hitbox['outfit'] = False
+ c.move_and_hide_collection(c.get_hitboxes(), "Hitboxes " + c.get_name())
+ c.print_timer('separate_hitboxes')
+
+ def delete_mask_quad(self):
+ '''delete the mask material if not in smr mode'''
+ material_names = []
+ material_data = c.json_file_manager.get_materials_info()
+ for smr_name, smr_infos in material_data.items():
+ if smr_name.startswith('o_Mask'):
+ material_names.extend([
+ item['MaterialName']
+ for smr_info in smr_infos
+ for item in smr_info['MaterialInformation']
+ if item['ShaderName'] == "Shader Forge/AlphaMaskMultiply"
+ ])
+ material_names = set(material_names)
+
+ for outfit in c.get_outfits():
+ for mat in outfit.material_slots:
+ if mat.name in material_names:
+ self.delete_materials(outfit, [mat])
+ c.print_timer('delete_mask_quad')
+
+ def remove_unused_shapekeys(self):
+ '''remove shapekeys on all hair and clothes objects'''
+ if bpy.context.scene.kkbp.shapekeys_dropdown not in ['A', 'B']:
+ return
+ object_list = c.get_outfits()
+ object_list.extend(c.get_alts())
+ object_list.extend(c.get_hairs())
+ object_list.extend(c.get_hitboxes())
+ object_list = [o for o in object_list if o.data.shape_keys]
+ for obj in object_list:
+ for key in obj.data.shape_keys.key_blocks.keys():
+ obj.shape_key_remove(obj.data.shape_keys.key_blocks[key])
+ c.print_timer('remove_unused_shapekeys')
+
+ def rename_uv_maps(self):
+ #Make UV map names clearer
+ c.get_body().data.uv_layers[0].name = 'uv_main'
+ c.get_body().data.uv_layers[1].name = 'uv_nipple_and_shine'
+ c.get_body().data.uv_layers[2].name = 'uv_underhair'
+ c.get_body().data.uv_layers[3].name = 'uv_eyeshadow'
+
+ for outfit in c.get_outfits():
+ outfit.data.uv_layers[0].name = 'uv_main'
+ outfit.data.uv_layers[1].name = 'uv_nipple_and_shine'
+ outfit.data.uv_layers[2].name = 'uv_underhair'
+ outfit.data.uv_layers[3].name = 'uv_eyeshadow'
+ c.print_timer('rename_uv_maps')
+
+ def translate_shapekeys(self):
+ '''Renames the face shapekeys to english'''
+ if not bpy.context.scene.kkbp.shapekeys_dropdown in ['A', 'B']:
+ return
+ translation_dict = {
+ #Prefixes
+ "eye_face.f00": "Eyes",
+ "kuti_face.f00": "Lips",
+ "eye_siroL.sL00": "EyeWhitesL",
+ "eye_siroR.sR00": "EyeWhitesR",
+ "eye_line_u.elu00": "Eyelashes1",
+ "eye_line_l.ell00": "Eyelashes2",
+ "eye_naM.naM00": "EyelashesPos",
+ "eye_nose.nl00": "NoseTop",
+ "kuti_nose.nl00": "NoseBot",
+ "kuti_ha.ha00": "Teeth",
+ "kuti_yaeba.y00": "Fangs",
+ "kuti_sita.t00": "Tongue",
+ "mayuge.mayu00": "KK Eyebrows",
+ "eye_naL.naL00": "Tear_big",
+ "eye_naM.naM00": "Tear_med",
+ "eye_naS.naS00": "Tear_small",
+
+ #Prefixes (Yelan headmod exception)
+ "namida_l": "Tear_big",
+ "namida_m": "Tear_med",
+ "namida_s": "Tear_small",
+ 'tang.': 'Tongue',
+
+ #Emotions (eyes and mouth)
+ "_def_": "_default_",
+ "_egao_": "_smile_",
+ "_bisyou_": "_smile_sharp_",
+ "_uresi_ss_": "_happy_slight_",
+ "_uresi_s_": "_happy_moderate_",
+ "_uresi_": "_happy_broad_",
+ "_doki_ss_": "_doki_slight_",
+ "_doki_s_": "_doki_moderate_",
+ "_ikari_": "_angry_",
+ "_ikari02_": "_angry_2_",
+ "_sinken_": "_serious_",
+ "_sinken02_": "_serious_1_",
+ "_sinken03_": "_serious_2_",
+ "_keno_": "_hate_",
+ "_sabisi_": "_lonely_",
+ "_aseri_": "_impatient_",
+ "_huan_": "_displeased_",
+ "_human_": "_displeased_",
+ "_akire_": "_amazed_",
+ "_odoro_": "_shocked_",
+ "_odoro_s_": "_shocked_moderate_",
+ "_doya_": "_smug_",
+ "_pero_": "_lick_",
+ "_name_": "_eating_",
+ "_tabe_": "_eating_2_",
+ "_kuwae_": "_hold_in_mouth_",
+ "_kisu_": "_kiss_",
+ "_name02_": "_tongue_out_",
+ "_mogu_": "_chewing_",
+ "_niko_": "_cartoon_mouth_",
+ "_san_": "_triangle_",
+
+ #Emotions (Eyes)
+ "_winkl_": "_wink_left_",
+ "_winkr_": "_wink_right_",
+ "_setunai_": "_distress_",
+ "_tere_": "_shy_",
+ "_tmara_": "_bored_",
+ "_tumara_": "_bored_",
+ "_kurusi_": "_pain_",
+ "_sian_": "_thinking_",
+ "_kanasi_": "_sad_",
+ "_naki_": "_crying_",
+ "_rakutan_": "_dejected_",
+ "_komaru_": "_worried_",
+ "_gag": "_gageye",
+ "_gyul_": "_squeeze_left_",
+ "_gyur_": "_squeeze_right_",
+ "_gyu_": "_squeeze_",
+ "_gyul02_": "_squeeze_left_2_",
+ "_gyur02_": "_squeeze_right_2_",
+ "_gyu02_": "_squeeze_2_",
+
+ #Emotions (Eyebrows)
+ "_koma_": "_worried_",
+ "_gimoL_": "_doubt_left_",
+ "_gimoR_": "_doubt_right_",
+ "_sianL_": "_thinking_left_",
+ "_sianR_": "_thinking_right_",
+ "_oko_": "_angry_",
+ "_oko2L_": "_angry_left_",
+ "_oko2R_": "_angry_right_",
+
+ #Emotions extra
+ "_s_": "_small_",
+ "_l_": "_big_",
+
+ #Emotions Yelan headmod exception
+ 'T_Default': '_default_op',
+ }
+
+ c.get_body().active_shape_key_index = 0
+
+ originalExists = False
+ for shapekey in bpy.data.shape_keys:
+ for keyblock in shapekey.key_blocks:
+ #check if the original shapekeys still exists
+ if 'Basis' not in keyblock.name:
+ if 'Lips' in keyblock.name:
+ originalExists = True
+
+ #rename original shapekeys
+ for shapekey in bpy.data.shape_keys:
+ for keyblock in shapekey.key_blocks:
+ for key in translation_dict:
+ if 'gageye' not in keyblock.name:
+ keyblock.name = keyblock.name.replace(key, translation_dict[key])
+ try:
+ #delete the KK shapekeys if the original shapekeys still exist
+ if originalExists and 'KK ' in keyblock.name and 'KK Eyebrows' not in keyblock.name:
+ c.get_body().active_shape_key_index = c.get_body().data.shape_keys.key_blocks.keys().index(keyblock.name)
+ bpy.ops.object.shape_key_remove() #only way to do this is with ops?
+ except:
+ #or not
+ c.kklog("Couldn't delete shapekey: " + keyblock.name, 'error')
+ pass
+ c.print_timer('translate_shapekeys')
+
+ def combine_shapekeys(self):
+ '''Creates new, full shapekeys using the existing partial shapekeys, and deletes the partial shapekeys if user didn't elect to keep them in the panel'''
+ if not bpy.context.scene.kkbp.shapekeys_dropdown in ['A', 'B']:
+ return
+
+ #make the basis shapekey active
+ c.switch(c.get_body(), 'object')
+ c.get_body().active_shape_key_index = 0
+
+ def whatCat(keyName):
+ #Eyelashes1 is used because I couldn't see a difference between the other one and they overlap if both are used
+ #EyelashPos is unused because Eyelashes work better and it overlaps with Eyelashes
+ eyes = [keyName.find("Eyes"),
+ keyName.find("NoseT"),
+ keyName.find("Eyelashes1"),
+ keyName.find("EyeWhites"),
+ keyName.find('Tear_big'),
+ keyName.find('Tear_med'),
+ keyName.find('Tear_small')]
+ if not all(v == -1 for v in eyes):
+ return 'Eyes'
+ mouth = [keyName.find("NoseB"),
+ keyName.find("Lips"),
+ keyName.find("Tongue"),
+ keyName.find("Teeth"),
+ keyName.find("Fangs")]
+ if not all(v==-1 for v in mouth):
+ return 'Mouth'
+ return 'None'
+
+ #setup two arrays to keep track of the shapekeys that have been used
+ #and the shapekeys currently in use
+ used = []
+ inUse = []
+ #These mouth shapekeys require the default teeth and tongue shapekeys to be active
+ correctionList = ['_u_small_op', '_u_big_op', '_e_big_op', '_o_small_op', '_o_big_op', '_neko_op', '_triangle_op']
+ shapekey_block = bpy.data.shape_keys[c.get_body().data.shape_keys.name].key_blocks
+
+ ACTIVE = 0.9
+ def activate_shapekey(key_act):
+ if shapekey_block.get(key_act) != None:
+ shapekey_block[key_act].value = ACTIVE
+ #go through the keyblock list twice
+ #Do eye shapekeys first then mouth shapekeys
+ for type in ['Eyes_', 'Lips_']:
+ counter = len(shapekey_block)
+ for current_keyblock in shapekey_block:
+ counter = counter - 1
+ if (counter == 0):
+ break
+ #categorize the shapekey (eye or mouth)
+ cat = whatCat(current_keyblock.name)
+ #get the emotion from the shapekey name
+ if (cat != 'None') and ('KK' not in current_keyblock.name) and (type in current_keyblock.name):
+ emotion = current_keyblock.name[current_keyblock.name.find("_"):]
+ #go through every shapekey to check if any match the current shapekey's emotion
+ for supporting_shapekey in shapekey_block:
+ #If the's emotion matches the current one and is the correct category...
+ if emotion in supporting_shapekey.name and cat == whatCat(supporting_shapekey.name):
+ #and this key has hasn't been used yet activate it, else skip to the next
+ if (supporting_shapekey.name not in used):
+ supporting_shapekey.value = ACTIVE
+ inUse.append(supporting_shapekey.name)
+ #The shapekeys for the current emotion are now all active
+ #Some need manual corrections
+ correction_needed = False
+ for cor in correctionList:
+ if cor in current_keyblock.name:
+ correction_needed = True
+ if correction_needed:
+ activate_shapekey('Fangs_default_op')
+ activate_shapekey('Teeth_default_op')
+ activate_shapekey('Tongue_default_op')
+ if ('_e_small_op' in current_keyblock.name):
+ activate_shapekey('Fangs_default_op')
+ activate_shapekey('Lips_e_small_op')
+ if ('_cartoon_mouth_op' in current_keyblock.name):
+ activate_shapekey('Tongue_default_op')
+ activate_shapekey('Lips_cartoon_mouth_op')
+ if ('_smile_sharp_op' in current_keyblock.name and cat == 'Mouth'):
+ if shapekey_block.get('Teeth_smile_sharp_op1') != None:
+ shapekey_block['Teeth_smile_sharp_op1'].value = 0
+ activate_shapekey('Lips_smile_sharp_op')
+ if ('_eating_2_op' in current_keyblock.name):
+ activate_shapekey('Fangs_default_op')
+ activate_shapekey('Teeth_tongue_out_op')
+ activate_shapekey('Tongue_serious_2_op')
+ activate_shapekey('Lips_eating_2_op')
+ if ('_i_big_op' in current_keyblock.name):
+ activate_shapekey('Teeth_i_big_cl')
+ activate_shapekey('Fangs_default_op')
+ activate_shapekey('Lips_i_big_op')
+ if ('_i_small_op' in current_keyblock.name):
+ activate_shapekey('Teeth_i_small_cl')
+ activate_shapekey('Fangs_default_op')
+ activate_shapekey('Lips_i_small_op')
+ if (current_keyblock.name not in used):
+ c.get_body().shape_key_add(name=('KK ' + cat + emotion))
+ #make sure this shapekey set isn't used again
+ used.extend(inUse)
+ inUse =[]
+ #reset all shapekey values
+ for reset_keyblock in shapekey_block:
+ reset_keyblock.value = 0
+ #lazy crash prevention
+ if counter % 20 == 0:
+ bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
+
+ #Delete all shapekeys that don't have a "KK" in their name
+ #Don't delete the Basis shapekey though
+ #If no KK shapekeys were generated, something went wrong so don't delete any shapekeys
+ keep_partial_shapekeys = bpy.context.scene.kkbp.shapekeys_dropdown == 'B'
+ it_worked = True if [key for key in shapekey_block if 'KK ' in key.name] else False
+ if it_worked and not keep_partial_shapekeys:
+ for remove_shapekey in shapekey_block:
+ try:
+ if ('KK ' not in remove_shapekey.name and remove_shapekey.name != shapekey_block[0].name):
+ c.get_body().shape_key_remove(remove_shapekey)
+ except:
+ c.kklog('Couldn\'t remove shapekey ' + remove_shapekey.name, 'error')
+ pass
+ else:
+ c.kklog('Original shapekeys were not deleted', 'warn')
+ #make the basis shapekey active
+ c.get_body().active_shape_key_index = 0
+ #and reset the pivot point to median
+ bpy.context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
+ c.print_timer('combine_shapekeys')
+
+ def create_tear_shapekeys(self):
+ '''Separate tears from body and create tear shapekeys'''
+ if bpy.context.scene.kkbp.shapekeys_dropdown not in ['A', 'B']:
+ return
+ # check if the tear material even exists
+ try:
+ tear_material_name = c.get_material_names('cf_O_namida_L')[0]
+ except:
+ c.kklog('Tear material did not exist.', 'warn')
+ return
+ # Create a reverse shapekey for each tear material
+ c.switch(c.get_body(), 'edit')
+ # Move tears and gag backwards on the basis shapekey
+ # use head mesh as reference location
+ face_material = c.get_material_names('cf_O_face')
+ if face_material:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(face_material[0])
+ bpy.ops.object.material_slot_select()
+ # refresh selection, then get head location
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.mode_set(mode='EDIT')
+ selected_verts = [v.co.y for v in c.get_body().data.vertices if v.select]
+ loc = 0
+ for y in selected_verts:
+ loc += y
+ middle_of_head = loc / len(selected_verts)
+ c.switch(c.get_body(), 'edit')
+ tear_mats = {
+ 'cf_O_namida_L': ("Tears big", []),
+ 'cf_O_namida_M': ("Tears med", []),
+ 'cf_O_namida_S': ('Tears small', []),
+ 'cf_O_gag_eye_00': ("Gag eye 00", []),
+ 'cf_O_gag_eye_01': ("Gag eye 01", []),
+ 'cf_O_gag_eye_02': ("Gag eye 02", []),
+ }
+ for cat, cat_data in tear_mats.items():
+ mats = c.get_material_names(cat)
+ if (m_flag := ('M' in cat)) or 'S' in cat:
+ mats = [m + ('.001' if m_flag else '.002') for m in
+ mats] # tears share a material name, so add a .001
+ for mat in mats:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(mat)
+ bpy.ops.object.material_slot_select()
+ cat_data[1].extend(mats)
+
+ # refresh selection, then move tears a random amount backwards
+ bpy.ops.object.mode_set(mode='OBJECT')
+ bpy.ops.object.mode_set(mode='EDIT')
+ selected_verts = [v for v in c.get_body().data.vertices if v.select]
+ amount_to_move_tears_back = 2 * (selected_verts[0].co.y - middle_of_head)
+ bpy.ops.transform.translate(value=(0, abs(amount_to_move_tears_back), 0))
+
+ # move the tears forwards again the same amount in individual new shapekeys
+ for cat, cat_data in tear_mats.items():
+ for mat in cat_data[1]:
+ c.switch(c.get_body(), 'object')
+ bpy.ops.object.shape_key_add(from_mix=False)
+ c.get_body().data.shape_keys.key_blocks[-1].name = cat_data[0]
+ last_shapekey = len(c.get_body().data.shape_keys.key_blocks) - 1
+ bpy.context.object.active_shape_key_index = last_shapekey
+ c.switch(c.get_body(), 'edit')
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(mat)
+ if c.get_body().data.materials.find(mat) == -1:
+ bpy.context.object.active_material_index += 1
+ else:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(mat)
+ bpy.ops.object.material_slot_select()
+ # find a random vertex location of the tear and move it forwards
+ c.switch(c.get_body(), 'object')
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.transform.translate(value=(0, -1 * abs(amount_to_move_tears_back), 0))
+ c.switch(c.get_body(), 'object')
+ bpy.ops.object.shape_key_move(type='TOP' if tear_material_name in mat else 'BOTTOM')
+
+ # Move the Eye, eyewhite and eyeline materials back on the KK gageye shapekey
+ bpy.context.object.active_shape_key_index = bpy.context.object.data.shape_keys.key_blocks.find('KK Eyes_gageye')
+ c.switch(c.get_body(), 'edit')
+ for cat in [
+ 'cf_Ohitomi_L',
+ 'cf_Ohitomi_R',
+ 'cf_Ohitomi_L02',
+ 'cf_Ohitomi_R02',
+ 'cf_O_eyeline',
+ 'cf_O_eyeline_low']:
+ mats = c.get_material_names(cat)
+ # also append the duplicated eyewhite material
+ mats.append('cf_m_sirome_00.001')
+ for mat in mats:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(mat)
+ bpy.ops.object.material_slot_select()
+ # find a random vertex location of the eye and move it backwards
+ c.switch(c.get_body(), 'object')
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.transform.translate(value=(0, 2.5 * abs(amount_to_move_tears_back), 0))
+ c.switch(c.get_body(), 'object')
+
+ # Merge the tear materials
+ c.switch(c.get_body(), 'edit')
+
+ to_merge_materials = tear_mats['cf_O_namida_L'][1]
+ to_merge_materials.extend(tear_mats['cf_O_namida_M'][1])
+ to_merge_materials.extend(tear_mats['cf_O_namida_S'][1])
+
+ for mat in to_merge_materials:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(mat)
+ bpy.ops.object.material_slot_select()
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name)
+ bpy.ops.object.material_slot_assign()
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ # make a vertex group that does not contain the tears
+ bpy.ops.object.vertex_group_add()
+ bpy.ops.mesh.select_all(action='SELECT')
+ c.get_body().vertex_groups.active.name = "Body without Tears"
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name)
+ bpy.ops.object.material_slot_deselect()
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name + '.001')
+ bpy.ops.object.material_slot_deselect()
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name + '.002')
+ bpy.ops.object.material_slot_deselect()
+ bpy.ops.object.vertex_group_assign()
+
+ # Separate tears from body object
+ # link shapekeys of tears to body
+ tears = self.separate_materials(c.get_body(), to_merge_materials, 'Tears ' + c.get_name())
+ tears['tears'] = True
+ bpy.ops.object.mode_set(mode='OBJECT')
+ link_keys(c.get_body(), [tears])
+ c.print_timer('create_tear_shapekeys')
+
+ def create_gag_eye_shapekeys(self):
+ '''Separate gag eyes from body and create gag eye shapekeys'''
+ if bpy.context.scene.kkbp.shapekeys_dropdown not in ['A', 'B'] or len(c.get_material_names('cf_O_gag_eye_00')) == 0:
+ return
+ bpy.context.view_layer.objects.active=c.get_body()
+ gag_keys = [
+ 'Circle Eyes 1',
+ 'Circle Eyes 2',
+ 'Spiral Eyes',
+ 'Heart Eyes',
+ 'Fiery Eyes',
+ 'Cartoony Wink',
+ 'Vertical Line',
+ 'Cartoony Closed',
+ 'Horizontal Line',
+ 'Cartoony Crying'
+ ]
+ for key in gag_keys:
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bpy.ops.object.shape_key_add(from_mix=False)
+ last_shapekey = len(c.get_body().data.shape_keys.key_blocks)-1
+ c.get_body().data.shape_keys.key_blocks[-1].name = key
+ bpy.context.object.active_shape_key_index = last_shapekey
+ bpy.ops.object.shape_key_move(type='TOP')
+
+ def create_gag_eye_driver(keyblock: str, condition: str):
+ '''creates a gag eye driver'''
+ skey_driver = bpy.data.shape_keys[0].key_blocks[keyblock].driver_add('value')
+ skey_driver.driver.type = 'SCRIPTED'
+ for key in gag_keys:
+ newVar = skey_driver.driver.variables.new()
+ newVar.name = key.replace(' ','')
+ newVar.type = 'SINGLE_PROP'
+ newVar.targets[0].id_type = 'KEY'
+ newVar.targets[0].id = c.get_body().data.shape_keys
+ newVar.targets[0].data_path = 'key_blocks["' + key + '"].value'
+ skey_driver.driver.expression = condition
+
+ bpy.context.object.active_shape_key_index = 0
+ #make most gag eye shapekeys activate the body's gag key if the KK gageeye shapekey was created
+ if bpy.data.shape_keys[0].key_blocks.get('KK Eyes_gageye'):
+ condition = [key.replace(' ', '') for key in gag_keys if 'Fiery' not in key]
+ create_gag_eye_driver('KK Eyes_gageye', '1 if ' + ' or '.join(condition) + ' else 0' )
+ create_gag_eye_driver('Gag eye 00', '1 if CircleEyes1 or CircleEyes2 or VerticalLine or CartoonyClosed or HorizontalLine else 0' )
+ create_gag_eye_driver('Gag eye 01', '1 if HeartEyes or SpiralEyes else 0' )
+ create_gag_eye_driver('Gag eye 02', '1 if FieryEyes or CartoonyWink or CartoonyCrying else 0' )
+
+ #make a vertex group that does not contain the gag_eyes
+ bpy.ops.object.vertex_group_add()
+ c.switch(c.get_body(), 'edit')
+ bpy.ops.mesh.select_all(action='SELECT')
+ c.get_body().vertex_groups.active.name = "Body without Gag eyes"
+
+ gag_eye_materials = []
+
+ gag_eye_data = c.json_file_manager.get_material_info_by_smr('cf_O_gag_eye_00')
+ gag_eye_materials.extend([
+ item['MaterialName']
+ for smr_info in gag_eye_data
+ for item in smr_info['MaterialInformation']
+ ])
+
+ gag_eye_data = c.json_file_manager.get_material_info_by_smr('cf_O_gag_eye_01')
+ gag_eye_materials.extend([
+ item['MaterialName']
+ for smr_info in gag_eye_data
+ for item in smr_info['MaterialInformation']
+ ])
+
+ gag_eye_data = c.json_file_manager.get_material_info_by_smr('cf_O_gag_eye_02')
+ gag_eye_materials.extend([
+ item['MaterialName']
+ for smr_info in gag_eye_data
+ for item in smr_info['MaterialInformation']
+ ])
+
+ gag_eye_materials = list(set(gag_eye_materials))
+
+ for material_name in gag_eye_materials:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(material_name)
+ bpy.ops.object.material_slot_deselect()
+
+ bpy.ops.object.vertex_group_assign()
+
+ # Separate gag from body object
+ # link shapekeys of gag to body
+ if gag_eye_materials:
+ gag_eye = self.separate_materials(c.get_body(), gag_eye_materials, 'Gag Eyes ' + c.get_name())
+ gag_eye['gag'] = True
+ gag_eye['body'] = False
+ c.switch(c.get_body(), 'object')
+ link_keys(c.get_body(), [gag_eye])
+
+ c.print_timer('create gag_eye_shapekeys')
+ return
+ c.print_timer('ignored gag_eye_shapekeys')
+
+ def remove_body_seams(self):
+ '''merge certain materials for the body object to prevent odd shading issues later on'''
+ if not bpy.context.scene.kkbp.fix_seams:
+ return
+ c.switch(c.get_body(), 'edit')
+ mats = c.get_material_names('cf_O_face')
+ mats.extend(c.get_material_names('o_body_a'))
+
+ bpy.context.tool_settings.mesh_select_mode = (True, False, False) #enable vertex select in edit mode
+ for mat in mats:
+ bpy.context.object.active_material_index = c.get_body().data.materials.find(mat)
+ bpy.ops.object.material_slot_select()
+ bpy.ops.mesh.remove_doubles(threshold=0.00001)
+
+ # This operation still messes with the weights.
+ # Maybe it's possible to save the 3D positions, weights, and UV positions for each duplicate vertex
+ # then delete and make new vertices with saved info
+ # The vertices on the body object seem to be consistent across imports according to https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/82
+ c.print_timer('remove_body_seams')
+
+ def mark_body_freestyle_faces(self):
+ c.switch(c.get_body(), 'edit')
+ #mark certain materials as freestyle faces
+ def mark_as_freestyle(mat_list: bpy.types.Material):
+ for mat in mat_list:
+ mat_found = c.get_body().data.materials.find(mat)
+ if mat_found > -1:
+ bpy.context.object.active_material_index = mat_found
+ bpy.ops.object.material_slot_select()
+ else:
+ c.kklog('Material wasn\'t found when freestyling body materials: ' + mat, 'warn')
+ bpy.ops.mesh.mark_freestyle_face(clear=False)
+ freestyle_list = [
+ 'cf_Ohitomi_L02',
+ 'cf_Ohitomi_R02',
+ 'cf_Ohitomi_L',
+ 'cf_Ohitomi_R',
+ 'cf_O_eyeline_low',
+ 'cf_O_eyeline',
+ 'cf_O_noseline',
+ 'cf_O_mayuge',]
+ mats = []
+ for mat in freestyle_list:
+ mats.extend(c.get_material_names(mat))
+ mark_as_freestyle(mats)
+ bpy.ops.mesh.select_all(action = 'DESELECT')
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ c.print_timer('mark_body_freestyle_faces')
+
+
+ def separate_materials(self, object: bpy.types.Object, mat_list: list[bpy.types.Material], new_object_name: str, search_type = 'exact') -> bpy.types.Object:
+ '''Separates the materials in the mat_list on object, and renames the separated object to "new_object_name".
+ Returns the separated object, or None if there was an error'''
+ c.switch(object, 'edit')
+ for mat in mat_list:
+ mat_found = -1
+ if search_type == 'fuzzy' and ('cm_m_' in mat or 'c_m_' in mat or 'o_hit_' in mat or mat == 'cf_O_face_atari_M'):
+ for matindex in range(0, len(object.data.materials), 1):
+ if mat in object.data.materials[matindex].name:
+ mat_found = matindex
+ else:
+ mat_found = object.data.materials.find(mat)
+ if mat_found > -1:
+ bpy.context.object.active_material_index = mat_found
+ #moves the materials in a specific order to prevent transparency issues on body
+ def moveUp():
+ return bpy.ops.object.material_slot_move(direction='UP')
+ while moveUp() != {"CANCELLED"}:
+ pass
+ bpy.ops.object.material_slot_select()
+ else:
+ c.kklog('Material wasn\'t found when separating materials: ' + mat, 'warn')
+ try:
+ bpy.ops.mesh.separate(type='SELECTED')
+ new_object = bpy.context.selected_objects[1]
+ new_object.name = new_object_name
+ return new_object
+ except:
+ c.kklog('Nothing was selected when separating materials from: ' + object.name, 'warn')
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ return None
+
+ def delete_materials(self, object: bpy.types.Object, mat_list: bpy.types.Material):
+ '''Deletes the materials in mat_list from object'''
+ for mat in mat_list:
+ if object.data.materials.find(mat.name) > -1:
+ c.switch(object, 'edit')
+ bpy.context.object.active_material_index = object.data.materials.find(mat.name)
+ bpy.ops.object.material_slot_select()
+ bpy.ops.mesh.delete(type='VERT')
+
+
diff --git a/importing/postoperations.py b/importing/postoperations.py
new file mode 100644
index 0000000..7d4dde4
--- /dev/null
+++ b/importing/postoperations.py
@@ -0,0 +1,565 @@
+
+# This file performs the following operations
+
+# Hide all clothes except the first outfit (alts are always hidden)
+
+# (Cycles) Applies Cycles conversion script
+# (Eevee Mod) Applies Eevee Mod conversion script
+# (Rigify) Applies Rigify conversion script
+# (SFW) Runs SFW cleanup script
+
+# Clean orphaned data as long as users = 0 and fake user = False
+
+# Parts of cycles replacement was taken from https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/234
+
+
+import bpy, traceback
+from .. import common as c
+
+class post_operations(bpy.types.Operator):
+ bl_idname = "kkbp.postoperations"
+ bl_label = bl_idname
+ bl_description = bl_idname
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ try:
+ self.hide_unused_objects()
+
+ self.apply_cycles()
+ self.apply_eeveemod()
+ self.apply_rigify()
+ self.apply_sfw()
+ self.separate_meshes()
+
+ c.clean_orphaned_data()
+ c.set_viewport_shading('SOLID')
+
+ return {'FINISHED'}
+ except Exception as error:
+ c.handle_error(self, error)
+ return {"CANCELLED"}
+
+ # %% Main functions
+ def hide_unused_objects(self):
+ """
+ Hides unused objects in the Blender scene based on certain conditions.
+
+ This method performs the following operations:
+ 1. Ensures the armature is not hidden.
+ 3. Hides all outfits except the one with the lowest ID.
+ 4. Moves eyegags and tears into their own collection.
+ 5. Always hides the rigged tongue if present.
+ 6. Always hides the Bone Widgets collection.
+ """
+ c.get_armature().hide_set(False)
+ #hide all outfits except the first one
+ #but don't hide the collection if separate by material is enabled
+ clothes_and_hair = c.get_outfits()
+ clothes_and_hair.extend(c.get_hairs())
+ outfit_ids = (int(c['id']) for c in clothes_and_hair if c.get('id'))
+ outfit_ids = list(set(outfit_ids))
+ for id in outfit_ids:
+ clothes_in_this_id = [c for c in clothes_and_hair if c.get('id') == str(id).zfill(2)]
+ c.move_and_hide_collection(clothes_in_this_id, 'Outfit ' + str(id).zfill(2) + ' ' + c.get_name(), hide = (id != min(outfit_ids) and bpy.context.scene.kkbp.categorize_dropdown != 'B'))
+
+ #put any clothes variations into their own collection
+ outfit_ids = (int(c['id']) for c in c.get_alts() if c.get('id'))
+ outfit_ids = list(set(outfit_ids))
+ for index, id in enumerate(outfit_ids):
+ clothes_in_this_id = [c for c in c.get_alts() if c.get('id') == str(id).zfill(2)]
+ c.switch(clothes_in_this_id[0], 'OBJECT')
+ #find the character index
+ character_collection_index = len(bpy.context.view_layer.layer_collection.children)-1
+ #find the index of the outfit collection
+ for i, child in enumerate(bpy.context.view_layer.layer_collection.children[character_collection_index].children):
+ if child.name == 'Outfit ' + str(id).zfill(2) + ' ' + c.get_name():
+ break
+ for ob in clothes_in_this_id:
+ ob.select_set(True)
+ bpy.context.view_layer.objects.active=ob
+ new_collection_name = 'Alts ' + str(id).zfill(2) + ' ' + c.get_name()
+ #extremely confusing move to under the clothes collection. Index is the outfit index + outfit collection index (starts at 1) + Scene collection (1) + + character collection index (usually 0) + 1
+ bpy.ops.object.move_to_collection(collection_index = (index+i) + 1 + (character_collection_index + 1), is_new = True, new_collection_name = new_collection_name)
+ #then hide the alts
+ child.children[0].exclude = True
+
+ #put the eyegags and tears into their own collection
+ face_objects = []
+ if c.get_gags():
+ face_objects.append(c.get_gags())
+ if c.get_tears():
+ face_objects.append(c.get_tears())
+ if face_objects:
+ c.move_and_hide_collection(face_objects, 'Tears and gag eyes ' + c.get_name(), hide = False)
+
+ #always hide the rigged tongue if present
+ if c.get_tongue():
+ c.move_and_hide_collection([c.get_tongue()], 'Rigged tongue ' + c.get_name(), hide = True)
+
+ #always hide the hitboxes collection
+ if bpy.data.collections.get('Hitboxes ' + c.get_name()):
+ c.switch(c.get_armature(), 'OBJECT')
+ for child in bpy.context.view_layer.layer_collection.children[0].children:
+ if ('Hitboxes ' + c.get_name()) in child.name:
+ child.exclude = True
+
+ #always hide the bone widgets collection
+ if bpy.data.collections.get('Bone Widgets'):
+ c.switch(c.get_armature(), 'OBJECT')
+ for child in bpy.context.view_layer.layer_collection.children[0].children:
+ if child.name == 'Bone Widgets':
+ child.exclude = True
+
+ def apply_cycles(self):
+ if not bpy.context.scene.kkbp.shader_dropdown in ['B', 'D']:
+ return
+ c.kklog('Applying Cycles adjustments...')
+ c.import_from_library_file('NodeTree', ['.Cycles', '.Cycles no shadows', '.Cycles Classic'], bpy.context.scene.kkbp.use_material_fake_user)
+ c.import_from_library_file('Image', ['Template: Black'], bpy.context.scene.kkbp.use_material_fake_user)
+
+ #remove outline modifier
+ for o in bpy.context.view_layer.objects:
+ for m in o.modifiers:
+ if(m.name == "Outline Modifier"):
+ m.show_viewport = False
+ m.show_render = False
+
+ ####fix the eyelash mesh overlap
+ # deselect everything and make body active object
+ body = c.get_body()
+ bpy.ops.object.select_all(action='DESELECT')
+ body.select_set(True)
+ bpy.context.view_layer.objects.active=body
+ bpy.ops.object.mode_set(mode = 'EDIT')
+ # define some stuff
+ ops = bpy.ops
+ obj = ops.object
+ mesh = ops.mesh
+ context = bpy.context
+ object = context.object
+ # edit mode and deselect everything
+ obj.mode_set(mode='EDIT')
+ mesh.select_all(action='DESELECT')
+ # delete eyeline down verts and kage faces
+ object.active_material_index = 6
+ obj.material_slot_select()
+ mesh.delete(type='VERT')
+ object.active_material_index = 5
+ obj.material_slot_select()
+ mesh.delete(type='ONLY_FACE')
+ mesh.select_all(action='DESELECT')
+
+ ignore_list = [
+ 'KK Eyebrows (mayuge) ' + c.get_name(),
+ 'KK EyeL (hitomi) ' + c.get_name(),
+ 'KK EyeR (hitomi) ' + c.get_name(),
+ 'KK Eyeline up ' + c.get_name(),
+ 'KK Eyewhites (sirome) ' + c.get_name()]
+ everything = [c.get_body()]
+ everything.extend(c.get_hairs())
+ everything.extend(c.get_alts())
+ everything.extend(c.get_outfits())
+
+ #add cycles node group
+ for object in everything:
+ for node_tree in [mat_slot.material.node_tree for mat_slot in object.material_slots if mat_slot.material.get('bake') and mat_slot.material.name not in ignore_list]:
+ nodes = node_tree.nodes
+ links = node_tree.links
+ if nodes.get('combine'):
+ nodes['combine'].node_tree = bpy.data.node_groups['.Cycles' if bpy.context.scene.kkbp.shader_dropdown == 'B' else '.Cycles Classic']
+ #setup the node links again because they break when you replace the node group
+ def relink(outnode, outport, innode, inport):
+ try:
+ links.new(nodes[outnode].outputs[outport], nodes[innode].inputs[inport])
+ except:
+ c.kklog(f'Could not link these nodes on tree: {node_tree.name} | {outnode}:{outport} to {innode}:{inport}')
+ relink('combine', 0, 'out', 0)
+ relink('light', 0, 'combine', 'Light colors')
+ relink('dark', 0, 'combine', 'Dark colors')
+ relink('textures', 'Main texture (alpha)', 'combine', 'Main texture (alpha)')
+ relink('textures', 'Alpha mask', 'combine', 'Alpha mask')
+ relink('textures', 'Alpha mask (alpha)', 'combine', 'Alpha mask (alpha)')
+ relink('textures', 'Alpha mask (custom)', 'combine', 'Alpha mask (custom)')
+
+ #Cycles makes missing images PINK (?!) instead of black for some reason and this screws with the shaders
+ #If an image is missing, fill it in with Template: Black
+ if nodes.get('textures'):
+ for image_node in [n for n in nodes['textures'].node_tree.nodes if n.type == 'TEX_IMAGE']:
+ if not image_node.image:
+ image_node.image = bpy.data.images['Template: Black']
+
+ #disable detail shine color too
+ if nodes.get('light'):
+ if nodes['light'].inputs.get('Detail intensity (shine)'):
+ nodes['light'].inputs['Detail intensity (shine)'].default_value = 0
+
+ if nodes.get('dark'):
+ if nodes['dark'].inputs.get('Detail intensity (shine)'):
+ nodes['dark'].inputs['Detail intensity (shine)'].default_value = 0
+
+ #remove linemask and blush on face material
+ if c.get_body():
+ face_material = [m.material for m in c.get_body().material_slots if 'KK Face' in m.material.name]
+ if face_material:
+ face_material[0].node_tree.nodes['light'].inputs['Linemask intensity'].default_value = 0
+ face_material[0].node_tree.nodes['dark'].inputs['Linemask intensity'].default_value = 0
+ face_material[0].node_tree.nodes['light'].inputs['Blush intensity'].default_value = 0
+ face_material[0].node_tree.nodes['dark'].inputs['Blush intensity'].default_value = 0
+
+ #set eyeline up and eyebrows as shadowless
+ shadowless_mats = [m.material for m in c.get_body().material_slots if 'KK Eyeline up' in m.material.name]
+ shadowless_mats.extend([m.material for m in c.get_body().material_slots if 'KK Eyebrows (mayuge)' in m.material.name])
+ for mat in shadowless_mats:
+ mat.node_tree.nodes['combine'].node_tree = bpy.data.node_groups['.Cycles no shadows']
+ nodes = mat.node_tree.nodes
+ links = mat.node_tree.links
+ def relink(outnode, outport, innode, inport):
+ try:
+ links.new(nodes[outnode].outputs[outport], nodes[innode].inputs[inport])
+ except:
+ c.kklog(f'Could not link these nodes on tree: {node_tree.name} | {outnode}:{outport} to {innode}:{inport}')
+ relink('combine', 0, 'out', 0)
+ relink('light', 0, 'combine', 'Light colors')
+ relink('dark', 0, 'combine', 'Dark colors')
+ relink('textures', 'Main texture (alpha)', 'combine', 'Main texture (alpha)')
+ relink('textures', 'Alpha mask', 'combine', 'Alpha mask')
+ relink('textures', 'Alpha mask (alpha)', 'combine', 'Alpha mask (alpha)')
+ relink('textures', 'Alpha mask (custom)', 'combine', 'Alpha mask (custom)')
+
+ bpy.context.scene.render.engine = 'CYCLES'
+ bpy.context.scene.cycles.preview_samples = 10
+ mesh.select_all(action='DESELECT')
+ obj.mode_set(mode='OBJECT')
+
+ def apply_eeveemod(self):
+ if not bpy.context.scene.kkbp.shader_dropdown == 'C':
+ return
+ c.import_from_library_file('NodeTree', ['.Eevee Mod', '.Eevee Mod (face)'], bpy.context.scene.kkbp.use_material_fake_user)
+
+ c.kklog('Applying Eevee Shader adjustments...')
+ #Import eevee mod node group and replace the combine colors group with the eevee mod group
+ ignore_list = [
+ 'KK Eyebrows (mayuge) ' + c.get_name(),
+ 'KK EyeL (hitomi) ' + c.get_name(),
+ 'KK EyeR (hitomi) ' + c.get_name(),
+ 'KK Eyeline up ' + c.get_name(),
+ 'KK Eyewhites (sirome) ' + c.get_name()]
+ everything = [c.get_body()]
+ everything.extend(c.get_hairs())
+ everything.extend(c.get_alts())
+ everything.extend(c.get_outfits())
+
+ for object in everything:
+ for node_tree in [mat_slot.material.node_tree for mat_slot in object.material_slots if mat_slot.material.get('bake') and mat_slot.material.name not in ignore_list]:
+ nodes = node_tree.nodes
+ links = node_tree.links
+ if nodes.get('combine'):
+ nodes['combine'].node_tree = bpy.data.node_groups['.Eevee Mod']
+ #setup the node links again because they break when you replace the node group
+ def relink(outnode, outport, innode, inport):
+ try:
+ links.new(nodes[outnode].outputs[outport], nodes[innode].inputs[inport])
+ except:
+ c.kklog(f'Could not link these nodes on tree: {node_tree.name} | {outnode}:{outport} to {innode}:{inport}')
+ relink('combine', 0, 'out', 0)
+ relink('light', 0, 'combine', 'Light colors')
+ relink('dark', 0, 'combine', 'Dark colors')
+ relink('textures', 'Main texture (alpha)', 'combine', 'Main texture (alpha)')
+ relink('textures', 'Alpha mask', 'combine', 'Alpha mask')
+ relink('textures', 'Alpha mask (alpha)', 'combine', 'Alpha mask (alpha)')
+ relink('textures', 'Alpha mask (custom)', 'combine', 'Alpha mask (custom)')
+
+ if bpy.app.version[0] == 3:
+ #turn on ambient occlusion and bloom in render settings
+ bpy.context.scene.eevee.use_gtao = True
+
+ #turn on bloom in render settings
+ bpy.context.scene.eevee.use_bloom = True
+
+ #face has special normal setup. make a copy and add the normals inside of the copy
+ #this group prevents Amb Occ issues around nose, and mouth interior
+ face_nodes = bpy.data.node_groups['.Eevee Mod (face)']
+ face_nodes.use_fake_user = True
+
+ #select entire face and body, then reset vectors to prevent Amb Occ seam around the neck
+ body = c.get_body()
+ bpy.ops.object.select_all(action='DESELECT')
+ body.select_set(True)
+ bpy.context.view_layer.objects.active=body
+ bpy.ops.object.mode_set(mode = 'EDIT')
+ body.active_material_index = 1
+ bpy.ops.object.material_slot_select()
+ bpy.ops.mesh.normals_tools(mode='RESET')
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+
+ @classmethod
+ def apply_rigify(cls):
+ self = cls
+ #correct some bone layering errors. I don't feel like tracking these down, so do it here before the rigify script
+ layer0_bones = [
+ 'MasterFootIK.L',
+ 'MasterFootIK.R',
+ 'Eyesx',
+ 'cf_pv_root_upper',
+ 'cf_pv_elbo_R',
+ 'cf_pv_elbo_L',
+ 'cf_pv_knee_L',
+ 'cf_pv_knee_R',
+ 'cf_pv_hand_L',
+ 'cf_pv_hand_R',
+ ]
+ layer1_bones = [
+ 'Left toe',
+ 'Right toe',
+ 'cf_pv_foot_L',
+ 'FootPin.L',
+ 'ToePin.L',
+ 'cf_pv_foot_R',
+ 'FootPin.R',
+ 'ToePin.R',
+ ]
+ armature = c.get_armature()
+ def set_armature_layer(bone_name, show_layer, hidden = False):
+ '''Assigns a bone to a bone collection.'''
+ bone = armature.data.bones.get(bone_name)
+ if bone:
+ if bpy.app.version[0] == 3:
+ armature.data.bones[bone_name].layers = (
+ True, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False,
+ False, False, False, False, False, False, False, False
+ )
+ #have to show the bone on both layer 1 and chosen layer before setting it to just chosen layer
+ armature.data.bones[bone_name].layers[show_layer] = True
+ armature.data.bones[bone_name].layers[0] = False
+ armature.data.bones[bone_name].hide = hidden
+ else:
+ show_layer = str(show_layer)
+ bone.collections.clear()
+ if armature.data.bones.get(bone_name):
+ if armature.data.collections.get(show_layer):
+ armature.data.collections[show_layer].assign(armature.data.bones.get(bone_name))
+ else:
+ armature.data.collections.new(show_layer)
+ armature.data.collections[show_layer].assign(armature.data.bones.get(bone_name))
+ armature.data.bones[bone_name].hide = hidden
+
+ c.switch(armature, 'OBJECT')
+ for bone in layer0_bones:
+ set_armature_layer(bone, 0)
+ for bone in layer1_bones:
+ set_armature_layer(bone, 1)
+
+ if not bpy.context.scene.kkbp.armature_dropdown == 'B':
+ return
+ c.kklog('Running Rigify conversion scripts...')
+ c.switch(armature, 'object')
+ try:
+ bpy.ops.kkbp.rigbefore('INVOKE_DEFAULT')
+ #remove the left ankle and right ankle's super copy prop
+ if bpy.app.version[0] != 3:
+ armature.pose.bones['Left ankle'].rigify_type = ""
+ armature.pose.bones['Right ankle'].rigify_type = ""
+ except:
+ if 'Calling operator "bpy.ops.pose.rigify_layer_init" error, could not be found' in traceback.format_exc():
+ c.kklog("There was an issue preparing the rigify metarig. \nMake sure the Rigify addon is installed and enabled. Skipping operation...", 'error')
+ c.kklog(traceback.format_exc())
+ return
+
+ bpy.ops.pose.rigify_generate()
+
+ bpy.ops.kkbp.rigafter('INVOKE_DEFAULT')
+ #make sure the new bones on the generated rig retain the KKBP outfit id entry
+ rig = bpy.context.active_object
+ rig['rig'] = True
+ rig['name'] = c.get_name()
+
+ #Take the IDs from all org bones and copy them over to the generated / helper bones
+ for bone in rig.data.bones:
+ if bone.get('id') and bone.name.startswith('ORG-'):
+ bone_base_name = bone.name[4:] # Remove 'ORG-' prefix
+ for bone_name in [
+ bone_base_name,
+ 'DEF-' + bone_base_name,
+ bone_base_name + '_ik',
+ bone_base_name + '_ik.parent',
+ bone_base_name + '_master',
+ 'MCH-' + bone_base_name,
+ 'MCH-' + bone_base_name + '_drv',
+ ]:
+ if rig.data.bones.get(bone_name):
+ rig.data.bones[bone_name]['id'] = bone['id']
+
+ armature.hide_set(True)
+ bpy.ops.object.select_all(action='DESELECT')
+
+ #make sure everything is deselected in edit mode for the body
+ body = c.get_body()
+ bpy.ops.object.select_all(action='DESELECT')
+ body.select_set(True)
+ bpy.context.view_layer.objects.active=body
+ bpy.ops.object.mode_set(mode = 'EDIT')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bpy.ops.object.select_all(action='DESELECT')
+
+ rig.select_set(True)
+ bpy.context.view_layer.objects.active=rig
+ rig.show_in_front = True
+ bpy.context.scene.tool_settings.transform_pivot_point = 'INDIVIDUAL_ORIGINS'
+ bpy.context.tool_settings.mesh_select_mode = (False, False, True) #enable face select in edit mode
+ return {'FINISHED'}
+
+ def apply_sfw(self):
+ if not bpy.context.scene.kkbp.sfw_mode:
+ return
+ c.kklog('Applying mesh adjustments...')
+ #mark nsfw parts of mesh as freestyle faces so they don't show up in the outline
+ body = c.get_body()
+ c.switch(body, mode = 'EDIT')
+ def mark_group_as_freestyle(group_list):
+ for group in group_list:
+ group_found = body.vertex_groups.find(group)
+ if group_found > -1:
+ bpy.context.object.active_material_index = group_found
+ bpy.ops.object.vertex_group_select()
+ # else:
+ # c.kklog('Group wasn\'t found when freestyling vertex groups: ' + group, 'warn')
+ bpy.ops.mesh.mark_freestyle_face(clear=False)
+ freestyle_list = [
+ 'cf_j_bnip02_L', 'cf_j_bnip02_R',
+ 'cf_s_bust03_L', 'cf_s_bust03_R']
+ mark_group_as_freestyle(freestyle_list)
+ bpy.ops.mesh.select_all(action = 'DESELECT')
+
+ #delete nsfw parts of the mesh
+ def delete_group_and_bone(ob, group_list):
+ c.switch(ob, 'EDIT')
+ bpy.ops.mesh.select_all(action = 'DESELECT')
+ for group in group_list:
+ group_found = ob.vertex_groups.find(group)
+ if group_found > -1:
+ bpy.context.object.vertex_groups.active_index = group_found
+ bpy.ops.object.vertex_group_select()
+ # else:
+ # c.kklog('Group wasn\'t found when deleting vertex groups: ' + group, 'warn')
+ bpy.ops.mesh.delete(type='VERT')
+ bpy.ops.mesh.select_all(action = 'DESELECT')
+
+ delete_list = ['cf_s_bnip025_L', 'cf_s_bnip025_R', 'cf_s_bnip02_L', 'cf_s_bnip02_R',
+ 'cf_j_kokan', 'cf_j_ana', 'cf_d_ana', 'cf_d_kokan', 'cf_s_ana',
+ 'Vagina_Root', 'Vagina_B', 'Vagina_F', 'Vagina_001_L', 'Vagina_002_L',
+ 'Vagina_003_L', 'Vagina_004_L', 'Vagina_005_L', 'Vagina_001_R', 'Vagina_002_R',
+ 'Vagina_003_R', 'Vagina_004_R', 'Vagina_005_R']
+ delete_group_and_bone(body, delete_list)
+ #also do this on the clothes because the bra can show up
+ delete_list = ['cf_s_bnip02_L', 'cf_s_bnip02_R', 'cf_s_bnip025_L', 'cf_s_bnip025_R', ]
+ for ob in [o for o in bpy.data.objects if o.get('KKBP tag') == 'outfit']:
+ delete_group_and_bone(ob, delete_list)
+
+ #force the sfw alpha mask on the body
+ for mat_prefix in ['KK Body', 'Outline Body']:
+ body_mat = body.material_slots[mat_prefix + ' ' + c.get_name()].material
+ body_mat.node_tree.nodes["combine"].inputs['Force custom mask'].default_value = 1
+ new_group = body_mat.node_tree.nodes['combine'].node_tree.copy()
+ body_mat.node_tree.nodes['combine'].node_tree = new_group
+ if bpy.app.version[0] == 3:
+ new_group.inputs['Force custom mask'].hide_value = True
+ else:
+ new_group.interface.items_tree['Force custom mask'].hide_value = True
+
+ #get rid of the nsfw groups on the body
+ body_mat = body.material_slots['KK Body ' + c.get_name()].material
+ body_mat.node_tree.nodes.remove(body_mat.node_tree.nodes['texturesnsfw'])
+
+ for nono in [
+ 'Nipple',
+ 'Nipple (alpha)',
+ 'Genital',
+ 'Underhair',
+ 'Genital intensity',
+ 'Genital saturation',
+ 'Genital hue',
+ 'Underhair color',
+ 'Underhair intensity',
+ 'Nipple base',
+ 'Nipple base 2',
+ 'Nipple shine',
+ 'Nipple rim']:
+ if bpy.app.version[0] == 3:
+ body_mat.node_tree.nodes['light'].node_tree.inputs.remove(body_mat.node_tree.nodes['light'].node_tree.inputs[nono])
+ else:
+ body_mat.node_tree.nodes['light'].node_tree.interface.remove(body_mat.node_tree.nodes['light'].node_tree.interface.items_tree[nono])
+
+ #delete nsfw bones if sfw mode enebled
+ rig = c.get_rig()
+ if bpy.context.scene.kkbp.sfw_mode and bpy.context.scene.kkbp.armature_dropdown == 'B':
+ if bpy.app.version[0] != 3:
+ rig.data.collections_all['29'].is_visible = True
+ def delete_bone(group_list):
+ #delete bones too
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+ bpy.ops.object.select_all(action='DESELECT')
+ rig.select_set(True)
+ bpy.context.view_layer.objects.active = rig
+ bpy.ops.object.mode_set(mode = 'EDIT')
+ bpy.ops.armature.select_all(action='DESELECT')
+ for bone in group_list:
+ if rig.data.bones.get(bone):
+ rig.data.edit_bones[bone].select = True
+ bpy.ops.kkbp.cats_merge_weights()
+ # else:
+ # c.kklog('Bone wasn\'t found when deleting bones: ' + bone, 'warn')
+ bpy.ops.armature.select_all(action='DESELECT')
+ bpy.ops.object.mode_set(mode = 'OBJECT')
+
+ delete_list = ['cf_s_bnip025_L', 'cf_s_bnip025_R',
+ 'cf_j_kokan', 'cf_j_ana', 'cf_d_ana', 'cf_d_kokan', 'cf_s_ana',
+ 'cf_J_Vagina_root',
+ 'cf_J_Vagina_B',
+ 'cf_J_Vagina_F',
+ 'cf_J_Vagina_L.005',
+ 'cf_J_Vagina_R.005',
+ 'cf_J_Vagina_L.004',
+ 'cf_J_Vagina_L.001',
+ 'cf_J_Vagina_L.002',
+ 'cf_J_Vagina_L.003',
+ 'cf_J_Vagina_R.001',
+ 'cf_J_Vagina_R.002',
+ 'cf_J_Vagina_R.003',
+ 'cf_J_Vagina_R.004',
+ 'cf_j_bnip02root_L',
+ 'cf_j_bnip02_L',
+ 'cf_s_bnip01_L',
+ #'cf_s_bust03_L',
+ 'cf_s_bust02_L',
+ 'cf_j_bnip02root_R',
+ 'cf_j_bnip02_R',
+ 'cf_s_bnip01_R',
+ #'cf_s_bust03_R',
+ 'cf_s_bust02_R',]
+ delete_bone(delete_list)
+ if bpy.app.version[0] != 3:
+ rig.data.collections_all['29'].is_visible = False
+
+ def separate_meshes(self):
+ if bpy.context.scene.kkbp.categorize_dropdown == 'B':
+ #separate each outfit by material
+ for obj in c.get_outfits():
+ if obj.modifiers.get('Outline Modifier'):
+ obj.modifiers['Outline Modifier'].show_render = False
+ obj.modifiers['Outline Modifier'].show_viewport = False
+ c.switch(obj, 'OBJECT')
+ bpy.ops.object.material_slot_remove_unused()
+ c.switch(obj, 'EDIT')
+ bpy.ops.mesh.separate(type='MATERIAL')
+
+ #once they are all separated, rename them to their material name
+ for obj in c.get_outfits():
+ try:
+ obj.name = obj.material_slots[0].name
+ except:
+ #oh well
+ pass
diff --git a/interface/__pycache__/dictionary_en.cpython-311.pyc b/interface/__pycache__/dictionary_en.cpython-311.pyc
new file mode 100644
index 0000000..91e4baf
Binary files /dev/null and b/interface/__pycache__/dictionary_en.cpython-311.pyc differ
diff --git a/interface/__pycache__/dictionary_jp.cpython-311.pyc b/interface/__pycache__/dictionary_jp.cpython-311.pyc
new file mode 100644
index 0000000..4a10010
Binary files /dev/null and b/interface/__pycache__/dictionary_jp.cpython-311.pyc differ
diff --git a/interface/__pycache__/dictionary_zh.cpython-311.pyc b/interface/__pycache__/dictionary_zh.cpython-311.pyc
new file mode 100644
index 0000000..c6024a0
Binary files /dev/null and b/interface/__pycache__/dictionary_zh.cpython-311.pyc differ
diff --git a/interface/dictionary_en.py b/interface/dictionary_en.py
new file mode 100644
index 0000000..5132070
--- /dev/null
+++ b/interface/dictionary_en.py
@@ -0,0 +1,166 @@
+from bpy.app.translations import locale
+from .dictionary_jp import translation_dictionary as jp_translation
+from .dictionary_zh import translation_dictionary as zh_translation
+
+translation_dictionary = {
+
+ 'seams' : "Fix body seams",
+ 'seams_tt' : 'This performs a "remove doubles" operation on the body materials. Removing doubles screws with the weights around certain areas. Disabling this will preserve the weights, but may cause seams to appear around the neck and down the chest when the outline modifier is on',
+
+ 'outline' : 'Use single outline',
+ 'outline_tt' : "Enable to use one generic outline material as opposed to using several unique ones. Checking this may cause outline transparency issues",
+
+ 'keep_templates' : "Keep material templates",
+ 'keep_templates_tt' : "Keep enabled to set the KKBP material templates to fake user. This will keep them from being deleted when blender is closed. Useful if you want to apply them to other objects after your character is finished",
+
+ 'sfw_mode' : 'SFW mode',
+ 'sfw_mode_tt' : 'Attempts to cover up some NSFW things',
+
+ 'arm_drop' : "Armature type",
+ 'arm_drop_A' : "Use KKBP Armature",
+ 'arm_drop_A_tt' : "Use the KKBP armature. This will slightly modify the armature and give it basic IKs",
+ 'arm_drop_B' : "Use Rigify Armature",
+ 'arm_drop_B_tt' : "Use the Rigify armature. This is an advanced armature suitable for use in Blender",
+ 'arm_drop_C' : "Use Koikatsu Armature",
+ 'arm_drop_C_tt' : "Use the stock Koikatsu armature. This will match the bone naming and structure of the one in-game",
+ 'arm_drop_D' : "Use PMX Armature",
+ 'arm_drop_D_tt' : "Use the stock PMX armature. This is the armature you get from the KKBP exporter",
+
+ 'cat_drop' : 'Run type',
+ 'cat_drop_A' : "Single clothes object",
+ 'cat_drop_A_tt' : "Import everything and get a single object containing all your model's clothes. Hides any alternate clothes by default",
+ 'cat_drop_B' : "Separate every object",
+ 'cat_drop_B_tt' : "Import everything and automatically separate every single piece of clothing into several objects",
+
+ 'dark' : "Dark colors",
+ 'dark_C' : "Do not use dark colors",
+ 'dark_C_tt' : "Makes the dark colors the same as the light colors",
+ 'dark_F' : 'Automatic dark colors',
+ 'dark_F_tt' : "Uses an automatic method to set the dark colors",
+
+ 'prep_drop' : "Export type",
+ 'prep_drop_A' : "Unity - VRM compatible",
+ 'prep_drop_A_tt' : """Removes the outline and...
+ removes duplicate Eyewhite material slot if present,
+ edits bone hierarchy to allow Unity to automatically detect the right bones""",
+ 'prep_drop_B' : "Generic FBX - No changes",
+ 'prep_drop_B_tt' : """Removes the outline and...
+ removes duplicate Eyewhite material slot if present""",
+ 'prep_drop_D' : "Unity - VRChat compatible",
+ 'prep_drop_D_tt' : """Removes the outline and...
+ removes duplicate Eyewhite material slot if present,
+ removes the "Upper Chest" bone,
+ edits bone hierarchy to allow Unity to automatically detect the right bones""",
+ 'prep_drop_E' : "Unreal Engine",
+ 'prep_drop_E_tt' : """Removes the outline and...
+ removes duplicate Eyewhite material slot if present,
+ edits bone hierarchy to match Epic Mannequin skeleton""",
+
+ 'simp_drop' : 'Armature simplification type',
+ 'simp_drop_A' : 'Very simple (SLOW)',
+ 'simp_drop_A_tt': 'Use this option if you want a very low bone count. Moves the pupil bones to layer 1 and simplifies bones on armature layers 3-5, 11-12, and 17-19 (Leaves you with ~100 bones not counting the skirt bones)',
+ 'simp_drop_B' : 'Simple',
+ 'simp_drop_B_tt': 'Moves the pupil bones to layer 1 and simplifies the useless bones on armature layer 11 (Leaves you with ~500 bones)',
+ 'simp_drop_C' : 'No changes (FAST)',
+ 'simp_drop_C_tt': 'Does not simplify anything',
+
+ 'bake' : 'Finalize materials',
+ 'bake_light' : "Light",
+ 'bake_light_tt' : "Finalize light version of all textures",
+ 'bake_dark' : "Dark",
+ 'bake_dark_tt' : "Finalize dark version of all textures",
+ 'bake_norm' : "Normal",
+ 'bake_norm_tt' : "Finalize normal version of all textures",
+ 'bake_mult' : 'Finalize multiplier',
+ 'bake_mult_tt' : "Set this to 2 or 3 if the finalized texture is blurry",
+ 'old_bake' : 'Use V4 baker',
+ 'old_bake_tt' : 'Enable to use the old finalization system. This system will not bake any extra UV maps like hair shine or eyeshadow, but it may help if you are encountering corruption in the finalized images',
+
+ 'shape_A' : 'Use KKBP shapekeys',
+ 'shape_A_tt' : 'Rename and delete the old shapekeys. This will merge the shapekeys that are part of the same expression and delete the rest',
+ 'shape_B' : "Save partial shapekeys",
+ 'shape_B_tt' : "Save the partial shapekeys that are used to generate the KK shapekeys. These are useless on their own",
+ 'shape_C' : "Skip modifying shapekeys",
+ 'shape_C_tt' : "Use the stock Koikatsu shapekeys. This will not change the shapekeys in any way",
+
+ 'shader_A' : 'Use Eevee',
+ 'shader_B' : "Use Cycles (toon)",
+ 'shader_D' : "Use Cycles (classic)",
+ 'shader_C' : "Use Eevee mod",
+ 'shader_C_tt' : "Uses a modified shader setup for Eevee",
+
+ 'import_export' : 'Importing and Exporting',
+ 'extras' : 'KKBP Extras',
+ 'import_model' : 'Import model',
+ 'prep' : 'Prep for target application',
+
+ 'studio_object' : 'Import studio object',
+ 'studio_object_tt' : 'Open the folder containing the fbx files exported with SB3Utility',
+ 'convert_texture' : 'Convert texture?',
+ 'convert_texture_tt' : '''Enable this if you want the plugin to saturate the item's textures using the in-game LUT''',
+ 'single_animation' : 'Import single animation file',
+ 'single_animation_tt' : 'Only available for the Rigify armature. Imports an exported Koikatsu .fbx animation file and applies it to your character. Mixamo .fbx files are also supported if you use the toggle below',
+ 'animation_koi' : 'Import Koikatsu animation',
+ 'animation_mix' : 'Import Mixamo animation',
+ 'animation_type_tt' : 'Disable this if you are importing a Koikatsu .fbx animation. Enable this if you are importing a Mixamo .fbx animation',
+ 'animation_library' : 'Create animation library',
+ 'animation_library_tt' : "Only available for the Rigify Armature. Creates an animation library using the current file and current character. Will not save over the current file in case you want to reuse it. Open the folder containing the animation files exported with SB3Utility",
+ 'animation_library_scale' : 'Scale arms',
+ 'animation_library_scale_tt': 'Check this to scale the arms on the y axis by 5%. This will make certain poses more accurate to the in-game one',
+ 'map_library' : 'Create map asset library',
+ 'map_library_tt' : "Creates an asset library using ripped map data. Open the folder containing the map files exported with SB3Utility. Takes 40 to 500 seconds per map",
+
+ 'rigify_convert' : "Convert for Rigify",
+ 'rigify_convert_tt' : "Runs several scripts to convert a KKBP armature to be Rigify compatible",
+ 'sep_eye' : "Separate Eyes and Eyebrows",
+ 'sep_eye_tt' : "Separates the Eyes and Eyebrows from the Body object and links the shapekeys to the Body object. Useful for when you want to make eyes or eyebrows appear through the hair using the Cryptomatte features in the compositor",
+ 'bone_visibility' : "Show bones for current outfit",
+ 'bone_visibility_tt' : "This will update visibility for all accessory bones. For example, if you have an Outfit 00 and an Outfit 01, both of them are visible then all accessory bones will be shown. If you hide Outfit 00 and click this button, only Outfit 01's accessory bones will be shown",
+ 'link_hair' : 'Update hair materials',
+ 'link_hair_tt' : 'Click to copy the current colors, detail intensity, etc to the other hair materials on this object',
+
+ 'kkbp_import_tt' : "Imports a Koikatsu model (.pmx format) and applies fixes to it",
+ 'export_prep_tt' : "Use the KKBP Armature for the best results. Check the dropdown for more info",
+ 'bake_mats_tt' : "Finalize materials as .png files. These will be stored in the original .pmx folder",
+
+ 'delete_cache' : 'Delete cache',
+ 'delete_cache_tt' : 'Enable this to delete the cache files. Cache files are generated when you import a model or finalize materials. These are stored in the pmx folder as "atlas_files", "baked_files", "dark_files" and "saturated_files". Enabling this option will delete ALL files inside of these folders',
+
+ 'use_atlas' : 'Create atlas',
+ 'use_atlas_tt': 'Enable this to create a material atlas when finalizing materials',
+ 'dont_use_atlas' : 'Don\'t create Atlas',
+
+ 'mat_comb_tt' : 'KKBP uses parts of Shotariya\'s Material Combiner addon to automatically merge your materials into an atlas. Click this if you want to manually combine your materials instead of letting KKBP do it for you (requires you to download the Material Combiner addon. Also, make sure you have already clicked the Finalize Materials button in the KKBP panel or it will not work) ',
+ 'matcomb' : 'Setup materials for Material Combiner',
+ 'mat_comb_switch' : 'Toggle light / dark for Material Combiner',
+ 'mat_comb_switch_tt' : 'Click this to toggle texture state to get both a light and dark atlas from Material Combiner',
+
+ 'pillow' : 'Install PIL to use atlas feature',
+ 'pillow_tt':'Click to install Pillow. This could take a while and might require you to run Blender as Admin',
+ 'reset_mats' : 'Reset finalized materials',
+ 'reset_mats_tt' : 'Click this to reset ALL of your finalized materials back to the -ORG version. Handy if you want to refinalize everything',
+
+ 'max_thread_num' : 'Max threads',
+ 'max_image_num' : 'Max parallel images',
+ 'batch_rows' : 'Rows per batch',
+ 'max_thread_num_tt' : 'KKBP saturates your character\'s textures during model import. This option determines how many CPU cores you want to use to perform the saturation. If you have more cores to spare, you can set it higher. Default is 8.',
+ 'max_image_num_tt' : 'KKBP saturates your character\'s textures during model import. This option determines how many images can be saturated at once. This setting affects memory usage. For example, if the program loads two 4096 x 4096 images, the peak memory usage could reach 8 GB. If you don\'t have 8GB of free memory Blender could crash. Default is 2',
+ 'batch_rows_tt' : 'KKBP saturates your character\'s textures during model import. This option determines how many rows of pixels to process in one batch. For example, if this setting is set to 512 rows and the program is saturating a 1024 x 1024 image, it will be processed in two 512 x 1024 batches. Increasing this value can allow you to process the full image in a single batch, but will increase CPU and memory usage. Default is 512',
+ }
+
+def t(text_entry):
+ try:
+ if locale == 'ja_JP':
+ return jp_translation[text_entry]
+ #Blender 4 changed the language code for Simplified Chinese from 'zh_CN' to 'zh_HANS'
+ elif locale in ['zh_HANS', 'zh_CN']:
+ return zh_translation[text_entry]
+ else:
+ return translation_dictionary[text_entry]
+ except KeyError:
+ #default to english if not translated
+ if translation_dictionary.get(text_entry):
+ return translation_dictionary[text_entry]
+ else:
+ return text_entry
+
diff --git a/interface/dictionary_jp.py b/interface/dictionary_jp.py
new file mode 100644
index 0000000..4cc1de6
--- /dev/null
+++ b/interface/dictionary_jp.py
@@ -0,0 +1,149 @@
+translation_dictionary = {
+
+ 'seams' : "体を縫合する",
+ 'seams_tt' : '体に近い頂点をマージ。 このマージには首のウエイトが台無しになる可能性がある(テクスチャアトラスが台無しになる可能性もある)。 このオプションを無効にしたらウエイトが保存されるけど体に縫い目が見えるかも',
+
+ 'outline' : '一つのアウトラインモード',
+ 'outline_tt' : "このオプションにしたらシングルのアウトラインを使われる。 このオプションにしたらアウトライン透明の問題が起こるかも",
+
+ 'keep_templates' : "マテリアルテンプレートを保存",
+ 'keep_templates_tt' : "KKBPマテリアルテンプレートをフェイクユーザーに設定する",
+
+ 'sfw_mode' : 'エロ無しモード',
+ 'sfw_mode_tt' : 'プラグインがエロの部分を隠してみる',
+
+ 'arm_drop' :"アーマチュアのタイプ",
+ 'arm_drop_A' : "KKBPアーマチュアにする",
+ 'arm_drop_A_tt' : "KKBPアーマチュアにする。この設定にはアーマチュアは改造されて、ベーシックなIKがジェネレートされる",
+ 'arm_drop_B' : "Rigifyアーマチュアにする",
+ 'arm_drop_B_tt' : "Rigifyアーマチュアにする。この設定にはBlenderでの使用アーマチュアがジェネレートされる",
+ 'arm_drop_C' : "コイカツのアーマチュアにする",
+ 'arm_drop_C_tt' : "コイカツのアーマチュアにする。この設定にはボーンの名前、アーマチュアの構造はコイカツのアーマチュアと一致される",
+ 'arm_drop_D' : "PMXアーマチュアにする",
+ 'arm_drop_D_tt' : "PMXアーマチュアにする。この設定にはアーマチュアが改造されない",
+
+ 'cat_drop' : '操作タイプ',
+ 'cat_drop_A' : "別々にしない",
+ 'cat_drop_A_tt' : "すべてをインポートして一つの服オブジェクトにする。別の服はひとりでに隠される",
+ 'cat_drop_B' : "すべてを別々にする",
+ 'cat_drop_B_tt' : "すべてをインポートして個々の服オブジェクトにする",
+
+ 'dark' : "ダークな色",
+ 'dark_C' : "ダークな色は変更しない",
+ 'dark_C_tt' : "ダークな色はライトな色と同じにする",
+ 'dark_F' : 'AUTO',
+ 'dark_F_tt' : "実験的なスクリプトでダークな色をセットする",
+
+ 'prep_drop' : "エクスポートタイプ",
+ 'prep_drop_A' : "Unity - VRMコンパチ",
+ 'prep_drop_A_tt' : """アウトラインを削除して...
+ Eyewhiteの複写を削除して,
+ Unityがボーンをひとりでに見つけられるためにアーマチュアを改造して""",
+ 'prep_drop_B' : "汎用FBX - 変更なし",
+ 'prep_drop_B_tt' : """アウトラインを削除して...
+ Eyewhiteの複写を削除して""",
+ 'prep_drop_D' : "Unity - VRChatコンパチ",
+ 'prep_drop_D_tt' : """アウトラインを削除して...
+ Eyewhiteの複写を削除して,
+ Upper Chestの骨を削除して、
+ Unityがボーンをひとりでに見つけられるためにアーマチュアを改造して""",
+ 'prep_drop_E' : "Unreal Engineコンパチ",
+ 'prep_drop_E_tt' : """アウトラインを削除して...
+ Eyewhiteの複写を削除して,
+ Unreal アニメーションのマネキンのボーンに合わせてボーンを改造して""",
+
+ 'simp_drop' : 'アーマチュアの簡略化',
+ 'simp_drop_A' : '超シンプル (遅い)',
+ 'simp_drop_A_tt': 'この設定には骨の数が減る。瞳の骨をアーマチュアレイヤー1に移って, アーマチュアレイヤー3,4,5,11,12,17,18,19の骨が簡略化される (約100骨が残る)',
+ 'simp_drop_B' : 'シンプル',
+ 'simp_drop_B_tt': 'この設定には骨の数が減る。瞳の骨をアーマチュアレイヤー1に移って, アーマチュアレイヤー11の骨が簡略化される (約500骨が残る)',
+ 'simp_drop_C' : '簡略化してない (早い)',
+ 'simp_drop_C_tt': 'アーマチュアが簡略化されない',
+
+ 'bake' : 'マテリアルテンプレートをファイナライズ',
+ 'bake_light' : "ライト",
+ 'bake_light_tt' : "ライトテクスチャーをファイナライズ",
+ 'bake_dark' : "ダーク",
+ 'bake_dark_tt' : "ダークテクスチャーをファイナライズ",
+ 'bake_norm' : "ノーマル",
+ 'bake_norm_tt' : "ノーマルテクスチャーをファイナライズ",
+ 'bake_mult' : 'ファイナライズ乗数',
+ 'bake_mult_tt' : "ファイナライズしたテクスチャーがぼやけている場合は2,3にしてみて",
+ 'old_bake' : 'V4ベーカー',
+ 'old_bake_tt' : 'ふるいテクスチャーベーカーを使う。V4ベーカーはUVMap2,3,4をファイナライズできないから髪のハイライトや目のアイシャドウをファイナライズされない',
+
+ 'shape_A' : 'KKBPシェイプキーにする',
+ 'shape_A_tt' : 'シェイプキーを変更して部分的なシェイプキーを削除する',
+ 'shape_B' : "部分的なシェイプキーを保存",
+ 'shape_B_tt' : "シェイプキーを変更して部分的なシェイプキーを保存する",
+ 'shape_C' : "シェイプキー変更しない",
+ 'shape_C_tt' : "コイカツのシェイプキーにする。シェイプキーが変更されない",
+
+ 'shader_A' : 'Eeveeにする',
+ 'shader_B' : "Cycles(トゥーンレンダリング)にする",
+ 'shader_D' : "Cyclesにする",
+ 'shader_C' : "Eevee modにする",
+ 'shader_C_tt' : "別のEeveeシェーダー設定にする",
+
+ 'import_export' : 'インポート ・ エクスポート',
+ 'extras' : '追加機能',
+ 'import_model' : 'モデルをインポート',
+ 'prep' : 'ターゲットアプリのために準備する',
+
+ 'studio_object' : 'スタジオオブジェクトをインポート',
+ 'studio_object_tt' : 'SB3Utilityでエクスポートされた.fbxファイルのフォルダを読み取る',
+ 'convert_texture' : 'テクスチャを和度する?',
+ 'convert_texture_tt' : '''このオプションにしたら、コイカツのLUTでテクスチャの色を和度される''',
+ 'single_animation' : '一つのアニメーションファイルをインポート',
+ 'single_animation_tt' : 'Rigifyアーマチュアが必要です。SB3UtilityでエクスポートしたFBXのアニメーションファイルをチャラのアーマチュアにインポートする。MixamoのFBXアニメーションファイルもインポートできる(下のトグルをクリックして)',
+ 'animation_koi' : 'コイカツアニメーションをインポート',
+ 'animation_mix' : 'Mixamoアニメーションをインポート',
+ 'animation_type_tt' : 'コイカツの.fbxアニメーションをインポートする場合はこの設定を無効にして。Mixamoの.fbxアニメーションをインポートする場合はこの設定を有効にして。',
+ 'animation_library' : 'Rigifyアーマチュアが必要です。アニメーションライブラリをジェネレートする',
+ 'animation_library_tt' : "現行ファイルとキャラクタでアニメーションライブラリをジェネレートする。SB3UtilityでエクスポートしたFBXファイルのフォルダを選択して",
+ 'animation_library_scale' : '腕をスケール',
+ 'animation_library_scale_tt': 'このオプションにしたら腕のYスケールを5%で掛けられる。(ポースの精度は高める)',
+ 'map_library' : 'マップアセットライブラリをジェネレートする',
+ 'map_library_tt' : "現行ファイルでマップアセットライブラリをジェネレートする。SB3UtilityでエクスポートしたFBXファイルのフォルダを選択して。マップ一つにつき500秒まで掛かれる",
+
+ 'rigify_convert' : "Rigifyアーマチュアに変えて",
+ 'rigify_convert_tt' : "このボタンをクリックしたら、KKBPアーマチュアをRigifyアーマチュアに変えて",
+ 'sep_eye' : "EyesやEyebrowsやBodyのオブジェクトから別々になって",
+ 'sep_eye_tt' : "BodyのオブジェクトからEyesやEyebrowsマテリアルを別のオブジェクトを別々にして、シェイプ キーをBodyオブジェクトにリンクされる",
+ 'bone_visibility' : "現在の服のボーンを表示して",
+ 'bone_visibility_tt' : "このボタンをクリックしたら、不要なアクセサリボーンをビューポートで隠くされる。たとえば、Outfit 00 と Outfit 01 ビューポートで隠されない場合は、すべてのアクセサリボーンが表示される。Outfit 00 をビューポートで隠してこのボタンをクリックすると、Outfit 01 のアクセサリボーンのみが表示されます。",
+ 'link_hair' : '他の髪マテリアルをアップデート',
+ 'link_hair_tt' : 'クリックすると、このマテリアルの色やDetail Intensityなどがこのオブジェクトの他の髪マテリアルにコピーされる',
+
+ 'kkbp_import_tt' : "コイカツモデル(.PMXフォーマット)をインポートして改造して",
+ 'export_prep_tt' : "KKBPアーマチュアが必要です。メニューの情報をチェックして",
+ 'bake_mats_tt' : "マテリアルテンプレートをPNGファイルにする。PNGファイルはPMXフォルダーに保存される",
+
+ 'delete_cache' : 'キャッシュを削除する',
+ 'delete_cache_tt' : """このオプションにしたらキャッシュファイルを削除される。
+モデルをインポートされるときに、マテリアルをファイナライズされるときに、キャッシュファイルが生成される。
+キャッシュファイルはPMXフォルダーに「atlas_files」、「baked_files」、「dark_files」、「saturated_files」フォルダーに保存される。
+このオプションにしたら、それらのフォルダーのすべてのファイルを削除される""",
+
+ 'use_atlas' : 'アトラスを生成する',
+ 'use_atlas_tt': '時間を節約したい場合は、アトラスの生成をしないことができる',
+ 'dont_use_atlas' : 'アトラスを生成しない',
+
+ 'mat_comb_tt' : 'KKBPは Shotariya の Material Combiner アドオンの一部を使用して、マテリアルをアトラスに自動的に結合します。KKBPにマテリアルを結合させるのではなく、手動でマテリアルを結合したい場合は、このボタンをクリックして (Material Combiner アドオンをダウンロードする必要があります。そして、KKBP パネルの [マテリアルテンプレートをファイナライズ] ボタンを既にクリックしていることを確認してください。クリックしていないと機能しません)',
+ 'matcomb' : 'Material Combinerの手動準備',
+ 'mat_comb_switch' : 'ライトとダークのテクスチャを切り替える',
+ 'mat_comb_switch_tt' : 'クリックすると、ライトとダークのテクスチャを切り替え、Material Combinerからライトとダークのアトラスを手に入れる',
+
+ 'pillow' : 'アトラス機能を使用するにはPILをインストール',
+ 'pillow_tt':'Pillow をインストールするにはクリックして。インストールはしばらく時間がかかる。',
+ 'reset_mats' : 'ファイナライズしたマテリアルをリセット',
+ 'reset_mats_tt' : 'このボタンをクリックしたら、すべてのファイナライズしたマテリアルが -ORG バージョンにリセットされます。すべてを再ファイナライズしたい場合に便利です。',
+
+ 'max_thread_num' : 'CPUスレッド数',
+ 'max_image_num' : 'テクスチャ数',
+ 'batch_rows' : '画像ライン数',
+ 'max_thread_num_tt' : 'モデルのインポート中にテクスチャの色を飽和される。この設定は、テクスチャの色を飽和するCPUコアの数を決定する。デフォルトは 8',
+ 'max_image_num_tt' : 'モデルのインポート中にテクスチャの色を飽和される。この設定は、同時に処理するテクスチャの数を決定する。 この設定を2にして、4096x4096ピクセルのイメージを2枚同時に処理している場合は、8GBのRAMが必要です。 8GBのRAMがない場合は、Blenderがクラッシュかも。デフォルトは 2',
+ 'batch_rows_tt' : 'モデルのインポート中にテクスチャの色を飽和される。この設定は、同時に処理する画像ラインの数を決定する。 この設定を512にして、1024x1024ピクセルのイメージを処理している場合は、そのイメージが2つの512x1024のバッチで処理される。この設定を増える場合は、1つの1024x1024バッチで処理されるけど、CPUやRAMの利用が増える。 デフォルトは 512',
+ }
+
diff --git a/interface/dictionary_zh.py b/interface/dictionary_zh.py
new file mode 100644
index 0000000..aa6f101
--- /dev/null
+++ b/interface/dictionary_zh.py
@@ -0,0 +1,150 @@
+translation_dictionary = {
+
+ 'seams' : "修复身体接缝",
+ 'seams_tt' : '合并靠近身体的顶点。这种合并可能会破坏颈部权重。如果禁用此选项,将保存权重,但可能会在身体上看到接缝',
+
+ 'outline' : '使用通用轮廓线',
+ 'outline_tt' : "启用来使用一种通用轮廓材质而不是几个单独的轮廓材质。选中此选项可能会导致轮廓线透明度问题",
+
+ 'keep_templates' : "保留材质模板",
+ 'keep_templates_tt' : "保持启用以将KKBP材质模板设置为假用户。这将防止它们在blender关闭时被删除。如果要在角色完成后将其应用于其他对象,则此选项非常有用",
+
+ 'sfw_mode' : '和谐模式',
+ 'sfw_mode_tt' : '删除相关区域来实现和谐',
+
+ 'arm_drop' :"骨架类型",
+ 'arm_drop_A' : "使用KKBP骨架",
+ 'arm_drop_A_tt' : "使用KKBP骨架。这将稍微修改骨架,使其具有基本的IK骨骼",
+ 'arm_drop_B' : "使用Rigify骨架",
+ 'arm_drop_B_tt' : "使用Rigify骨架。这是一种先进的骨架,适用于blender",
+ 'arm_drop_C' : "使用恋活骨架",
+ 'arm_drop_C_tt' : "使用恋活骨架。这将与游戏中的骨骼命名和结构相匹配",
+ 'arm_drop_D' : "使用pmx骨架",
+ 'arm_drop_D_tt' : "使用pmx骨架。这是你从KKBP导出时得到的骨架",
+
+ 'cat_drop' : '分类类型',
+ 'cat_drop_A' : "单一服装",
+ 'cat_drop_A_tt' : "导入所有内容并获得包含所有衣服模型的单个对象。默认情况下隐藏任何备用衣服",
+ 'cat_drop_B' : "分开所有对象",
+ 'cat_drop_B_tt' : "导入所有内容并自动将每件衣服分成多个对象",
+
+ 'dark' : "暗色",
+ 'dark_C' : "不使用暗色",
+ 'dark_C_tt' : "使暗色与亮色相同",
+ 'dark_F' : '自动',
+ 'dark_F_tt' : "使用自动方法设置暗色",
+
+ 'prep_drop' : "导出类型",
+ 'prep_drop_A' : "Unity - VRM兼容",
+ 'prep_drop_A_tt' : """删除轮廓...
+ 移除重复的眼白材质槽(如果存在),
+ 编辑骨骼层级以允许Unity自动检测正确骨骼""",
+ 'prep_drop_B' : "通用FBX-无更改",
+ 'prep_drop_B_tt' : """删除轮廓...
+ 移除重复的眼白材质槽(如果存在)""",
+ 'prep_drop_D' : "Unity-兼容VRChat",
+ 'prep_drop_D_tt' : """删除轮廓...
+ 移除重复的眼白材质槽(如果存在)
+ 移除“Upper Chest”骨骼,
+ 编辑骨骼层级以允许Unity自动检测正确骨骼""",
+ 'prep_drop_E' : '虚幻引擎',
+ 'prep_drop_E_tt' : """删除轮廓...
+ 移除重复的眼白材质槽(如果存在),
+ 编辑骨骼层级以允许Unity自动检测正确骨骼""",
+
+ 'simp_drop' : '骨骼简化类型',
+ 'simp_drop_A' : '非常简单(慢)',
+ 'simp_drop_A_tt': '如果希望骨骼数很少,请使用此选项。将瞳孔骨骼移动到第1层,并简化骨架层3-5、11-12和17-19上的骨骼(留下约110个骨骼,不包括裙子骨骼)',
+ 'simp_drop_B' : '简单',
+ 'simp_drop_B_tt': '将瞳孔骨骼移动到层1并简化骨架层11上无用的骨骼(留下大约1000个骨骼)',
+ 'simp_drop_C' : '无更改(快)',
+ 'simp_drop_C_tt': '不简化任何东西',
+
+ 'bake' : '最终确定材质',
+ 'bake_light' : "亮色",
+ 'bake_light_tt' : "烘焙所有纹理的亮色版本",
+ 'bake_dark' : "暗色",
+ 'bake_dark_tt' : "烘焙所有纹理的暗色版本",
+ 'bake_norm' : "法线",
+ 'bake_norm_tt' : "烘焙所有纹理的法线版本",
+ 'bake_mult' : '烘焙乘数:',
+ 'bake_mult_tt' : "如果烘焙纹理模糊,请将其设置为2或3",
+ 'old_bake' : '使用旧版烘焙',
+ 'old_bake_tt' : '启用以使用旧的烘焙系统。该系统不会烘焙任何额外的UV贴图,如头发光泽或眼影。但如果您发现烘培的材质损坏,这可能有帮助',
+
+ 'shape_A' : '使用KKBP形态键',
+ 'shape_A_tt' : '重命名并删除旧的形态键。这将合并属于同一表情的形态键,并删除其余部分',
+ 'shape_B' : "保存部分形态键",
+ 'shape_B_tt' : "保存用于生成恋活形态键的部分形态键。这些本身是无用的",
+ 'shape_C' : "跳过修改形态键",
+ 'shape_C_tt' : "使用恋活形态键。这不会以任何方式改变形态键",
+
+ 'shader_A' : '使用Eevee',
+ 'shader_B' : "使用Cycles",
+ 'shader_C' : "使用修改的Eevee",
+ 'shader_C_tt' : "使用为Eevee修改的着色器",
+
+ 'atlas' : '图集类型',
+
+ 'export_fbx' : '导出FBX',
+ 'export_fbx_tt' : '将所有可见对象导出为FBX文件。这与文件-菜单中的FBX导出功能相同',
+
+ 'import_export' : '导入导出',
+ 'extras' : 'KKBP 杂项功能',
+ 'import_model' : '导入模型',
+ 'prep' : '为目标应用程序处理',
+
+ 'studio_object' : '导入工作室物体',
+ 'studio_object_tt' : '打开包含导出自SB3Utility的.fbx的目录',
+ 'convert_texture' : '转换纹理?',
+ 'convert_texture_tt' : '''如果您希望插件使用游戏中的LUT对物体纹理进行饱和处理,请启用此项''',
+ 'single_animation' : '导入单一动画文件',
+ 'single_animation_tt' : '只适用于Rigify骨架,导入一个来自恋活的.fbx文件并应用于您的角色。来自Mixamo的.fbx文件也受支持。',
+ 'animation_koi' : '导入恋活动画',
+ 'animation_mix' : '导入Mixamo动画',
+ 'animation_type_tt' : '禁用此开关以从恋活导入动画,启用此选项以从Mixamo导入动画',
+ 'animation_library' : '创建动画库',
+ 'animation_library_tt' : "只适用于Rigify骨架,使用当前文件和角色创建一个动画库。不会保存当前文件,以防您需要再次使用它。打开包含用 SB3Utility 导出的动画文件的文件夹",
+ 'animation_library_scale' : '缩放手臂',
+ 'animation_library_scale_tt': '勾选此项可将手臂的Y轴缩放5%,这将使某些姿势更接近游戏中的样子',
+ 'map_library' : '创建一个地图资源库',
+ 'map_library_tt' : "使用解包的地图资源创建地图库。使用SB3Utility打开包含地图的目录。每张地图将花费40-500秒打开",
+
+ 'rigify_convert' : "转换为Rigify",
+ 'rigify_convert_tt' : "运行几个脚本,将KKBP骨架转换为兼容Rigify的骨架",
+ 'sep_eye' : "分离眼睛和眉毛",
+ 'sep_eye_tt' : "将眼睛和眉毛从身体对象中分离出来,并将形态键链接到身体对象。当您想让眼睛或眉毛通过头发透视时,该功能非常有用",
+ 'bone_visibility' : "更新骨骼可见性",
+ 'bone_visibility_tt' : "根据隐藏的服装更新骨骼的可见性",
+ 'link_hair' : "更新头发材质",
+ 'link_hair_tt' : "点击以将当前颜色、细节强度等参数复制到该物体的其他头发材质上。",
+
+ 'kkbp_import_tt' : "导入恋活模型(.pmx格式),并对其应用修复",
+ 'export_prep_tt' : "不适用于Rigify骨架,查看下拉列表以了解更多信息",
+ 'bake_mats_tt' : "打开要将材质模板烘焙到的文件夹",
+
+ 'delete_cache' : '删除缓存',
+ 'delete_cache_tt' : '启用此选项可删除缓存文件。缓存文件在导入模型或最终确定材质时生成。这些文件被命名为 “atlas_files”、“baked_files”、“dark_files ”和 “saturated_files ”存储在pmx文件夹中。启用此选项将删除这些文件夹中的所有文件',
+
+ 'use_atlas' : '创建材质图集',
+ 'use_atlas_tt' : '禁用此功能可在最终确定材质时节省大量时间,但不会创建材质图集。',
+ 'dont_use_atlas' : '不创建材质图集',
+
+ 'mat_comb_tt' : '该插件使用Shotariya\'s Material Combiner插件来把材质转化为图集。如果你想手动实现该过程的话,启用该选项。(这样的话,你需要自行下载相关插件。当然,你需要先确定最终材质(烘培),否则不会生效),',
+ 'matcomb' : '自动/手动结合材质',
+ 'mat_comb_switch' : '切换 Material Combiner 的明/暗模式',
+ 'mat_comb_switch_tt' : '点击此处可切换贴图状态,以便从 Material Combiner 获取亮色和暗色两种图集',
+
+ 'pillow' : '安装Pillow以使用图集功能',
+ 'pillow_tt' : '点击安装Pillow库,这可能需要一段时间,并且可能需要你以管理员身份运行blender。大陆用户如果遇到下载进度缓慢或无法下载的可以去网上搜索“pip换源”,“pip代理设置”',
+ 'reset_mats' : '重置已最终化的材质',
+ 'reset_mats_tt' : '点击此处可将你所有已最终确定的材质重置回 -ORG 版本.如果你想重新最终化(Refinalize)所有材质,这会很方便。',
+
+ 'max_thread_num' : '并行线程数量',
+ 'max_image_num' : '并行处理图片数量',
+ 'batch_rows' : '每批处理的行数',
+ 'max_thread_num_tt' : '插件会在导入模型时调整材质饱和度。这个选项指示在该过程中并行处理的线程数量。如果你有更多的CPU核心数量,可以适当调高些。默认是8',
+ 'max_image_num_tt' : '插件会在导入模型时调整材质饱和度。这个选项指示有多少张图片会被并行处理。这个选项影响内存使用。例如,如果同时处理两张4096 x 4096大小饿图片,内存使用峰值会达到8GB.如果你没这么多内存,程序会崩溃。默认为2',
+ 'batch_rows_tt' : '插件会在导入模型时调整材质饱和度。这个选项指示每个线程会处理多少行像素。例如,设置为512, 当处理一张1024 x 1024的图片时,程序会把它放到两个线程中,每个线程处理512 x 1024的数据。',
+
+ }
diff --git a/preferences.py b/preferences.py
new file mode 100644
index 0000000..9309cc8
--- /dev/null
+++ b/preferences.py
@@ -0,0 +1,189 @@
+#The preferences for the plugin
+
+import bpy
+from bpy.props import BoolProperty, EnumProperty, StringProperty, IntProperty
+
+from .interface.dictionary_en import t
+
+from .interface.dictionary_en import t
+
+class KKBPPreferences(bpy.types.AddonPreferences):
+ # this must match the add-on name, use '__package__'
+ # when defining this in a submodule of a python package.
+ bl_idname = __package__
+
+ sfw_mode : BoolProperty(
+ description=t('sfw_mode_tt'),
+ default = False)
+
+ fix_seams : BoolProperty(
+ description=t('seams_tt'),
+ default = True)
+
+ use_single_outline : BoolProperty(
+ description= t('outline_tt'),
+ default = False)
+
+ use_material_fake_user : BoolProperty(
+ description=t('keep_templates_tt'),
+ default = True)
+
+ old_bake_bool : BoolProperty(
+ description=t('old_bake_tt'),
+ default = False)
+
+ armature_dropdown : EnumProperty(
+ items=(
+ ("A", t('arm_drop_A'), t('arm_drop_A_tt')),
+ ("B", t('arm_drop_B'), t('arm_drop_B_tt')),
+ ("C", t('arm_drop_C'), t('arm_drop_C_tt')),
+ ("D", t('arm_drop_D'), t('arm_drop_D_tt')),
+ ), name="", default="A", description=t('arm_drop'))
+
+ categorize_dropdown : EnumProperty(
+ items=(
+ ("A", t('cat_drop_A'), t('cat_drop_A_tt')),
+ ("B", t('cat_drop_B'), t('cat_drop_B_tt')),
+ ), name="", default="A", description=t('cat_drop'))
+
+ colors_dropdown : BoolProperty(
+ description=t('dark_F_tt'),
+ default = True)
+
+ bake_light_bool : BoolProperty(
+ description=t('bake_light_tt'),
+ default = True)
+
+ bake_dark_bool : BoolProperty(
+ description=t('bake_dark_tt'),
+ default = True)
+
+ bake_norm_bool : BoolProperty(
+ description=t('bake_norm_tt'),
+ default = False)
+
+ use_atlas : BoolProperty(
+ description=t('use_atlas'),
+ default = False)
+
+ delete_cache : BoolProperty(
+ description=t('delete_cache'),
+ default = False)
+
+ prep_dropdown : EnumProperty(
+ items=(
+ ("A", t('prep_drop_A'), t('prep_drop_A_tt')),
+ #("C", "MikuMikuDance - PMX compatible", " "),
+ ("D", t('prep_drop_D'), t('prep_drop_D_tt')),
+ ("B", t('prep_drop_B'), t('prep_drop_B_tt')),
+ ), name="", default="A", description=t('prep_drop'))
+
+ simp_dropdown : EnumProperty(
+ items=(
+ ("A", t('simp_drop_A'), t('simp_drop_A_tt')),
+ ("B", t('simp_drop_B'), t('simp_drop_B_tt')),
+ ("C", t('simp_drop_C'), t('simp_drop_C_tt')),
+ ), name="", default="A", description=t('simp_drop'))
+
+ bake_light_bool : BoolProperty(
+ description=t('bake_light_tt'),
+ default = True)
+
+ bake_dark_bool : BoolProperty(
+ description=t('bake_dark_tt'),
+ default = True)
+
+ bake_norm_bool : BoolProperty(
+ description=t('bake_norm_tt'),
+ default = False)
+
+ shapekeys_dropdown : EnumProperty(
+ items=(
+ ("A", t('shape_A'), t('shape_A_tt')),
+ ("B", t('shape_B'), t('shape_B_tt')),
+ ("C", t('shape_C'), t('shape_C_tt')),
+ ), name="", default="A", description="")
+
+ shader_dropdown : EnumProperty(
+ items=(
+ ("A", t('shader_A'), ''),
+ ("B", t('shader_B'), ''),
+ ("D", t('shader_D'), ''),
+ ("C", t('shader_C'), t('shader_C_tt')),
+ ), name="", default="A", description="Shader")
+
+ max_thread_num: IntProperty(
+ min=1, max = 64,
+ default=8,
+ description=t('max_thread_num_tt'))
+
+ max_image_num: IntProperty(
+ min=1, max = 10,
+ default=2,
+ description=t('max_image_num_tt'))
+
+ batch_rows: IntProperty(
+ min=256, max = 4096,
+ default=512,
+ description=t('batch_rows_tt'))
+
+ def draw(self, context):
+ layout = self.layout
+ splitfac = 0.5
+
+ col = layout.column(align=True)
+ row = col.row(align=True)
+ row.label(text='Change the default options for the KKBP Importer below:')
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(self, "categorize_dropdown")
+ split.prop(self, "armature_dropdown")
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(self, "shapekeys_dropdown")
+ split.prop(self, "shader_dropdown")
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(self, "colors_dropdown", toggle=True, text = t('dark_F'))
+ split.prop(self, "delete_cache", toggle=True, text = t('delete_cache'))
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(self, "fix_seams", toggle=True, text = t('seams'))
+ split.prop(self, "use_material_fake_user", toggle=True, text = t('keep_templates'))
+
+ row = col.row(align=True)
+ split = row.split(align = True, factor=splitfac)
+ split.prop(self, "use_single_outline", toggle=True, text = t('outline'))
+ split.prop(self, "sfw_mode", toggle=True, text = t('sfw_mode'))
+
+ col = layout.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=0.33)
+ split.prop(self, "bake_light_bool", toggle=True, text = t('bake_light'))
+ split.prop(self, "bake_dark_bool", toggle=True, text = t('bake_dark'))
+ split.prop(self, "bake_norm_bool", toggle=True, text = t('bake_norm'))
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.prop(self, 'old_bake_bool', toggle=True, text = t('old_bake'))
+ row = col.row(align = True)
+ split = row.split(align=True, factor=splitfac)
+ split.prop(self, "use_atlas", toggle=True, text = t('use_atlas'))
+
+ col = layout.column(align=True)
+ row = col.row(align=True)
+ split = row.split(align=True, factor=splitfac)
+ split.prop(self, "simp_dropdown")
+ split.prop(self, "prep_dropdown")
+
+ col = layout.column(align=True)
+ row = col.row(align=True)
+ row.label(text='Change these options based on your computer specs to speed up the import process:')
+ row = col.row(align=True)
+ split = row.split(align=True, factor=0.33)
+ split.prop(self, "max_thread_num", text = t('max_thread_num'))
+ split.prop(self, "max_image_num", text = t('max_image_num'))
+ split.prop(self, "batch_rows", text = t('batch_rows'))
+
diff --git a/wiki/README.md b/wiki/README.md
new file mode 100644
index 0000000..a2ec34a
--- /dev/null
+++ b/wiki/README.md
@@ -0,0 +1,65 @@
+## How to use the exporter
+
+1. Install the HF Patch for Koikatsu, or the HF Patch for Koikatsu Sunshine.
+**Pre-modded repacks will not work** unless you update with the repack's auto-updater or install the HF patch.
+[Click here for repack workaround #1.](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/465#issuecomment-2602969013)
+[Click here for repack workaround #2.](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/523)
+[Click here for repack workaround #3.](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/560)
+
+1. Find your Koikatsu install directory and drag the KKBP exporter into the /bepinex/plugins/ folder
+
+1. Start Koikatsu and open the Character Maker
+
+1. Enable the KKBP Exporter window.
+
+
+1. Change your export options. [See this page for an explanation of all export options](export_panel).
+
+1. Click the "Export Model for KKBP" button at the top of the screen. This may take a few minutes depending on your computer hardware.
+
+3. A folder in your Koikatsu install directory will popup when the export is finished
+
+## How to use the importer
+1. Open Blender 4.2 LTS. **Other versions are not guaranteed to work.** [Click here if you are not using Blender 4.2](faq)
+
+1. Install mmd_tools in Blender
+
+1. Install KKBP Importer 8.0.0 in Blender
+
+1. After you install both addons, you can click the "Import model" button in the KKBP panel. [An explanation of all import options can be found here](import_panel)
+
+
+1. Choose the .pmx file from the export folder
+
+
+1. The blender console will appear and begin importing the model. This may take a few minutes depending on your computer hardware
+
+
+1. Check there were no errors during import in the scripting tab. A successful import will end in "KKBP import finished in XX minutes"
+
+
+## Editing the model in Blender
+1. If there were no errors, you can start using the model as is by activating the material preview on the top right. If your computer is taking a long time to compile all the shaders (longer than 60 seconds), it is recommended to finalize the materials by clicking the button in the KKBP panel. If you finalize the materials then the shaders will compile very quickly. Finalizing the materials can take anywhere from 5 to 30 minutes depending on your hardware and model.
+
+
+1. If there were no errors but something doesn't look right, check the [FAQ](faq) or the [material breakdown](material_breakdown) page for information on how to edit the materials.
+
+1. If you got an error during import, check the [FAQ](faq), or search [the issues page on the Github repo](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues) to see if anyone else had the same issue as you.
+
+## Exporting from Blender to fbx:
+
+1. Enable the Atlas option, then click the Finalize Materials button in the KKBP panel
+
+
+1. This does three things
+ * Finalizes all of the materials to png files and saves them to the baked_files folder in the export folder
+ * Creates an atlas file for your body / hair / clothes and saves them to the atlas_files folder in your export folder
+ * Creates a new collection that uses the atlas
+
+1. Hide the original collection in the outliner and show the new collection
+
+
+1. If you want to reduce the bone count, or convert the model's armature for VRM / VRChat / Unreal Engine, click the "Prep for target application" button.
+
+1. Click the export button in the collection tab to export an fbx file to the atlas_files folder in the export folder
+
diff --git a/wiki/README_zh.md b/wiki/README_zh.md
new file mode 100644
index 0000000..08f4a34
--- /dev/null
+++ b/wiki/README_zh.md
@@ -0,0 +1,65 @@
+## 如何使用导出器
+
+1. 安装 Koikatsu 的 HF 补丁,或 Koikatsu Sunshine 的 HF 补丁。
+**预修改的重新打包版本将无法工作**,除非你使用重新打包的自动更新程序更新或安装 HF 补丁。
+[点击这里查看重新打包解决方法 #1。](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/465#issuecomment-2602969013)
+[点击这里查看重新打包解决方法 #2。](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/523)
+[点击这里查看重新打包解决方法 #3。](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/560)
+
+1. 找到你的 Koikatsu 安装目录,并将 KKBP 导出器拖到 /bepinex/plugins/ 文件夹中
+
+1. 启动 Koikatsu 并打开角色制作器
+
+1. 启用 KKBP 导出器窗口。
+
+
+1. 更改你的导出选项。[有关所有导出选项的说明,请参见此页面](export_panel)。
+
+1. 点击屏幕顶部的"Export Model for KKBP"按钮。这可能需要几分钟,具体取决于你的计算机硬件。
+
+3. 导出完成后,Koikatsu 安装目录中的一个文件夹将弹出
+
+## 如何使用导入器
+1. 打开 Blender 4.2 LTS。**不保证其他版本可以工作。** [如果你没有使用 Blender 4.2,请点击这里](faq)
+
+1. 在 Blender 中安装 mmd_tools
+
+1. 在 Blender 中安装 KKBP Importer 8.0.0
+
+1. 安装两个插件后,你可以点击 KKBP 面板中的"Import model"按钮。[所有导入选项的说明可以在这里找到](import_panel)
+
+
+1. 从导出文件夹中选择 .pmx 文件
+
+
+1. Blender 控制台将出现并开始导入模型。这可能需要几分钟,具体取决于你的计算机硬件
+
+
+1. 在脚本选项卡中检查导入期间是否没有错误。成功的导入将以"KKBP import finished in XX minutes"结束
+
+
+## 在 Blender 中编辑模型
+1. 如果没有错误,你可以通过激活右上角的材质预览来开始按原样使用模型。如果你的计算机编译所有着色器需要很长时间(超过 60 秒),建议通过点击 KKBP 面板中的按钮来完成材质。如果你完成材质,则着色器将非常快速地编译。完成材质可能需要 5 到 30 分钟,具体取决于你的硬件和模型。
+
+
+1. 如果没有错误但某些东西看起来不对,请查看 [FAQ](faq) 或[材质分解](material_breakdown)页面以获取有关如何编辑材质的信息。
+
+1. 如果你在导入期间遇到错误,请查看 [FAQ](faq),或搜索 [Github 仓库上的问题页面](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues)以查看是否有其他人遇到与你相同的问题。
+
+## 从 Blender 导出到 fbx:
+
+1. 启用 Atlas 选项,然后点击 KKBP 面板中的完成材质按钮
+
+
+1. 这会做三件事
+ * 将所有材质完成为 png 文件并将它们保存到导出文件夹中的 baked_files 文件夹
+ * 为你的身体/头发/衣服创建一个图集文件并将它们保存到导出文件夹中的 atlas_files 文件夹
+ * 创建一个使用图集的新集合
+
+1. 在大纲视图中隐藏原始集合并显示新集合
+
+
+1. 如果你想减少骨骼数量,或为 VRM / VRChat / Unreal Engine 转换模型的骨架,请点击"Prep for target application"按钮。
+
+1. 点击集合选项卡中的导出按钮,将 fbx 文件导出到导出文件夹中的 atlas_files 文件夹
+
diff --git a/wiki/armature.md b/wiki/armature.md
new file mode 100644
index 0000000..f9d39a2
--- /dev/null
+++ b/wiki/armature.md
@@ -0,0 +1,68 @@
+---
+layout: default
+---
+
+# Armature
+
+## KKBP Armature
+
+The KKBP armature is the best armature to use **if you plan on exporting the model**.
+The KKBP armature features the following:
+* Eye controller bone
+* Hand and Leg IKs
+* A heel IK
+* A center bone that lets you move all bones at once
+
+## Rigify Armature
+
+The Rigify armature is the best armature to use **if you plan on using the model in Blender**.
+The Rigify addon must be enabled for this armature to work.
+The Rigify armature includes many different features:
+* Eye controller bone
+* Hand and Leg IKs
+* A heel IK
+* A center bone that lets you move all bones at once
+* IK to FK sliders
+* IK to FK transition helpers
+* IK limb stretches
+* Tweak bones for limb deformation
+* Easy-to-use finger bones
+* Finger IKs
+* Palm bones
+* Foot spin bone
+* Eye look target
+* Individual eye bones
+* FK eye bones
+* Tongue IK
+* Head and Neck follow sliders
+* Head look target
+
+Check [this video series for understanding what the Rigify addon is and how it can be used in general](https://www.youtube.com/watch?v=-JSFcSxsaTs&list=PLdcL5aF8ZcJv68SSdwxip33M7snakl6Dx).
+
+Check [this page for information specific to the KKBP Rigify armature](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/78).
+
+
+
+
+
+
+## Koikatsu Armature
+
+The Koikatsu Armature is an armature that tries to match the bone structure of the in-game armature. This armature does not come with IKs and is FK only.
+
+## PMX Armature
+
+The PMX Armature is the armature that comes straight out of the KKBP exporter. This armature does not come with IKs and is FK only.
+
+## Bone Collections
+The KKBP armature hides some bone collections by default so the view is less cluttered. These bones can be found in each collection.
+
+
+The bone collection panel can be found by selecting the armature and looking in the Data tab. You can click the eye next to a collection to hide/show it.
+
+
+## Joint corrections
+The KKBP and Rigify armatures make use of helper bones to make the mesh look better when it deforms. These helper bones can be found in bone collections 2 and 3 of the KKBP armature. Some joint corrections are implemented by giving the helper bones bone contraints and other corrections are implemented by giving the helper bones drivers to automatically set their location and rotation when a specific bone is rotated. An example of a joint correction bone is shown below. In the default position, the cf_s_elboback_L bone stays in place. When the arm is bent the location drivers for cf_s_elboback_L cause the bone to move out and deform the arm properly. The third image is an example of what the arm would look like without the location drivers enabled. **Joint corrections are not carried over when you export the model, so you will likely experience weird bends like this unless you re-implement the drivers in your target program.** A list of all helper bones can be found in the create_joint_drivers function of [modifyarmature.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/modifyarmature.py).
+
+
+
diff --git a/wiki/armature_zh.md b/wiki/armature_zh.md
new file mode 100644
index 0000000..a1cb9f3
--- /dev/null
+++ b/wiki/armature_zh.md
@@ -0,0 +1,67 @@
+---
+layout: default
+---
+
+# 骨架
+
+## KKBP 骨架
+
+如果你**计划导出模型**,KKBP 骨架是最佳选择。
+KKBP 骨架具有以下特性:
+* 眼睛控制器骨骼
+* 手部和腿部 IK
+* 脚跟 IK
+* 可以一次移动所有骨骼的中心骨骼
+
+## Rigify 骨架
+
+如果你**计划在 Blender 中使用模型**,Rigify 骨架是最佳选择。
+必须启用 Rigify 插件才能使此骨架正常工作。
+Rigify 骨架包含许多不同的功能:
+* 眼睛控制器骨骼
+* 手部和腿部 IK
+* 脚跟 IK
+* 可以一次移动所有骨骼的中心骨骼
+* IK 到 FK 滑块
+* IK 到 FK 过渡辅助器
+* IK 肢体拉伸
+* 用于肢体变形的微调骨骼
+* 易于使用的手指骨骼
+* 手指 IK
+* 手掌骨骼
+* 脚部旋转骨骼
+* 眼睛注视目标
+* 独立眼睛骨骼
+* FK 眼睛骨骼
+* 舌头 IK
+* 头部和颈部跟随滑块
+* 头部注视目标
+
+查看[此视频系列以了解 Rigify 插件是什么以及如何一般使用它](https://www.youtube.com/watch?v=-JSFcSxsaTs&list=PLdcL5aF8ZcJv68SSdwxip33M7snakl6Dx)。
+
+查看[此页面以获取有关 KKBP Rigify 骨架的特定信息](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/78)。
+
+
+
+
+
+
+## Koikatsu 骨架
+
+Koikatsu 骨架是一个试图匹配游戏内骨架结构的骨架。此骨架不带 IK,仅支持 FK。
+
+## PMX 骨架
+
+PMX 骨架是直接从 KKBP 导出器导出的骨架。此骨架不带 IK,仅支持 FK。
+
+## 骨骼集合
+KKBP 骨架默认隐藏一些骨骼集合,以使视图不那么杂乱。这些骨骼可以在每个集合中找到。
+
+
+可以通过选择骨架并在数据选项卡中查找来找到骨骼集合面板。你可以点击集合旁边的眼睛图标来隐藏/显示它。
+
+
+## 关节修正
+KKBP 和 Rigify 骨架使用辅助骨骼来使网格在变形时看起来更好。这些辅助骨骼可以在 KKBP 骨架的骨骼集合 2 和 3 中找到。一些关节修正是通过给辅助骨骼添加骨骼约束来实现的,其他修正则是通过给辅助骨骼添加驱动器来实现的,当特定骨骼旋转时自动设置它们的位置和旋转。下面显示了一个关节修正骨骼的示例。在默认位置,cf_s_elboback_L 骨骼保持在原位。当手臂弯曲时,cf_s_elboback_L 的位置驱动器会使骨骼向外移动并正确变形手臂。第三张图片是没有启用位置驱动器时手臂的样子。**导出模型时不会保留关节修正,因此除非在目标程序中重新实现驱动器,否则可能会遇到这样的奇怪弯曲。** 所有辅助骨骼的列表可以在 [modifyarmature.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/modifyarmature.py) 的 create_joint_drivers 函数中找到。
+
+
diff --git a/wiki/export_panel.md b/wiki/export_panel.md
new file mode 100644
index 0000000..fe93781
--- /dev/null
+++ b/wiki/export_panel.md
@@ -0,0 +1,45 @@
+## Exporter Panel settings
+Each setting in the exporter panel will be briefly detailed on this page.
+
+
+
+### Freeze current pose
+
+The pose is usually reset to a T pose during export. Enable this option to not reset to a T pose, and to keep the pose you have selected in the character maker. Because a pose is pre-applied to the armature, the armature will not work correctly in blender. This setting may be useful for 3D prints of koikatsu models or for stills. This exporter setting will force the Koikatsu armature in blender. See the picture below for an example of a pose being applied to the model.
+
+
+
+### Export without physics
+
+Some outfits rely on in-game physics to look correct. Enable this option to disable the in-game physics during export. This may make certain outfits look weird because of the lack of physics, but it is useful if you want to apply your own physics to the model in blender.
+
+
+
+### Freeze shapekeys
+
+Shapekeys are reverted to their default values during export. Enable this option to freeze any shapekeys you have enabled in the character maker to the base mesh. Because the mesh is pre-deformed, other shapekeys will not work correctly in blender. Tear and gag eye shapekeys will also not work in blender. See the picture below for an example of some shapekeys being pre-applied to the base mesh.
+
+
+
+### Export variations
+
+This will export any available clothing variations. Not all clothes have variations.
+When imported, the variations will be hidden in the outliner by default.
+
+
+
+### Export all outfits
+
+This will make the exporter export all of the card's outfits instead of just the currently selected one. This will dramatically increase koikatsu export times and blender import times.
+
+### Export hit meshes
+
+This will export the hit mesh. An example of the hit mesh is shown below.
+
+
+
+### Enable pushups
+
+Outfit pushups (shown below) are disabled during export. Enable this option to keep them on.
+
+
diff --git a/wiki/export_panel_zh.md b/wiki/export_panel_zh.md
new file mode 100644
index 0000000..5152756
--- /dev/null
+++ b/wiki/export_panel_zh.md
@@ -0,0 +1,45 @@
+## 导出器面板设置
+本页将简要介绍导出器面板中的每个设置。
+
+
+
+### 冻结当前姿势
+
+导出期间通常会将姿势重置为 T 姿势。启用此选项可不重置为 T 姿势,并保持你在角色制作器中选择的姿势。由于姿势已预先应用到骨架上,骨架在 Blender 中将无法正常工作。此设置可能对 Koikatsu 模型的 3D 打印或静态图像有用。此导出器设置将强制使用 Blender 中的 Koikatsu 骨架。请参见下图以了解应用到模型的姿势示例。
+
+
+
+### 导出时不使用物理
+
+某些服装依赖游戏内物理才能看起来正确。启用此选项可在导出期间禁用游戏内物理。这可能会使某些服装因缺少物理而看起来很奇怪,但如果你想在 Blender 中为模型应用自己的物理,这会很有用。
+
+
+
+### 冻结形态键
+
+导出期间形态键会恢复为默认值。启用此选项可将你在角色制作器中启用的任何形态键冻结到基础网格。由于网格已预先变形,其他形态键在 Blender 中将无法正常工作。泪水和口塞眼睛形态键在 Blender 中也将无法工作。请参见下图以了解一些预先应用到基础网格的形态键示例。
+
+
+
+### 导出变体
+
+这将导出任何可用的服装变体。并非所有服装都有变体。
+导入时,变体将默认在大纲视图中隐藏。
+
+
+
+### 导出所有服装
+
+这将使导出器导出卡片的所有服装,而不仅仅是当前选择的服装。这将大大增加 Koikatsu 导出时间和 Blender 导入时间。
+
+### 导出碰撞网格
+
+这将导出碰撞网格。下面显示了碰撞网格的示例。
+
+
+
+### 启用推高效果
+
+导出期间会禁用服装推高效果(如下所示)。启用此选项可保持它们开启。
+
+
diff --git a/wiki/faq.md b/wiki/faq.md
new file mode 100644
index 0000000..b2a70d1
--- /dev/null
+++ b/wiki/faq.md
@@ -0,0 +1,53 @@
+## Frequency asked questions
+
+## I don't know what versions of the programs or addons to use
+Check the table below. KKBP has been updated many times to keep up with newer Blender versions and is **not** backwards compatible, so the version of KKBP you need to use depends on what your Blender version is.
+
+|Blender version|Last working KKBP version|PMX Importer dependency version|Koikatsu HF Patch version|Koikatsu Sunshine HF Patch version|Video guide link|
+|---|---|---|---|---|---|
+4.4.0|8.0.0|mmd_tools 4.2.2|HF Patch 3.32|HF Patch for KKS 1.22|[Here](https://www.youtube.com/watch?v=QvXl4jRppP4&list=PLhiuav2SCuveMgQUA2YqqbSE7BtOrkZ-Q)|
+4.3.2|8.0.0|mmd_tools 4.2.2|HF Patch 3.32|HF Patch for KKS 1.22|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuveWvSwKg18l6mDSl5xl4x7o)|
+4.2.2 LTS|8.0.0|mmd_tools 4.2.2|HF Patch 3.32|HF Patch for KKS 1.22|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuveWvSwKg18l6mDSl5xl4x7o)|
+|3.6.9 LTS|6.6.3|mmd_tools 2.9.2|HF Patch 3.22|HF Patch for KKS 1.7|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.5|6.5.0|mmd_tools 2.9.2|HF Patch 3.22|HF Patch for KKS 1.7|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.4|6.4.2|mmd_tools 2.9.2|HF Patch 3.22|HF Patch for KKS 1.7|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.3|6.2.1|mmd_tools 2.9.2|HF Patch 3.17|HF Patch for KKS 1.7|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.2|5.1.1|CATS Blender Plugin 0.19|HF Patch 3.13|--|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvdEAbUzJxSqp61fNiPTFfwb)|
+|2.93|4.3.0|CATS Blender Plugin 0.18|HF Patch 3.7|--|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvd5eAOb3Ct1eovFAlgv-iwe)|
+|2.91.2|4.2.1|CATS Blender Plugin 0.18|HF Patch 3.7|--|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvd5eAOb3Ct1eovFAlgv-iwe)|
+|2.83.4|3.06|CATS Blender Plugin 0.17|HF Patch 3.0|--|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvfIJ20QrEzkoFl__F9VaRk2)|
+|2.82.7|V2|[This outdated mmd_tools version](https://github.com/powroupi/blender_mmd_tools?tab=readme-ov-file)|--|--|[Here](https://www.youtube.com/playlist?list=PLhiuav2SCuvfx_IJw2TnYmPdWYwIzo7SO)|
+
+## I'm using the right versions but it's still not working
+**Some heavily modded characters will not import correctly.** Custom head mods, body mods and other mods that change body material names or shapekey names from the default will likely not work. Try exporting the default character that you get when you startup the character creator, and double check the table for choosing the right version at the top of the page. If that works, slowly try adding back each piece of clothing / hair until the item that causes the issue is found. If that didn't work and you are having issues with the default character, triple check you installed everything in the version table at the top of the page.
+
+## My top is missing
+KKBP applies the alphamask to the body by default. If your character is supposed to be a shirtless guy, you can find the body material...
+
+
+
+and enable the "Force visibility" slider to show the top of the body
+
+
+
+If you're using multiple outfits, the alphamask may change based on the outfit. If you need to change the alphamask you can open up the Body textures node group on the body material...
+
+
+
+and change the alphamask to the body_AM_## file that corresponds to your outfit
+
+
+
+## My clothes are missing
+Make sure the clothing you're looking for isn't hidden in the outliner. In certain situations, KKBP will automatically hide some clothing objects from view. In rare cases, the outfit will just not export. Check the "Outfit 00" folder in your export folder for a .pmx file. If there's no .pmx file the exporter failed to get the clothing. Please submit a new issue on the github if you find a card that does this.
+
+## I'm getting fully white textures after importing my character
+The import script failed somewhere. In Blender, click the Scripting tab on the top of the window. Any errors will appear at the bottom of the log. A successful import log will end in "KKBP import finished"
+
+## I don't an error and my clothing or hair doesn't look correct
+KKBP should load in a majority of clothing and hair correctly. If it doesn't, check the [material breakdown](material_breakdown) page for information on how to fix material issues.
+Please submit a new issue on the github if you find a card that does this.
+
+## Blender crashed during import
+Try importing again. It does that every once in a while. The import process can take around 1GB of RAM and 2GB of VRAM (or around 4.5GB of RAM and 3GB of VRAM on complex cards with many accessories), so if you're low on either of those it may crash because of that too.
+
diff --git a/wiki/faq_zh.md b/wiki/faq_zh.md
new file mode 100644
index 0000000..968bf4c
--- /dev/null
+++ b/wiki/faq_zh.md
@@ -0,0 +1,52 @@
+## 常见问题
+
+## 我不知道应该使用哪个版本的程序或插件
+请查看下表。KKBP 已多次更新以跟上较新的 Blender 版本,并且**不**向后兼容,因此你需要使用的 KKBP 版本取决于你的 Blender 版本。
+
+|Blender 版本|最后可用的 KKBP 版本|PMX 导入器依赖版本|Koikatsu HF 补丁版本|Koikatsu Sunshine HF 补丁版本|视频指南链接|
+|---|---|---|---|---|---|
+4.4.0|8.0.0|mmd_tools 4.2.2|HF Patch 3.32|HF Patch for KKS 1.22|[这里](https://www.youtube.com/watch?v=QvXl4jRppP4&list=PLhiuav2SCuveMgQUA2YqqbSE7BtOrkZ-Q)|
+4.3.2|8.0.0|mmd_tools 4.2.2|HF Patch 3.32|HF Patch for KKS 1.22|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuveWvSwKg18l6mDSl5xl4x7o)|
+4.2.2 LTS|8.0.0|mmd_tools 4.2.2|HF Patch 3.32|HF Patch for KKS 1.22|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuveWvSwKg18l6mDSl5xl4x7o)|
+|3.6.9 LTS|6.6.3|mmd_tools 2.9.2|HF Patch 3.22|HF Patch for KKS 1.7|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.5|6.5.0|mmd_tools 2.9.2|HF Patch 3.22|HF Patch for KKS 1.7|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.4|6.4.2|mmd_tools 2.9.2|HF Patch 3.22|HF Patch for KKS 1.7|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.3|6.2.1|mmd_tools 2.9.2|HF Patch 3.17|HF Patch for KKS 1.7|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvc-wbexi2vwSnVHnZFwkYNP)|
+|3.2|5.1.1|CATS Blender Plugin 0.19|HF Patch 3.13|--|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvdEAbUzJxSqp61fNiPTFfwb)|
+|2.93|4.3.0|CATS Blender Plugin 0.18|HF Patch 3.7|--|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvd5eAOb3Ct1eovFAlgv-iwe)|
+|2.91.2|4.2.1|CATS Blender Plugin 0.18|HF Patch 3.7|--|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvd5eAOb3Ct1eovFAlgv-iwe)|
+|2.83.4|3.06|CATS Blender Plugin 0.17|HF Patch 3.0|--|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvfIJ20QrEzkoFl__F9VaRk2)|
+|2.82.7|V2|[这个过时的 mmd_tools 版本](https://github.com/powroupi/blender_mmd_tools?tab=readme-ov-file)|--|--|[这里](https://www.youtube.com/playlist?list=PLhiuav2SCuvfx_IJw2TnYmPdWYwIzo7SO)|
+
+## 我使用了正确的版本但仍然无法工作
+**一些高度修改的角色将无法正确导入。** 自定义头部模组、身体模组和其他更改默认身体材质名称或形态键名称的模组可能无法工作。尝试导出启动角色创建器时获得的默认角色,并仔细检查页面顶部的版本选择表。如果可以工作,请慢慢尝试添加回每件服装/头发,直到找到导致问题的项目。如果这不起作用并且你在使用默认角色时遇到问题,请再次检查你是否安装了页面顶部版本表中的所有内容。
+
+## 我的上衣不见了
+KKBP 默认将 alpha 蒙版应用于身体。如果你的角色应该是一个赤裸上身的男性,你可以找到身体材质...
+
+
+
+并启用"强制可见性"滑块以显示身体的上部
+
+
+
+如果你使用多套服装,alpha 蒙版可能会根据服装而变化。如果你需要更改 alpha 蒙版,可以打开身体材质上的 Body textures 节点组...
+
+
+
+并将 alpha 蒙版更改为与你的服装对应的 body_AM_## 文件
+
+
+
+## 我的衣服不见了
+确保你要查找的服装在大纲视图中没有被隐藏。在某些情况下,KKBP 会自动隐藏一些服装对象。在极少数情况下,服装根本不会导出。检查导出文件夹中的"Outfit 00"文件夹是否有 .pmx 文件。如果没有 .pmx 文件,则导出器未能获取服装。如果你发现这样的卡片,请在 github 上提交新问题。
+
+## 导入角色后我得到了全白的纹理
+导入脚本在某处失败了。在 Blender 中,点击窗口顶部的脚本选项卡。任何错误都会出现在日志底部。成功的导入日志将以"KKBP import finished"结束
+
+## 我没有错误,但我的服装或头发看起来不正确
+KKBP 应该正确加载大多数服装和头发。如果没有,请查看[材质分解](material_breakdown)页面以获取有关如何修复材质问题的信息。
+如果你发现这样的卡片,请在 github 上提交新问题。
+
+## Blender 在导入期间崩溃了
+尝试再次导入。它偶尔会这样做。导入过程可能需要大约 1GB 的 RAM 和 2GB 的 VRAM(或在具有许多配饰的复杂卡片上大约 4.5GB 的 RAM 和 3GB 的 VRAM),因此如果你的任何一个都不足,它可能会因此而崩溃。
diff --git a/wiki/import_panel.md b/wiki/import_panel.md
new file mode 100644
index 0000000..ee1fdaf
--- /dev/null
+++ b/wiki/import_panel.md
@@ -0,0 +1,19 @@
+# Import panel options
+
+**You can hold your mouse over each option in the KKBP panel to get an explanation on what it does!**
+
+This page will only cover things that are not immediately obvious from the explanations built into the panel.
+
+## Default panel settings
+If you expand KKBP in Blender's addon menu, you can set default settings for the panel (so you don't have to set them each time you restart Blender).
+
+
+## Extras panel
+
+
+
+* Instructions for exporting studio objects from the game and importing them using the button in the panel [can be found here](https://www.youtube.com/watch?v=PeryYTsAN6E)
+* Instructions for exporting animations from the game and applying them using the button in the panel can be found [here if you prefer text](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/extras/animationlibrary/createanimationlibrary.py) or [here if you prefer video](https://www.youtube.com/watch?v=Ezsy6kwgBE0)
+* If the "Use KKBP Armature" option was selected during import but you now want to swap to a Rigify armature, you can click the "Convert for Rigify" button to permanentally convert the KKBP one to a Rigify one.
+* The "Separate Eyes and Eyebrows" button will separate the eyes and eyebrows into separate objects, then link their shapekeys to the Body object's shapekeys. This can be combined with the Cryptomatte compositor features to make the Eyes and Eyebrows show through the hair. See [this video for an example of selecting objects with Cryptomatte](https://www.youtube.com/watch?v=3UR4eXxMlsU).
+* The "Setup Materials for Material Combiner" button will set up your materials for [Shotariya's Material Combiner addon](https://github.com/Grim-es/material-combiner-addon). You must Finalize your materials before you can do this.
diff --git a/wiki/import_panel_zh.md b/wiki/import_panel_zh.md
new file mode 100644
index 0000000..0a1ac7e
--- /dev/null
+++ b/wiki/import_panel_zh.md
@@ -0,0 +1,19 @@
+# 导入面板选项
+
+**你可以将鼠标悬停在 KKBP 面板中的每个选项上以获取有关其功能的说明!**
+
+本页仅涵盖面板内置说明中不太明显的内容。
+
+## 默认面板设置
+如果你在 Blender 的插件菜单中展开 KKBP,可以为面板设置默认设置(这样你就不必在每次重启 Blender 时都设置它们)。
+
+
+## 额外功能面板
+
+
+
+* 从游戏中导出工作室对象并使用面板中的按钮导入它们的说明[可以在这里找到](https://www.youtube.com/watch?v=PeryYTsAN6E)
+* 从游戏中导出动画并使用面板中的按钮应用它们的说明可以在[这里找到(如果你喜欢文本)](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/extras/animationlibrary/createanimationlibrary.py)或[这里(如果你喜欢视频)](https://www.youtube.com/watch?v=Ezsy6kwgBE0)
+* 如果在导入期间选择了"使用 KKBP 骨架"选项,但你现在想切换到 Rigify 骨架,可以点击"转换为 Rigify"按钮将 KKBP 骨架永久转换为 Rigify 骨架。
+* "分离眼睛和眉毛"按钮将眼睛和眉毛分离为单独的对象,然后将它们的形态键链接到 Body 对象的形态键。这可以与 Cryptomatte 合成器功能结合使用,使眼睛和眉毛透过头发显示。请参见[此视频以了解使用 Cryptomatte 选择对象的示例](https://www.youtube.com/watch?v=3UR4eXxMlsU)。
+* "为材质合并器设置材质"按钮将为 [Shotariya 的材质合并器插件](https://github.com/Grim-es/material-combiner-addon)设置你的材质。在执行此操作之前,你必须先完成材质。
diff --git a/wiki/material.md b/wiki/material.md
new file mode 100644
index 0000000..a7b6719
--- /dev/null
+++ b/wiki/material.md
@@ -0,0 +1,107 @@
+## Material
+
+## Before you begin...
+**There's a lot of information on the [material breakdown](material_breakdown) if you're having issues with a material or want to know how a typical KKBP material works.**
+
+## Updating a finalized material
+If you want to go back to the original material to edit it again, you can set the material back to the -ORG version of the material.
+
+
+
+Here I have updated the hair color on the original shader
+
+
+
+To finalize the material again, just click the "Finalize Materials" button on the KKBP panel. The material will be re-finalized and the lightweight shader + atlas model will be updated.
+
+
+
+## Special materials (Outlines)
+Outlines have a different material setup. If an alphamask is available for this material, the alphamask's red channel will be used to determine where the outline should be transparent. If it is not available, the maintex alpha channel will be used for determining transparency instead. If neither are available, the outline will be visible everywhere on the material. The outline materials will not be converted to png files when you click the Finalize Materials button.
+
+
+
+## Special materials (Eyes)
+The eye texture is automatically created by the KKBP Exporter. If you want to use the custom eye slider and change the colors of the eye on the fly, you have to extract certain eye textures from the game using SB3Utility, then load them into the green texture group and recreate the eye from scratch. [Here's an example of doing that](https://www.youtube.com/watch?v=XFt12n7ByBI&t=231)
+
+## Special materials (Body)
+The body has a transparency mask to prevent it from clipping through some clothes [Check here for making the body visible](https://flailingfog.github.io/faq).
+
+If you are finding that the body is still transparent in the wrong areas, you can either edit the body alpha mask (location shown below), or load a different body_AM file from your pmx export folder. The first body alpha mask for the first outfit is automatically loaded in (cf_m_body_AM.png), so if you switch to a different outfit you'll have to switch the body mask to the right one (ex. body_AM_01 for outfit 01, 02 for outfit 02, etc).
+
+
+
+## Special materials (Hair)
+Hair materials are not linked. If you modify one hair material and then click the update hair materials button, the rest of the hair materials on this object will be updated using the settings of the current hair material.
+
+
+
+## Special materials (Tears)
+Tears have a special material that uses a gradient to get a look similar to the in-game one. You can make the tears use an HDRI or a flat color instead by using the sliders in the Tears material.
+
+
+
+Tears can be activated in the shapekey panel
+
+
+
+
+## Special materials (Gag eyes)
+There are three sets of Gag eyes: Gag00, Gag01 and Gag02
+Gag00 images are displayed in a mirrored fashion (both eyes mirror each other)
+Gag01 images are displayed as is
+Gag02 images are animated by continuosly changing the location of the UV map over time (also used by the Cartoony Wink expression)
+
+Gag eyes use drivers to determine what expression to display. When you activate a gag shapekey on the Body object, certain eye materials are moved back into the head to hide them, the gag eye mesh comes out to the front of the head and an image is displayed depending on which gag shapekey was activated.
+
+The default import comes with settings that make the face area behind the gag eyes look bad. Disabling the face eyeshadow intensity, setting the face rim to "None", and using the features in the "Permanent light / dark settings" section below can help with that.
+
+
+
+Gag eyes can be activated in the shapekey panel
+
+
+
+## Koikatsu color conversion
+The colors you see in Koikatsu are not the real colors being used; they're actually being saturated. In order to replicate the colors shown in koikatsu, all colors and images are run through the color_to_KK and image_to_KK functions in [converttextures.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/converttextures.py) to convert the base colors to the "real" colors seen visually in the game. This process is also run on all Maintex images.
+
+Here's an example of a material before and after color saturation
+
+
+
+## Koikatsu dark color conversion
+The game uses a second color conversion process to automatically get dark colors for every light color. The skin_dark_color and clothes_dark_color functions in [modifymaterial.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/modifymaterial.py) are used to replicate this process in Blender and to automatically obtain dark colors for every piece of clothing and create dark Maintex images. If this is disabled in the panel, you'll only get the light colors.
+
+## Cycles support
+Basic Cycles support is available by selecting "Use Cycles" in the panel. This will replace the Rim node group in the KK shaders with a Toon-like shader compatible with Cycles. The outline is disabled in the mode.
+
+
+
+
+
+## Normal blending methods
+A different normal blending method is available in the Toon Shading group. Enter it and find the node to use the Unity tech demo blending method. These blending methods [were taken from this page](https://blog.selfshadow.com/publications/blending-in-detail/) and [also this page](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/166)
+
+
+
+
+## Normal quality settings
+Normals are set to lower quality by default to increase animation playback performance. To change to higher quality normals, just enter the Toon Shading group, connect the higher quality output node and unmute the node by selecting it and pressing the M key.
+This is done automatically when baking materials so you don't need to worry about it if you plan on exporting the model from blender.
+
+
+
+
+## Toon shading settings
+Toon shading is created by the color ramps in the Toon Shading group. You can edit the color ramps in this section to increase or decrease the amount of light needed to show the light or dark version of the materials.
+
+
+
+
+## Permanent light / dark settings
+Parts of each material can be made permanently light or dark using the Permalight image in the texture node group. The red channel of the image is used for permalight and the blue channel is used for permadark
+
+
+
+## Editing the KKBP shader
+The KKBP library file can be edited if you want to use a custom material or node setup for all imported models. Keep in mind the import scripts will error out if certain things are missing, so don't delete anything already in the file. The library file is usually located here: ```C:\Users\[your username]\AppData\Roaming\Blender Foundation\Blender\[your blender version]\scripts\addons\KK-Blender-Porter-Pack\KK Shader.blend```
\ No newline at end of file
diff --git a/wiki/material_breakdown.md b/wiki/material_breakdown.md
new file mode 100644
index 0000000..2153395
--- /dev/null
+++ b/wiki/material_breakdown.md
@@ -0,0 +1,198 @@
+# KKBP material breakdown
+
+This page will fully break down the material node logic for KKBP 8.0's
+"KK General" material shader. The simple node logic in the KK General
+material is used throughout the entire project, so reading and
+understanding this page should allow you to learn how to fix or debug
+any KKBP material you encounter.
+
+## Before you begin...
+
+A large amount of cards were chosen at random and tested during the
+development of KKBP 8.0.
+
+Out of 133 cards...
+* 104 (78%) worked out of the box
+* 22 (17%) had odd material issues that took 5 minutes to fix
+* 5 (4%) exported but did not import
+* 1 (0.5%) had odd material issues that would likely take someone a while to figure out
+* 1 (0.5%) did not export and crashed the game
+
+This means if you're having issues **there's a 94.5% chance** reading the
+"Basic options" section below will help you fix the appearance of your
+model. It also means **there is a 0.5% chance** that you'll have to dig
+through this entire thing to find out why a material isn't looking the
+way it's supposed to look.
+
+And before you waste your time, **please make sure you didn't get an error**
+during the import process as described on the front page!
+**This page will not help you if your model imported with errors!**
+
+# Basic shader breakdown
+## Intro
+
+This breakdown will begin by using the clothing found on Chika, the
+default Koikatsu model. Let's start by opening up the shading tab in
+blender.
+
+
+
+This is the material setup that is used in every material. The texture
+files from the export folder are loaded into the green group on the
+left. The colors are generated in the blue group in the middle and then
+fed to the output node in the red group.
+
+There are light colors and dark colors for every material. Right now,
+the light colors are showing. You can toggle between the light and dark
+colors by using the viewport shading buttons on the top right. Here's
+what the dark colors look like.
+
+
+
+The remainder of this documentation will use the light colors only. You
+can expand the options for the light colors of the model by clicking on
+the arrow in the material panel
+
+
+
+## Detail color
+
+Let's go through each option. The first option is override detail
+color. KKBP automatically generates a detail color for you but if you
+want to override the color it gives you, you can enable this slider and
+set the detail color to whatever you want. Here's what it looks like
+when it's set to yellow
+
+
+
+Here's what it looks like when it's set to red
+
+
+
+## Detail intensity
+
+The next option is green detail intensity. Green detail intensity
+usually controls the soft details on clothes. Here's what the jacket
+look like at the default of .8 intensity
+
+
+
+Here's what the jacket look like when it's set to five green intensity
+
+
+
+And here's what they look like when the detail is set to 10 green
+intensity
+
+
+
+The blue detail intensity usually controls hardline details on clothes.
+Here's what the jacket look like it's set to the default of 0.1.
+
+
+
+Here's what it look like when it's set to one
+
+
+
+And here's what it look like when it's set to five
+
+
+
+## Shine
+
+Some clothes also have a shininess that can be adjusted. The default
+loafers have an adjustable shine. The color of the shine can be changed.
+Here's what the loafers look like with a white shine.
+
+
+
+And here's what they look like with a green shine
+
+
+
+The intensity of the shine can also be adjusted. Here's what the loafers
+look like with the default shine intensity of one.
+
+
+
+Here's what the loafers look like with the shine intensity set to zero
+
+
+
+## Maintex HSV
+
+Most clothes have what is called a main texture or a maintex for short.
+The hue, saturation and value of the main texture can be easily adjusted
+with the maintex sliders. Here's what the shoes look like with their HSV
+values adjusted
+
+
+
+## Color mask colors
+
+If a piece of clothing has a main texture, it will also have what is
+called a plain main texture. The difference between a main texture and a
+plain main texture will be explained later on. For now, all you need to
+know is that each color on every piece of clothing can be changed, just like in the game. In order to do this, the plain main texture must be enabled. The slider to enable the plain main texture is set to 1 in the picture below.
+
+
+
+Once the slider is enabled, you can change the colors of the piece of
+clothing with the color mask color inputs. The color mask colors of the
+shoes are changed below.
+
+
+
+You can return to the original colors at any point by resetting the
+color mask colors to their original values. Or by disabling the plain
+main texture slider again (thus, the regular main texture will be used
+again).
+
+
+
+## Pattern colors
+
+Some clothing like the default skirt have pattern colors that can be adjusted. In order to adjust the pattern colors, you need to use the plain main texture. The picture below shows the plain main texture slider enabled for the skirt.
+
+
+
+The default skirt only has one pattern color that can be adjusted.
+Here's what it looks like with the pattern color set to blue.
+
+
+
+If this piece of clothing had more patterns, their colors would be
+adjusted with the pattern color green and pattern color blue color
+inputs
+
+## Visibility slider
+
+Some pieces of clothing have what is called an alpha mask. The alpha
+mask hides certain parts of clothing to prevent it from clipping through
+other clothing. The default inner shirt is an example of a piece of
+clothing that has an alpha mask
+
+
+
+Portions of this clothing are hidden to prevent it from clipping through
+the jacket. If you want to make the clothing visible anyway, even though it
+shouldn't be, you can enable the force visibility slider. Here's what
+the shirt looks like with the slider enabled.
+
+
+
+And here's what the shirt and jacket look like with the shirt fully
+visible. Notice that some parts are clipping through the jacket when
+they shouldn't be. That's why the alpha mask is enabled by default.
+
+
+
+
+
+# In-depth shader breakdown
+
+That concludes all basic clothing options.
+
+If you are part of the 0.5% and none of these helped fix the material you're having issues with, you can check [the in-depth shader breakdown page](material_breakdown_advanced) for even more information on the KKBP "KK General" shader.
+
diff --git a/wiki/material_breakdown_advanced.md b/wiki/material_breakdown_advanced.md
new file mode 100644
index 0000000..cb22af8
--- /dev/null
+++ b/wiki/material_breakdown_advanced.md
@@ -0,0 +1,732 @@
+# In depth shader breakdown
+
+## Advanced intro
+
+This page will take a look at the three main node groups in detail.
+Namely, the texture files group, the KK general group and the combine
+colors group. It will also examine the sub-groups inside of each node
+group.
+
+
+## Enabling Node Wrangler
+
+Let's take a quick detour and enable node wrangler. This add-on is
+invaluable for debugging node groups. In the blender add-on window
+search for Node Wrangler and enable it
+
+
+
+## Texture files group
+
+Let's start with the texture files group. Open it by clicking the icon
+on the top right of the group
+
+
+
+Here's the texture files group.
+
+
+
+
+Each piece of clothing can have multiple textures that go with it. Each
+texture file from the export folder is loaded into each outfit material
+in blender. For example, the jacket CM.png from the export folder...
+
+
+
+Is loaded into the corresponding jacket CM image node in blender
+
+
+
+## Pattern slots
+
+There's a lot of textures, so let's start at the top and work our way
+to the bottom. The pattern files are loaded up here.
+
+
+
+The skirt only has one pattern (PM1.png) and it's loaded into the
+pattern red slot.
+
+
+
+if you enabled node wrangler, you can see what the pattern looks like by
+Control + Shift clicking the image node
+
+
+
+# Positioning with the Scale Vector group
+
+the pattern scale and positioning can be controlled by opening the
+position node group to the left of the image nodes.
+
+
+
+this is what the red pattern looks like after being scaled and shifted.
+
+
+
+most things you can position can also be rotated using the rotation
+slider
+
+
+
+# One more example of the Scale Vector group
+
+It can be a little hard to see how the scale vector group works with patterns, so let's also check out the positioning group in the eye material. Here's what the eyes look like at their default scale. If you're having difficulty locating the thing you're positioning on the 3D model, recall that you can preview what the material looks like in the materials tab.
+
+
+
+Here's what the eyes look like when they're scaled down.
+
+
+
+Here's what the eyes look like when they're scaled down, moved to the right, and rotated 90°
+
+
+
+## Colored main texture, plain main texture and dark main texture slots
+
+let's move onto the rest of the textures in the group now. here's what
+the main texture looks like when the image node has been control shift
+clicked. this is the **colored** main texture of the outfit. We'll take a look at this more in depth later on.
+
+
+
+here's what the plain main texture looks like. this is the **uncolored** main texture
+of the outfit. notice that some features like
+the buttons are still fully textured
+
+
+
+here's what the dark main texture looks like. KKBP automatically
+generates a dark version of the colored main texture and loads it into
+this slot.
+
+
+
+## Alpha mask slots
+
+If a piece of clothing has an alpha mask. It'll be loaded into this
+slot. here's what an alpha mask can look like
+
+
+
+Look familiar? This is what the complete material looked like. The
+visible portions match up with the yellow portions of the alpha mask
+
+
+
+## Color masks slots
+
+there are two slots for the color mask. the top slot is used for
+materials that are supposed to be partially or fully transparent. the
+bottom slot is used for fully opaque clothing. fully opaque clothing is
+not supposed to have a color mask, but the KKBP exporter exports one
+anyway. Some materials will look incorrect if a color mask is loaded
+into an opaque clothing material so that's why there's two slots. A
+list of in game shaders that are identified as opaque are listed below
+
+Koikano/main_clothes_opaque', 'Shader
+Forge/main_opaque', 'xukmi/MainOpaquePlus',
+'xukmi/MainOpaquePlusTess', 'Shader Forge/main_opaque2', 'Shader
+Forge/main_opaque_low'
+
+We'll be taking a look at both color masks but for right now here's what
+the bottom color mask looks like
+
+
+
+Look familiar? This is what the fully assembled material looks like. The
+brown, dark brown, and green portions of the shoes match up with the
+red, green, and blue portions of the color mask.
+
+
+
+## Detail mask slots
+
+This is the detail mask for the jacket.
+
+
+
+Look familiar? This is what the fully assembled material looks like with
+the detail intensity cranked up to the max. The green portions of the
+detail mask match up with the soft details on the jacket. The blue
+portions of the detail mask match up with the hard lines on the jacket.
+
+
+
+and if we take a look at the shoes, You can see the red portion of
+the detail mask
+
+
+
+the red portion matches up with the location of the shiny parts of the shoes
+
+
+
+## Detail masks and metallic materials
+
+the KKBP importer has a shortcoming with the detail mask related to
+metallic materials. The red portion of a detail mask can either mean
+shine or metal. KKBP does not support metal materials, so if it detects a
+metal material, it'll automatically set the detail shine slider to
+zero. For example, if we take a closer look at the jacket's detail mask,
+the buttons are obviously red.
+
+
+
+but when we take a look at the fully assembled material, the detail mask
+intensity has been disabled
+
+
+
+turning it back on results in erroneous appearance
+
+
+
+this is because the buttons are supposed to gain a metallic appearance
+from the red channel of the detail mask but since KKBP does not support
+metallic materials. It just turns white. KKBP determines if a material is
+metallic by the presence of the AR.png texture. If a material has an AR
+texture, then it is assumed to be metallic, and the detail shine
+intensity will be disabled automatically to avoid issues like the one
+seen above.
+
+For example the jacket has such a texture
+
+
+
+but the shoes do not have that texture so that's why the shine on the
+shoes were not disabled
+
+
+
+## Normal map
+anything related to hair will be skipped in this explanation, so the
+next node is the normal map. The normal maps are grayscale in appearance. The color of the normal map along with its alpha channel are used to create a proper normal map
+
+
+
+## Normal map detail
+
+and the final node is normal map detail. Most pieces of clothing do not
+have a normal map detail so you'll find this placeholder instead.
+
+
+
+## Final things to note about the textures group
+
+First thing:
+Textures are loaded into each image node by their suffix. For example, the material name for the jacket is cf_m_top_jacket06 8480. The KKBP importer searches the export folder for an image called cf_m_top_jacket06 8480_CM.png and if it finds the image it loads it into the _CM.png node. Each one of these nodes has an "_ABC.png" name tag on them to ensure each texture is loaded into the correct image node
+
+
+
+Second thing:
+Main textures exported by the KKBP exporter are not saturated. These files end in "_MT_CT.png" and "_MT.png". In the game, the textures are saturated in realtime to make them look like they normally do. In blender, the KKBP importer saturates these images once, then places them into a new "saturated_files" folder. When the KKBP Importer loads the main textures, it loads the saturated _ST versions of the main textures instead of the unsaturated _MT ones. It does the same thing with dark main textures too. It will grab the _ST version of the main texture, create a dark _DT version of it, save it in a new "dark_files" folder, then load it into the dark main texture slot.
+
+
+
+## Permalight image
+
+let's also take note of this permalight mask texture. This is
+an optional texture. You can create one yourself to make portions of
+clothing, hair or body materials, permanently light or permanently
+dark. An example of this will be shown later on
+
+
+
+This texture along with the normal textures are fed into the toon
+shading group. Let's go into this group.
+
+
+
+## Toon shading group
+
+this is the group that creates the light and dark portions of every
+material. Let's work from left to right
+
+
+
+## Normal map blending and optimizing performance
+
+this section is for determining what normal data will be fed into the
+BSDF shader. By default, normals are enabled and are fed into the "normal
+detail map blending" node group. This group accepts the
+grayscale + alpha normal map from the game and the detail normal map (if it exists). There are two built-in methods for blending the two normal maps together. The default is the whiteout method. if you
+want to use the unity tech demo blending method, you can set the slider
+to zero.
+
+
+
+Digging into the node group we can see both methods.
+
+
+
+both methods were taken from
+, so these
+are just blender node implementations of the math equations found on that page
+
+
+
+Returning to the normal section, The output of that group is fed into a
+mix node. if normals are enabled, the output is fed into the Normal
+Map optimization group. If normals are disabled, a pure purple color
+will be fed to the Normal map optimization group. here's what the
+output of the mix node looks like when normals are enabled
+
+
+
+Blender has a very poor performance with the normal map node when it's
+set to tangent space. The optimization group is used to improve
+animation playback. When you're done animating and ready to use the
+full quality normals for rendering, you can unmute the normal map node by selecting it
+and pressing M, Then you can connect the output of the normal map node
+to the inputs of the diffuse BSDF node. The image below shows the normal map unmuted and attached to the BSDF normal input
+
+The normal map optimization group was taken from
+
+
+
+
+## Toon shading with the BSDF node
+
+the normals are fed into the diffuse BSDF node. This is the output of
+the BSDF node. It looks like realistic shading
+
+
+
+the black-and-white portions of the output are then crunched down using
+a color ramp. Here's what the output of the color ramp looks like. It
+looks more toon-like now. The white portions of this output will ultimately get the light versions of all colors. The black portions of this output will ultimately get the dark version of all colors.
+
+
+
+## Permamask node
+
+The final section makes the material permanently light or permanently dark
+through the use of the permalight mask. Here's what it looks like with
+no perma mask loaded in
+
+
+
+here's what it looks like with a new mask loaded in. The red portions
+of the image are now permanently light, and the blue portions of the
+image are now permanently dark
+
+
+
+here's what the fully assembled material looks like with the permamask
+loaded in
+
+
+
+## KK General node group
+
+that was everything in the textures group so let's move on to the KK
+general node group
+
+
+
+this node group takes all of the textures from earlier and uses them to
+form the colors you see on the clothes. Let's again start from the left
+and work our way to the right.
+
+
+
+## Colored or plain main texture
+
+first, we choose between the colored main texture and the plain main
+texture. the output of the "use plain main texture?" output is currently
+zero so it outputs pure black on the entire piece of clothing
+
+
+
+When the slider is set to zero the factor of the mix node is set to
+zero. because of this the A input of the mix node is placed on the output of the node. In this case, the colored main texture is attached to the A input
+of the mix node, So that's what gets passed through
+
+
+
+when the "use plain main texture?" slider is set to one, the node will output
+pure white to the clothes.
+
+
+
+because the factor of the mix node is now set to one. The B input gets
+passed through the mix node. In this case, the B input is the plain main
+texture
+
+
+
+## HSV node
+
+once the main texture type is chosen It's sent through an HSV node.
+
+
+
+the HSV values in the material tab for the main texture directly
+correlate to the inputs of this node.
+
+
+
+## Colormask and patterns
+
+Once the HSV values are adjusted, The color mask and the patterns are
+applied. Not every piece of clothing uses every feature here so we'll
+take a look at a few different pieces of clothing in this section
+
+let's start with the node inbetween the inputs and the "add color mask
+and patterns" node group. When you disable the "use plain main texture?"
+slider, the first color mask from the textures group is placed at the output of the mix node.
+
+
+
+If you recall from earlier, opaque materials that use the shaders below are not
+supposed to have a color mask
+
+'Koikano/main_clothes_opaque', 'Shader
+Forge/main_opaque', 'xukmi/MainOpaquePlus',
+'xukmi/MainOpaquePlusTess', 'Shader Forge/main_opaque2', 'Shader
+Forge/main_opaque_low'
+
+this piece of clothing happens to be an opaque material so that's why
+the colormask in the first slot was pure black.
+
+If you enable the "use plain main texture" slider, then the color mask loaded
+into the second color mask slot will be passed through the mix node.
+
+
+
+If we take a look at the output of the "add color mask and patterns" node
+group, you can see that the colors look correct when the plain main
+texture slider is set to zero
+
+
+
+and they still look correct when it's set to one
+
+
+
+this is because of the dual color mask setup. If I force the second
+color mask to be enabled while the colored main texture is active you
+can see that the color is applied twice to the material and that's not
+something that you want to see
+
+
+
+so again that's why there's two color mask slots. Because if the
+clothing is using the opaque shader then you'll have a fully colored
+main texture and a black color mask in the first color mask slot,
+meaning no extra colors will be applied to the already colored main texture.
+
+ Then when you're using the plain main
+texture, you always want the colors applied to it because it's white,
+meaning you always want a color mask in the second slot.
+
+## Add colormask and patterns node group
+
+If it still doesn't make sense, let's take a look inside the "add color
+mask and patterns" node group.
+
+
+
+the group starts with the base color. The base color will always be pure
+white, But you can change it if you want with the color mask base color
+input in the material tab
+
+
+
+next the red color from the color mask is placed. (make sure the plain
+main texture slider is set to one or you won't see anything). if we get
+a preview of the color mask, you can see where the red color is going to
+go
+
+
+
+the red portion of the color mask is separated out with the "separate
+color" node
+
+
+
+and the red color is applied to the white masked out portion going into
+the factor input of the mix node
+
+
+
+the same thing happens with the green channel of the color mask
+
+
+
+the green channel is singled out
+
+
+
+and the green color is applied to that white area
+
+
+
+same with the blue channel
+
+
+
+and finally the plain main texture is multiplied by those colors. recall this is what the plain main texture looks like
+
+
+
+Once they are multipled, you get the resulting image
+
+
+
+if we take a step back and see what the B input of the multiply node
+looks like when the plain main texture slider is set to zero, we can see
+that it's pure white.
+
+
+
+Recall that this color mask is pure black so nothing shows up at the
+separate color mode output. A black section in the color mask basically means "do not put any color here, just keep it white".
+
+
+
+so when the slider is disabled and the color mask is black, the colored main texture is multiplied
+by pure white. Here's what the A input of the multiplying node looks
+like.
+
+
+
+and here's what the result looks like. (exactly the same)
+
+
+
+and if we take a step back one more time we can see how patterns work.
+When a pattern is loaded into a pattern slot in the textures group, The
+colors in the color mask (red) color input, and the pattern color (red)
+color input will be applied to the white and black portions of the
+pattern respectively, with this mix node
+
+
+
+the black-and-white pattern goes into the factor input of the mix
+node, So where the pattern is black the A input will be used which is
+the darker color, And where the pattern is white is where the B input
+will be used, which is the regular color mask color
+
+
+
+So to summarize the color mask behavior one more time...
+* Black = Do not add any color to the main texture, keep it the way it is
+* Red = Multiply the main texture by the red color mask color here
+* Green = Multiply the main texture by the green color mask color here
+* Blue = Multiply the main texture by the blue color mask color here
+* Any overlaps in the color mask = Blue overrides green, green overrides red
+
+## Green detail
+
+let's move onto group that applies the green channel of the detail mask
+
+
+
+## Generating detail color
+there's a few things that go on in this group. let's start with the
+frame on the left
+
+
+
+this section very intensely saturates the colors in order
+automatically get a detail color. This is what the clothing looks like at the output
+of the section
+
+the darkening code is taken from
+ so it's
+really just a node implementation of the equations found in that shader
+file
+
+
+
+the next section adds blue tint to the saturated color, but only if the
+color is detected as white, and never if the color is detected as dark.
+the light color detection is done by taking the saturation value of the
+material
+
+
+
+and sending it through a color ramp to make sure that only the least
+saturated colors get a blue tint applied to them. the white portions of
+the clothing will not get a blue tint. The black portions of the
+clothing will get a blue tint
+
+
+
+the dark color detection is done with the value of the color
+
+
+
+this value is then inverted and crunched down with another color ramp.
+The white portion of the clothing will not get a blue tint. The black
+portions of the clothing will get a blue tint
+
+
+
+these two values are then added together and sent into the multiply node
+factor input. Again, the white portions will not get the blue tint.
+Only the black portions will.
+
+
+
+This is what the detail color looks like, with the blue tint applied.
+Only the white-most portions of the jacket got a blue tint.
+
+
+
+## Overriding detail color
+
+you also have the option of overriding the detail color. If you override
+the detail color you can set it to whatever you want with the "detail
+color" color input. here's what it looks like without the override
+enabled
+
+
+
+here's what it looks like with the override enabled
+
+
+
+## Detail intensity
+and finally, the shader needs to know where to put this detail color, so
+the detail mask is brought in
+
+
+
+the green channel of the detail mask is used to locate where
+to put the detail color.
+
+
+
+The strength of the detail mask can also be increased with this multiply
+node. Here's what it looks like when it's set to the default of 0.8
+
+
+
+here's what it looks like when it's set to five
+
+
+
+and here's what it looks like after the mix node, with the detail
+color applied to the main texture (detail intensity still set to 5 for
+clarity)
+
+
+
+the same process is applied for the blue channel of the detail mask. In
+order to ensure only the blue channel of the detail mask is used, the
+"separate color" node is used to extract only the blue channel. This causes the blue channel to replace the RG and B channels of the detail mask so when the green channel is extracted later on, it's really just extracting the blue channel
+
+
+
+## Detail shine
+the final part of the KK general node group is the shine.
+
+
+
+Just like before the detail mask is loaded in.
+
+
+
+And the red channel is used along with the shine intensity to determine
+where the shine color will be applied..
+
+
+
+## Combine colors group
+
+the final portion of the KK general material Is the combine colors
+group. This group takes the light colors and the dark colors from the
+previous shader groups and combines them using the black-and-white
+mask from the toon shading group. This group also applies
+transparency to the model if its main texture is semi transparent, or
+if it has an alpha mask. Custom alpha masks can also be loaded in. You
+can check the body material for an example of a custom alpha mask.
+
+
+
+this is all there is inside of the combine colors group.
+
+
+
+To keep things simple I'm going to remove the custom alpha mask from
+the group, again you can check the body material if you want to see how
+it works yourself.
+
+
+
+so let's start with the alpha mask
+
+
+
+the red channel of the alpha mask is used to determine what parts of the
+clothes should be visible
+
+
+
+in some cases the alpha channel of the alpha mask is also used to
+determine what parts of the clothes should be visible. In this case, the
+alpha mask's alpha channel is pure white, so it does not affect the
+clothes alpha
+
+
+
+because the alpha channel is pure white, this is what it looks like when
+the red channel of the alpha mask and the alpha channel of the alpha
+mask are multiplied together.
+
+
+
+The alpha channel of the main texture can also determine the materials
+visibility. In this case, the main textures alpha channel is pure white
+so (again) it does not affect the materials visibility
+
+
+
+and finally if you want to force a material to be fully visible, then
+you can use the force visibility slider to add white to the result.
+Here's what it looks like when the slider is disabled
+
+
+
+and here's what it looks like when it's enabled
+
+
+
+if we look at the top, we can see the light colors are fed in
+through this node input
+
+
+
+the dark colors are fed in through this node input
+
+
+
+and the toon shading is loaded into this input
+
+
+
+The portions of the toon shading that are white will get the light colors and
+the portions that are dark will get the dark colors
+
+
+
+and finally the transparency mask from earlier is used to
+determine if the color is passed through or if a transparent shader is
+passed through to the material output
+
+
+
+## Outro
+
+and that was a complete walk-through of KKBP's KK general material node setup. If
+you check the hair, skin, eyes and any other KKBP material, you'll find
+that many of the node setups and simple masking strategies are reused
+everywhere.
+
+
diff --git a/wiki/material_breakdown_zh.md b/wiki/material_breakdown_zh.md
new file mode 100644
index 0000000..2a3fadd
--- /dev/null
+++ b/wiki/material_breakdown_zh.md
@@ -0,0 +1,144 @@
+# KKBP 材质分解
+
+本页将完整分解 KKBP 8.0 的"KK General"材质着色器的材质节点逻辑。KK General 材质中的简单节点逻辑在整个项目中使用,因此阅读和理解本页应该能让你学会如何修复或调试你遇到的任何 KKBP 材质。
+
+## 开始之前...
+
+在 KKBP 8.0 的开发过程中,随机选择了大量卡片进行测试。
+
+在 133 张卡片中...
+* 104 张(78%)开箱即用
+* 22 张(17%)有奇怪的材质问题,需要 5 分钟修复
+* 5 张(4%)导出但未导入
+* 1 张(0.5%)有奇怪的材质问题,可能需要一段时间才能弄清楚
+* 1 张(0.5%)未导出并导致游戏崩溃
+
+这意味着如果你遇到问题,**有 94.5% 的机会**阅读下面的"基本选项"部分将帮助你修复模型的外观。这也意味着**有 0.5% 的机会**你必须挖掘整个内容才能找出材质为什么看起来不对。
+
+在你浪费时间之前,**请确保你在导入过程中没有收到错误**,如首页所述!
+**如果你的模型导入时出现错误,本页将无法帮助你!**
+
+# 基础着色器分解
+## 简介
+
+此分解将首先使用默认 Koikatsu 模型 Chika 上的服装。让我们首先在 Blender 中打开着色选项卡。
+
+
+
+这是每个材质中使用的材质设置。导出文件夹中的纹理文件加载到左侧的绿色组中。颜色在中间的蓝色组中生成,然后馈送到红色组中的输出节点。
+
+每个材质都有浅色和深色。现在显示的是浅色。你可以使用右上角的视口着色按钮在浅色和深色之间切换。这是深色的样子。
+
+
+
+本文档的其余部分将仅使用浅色。你可以通过点击材质面板中的箭头来展开模型浅色的选项
+
+
+
+## 细节颜色
+
+让我们逐个了解每个选项。第一个选项是覆盖细节颜色。KKBP 会自动为你生成细节颜色,但如果你想覆盖它给你的颜色,可以启用此滑块并将细节颜色设置为你想要的任何颜色。这是设置为黄色时的样子
+
+
+
+这是设置为红色时的样子
+
+
+
+## 细节强度
+
+下一个选项是绿色细节强度。绿色细节强度通常控制衣服上的柔和细节。这是夹克在默认 0.8 强度下的样子
+
+
+
+这是夹克设置为 5 绿色强度时的样子
+
+
+
+这是细节设置为 10 绿色强度时的样子
+
+
+
+蓝色细节强度通常控制衣服上的硬线细节。这是夹克设置为默认 0.1 时的样子。
+
+
+
+这是设置为 1 时的样子
+
+
+
+这是设置为 5 时的样子
+
+
+
+## 光泽
+
+一些衣服还具有可调节的光泽度。默认的乐福鞋具有可调节的光泽。可以更改光泽的颜色。这是乐福鞋带有白色光泽的样子。
+
+
+
+这是它们带有绿色光泽的样子
+
+
+
+光泽的强度也可以调整。这是乐福鞋在默认光泽强度为 1 时的样子。
+
+
+
+这是乐福鞋光泽强度设置为零时的样子
+
+
+
+## 主纹理 HSV
+
+大多数衣服都有所谓的主纹理或简称 maintex。可以使用 maintex 滑块轻松调整主纹理的色调、饱和度和值。这是鞋子调整其 HSV 值后的样子
+
+
+
+## 颜色蒙版颜色
+
+如果一件衣服有主纹理,它也会有所谓的纯主纹理。主纹理和纯主纹理之间的区别将在稍后解释。现在,你只需要知道每件衣服上的每种颜色都可以更改,就像在游戏中一样。为此,必须启用纯主纹理。启用纯主纹理的滑块在下图中设置为 1。
+
+
+
+启用滑块后,你可以使用颜色蒙版颜色输入更改衣服的颜色。下面更改了鞋子的颜色蒙版颜色。
+
+
+
+你可以随时通过将颜色蒙版颜色重置为其原始值来返回原始颜色。或者通过再次禁用纯主纹理滑块(因此,将再次使用常规主纹理)。
+
+
+
+## 图案颜色
+
+一些衣服(如默认裙子)具有可调整的图案颜色。为了调整图案颜色,你需要使用纯主纹理。下图显示了为裙子启用的纯主纹理滑块。
+
+
+
+默认裙子只有一种可以调整的图案颜色。这是图案颜色设置为蓝色时的样子。
+
+
+
+如果这件衣服有更多图案,它们的颜色将使用图案颜色绿色和图案颜色蓝色颜色输入进行调整
+
+## 可见性滑块
+
+一些衣服有所谓的 alpha 蒙版。alpha 蒙版隐藏衣服的某些部分以防止它穿过其他衣服。默认内衬衬衫是具有 alpha 蒙版的衣服的示例
+
+
+
+此衣服的部分被隐藏以防止它穿过夹克。如果你想让衣服可见,即使它不应该可见,你可以启用强制可见性滑块。这是启用滑块后衬衫的样子。
+
+
+
+这是衬衫完全可见时衬衫和夹克的样子。请注意,某些部分在不应该穿过夹克时穿过了夹克。这就是为什么默认启用 alpha 蒙版的原因。
+
+
+
+
+
+# 深入着色器分解
+
+这就是所有基本服装选项的结论。
+
+如果你是 0.5% 的一部分,并且这些都没有帮助修复你遇到问题的材质,你可以查看[深入着色器分解页面](material_breakdown_advanced)以获取有关 KKBP"KK General"着色器的更多信息。
diff --git a/wiki/material_zh.md b/wiki/material_zh.md
new file mode 100644
index 0000000..eda7da2
--- /dev/null
+++ b/wiki/material_zh.md
@@ -0,0 +1,107 @@
+## 材质
+
+## 开始之前...
+**如果你在材质方面遇到问题或想了解典型的 KKBP 材质如何工作,[材质分解](material_breakdown)上有很多信息。**
+
+## 更新已完成的材质
+如果你想返回原始材质再次编辑它,可以将材质设置回材质的 -ORG 版本。
+
+
+
+这里我已经更新了原始着色器上的头发颜色
+
+
+
+要再次完成材质,只需点击 KKBP 面板上的"完成材质"按钮。材质将被重新完成,轻量级着色器 + 图集模型将被更新。
+
+
+
+## 特殊材质(轮廓)
+轮廓具有不同的材质设置。如果此材质有可用的 alpha 蒙版,则 alpha 蒙版的红色通道将用于确定轮廓应该在哪里透明。如果不可用,则将使用主纹理 alpha 通道来确定透明度。如果两者都不可用,则轮廓将在材质的任何地方可见。当你点击完成材质按钮时,轮廓材质不会转换为 png 文件。
+
+
+
+## 特殊材质(眼睛)
+眼睛纹理由 KKBP 导出器自动创建。如果你想使用自定义眼睛滑块并动态更改眼睛的颜色,你必须使用 SB3Utility 从游戏中提取某些眼睛纹理,然后将它们加载到绿色纹理组中并从头开始重新创建眼睛。[这是一个这样做的示例](https://www.youtube.com/watch?v=XFt12n7ByBI&t=231)
+
+## 特殊材质(身体)
+身体有一个透明蒙版以防止它穿过某些衣服[点击这里使身体可见](https://flailingfog.github.io/faq)。
+
+如果你发现身体在错误的区域仍然透明,你可以编辑身体 alpha 蒙版(位置如下所示),或从 pmx 导出文件夹加载不同的 body_AM 文件。第一套服装的第一个身体 alpha 蒙版会自动加载(cf_m_body_AM.png),因此如果你切换到不同的服装,你必须将身体蒙版切换到正确的蒙版(例如,服装 01 使用 body_AM_01,服装 02 使用 02,等等)。
+
+
+
+## 特殊材质(头发)
+头发材质未链接。如果你修改一个头发材质,然后点击更新头发材质按钮,此对象上的其余头发材质将使用当前头发材质的设置进行更新。
+
+
+
+## 特殊材质(泪水)
+泪水有一个特殊的材质,使用渐变来获得类似游戏内的外观。你可以通过使用泪水材质中的滑块使泪水使用 HDRI 或纯色。
+
+
+
+可以在形态键面板中激活泪水
+
+
+
+
+## 特殊材质(口塞眼睛)
+有三组口塞眼睛:Gag00、Gag01 和 Gag02
+Gag00 图像以镜像方式显示(两只眼睛相互镜像)
+Gag01 图像按原样显示
+Gag02 图像通过随时间连续更改 UV 贴图的位置来动画化(也被卡通眨眼表情使用)
+
+口塞眼睛使用驱动器来确定要显示的表情。当你在 Body 对象上激活口塞形态键时,某些眼睛材质会移回头部以隐藏它们,口塞眼睛网格会出现在头部前面,并根据激活的口塞形态键显示图像。
+
+默认导入带有使口塞眼睛后面的面部区域看起来不好的设置。禁用面部眼影强度,将面部边缘设置为"无",并使用下面"永久明亮/黑暗设置"部分中的功能可以帮助解决这个问题。
+
+
+
+可以在形态键面板中激活口塞眼睛
+
+
+
+## Koikatsu 颜色转换
+你在 Koikatsu 中看到的颜色不是正在使用的真实颜色;它们实际上正在被饱和。为了复制 Koikatsu 中显示的颜色,所有颜色和图像都通过 [converttextures.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/converttextures.py) 中的 color_to_KK 和 image_to_KK 函数运行,以将基础颜色转换为游戏中视觉上看到的"真实"颜色。此过程也在所有主纹理图像上运行。
+
+这是颜色饱和前后的材质示例
+
+
+
+## Koikatsu 深色颜色转换
+游戏使用第二个颜色转换过程来自动获取每个浅色的深色。[modifymaterial.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/modifymaterial.py) 中的 skin_dark_color 和 clothes_dark_color 函数用于在 Blender 中复制此过程,并自动获取每件衣服的深色并创建深色主纹理图像。如果在面板中禁用此功能,你将只获得浅色。
+
+## Cycles 支持
+通过在面板中选择"使用 Cycles"可以获得基本的 Cycles 支持。这将用与 Cycles 兼容的类似卡通的着色器替换 KK 着色器中的 Rim 节点组。在此模式下禁用轮廓。
+
+
+
+
+
+## 法线混合方法
+在 Toon Shading 组中可以使用不同的法线混合方法。进入它并找到节点以使用 Unity 技术演示混合方法。这些混合方法[取自此页面](https://blog.selfshadow.com/publications/blending-in-detail/)和[此页面](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/166)
+
+
+
+
+## 法线质量设置
+默认情况下,法线设置为较低质量以提高动画播放性能。要更改为更高质量的法线,只需进入 Toon Shading 组,连接更高质量的输出节点并通过选择它并按 M 键取消静音节点。
+如果你计划从 Blender 导出模型,则在烘焙材质时会自动完成此操作,因此你无需担心。
+
+
+
+
+## 卡通着色设置
+卡通着色由 Toon Shading 组中的颜色渐变创建。你可以编辑此部分中的颜色渐变以增加或减少显示材质的浅色或深色版本所需的光量。
+
+
+
+
+## 永久明亮/黑暗设置
+可以使用纹理节点组中的 Permalight 图像使每个材质的部分永久明亮或黑暗。图像的红色通道用于永久明亮,蓝色通道用于永久黑暗
+
+
+
+## 编辑 KKBP 着色器
+如果你想为所有导入的模型使用自定义材质或节点设置,可以编辑 KKBP 库文件。请记住,如果缺少某些内容,导入脚本将出错,因此不要删除文件中已有的任何内容。库文件通常位于此处:```C:\Users\[your username]\AppData\Roaming\Blender Foundation\Blender\[your blender version]\scripts\addons\KK-Blender-Porter-Pack\KK Shader.blend```
diff --git a/wiki/mesh.md b/wiki/mesh.md
new file mode 100644
index 0000000..5c01de7
--- /dev/null
+++ b/wiki/mesh.md
@@ -0,0 +1,27 @@
+## Mesh
+
+## Face shapekeys
+Shapekeys from koikatsu are preserved when importing. Only the Body object will have shapekeys.
+The Face shapekeys originally come from the game as partial shapekeys; For example, if you wanted to activate the "Angry" mouth shapekey you would need to activate the angry lips shapekey, the angry tongue shapekey, the angry teeth shapekey and the angry nose shapekey to get the full expression. KKBP combines these shapekeys into a single shapekey called "KK Mouth Angry" for convienience. The same is done for the eye shapekeys. No modifications are made to the Eyebrow shapekeys. Once all shapekeys are combined, the partial shapekeys are deleted to clean up the shapekey list. If you want to preserve the partial shapekeys, or stop the plugin from editing the shapekeys at all you can do that by changing the Shapekey settings in the KKBP panel.
+
+Shapekeys are generated using the shapekey names of the base Type 1 head. Compatibility for different headmods can vary because different headmods can have different names for the shapekeys (such as the Yelan headmod). These differently named headmod shapekeys can be added to the translate_shapekeys list in [modifymesh.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/modifymesh.py) to allow them to work. If a headmod has not been added to the list, no KK shapekeys will be created and the original headmod shapekeys will be preserved.
+
+All shapekeys can be found by selecting the Body object and going to the Data tab:
+
+
+
+## Tear and gag eye shapekeys
+The gag eye and tear meshes are separated from the Body. Their shapekeys are automatically linked to the Body's shapekeys through drivers. This way you don't have to find the small tears object to change the shapekey, you can just click on the body and change the tear shapekey that way. The tear and gag eye meshes shrink into the head when not active. If your character has a really small head, you may have to [edit the shapekeys as shown in this issue](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/338).
+
+Tears and gag eyes can be activated in the shapekey panel
+
+
+
+## Body seams
+The Body has seams in several places (middle of neck, middle of chest, around mouth). These are visible in some situations, so a "merge by distance" operation can performed on the body to remove the seams. This unfortunately [modifies some bone weights as seen in this issue](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/69). The "merge by distance" operation will only occur if you enable the "Fix body seams" option on the Import panel.
+Clothes also have seams, but these seams are not automatically merged when this option is enabled.
+
+## Object organization
+This is the default object organization you'll get after importing. Some objects like the variations or extra outfits will be missing if you didn't enable them in the Koikatsu KKBP Exporter panel.
+
+
\ No newline at end of file
diff --git a/wiki/mesh_zh.md b/wiki/mesh_zh.md
new file mode 100644
index 0000000..34ed369
--- /dev/null
+++ b/wiki/mesh_zh.md
@@ -0,0 +1,27 @@
+## 网格
+
+## 面部形态键
+导入时会保留来自 Koikatsu 的形态键。只有 Body 对象会有形态键。
+面部形态键最初来自游戏时是部分形态键;例如,如果你想激活"愤怒"嘴部形态键,你需要激活愤怒嘴唇形态键、愤怒舌头形态键、愤怒牙齿形态键和愤怒鼻子形态键才能获得完整的表情。为了方便起见,KKBP 将这些形态键组合成一个名为"KK Mouth Angry"的单一形态键。眼睛形态键也是如此。眉毛形态键不做任何修改。组合所有形态键后,部分形态键将被删除以清理形态键列表。如果你想保留部分形态键,或阻止插件完全编辑形态键,可以通过更改 KKBP 面板中的形态键设置来实现。
+
+形态键是使用基础 Type 1 头部的形态键名称生成的。不同头部模组的兼容性可能会有所不同,因为不同的头部模组可能具有不同的形态键名称(例如 Yelan 头部模组)。这些不同命名的头部模组形态键可以添加到 [modifymesh.py](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/importing/modifymesh.py) 中的 translate_shapekeys 列表中以使它们工作。如果头部模组尚未添加到列表中,则不会创建 KK 形态键,并且将保留原始头部模组形态键。
+
+可以通过选择 Body 对象并转到数据选项卡来找到所有形态键:
+
+
+
+## 泪水和口塞眼睛形态键
+口塞眼睛和泪水网格与 Body 分离。它们的形态键通过驱动器自动链接到 Body 的形态键。这样你就不必找到小的泪水对象来更改形态键,你只需点击身体并以这种方式更改泪水形态键。泪水和口塞眼睛网格在不活动时会缩入头部。如果你的角色头部非常小,你可能需要[按照此问题中所示编辑形态键](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/338)。
+
+可以在形态键面板中激活泪水和口塞眼睛
+
+
+
+## 身体接缝
+Body 在几个地方有接缝(颈部中间、胸部中间、嘴巴周围)。这些在某些情况下是可见的,因此可以对身体执行"按距离合并"操作以去除接缝。不幸的是,这[会修改一些骨骼权重,如此问题中所示](https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/69)。只有在导入面板上启用"修复身体接缝"选项时,才会执行"按距离合并"操作。
+衣服也有接缝,但启用此选项时不会自动合并这些接缝。
+
+## 对象组织
+这是导入后你将获得的默认对象组织。如果你没有在 Koikatsu KKBP 导出器面板中启用它们,则某些对象(如变体或额外服装)将丢失。
+
+
diff --git a/wiki/misc.md b/wiki/misc.md
new file mode 100644
index 0000000..e365eaf
--- /dev/null
+++ b/wiki/misc.md
@@ -0,0 +1,51 @@
+## Misc
+
+## Getting info from the Koikatsu console
+The Bepinex debug console can be enabled by opening InitSetting.exe in your Koikatsu install folder and checking the Console box
+
+
+
+After you click the KKBP export button, the exporter will start logging it's progress in the console window. These will begin with ```[Info : Console]```
+
+
+
+## Getting info from the Blender console
+On Windows, the Blender console is automatically opened when you import a model or bake a material. If an error occurs, the console will stay open so the user can read the error message. The console will automatically close when a model is successfully imported without errors. This log is also saved to Blender's Scripting tab on the top.
+
+
+
+## Importing multiple characters
+The KKBP importer can't import more than one character into the same file. You have to import characters separately, then merge them into one file by using File > Append, then append the character collection from the second character's .blend file.
+
+## Finalizing materials
+Using the "Finalize materials" button in the Export panel will convert all KKBP materials into PNG files. This is done by applying a geometry nodes modifier that flattens each mesh into a flat plane, then a picture is taken of the flat plane. The entire mesh is folded, so some very small gaps are left if there are transparent parts of the mesh. Because of this, a second filler plane is placed right under the folded mesh to fill in those gaps.
+
+Because the mesh is folded, Z-fighting sometimes occurs and leads to a corrupt-looking image. This can be avoided by enabling the "Use old baker" checkbox in the KKBP panel. Using this option wil remove the folded mesh and only use the filler plane to bake images. The filler plane does not contain extra UV maps, so materials that rely on multiple UV maps like Hair will not bake properly with the old baker (for example the hair shine will not show up). Most other materials will bake properly with the old baker.
+
+Only visible objects will be finalized, so if you have alternate outfit pieces or other outfits you want to bake, make sure they're visible in the Outliner before clicking the Finalize button.
+
+
+
+## Working with Freestyle outlines
+Mark any faces you don't want to be freestyled as freestyle faces, enable Freestyle in Blender, and disable the outline modifier on the body object. Try using these freestyle settings if you don't want to experiment
+
+
+
+Then try these compositor settings if you don't want to experiment
+
+
+
+
+## Armatures in Unity
+The "Very Simple (SLOW)", "Simple" and "Unity - VRM / VRChat Compatible" options in the Export panel will make changes to the armature that allow Unity to automatically identify the bones needed for the Humanoid armature.
+
+
+
+## Springbones in Unity
+After the Koikatsu model is imported into Unity, springbones from the [UniVRM Unity package](https://github.com/vrm-c/UniVRM/releases) can be added to get realtime hair, skirt and accessory movement.
+
+In the below example, the right twintail is given springbones. The springbone script is added to the twintail bone at the end of the chain. The twintail chain has four bones in the chain: Joint2_002 > Joint3 > Joint4 > Joint5, so the springbone script is added to Joint5. The root bones along the chain are then added to the "Root bone elements" section on the right. There are three remaining bones, so the size is set to 3.
+
+
+
+[return to wiki home](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/wiki/Wiki%20top.md)
\ No newline at end of file
diff --git a/wiki/misc_zh.md b/wiki/misc_zh.md
new file mode 100644
index 0000000..0779bc9
--- /dev/null
+++ b/wiki/misc_zh.md
@@ -0,0 +1,51 @@
+## 杂项
+
+## 从 Koikatsu 控制台获取信息
+可以通过打开 Koikatsu 安装文件夹中的 InitSetting.exe 并勾选 Console 框来启用 Bepinex 调试控制台
+
+
+
+点击 KKBP 导出按钮后,导出器将开始在控制台窗口中记录其进度。这些将以 ```[Info : Console]``` 开头
+
+
+
+## 从 Blender 控制台获取信息
+在 Windows 上,当你导入模型或烘焙材质时,Blender 控制台会自动打开。如果发生错误,控制台将保持打开状态,以便用户可以阅读错误消息。当模型成功导入且没有错误时,控制台将自动关闭。此日志也保存到 Blender 顶部的脚本选项卡中。
+
+
+
+## 导入多个角色
+KKBP 导入器无法将多个角色导入到同一文件中。你必须分别导入角色,然后使用"文件">"追加"将它们合并到一个文件中,然后从第二个角色的 .blend 文件中追加角色集合。
+
+## 完成材质
+使用导出面板中的"完成材质"按钮将把所有 KKBP 材质转换为 PNG 文件。这是通过应用几何节点修改器来完成的,该修改器将每个网格展平为平面,然后拍摄平面的照片。整个网格被折叠,因此如果网格的透明部分存在,则会留下一些非常小的间隙。因此,在折叠网格下方放置第二个填充平面以填充这些间隙。
+
+由于网格被折叠,有时会发生 Z-fighting 并导致看起来损坏的图像。可以通过在 KKBP 面板中启用"使用旧烘焙器"复选框来避免这种情况。使用此选项将删除折叠网格,仅使用填充平面来烘焙图像。填充平面不包含额外的 UV 贴图,因此依赖多个 UV 贴图的材质(如头发)将无法使用旧烘焙器正确烘焙(例如头发光泽不会显示)。大多数其他材质将使用旧烘焙器正确烘焙。
+
+只有可见对象才会被完成,因此如果你有要烘焙的备用服装件或其他服装,请确保在点击完成按钮之前它们在大纲视图中可见。
+
+
+
+## 使用 Freestyle 轮廓
+将你不想使用 Freestyle 的任何面标记为 Freestyle 面,在 Blender 中启用 Freestyle,并禁用身体对象上的轮廓修改器。如果你不想实验,请尝试使用这些 Freestyle 设置
+
+
+
+如果你不想实验,请尝试这些合成器设置
+
+
+
+
+## Unity 中的骨架
+导出面板中的"非常简单(慢)"、"简单"和"Unity - VRM / VRChat 兼容"选项将对骨架进行更改,使 Unity 能够自动识别 Humanoid 骨架所需的骨骼。
+
+
+
+## Unity 中的弹簧骨骼
+将 Koikatsu 模型导入 Unity 后,可以从 [UniVRM Unity 包](https://github.com/vrm-c/UniVRM/releases)添加弹簧骨骼以获得实时头发、裙子和配饰运动。
+
+在下面的示例中,右侧双马尾被赋予弹簧骨骼。弹簧骨骼脚本被添加到链末端的双马尾骨骼。双马尾链在链中有四个骨骼:Joint2_002 > Joint3 > Joint4 > Joint5,因此弹簧骨骼脚本被添加到 Joint5。然后将链上的根骨骼添加到右侧的"根骨骼元素"部分。还有三个剩余的骨骼,因此大小设置为 3。
+
+
+
+[返回 wiki 主页](https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/master/wiki/Wiki%20top.md)
diff --git a/wiki/standard_manual_test_cases.md b/wiki/standard_manual_test_cases.md
new file mode 100644
index 0000000..daaaf53
--- /dev/null
+++ b/wiki/standard_manual_test_cases.md
@@ -0,0 +1,34 @@
+## Test cases
+
+1. In Koikatsu, export Chika with a single outfit (default exporter settings)
+ * Also export with only "Export All Outfits" enabled
+ * Also export with only "Export Variations" and "Export Hit Meshes" checked
+ * Also export with a pose selected, an emotion pattern selected and only "Freeze Current Pose" and "Freeze Shapekeys" checked
+1. In Blender, import the "Export Variations" and "Export Hit Meshes" model
+ * Check variations + hitboxes were separated
+ * Check eye shapekeys work
+ * Check mouth shapekeys work
+ * Check Gag eye shapekeys work
+1. In another file, import the "Freeze Current Pose" and "Freeze Shapekeys" model
+ * Check armature type was changed to Koikatsu
+ * Check pose and shapekeys were applied to base mesh
+1. In another file, import the 8 outfits export
+ * Check Outfits 0, 1, 2 exist, and have textures + converted colors
+ * Check Hair for Outfits 0, 1, 2 exist and have textures + converted colors
+1. In another file, import the default settings export
+ * Check Outfit 0 exists
+ * Check Hair for Outfit 0 exists
+ * Check hand IK, leg IK, hips bone and center bone work
+ * Check eye controller works
+ * Use the bake material templates button
+ * Use the "Prep for target application" button
+1. In another file, import the default settings export with the "Rigify Armature", "Skip modifying shapekeys", "Use Cycles" options. Check "Fix body seams" and enable the "SFW mode" checkbox
+ * Check Rigify hand IK, leg IK, hips bone and center bone work
+ * Check Rigify eye controller works
+ * Check shapekeys were not modified
+ * Switch to rendered view and check the model appears in the cycles viewport
+ * Go into edit mode and check the seam down the middle of the neck is gone
+ * Check the sfw alphamask was applied to the body material
+ * Check nsfw meshes were deleted
+1. In another file, import the default settings export with the "Separate every object" option selected
+ * Check all outfit objects are separated
diff --git a/wiki/standard_manual_test_cases_zh.md b/wiki/standard_manual_test_cases_zh.md
new file mode 100644
index 0000000..76548d4
--- /dev/null
+++ b/wiki/standard_manual_test_cases_zh.md
@@ -0,0 +1,34 @@
+## 测试用例
+
+1. 在 Koikatsu 中,使用单套服装导出 Chika(默认导出器设置)
+ * 还要仅启用"导出所有服装"进行导出
+ * 还要仅勾选"导出变体"和"导出碰撞网格"进行导出
+ * 还要选择一个姿势,选择一个情绪模式,并仅勾选"冻结当前姿势"和"冻结形态键"进行导出
+1. 在 Blender 中,导入"导出变体"和"导出碰撞网格"模型
+ * 检查变体 + 碰撞箱是否已分离
+ * 检查眼睛形态键是否工作
+ * 检查嘴部形态键是否工作
+ * 检查口塞眼睛形态键是否工作
+1. 在另一个文件中,导入"冻结当前姿势"和"冻结形态键"模型
+ * 检查骨架类型是否已更改为 Koikatsu
+ * 检查姿势和形态键是否已应用到基础网格
+1. 在另一个文件中,导入 8 套服装导出
+ * 检查服装 0、1、2 是否存在,并具有纹理 + 转换的颜色
+ * 检查服装 0、1、2 的头发是否存在并具有纹理 + 转换的颜色
+1. 在另一个文件中,导入默认设置导出
+ * 检查服装 0 是否存在
+ * 检查服装 0 的头发是否存在
+ * 检查手部 IK、腿部 IK、臀部骨骼和中心骨骼是否工作
+ * 检查眼睛控制器是否工作
+ * 使用烘焙材质模板按钮
+ * 使用"准备目标应用程序"按钮
+1. 在另一个文件中,使用"Rigify 骨架"、"跳过修改形态键"、"使用 Cycles"选项导入默认设置导出。勾选"修复身体接缝"并启用"SFW 模式"复选框
+ * 检查 Rigify 手部 IK、腿部 IK、臀部骨骼和中心骨骼是否工作
+ * 检查 Rigify 眼睛控制器是否工作
+ * 检查形态键是否未被修改
+ * 切换到渲染视图并检查模型是否出现在 Cycles 视口中
+ * 进入编辑模式并检查颈部中间的接缝是否消失
+ * 检查 SFW alpha 蒙版是否已应用于身体材质
+ * 检查 NSFW 网格是否已删除
+1. 在另一个文件中,使用选择的"分离每个对象"选项导入默认设置导出
+ * 检查所有服装对象是否已分离