1
Fork 0
This repository has been archived on 2025-04-12. You can view files and clone it, but cannot push or open issues or pull requests.
reds-avatar-tools/__init__.py

354 lines
13 KiB
Python

bl_info = {
"name": "RAT",
"author": "redstrate",
"version": (0, 1),
"blender": (2, 92, 3),
"location": "3D View -> RAT",
"description": "Non-destructive extea blender tooling for my VRChat avatars.",
"doc_url": "https://git.redstrate.com/redstrate/reds-avatar-tools",
"category": "Import-Export"
}
import bpy
from bpy.types import Operator, AddonPreferences
from bpy.props import StringProperty, BoolProperty, PointerProperty
from bpy_extras.io_utils import (
ExportHelper,
)
import shutil
from mathutils import Matrix
class RATAddonPreferences(AddonPreferences):
bl_idname = __name__
pcAssetPath: StringProperty(
name="PC Asset Dir Path",
subtype='DIR_PATH'
)
questAssetPath: StringProperty(
name="Quest Asset Dir Path",
subtype='DIR_PATH'
)
def draw(self, context):
layout = self.layout
layout.prop(self, "pcAssetPath")
layout.prop(self, "questAssetPath")
# From https://blender.stackexchange.com/questions/127403/change-active-collection?noredirect=1&lq=1
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
# also from stack overflow, used for parenting to bones but will be obsolete soon
def parent_set(object, armature, bone):
I = Matrix()
# parent from tip to head
offset = 0 # 0 tip, 1 head
# tip matrix world
T = Matrix.Translation(
(1 - offset) * (bone.tail - bone.head)
)
tmw = armature.matrix_world @ T @ bone.matrix
amwi = armature.matrix_world.inverted()
cmw = object.matrix_world.copy()
cmb = object.matrix_basis.copy()
cml = tmw.inverted() @ cmw
#object.matrix_parent_inverse = cml @ object.matrix_basis.inverted()
object.parent = armature
object.parent_bone = bone.name
object.parent_type = 'BONE'
object.matrix_world = cmw
class AvatarPrepOp(bpy.types.Operator):
bl_idname = "scene.avatar_prep"
bl_label = "Prep for export (debug)"
bl_options = {"REGISTER", "UNDO"}
perform_quest_optimizations: bpy.props.BoolProperty()
def execute(self, context):
arm_ob = recurLayerCollection(bpy.context.view_layer.layer_collection, 'Master').collection.objects[context.scene.rat_armature]
# we are going to prep the hair for export first, and then add it into the collection
bpy.context.view_layer.active_layer_collection = recurLayerCollection(bpy.context.view_layer.layer_collection, 'original hair')
for ob in bpy.context.view_layer.active_layer_collection.collection.objects:
if ob.type == 'CURVE' and not ob.hide_render:
if self.perform_quest_optimizations:
ob.data.resolution_u = 1
bpy.context.view_layer.objects.active = ob
bpy.context.object.select_set( state = True, view_layer = None)
bpy.ops.object.convert(target='MESH', keep_original=False)
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
bpy.context.object.select_set( state = False, view_layer = None)
recurLayerCollection(bpy.context.view_layer.layer_collection, 'Master').collection.objects.link(ob)
parent_set(ob, arm_ob, arm_ob.pose.bones.get("Head"))
ob.modifiers.new(name = 'Skeleton', type = 'ARMATURE')
ob.modifiers['Skeleton'].object = arm_ob
vtx_group = ob.vertex_groups.new(name = "Head")
vtx_group.add(range(0,len(ob.data.vertices)), 1.0, 'REPLACE')
#for edge in ob.data.edges:
# edge.use_seam = True # isn't working
bpy.context.view_layer.active_layer_collection = recurLayerCollection(bpy.context.view_layer.layer_collection, 'Master')
# first we want to prep our current collection, but we want to duplicate it just in case something goes wrong.
for window in context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'OUTLINER':
override = {'window': window, 'screen': screen, 'area': area}
bpy.ops.outliner.collection_duplicate(override)
break
# just for now, assume the second collection (from root) is our duplicate collection
context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[1]
current_collection = context.view_layer.active_layer_collection.collection
# disable the original collections
recurLayerCollection(bpy.context.view_layer.layer_collection, 'original hair').exclude = True
recurLayerCollection(bpy.context.view_layer.layer_collection, 'Master').exclude = True
# context override
c = {}
obs = []
# loop through all objects, delete objects that are render disabled (for some reason, duping the collection re-enables view but not render)
# and apply modifiers except for armature on objects that are not disabled
for ob in current_collection.objects:
if ob.hide_render:
bpy.ops.object.delete({"selected_objects": [ob]})
else:
if ob.type != 'ARMATURE':
bpy.context.view_layer.objects.active = ob
# delete the "to remove" vertex group if it exists
if self.perform_quest_optimizations and ob.vertex_groups.find('to remove') != -1:
bpy.ops.object.editmode_toggle()
ob.vertex_groups.active = ob.vertex_groups['to remove']
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_mode(type = 'FACE')
bpy.ops.object.vertex_group_select()
bpy.ops.mesh.delete(type='FACE')
bpy.ops.object.editmode_toggle()
for modifier in ob.modifiers:
if modifier.type == 'SUBSURF' and self.perform_quest_optimizations:
modifier.levels = 1
modifier.render_levels = 1
if modifier.type == 'SOLIDIFY' and self.perform_quest_optimizations:
bpy.ops.object.modifier_remove(modifier=modifier.name)
else:
bpy.ops.object.modifier_apply(modifier=modifier.name)
c["object"] = c["active_object"] = ob
obs.append(ob)
c["selected_objects"] = c["selected_editable_objects"] = obs
# join all objects that are left over
bpy.ops.object.join(c)
new_obj = current_collection.objects[1]
# delete all pre-existing uv maps
#new_obj.data.uv_layers.remove(new_obj.data.uv_layers.get("UVMap"))
# generate new smart uv map
bpy.context.view_layer.objects.active = new_obj
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_all(action='SELECT') # for all faces
bpy.ops.uv.smart_project(angle_limit=66.0, island_margin=0.000, area_weight=0.0)
bpy.ops.object.editmode_toggle()
# now select the new armature
bpy.context.view_layer.objects.active = current_collection.objects[0]
return {'FINISHED'}
class AvatarBakeOp(bpy.types.Operator):
bl_idname = "scene.avatar_bake"
bl_label = "Export to PC Unity Project"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.scene.avatar_prep(perform_quest_optimizations = False)
# bake options for PC
context.scene.bake_resolution = 2048
context.scene.bake_use_decimation = True
context.scene.bake_max_tris = 32000 - 55
context.scene.bake_preserve_seams = True
context.scene.bake_remove_doubles = False
context.scene.bake_generate_uvmap = False
context.scene.bake_prioritize_face = True
context.scene.bake_face_scale = 2.0
context.scene.bake_uv_overlap_correction = 'REPROJECT'
context.scene.bake_quick_compare = False
context.scene.bake_pass_diffuse = True
context.scene.bake_diffuse_vertex_colors = False
context.scene.bake_diffuse_alpha_pack = 'SMOOTHNESS'
context.scene.bake_pass_normal = True
context.scene.bake_normal_apply_trans = False
context.scene.bake_pass_ao = True
context.scene.bake_pass_questdiffuse = False
bpy.ops.cats_bake.bake()
for obj in bpy.data.collections['CATS Bake'].all_objects:
obj.select_set(True)
preferences = context.preferences
addon_prefs = preferences.addons[__name__].preferences
# now export
ops = {}
ops['filepath'] = addon_prefs.pcAssetPath + "/sakura.fbx"
ops['use_selection'] = True
ops['path_mode'] = 'STRIP'
ops['add_leaf_bones'] = False
bpy.ops.export_scene.fbx(**ops)
# copy baked images
shutil.copyfile(bpy.path.abspath("//") + '/CATS Bake/SCRIPT_diffuse.png', addon_prefs.pcAssetPath + '/SCRIPT_diffuse.png')
shutil.copyfile(bpy.path.abspath("//") + '/CATS Bake/SCRIPT_ao.png', addon_prefs.pcAssetPath + '/SCRIPT_ao.png')
shutil.copyfile(bpy.path.abspath("//") + '/CATS Bake/SCRIPT_normal.png', addon_prefs.pcAssetPath + '/SCRIPT_normal.png')
return {'FINISHED'}
class AvatarBakeQuestOp(bpy.types.Operator):
bl_idname = "scene.avatar_bake_quest"
bl_label = "Export to Quest Unity Project"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
bpy.ops.scene.avatar_prep(perform_quest_optimizations = True)
# bake!
context.scene.bake_resolution = 1024
context.scene.bake_use_decimation = True
context.scene.bake_max_tris = 7500 - 100
context.scene.bake_preserve_seams = True
context.scene.bake_remove_doubles = True
context.scene.bake_generate_uvmap = False
context.scene.bake_prioritize_face = True
context.scene.bake_face_scale = 5.0
context.scene.bake_uv_overlap_correction = 'NONE'
context.scene.bake_quick_compare = False
context.scene.bake_pass_diffuse = True
context.scene.bake_diffuse_vertex_colors = False
context.scene.bake_diffuse_alpha_pack = 'SMOOTHNESS'
context.scene.bake_pass_normal = True
context.scene.bake_normal_apply_trans = False
context.scene.bake_pass_ao = True
context.scene.bake_pass_questdiffuse = True
bpy.ops.cats_bake.bake()
for obj in bpy.data.collections['CATS Bake'].all_objects:
obj.select_set(True)
preferences = context.preferences
addon_prefs = preferences.addons[__name__].preferences
# now export
ops = {}
ops['filepath'] = addon_prefs.questAssetPath + "/sakura.fbx"
ops['use_selection'] = True
ops['path_mode'] = 'STRIP'
ops['add_leaf_bones'] = False
bpy.ops.export_scene.fbx(**ops)
# copy baked images
shutil.copyfile(bpy.path.abspath("//") + '\CATS Bake\SCRIPT_diffuse.png', addon_prefs.questAssetPath + '\SCRIPT_diffuse.png')
shutil.copyfile(bpy.path.abspath("//") + '\CATS Bake\SCRIPT_questdiffuse.png', addon_prefs.questAssetPath + '\SCRIPT_questdiffuse.png')
shutil.copyfile(bpy.path.abspath("//") + '\CATS Bake\SCRIPT_normal.png', addon_prefs.questAssetPath + '\SCRIPT_normal.png')
return {'FINISHED'}
class ToolPanel(object):
bl_idname = "OBJECT_AW_TOOLPANEL"
bl_label = "Red's Avatar Tools"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "RAT"
class ExportPanel(ToolPanel, bpy.types.Panel):
bl_idname = "OBJECT_AW_EXPORT"
bl_label = "Export"
def draw(self, context):
# unused for now
#self.layout.prop_search(context.scene,
# "master_collection",
# bpy.data,
# "collections")
#self.layout.prop_search(context.scene,
# "hair_collection",
# bpy.data,
# "collections")
self.layout.prop_search(context.scene,
"rat_armature",
bpy.data,
"objects")
self.layout.operator(AvatarPrepOp.bl_idname)
self.layout.operator(AvatarBakeOp.bl_idname)
self.layout.operator(AvatarBakeQuestOp.bl_idname)
def register():
bpy.utils.register_class(RATAddonPreferences)
bpy.types.Scene.rat_armature = StringProperty(default="None")
bpy.types.Scene.master_collection = StringProperty(default="None")
bpy.types.Scene.hair_collection = StringProperty(default="None")
bpy.utils.register_class(AvatarPrepOp)
bpy.utils.register_class(AvatarBakeOp)
bpy.utils.register_class(AvatarBakeQuestOp)
bpy.utils.register_class(ExportPanel)
def unregister():
bpy.utils.unregister_class(AvatarBakeQuestOp)
bpy.utils.unregister_class(AvatarBakeOp)
bpy.utils.unregister_class(ExportPanel)
bpy.utils.unregister_class(AvatarPrepOp)
bpy.utils.unregister_class(RATAddonPreferences)
if __name__ == "__main__":
register()