1
Fork 0

Add existing code

This commit is contained in:
redstrate 2021-08-11 18:25:18 -04:00
parent df1089d539
commit 9ac8a43f9e
3 changed files with 361 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Joshua Goins
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.

View file

@ -0,0 +1,10 @@
# Red's Avatar Tools
Non-destructive extra blender tooling for my VRChat avatars. Supports both PC and Quest thanks to CATs.
Right now it's barely usable except for my avatar, but it takes care of automatically calling CATs and all of that. Giving it a
directory of your asset's dir it will automatically export to Unity with all of the correct baking options. It also takes care of
my curve hair and all of my modifiers as part of my non-destructive character modeling workflow. It does this process for PC and
Quest and sets the options accordingly for Excellent rated avatars.
This is mostly an exercise in Blender addon development, please don't expect any support at the moment.

330
__init__.py Normal file
View file

@ -0,0 +1,330 @@
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['Armature.002']
# 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
# 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!
context.scene.bake_use_decimation = True
context.scene.bake_quick_compare = False
context.scene.bake_max_tris = 32000 - 55
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
context.scene.bake_resolution = 2048
context.scene.bake_generate_uvmap = True
context.scene.bake_pass_diffuse = True
context.scene.bake_diffuse_vertex_colors = False
context.scene.bake_preserve_seams = True
context.scene.bake_pass_diffuse = True
context.scene.bake_uv_overlap_correction = 'REPROJECT'
context.scene.bake_diffuse_alpha_pack = 'SMOOTHNESS'
context.scene.bake_prioritize_face = True
context.scene.bake_face_scale = 2.0
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_use_decimation = True
context.scene.bake_quick_compare = False
context.scene.bake_max_tris = 7500 - 100
context.scene.bake_pass_normal = True
context.scene.bake_pass_ao = True
context.scene.bake_pass_questdiffuse = True
context.scene.bake_resolution = 1024
context.scene.bake_generate_uvmap = True
context.scene.bake_diffuse_vertex_colors = False
context.scene.bake_normal_apply_trans = False
context.scene.bake_preserve_seams = True
context.scene.bake_pass_diffuse = True
context.scene.bake_remove_doubles = True
context.scene.bake_uv_overlap_correction = 'NONE'
context.scene.bake_diffuse_alpha_pack = 'SMOOTHNESS'
context.scene.bake_prioritize_face = True
context.scene.bake_face_scale = 5.0
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.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.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()