354 lines
13 KiB
Python
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()
|