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
This commit is contained in:
123
BONE_ORIENTATION_UPDATE.md
Normal file
123
BONE_ORIENTATION_UPDATE.md
Normal file
@@ -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` - 已修改的文件
|
||||
461
Changelog.md
Normal file
461
Changelog.md
Normal file
@@ -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
|
||||
BIN
KK Shader V8.0.blend
Normal file
BIN
KK Shader V8.0.blend
Normal file
Binary file not shown.
428
KKPanel.py
Normal file
428
KKPanel.py
Normal file
@@ -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()
|
||||
26
LICENSE.md
Normal file
26
LICENSE.md
Normal file
@@ -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.
|
||||
41
README.md
Normal file
41
README.md
Normal file
@@ -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)
|
||||
|
||||
122
__init__.py
Normal file
122
__init__.py
Normal file
@@ -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()
|
||||
BIN
__pycache__/KKPanel.cpython-311.pyc
Normal file
BIN
__pycache__/KKPanel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
BIN
__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/common.cpython-311.pyc
Normal file
BIN
__pycache__/common.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/preferences.cpython-311.pyc
Normal file
BIN
__pycache__/preferences.cpython-311.pyc
Normal file
Binary file not shown.
202
better_fbx_bone_data.py
Normal file
202
better_fbx_bone_data.py
Normal file
@@ -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
|
||||
1128
better_fbx_complete_bone_data.py
Normal file
1128
better_fbx_complete_bone_data.py
Normal file
File diff suppressed because it is too large
Load Diff
18
blender_manifest.toml
Normal file
18
blender_manifest.toml
Normal file
@@ -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"
|
||||
436
common.py
Normal file
436
common.py
Normal file
@@ -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()
|
||||
BIN
exporting/__pycache__/bakematerials.cpython-311.pyc
Normal file
BIN
exporting/__pycache__/bakematerials.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/__pycache__/exportprep.cpython-311.pyc
Normal file
BIN
exporting/__pycache__/exportprep.cpython-311.pyc
Normal file
Binary file not shown.
657
exporting/bakematerials.py
Normal file
657
exporting/bakematerials.py
Normal file
@@ -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"}
|
||||
|
||||
338
exporting/exportprep.py
Normal file
338
exporting/exportprep.py
Normal file
@@ -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"}
|
||||
|
||||
21
exporting/material_combiner/LICENSE
Normal file
21
exporting/material_combiner/LICENSE
Normal file
@@ -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.
|
||||
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/combiner.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/combiner.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/globs.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/globs.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/images.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/images.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/objects.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/objects.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/packer.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/packer.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporting/material_combiner/__pycache__/textures.cpython-311.pyc
Normal file
BIN
exporting/material_combiner/__pycache__/textures.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
126
exporting/material_combiner/combine_list.py
Normal file
126
exporting/material_combiner/combine_list.py
Normal file
@@ -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
|
||||
71
exporting/material_combiner/combiner.py
Normal file
71
exporting/material_combiner/combiner.py
Normal file
@@ -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'}
|
||||
|
||||
482
exporting/material_combiner/combiner_ops.py
Normal file
482
exporting/material_combiner/combiner_ops.py
Normal file
@@ -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)
|
||||
141
exporting/material_combiner/extend_types.py
Normal file
141
exporting/material_combiner/extend_types.py
Normal file
@@ -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
|
||||
30
exporting/material_combiner/get_pillow.py
Normal file
30
exporting/material_combiner/get_pillow.py
Normal file
@@ -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'])
|
||||
23
exporting/material_combiner/globs.py
Normal file
23
exporting/material_combiner/globs.py
Normal file
@@ -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
|
||||
19
exporting/material_combiner/images.py
Normal file
19
exporting/material_combiner/images.py
Normal file
@@ -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
|
||||
131
exporting/material_combiner/materials.py
Normal file
131
exporting/material_combiner/materials.py
Normal file
@@ -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
|
||||
38
exporting/material_combiner/objects.py
Normal file
38
exporting/material_combiner/objects.py
Normal file
@@ -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
|
||||
96
exporting/material_combiner/packer.py
Normal file
96
exporting/material_combiner/packer.py
Normal file
@@ -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
|
||||
13
exporting/material_combiner/textures.py
Normal file
13
exporting/material_combiner/textures.py
Normal file
@@ -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]}
|
||||
39
exporting/material_combiner/type_annotations.py
Normal file
39
exporting/material_combiner/type_annotations.py
Normal file
@@ -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]]
|
||||
BIN
extras/__pycache__/createanimationlibrary.cpython-311.pyc
Normal file
BIN
extras/__pycache__/createanimationlibrary.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/createmapassetlibrary.cpython-311.pyc
Normal file
BIN
extras/__pycache__/createmapassetlibrary.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/imageconvert.cpython-311.pyc
Normal file
BIN
extras/__pycache__/imageconvert.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/importanimation.cpython-311.pyc
Normal file
BIN
extras/__pycache__/importanimation.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/importstudio.cpython-311.pyc
Normal file
BIN
extras/__pycache__/importstudio.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/linkhair.cpython-311.pyc
Normal file
BIN
extras/__pycache__/linkhair.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/linkshapekeys.cpython-311.pyc
Normal file
BIN
extras/__pycache__/linkshapekeys.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/matcombsetup.cpython-311.pyc
Normal file
BIN
extras/__pycache__/matcombsetup.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/matcombswitch.cpython-311.pyc
Normal file
BIN
extras/__pycache__/matcombswitch.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/resetmaterials.cpython-311.pyc
Normal file
BIN
extras/__pycache__/resetmaterials.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/rigifywrapper.cpython-311.pyc
Normal file
BIN
extras/__pycache__/rigifywrapper.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/__pycache__/updatebones.cpython-311.pyc
Normal file
BIN
extras/__pycache__/updatebones.cpython-311.pyc
Normal file
Binary file not shown.
@@ -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": {}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
BIN
extras/catsscripts/__pycache__/armature_manual.cpython-311.pyc
Normal file
BIN
extras/catsscripts/__pycache__/armature_manual.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/catsscripts/__pycache__/common.cpython-311.pyc
Normal file
BIN
extras/catsscripts/__pycache__/common.cpython-311.pyc
Normal file
Binary file not shown.
105
extras/catsscripts/armature_manual.py
Normal file
105
extras/catsscripts/armature_manual.py
Normal file
@@ -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))
|
||||
2290
extras/catsscripts/common.py
Normal file
2290
extras/catsscripts/common.py
Normal file
File diff suppressed because it is too large
Load Diff
451
extras/createanimationlibrary.py
Normal file
451
extras/createanimationlibrary.py
Normal file
@@ -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'}
|
||||
355
extras/createmapassetlibrary.py
Normal file
355
extras/createmapassetlibrary.py
Normal file
@@ -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')))
|
||||
62
extras/imageconvert.py
Normal file
62
extras/imageconvert.py
Normal file
@@ -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'}
|
||||
315
extras/importanimation.py
Normal file
315
extras/importanimation.py
Normal file
@@ -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'}
|
||||
|
||||
413
extras/importstudio.py
Normal file
413
extras/importstudio.py
Normal file
@@ -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'}
|
||||
|
||||
26
extras/linkhair.py
Normal file
26
extras/linkhair.py
Normal file
@@ -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'}
|
||||
|
||||
99
extras/linkshapekeys.py
Normal file
99
extras/linkshapekeys.py
Normal file
@@ -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'}
|
||||
|
||||
114
extras/matcombsetup.py
Normal file
114
extras/matcombsetup.py
Normal file
@@ -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'}
|
||||
27
extras/matcombswitch.py
Normal file
27
extras/matcombswitch.py
Normal file
@@ -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'}
|
||||
18
extras/resetmaterials.py
Normal file
18
extras/resetmaterials.py
Normal file
@@ -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'}
|
||||
BIN
extras/rigifyscripts/__pycache__/commons.cpython-311.pyc
Normal file
BIN
extras/rigifyscripts/__pycache__/commons.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/rigifyscripts/__pycache__/rigify_after.cpython-311.pyc
Normal file
BIN
extras/rigifyscripts/__pycache__/rigify_after.cpython-311.pyc
Normal file
Binary file not shown.
BIN
extras/rigifyscripts/__pycache__/rigify_before.cpython-311.pyc
Normal file
BIN
extras/rigifyscripts/__pycache__/rigify_before.cpython-311.pyc
Normal file
Binary file not shown.
1474
extras/rigifyscripts/commons.py
Normal file
1474
extras/rigifyscripts/commons.py
Normal file
File diff suppressed because it is too large
Load Diff
312
extras/rigifyscripts/rigify_after.py
Normal file
312
extras/rigifyscripts/rigify_after.py
Normal file
@@ -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'}
|
||||
|
||||
2164
extras/rigifyscripts/rigify_before.py
Normal file
2164
extras/rigifyscripts/rigify_before.py
Normal file
File diff suppressed because it is too large
Load Diff
27
extras/rigifywrapper.py
Normal file
27
extras/rigifywrapper.py
Normal file
@@ -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"}
|
||||
|
||||
|
||||
35
extras/updatebones.py
Normal file
35
extras/updatebones.py
Normal file
@@ -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)
|
||||
BIN
importing/Lut_TimeDay.png
Normal file
BIN
importing/Lut_TimeDay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
importing/__pycache__/importbuttons.cpython-311.pyc
Normal file
BIN
importing/__pycache__/importbuttons.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/modifyarmature.cpython-311.pyc
Normal file
BIN
importing/__pycache__/modifyarmature.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/modifymaterial.cpython-311.pyc
Normal file
BIN
importing/__pycache__/modifymaterial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/modifymesh.cpython-311.pyc
Normal file
BIN
importing/__pycache__/modifymesh.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/postoperations.cpython-311.pyc
Normal file
BIN
importing/__pycache__/postoperations.cpython-311.pyc
Normal file
Binary file not shown.
165
importing/importbuttons.py
Normal file
165
importing/importbuttons.py
Normal file
@@ -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')
|
||||
2280
importing/modifyarmature.py
Normal file
2280
importing/modifyarmature.py
Normal file
File diff suppressed because it is too large
Load Diff
117
importing/modifymaterial.md
Normal file
117
importing/modifymaterial.md
Normal file
@@ -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);
|
||||
1579
importing/modifymaterial.py
Normal file
1579
importing/modifymaterial.py
Normal file
File diff suppressed because it is too large
Load Diff
138
importing/modifymesh.md
Normal file
138
importing/modifymesh.md
Normal file
@@ -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
|
||||
896
importing/modifymesh.py
Normal file
896
importing/modifymesh.py
Normal file
@@ -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')
|
||||
|
||||
|
||||
565
importing/postoperations.py
Normal file
565
importing/postoperations.py
Normal file
@@ -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
|
||||
BIN
interface/__pycache__/dictionary_en.cpython-311.pyc
Normal file
BIN
interface/__pycache__/dictionary_en.cpython-311.pyc
Normal file
Binary file not shown.
BIN
interface/__pycache__/dictionary_jp.cpython-311.pyc
Normal file
BIN
interface/__pycache__/dictionary_jp.cpython-311.pyc
Normal file
Binary file not shown.
BIN
interface/__pycache__/dictionary_zh.cpython-311.pyc
Normal file
BIN
interface/__pycache__/dictionary_zh.cpython-311.pyc
Normal file
Binary file not shown.
166
interface/dictionary_en.py
Normal file
166
interface/dictionary_en.py
Normal file
@@ -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
|
||||
|
||||
149
interface/dictionary_jp.py
Normal file
149
interface/dictionary_jp.py
Normal file
@@ -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',
|
||||
}
|
||||
|
||||
150
interface/dictionary_zh.py
Normal file
150
interface/dictionary_zh.py
Normal file
@@ -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的数据。',
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user