Adapted more debugging with Tracy, characters update

This commit is contained in:
2026-02-13 18:14:09 +03:00
parent 71b7f47bcd
commit a85152a675
23 changed files with 814 additions and 133 deletions

View File

@@ -11,12 +11,13 @@ add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${EDITED_BLEND}.blend
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${EDITED_BLEND}.blend
${CMAKE_BINARY_DIR}/assets/blender/vrm-vroid-normal-${EDITED_BLEND}.blend
${CMAKE_CURRENT_SOURCE_DIR}/copy_animations.py
COMMAND ${CMAKE_COMMAND}
-E copy ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${EDITED_BLEND}.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${EDITED_BLEND}.blend
COMMAND ${BLENDER} -b -Y
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${EDITED_BLEND}.blend
-P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/copy_animations.py --
-P ${CMAKE_CURRENT_SOURCE_DIR}/copy_animations.py --
${CMAKE_BINARY_DIR}/assets/blender/vrm-vroid-normal-${EDITED_BLEND}.blend ${EDITED_BLEND}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
@@ -30,16 +31,48 @@ set(VRM_IMPORTED_BLENDS
${CMAKE_BINARY_DIR}/assets/blender/shapes/male/vrm-vroid-normal-male-chibi.blend
)
#add_custom_command(
# OUTPUT ${CHARACTER_GLBS}
# COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
# COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py
# COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
# COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CHARACTER_GLBS}
# DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py ${VRM_IMPORTED_BLENDS} ${EDITED_BLEND_TARGETS}
# WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
set(FEMALE_OBJECTS "Body;Hair;Face;BackHair;Tops;Bottoms;Shoes;Accessory")
set(MALE_OBJECTS "Body;Hair;Face;BackHair;Tops;Bottoms;Shoes;Accessory")
add_custom_command(
OUTPUT ${CHARACTER_GLBS}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CHARACTER_GLBS}
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py ${VRM_IMPORTED_BLENDS} ${EDITED_BLEND_TARGETS}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
OUTPUT ${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male.blend
${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
"${MALE_OBJECTS}"
"male"
tmp-edited-male.blend
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
${VRM_IMPORTED_BLENDS}
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male.blend
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
VERBATIM
)
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
"${FEMALE_OBJECTS}"
"female"
tmp-edited-female.blend
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
${VRM_IMPORTED_BLENDS}
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
VERBATIM
)
set(VRM_SOURCE)

View File

@@ -0,0 +1,64 @@
import bpy
import sys
import os
argv = sys.argv
argv = argv[argv.index("--") + 1:]
sys.path.insert(0, os.getcwd() + "/assets/blender/scripts")
source_filepath = argv[0]
armature_name = argv[1]
print("starting...")
def copy_actions_and_create_nla_tracks(source_filepath, target_object_name):
"""
Copies actions from a source Blender file that do not exist in the current
file, creates NLA tracks for them on a target object, and saves the file.
Args:
source_filepath (str): The full path to the source Blender file.
target_object_name (str): The name of the object in the current file
to which the actions and NLA tracks will be applied.
"""
# 1. Link or Append Actions from the Source File
with bpy.data.libraries.load(source_filepath, link=False) as (data_from, data_to):
source_action_names = data_from.actions
current_action_names = {action.name for action in bpy.data.actions}
actions_to_append = [name for name in source_action_names if name not in current_action_names]
data_to.actions = actions_to_append
print(actions_to_append)
# Get the target object
target_object = bpy.data.objects.get(target_object_name)
if not target_object:
print(f"Error: Object '{target_object_name}' not found in the current file.")
return
# Ensure the object has an NLA editor
if not target_object.animation_data:
target_object.animation_data_create()
# 2. Iterate through newly imported actions and create NLA tracks
for action in data_to.actions:
# Check if an action with the same name already exists in the current file
# Add the action to the NLA editor as a strip
nla_track = target_object.animation_data.nla_tracks.new()
nla_track.name = f"NLA_Track_{action.name}"
nla_track.strips.new(name=action.name, start=1, action=action)
print(f"Created NLA track for action: {action.name}")
# 3. Save the current Blender file
bpy.ops.wm.save_as_mainfile(filepath=bpy.context.blend_data.filepath)
print(f"File saved: {bpy.context.blend_data.filepath}")
# --- Usage Example ---
if __name__ == "__main__":
# Replace with your actual source file path and target object name
source_file = source_filepath
target_obj = armature_name
copy_actions_and_create_nla_tracks(source_file, target_obj)

View File

@@ -0,0 +1,296 @@
#!/usr/bin/env python
import os, sys, time
import bpy
from math import pi
import glob
import shutil
from mathutils import Vector, Matrix
from math import radians, pi
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
#from settings import ExportMappingFemale, ExportMappingMale, ExportMappingMaleBabyShape, ExportMappingMaleEdited, ExportMappingFemaleEdited, ExportMappingMaleTestShapeEdited, ExportMappingMaleBaseShapeEdited
argv = sys.argv
argv = argv[argv.index("--") + 1:]
basepath = os.getcwd()
def check_bone(bname):
ok = True
baddie = ["ctrl_", "mch_", "MCH_", "Ctrl_", "Mch_"]
for bd in baddie:
if bname.startswith(bd):
ok = False
break
return ok
def prepare_armature(mapping):
print("Preparing...")
bpy.ops.object.armature_add(enter_editmode=False)
new_armature = bpy.context.object
orig_armature = bpy.data.objects[mapping.armature_name]
armature_name = orig_armature.name
orig_armature.name = orig_armature.name + "_orig"
new_armature.name = armature_name
queue = []
if new_armature.animation_data is None:
new_armature.animation_data_create()
bpy.context.view_layer.objects.active = new_armature
bpy.ops.object.mode_set(mode='EDIT')
for b in new_armature.data.edit_bones:
new_armature.data.edit_bones.remove(b)
bpy.context.view_layer.objects.active = orig_armature
bpy.ops.object.mode_set(mode='EDIT')
for b in orig_armature.data.edit_bones:
print(b.name)
if b.parent is None:
queue.append(b.name)
print("Copying bones...")
while len(queue) > 0:
item = queue.pop(0)
print(item)
itemb = orig_armature.data.edit_bones[item]
if not itemb.use_deform and not check_bone(item):
continue
for cb in orig_armature.data.edit_bones:
if cb.parent == itemb:
queue.append(cb.name)
nb = new_armature.data.edit_bones.new(item)
nb.name = item
nb.head = itemb.head
nb.tail = itemb.tail
nb.matrix = itemb.matrix
nb.use_deform = itemb.use_deform
if itemb.parent is not None:
ok = True
pname = itemb.parent.name
while not check_bone(pname):
bparent = itemb.parent.parent
if bparent is None:
ok = False
break
pname = bparent.name
if ok:
nb.parent = new_armature.data.edit_bones[itemb.parent.name]
else:
nb.parent = None
else:
nb.parent = None
nb.use_connect = itemb.use_connect
# drivers_data = new_armature.animation_data.drivers
print("Creating constraints...")
bpy.context.view_layer.objects.active = new_armature
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = orig_armature
bpy.ops.object.mode_set(mode='OBJECT')
for b in new_armature.pose.bones:
print(b.name)
c = b.constraints.new(type='COPY_TRANSFORMS')
c.target = orig_armature
c.subtarget = b.name
for obj in bpy.data.objects:
if obj.parent == orig_armature:
obj.parent = new_armature
for mod in obj.modifiers:
if mod.type == 'ARMATURE':
mod.object = new_armature
print("Baking actions...")
bpy.context.view_layer.objects.active = new_armature
bpy.ops.object.mode_set(mode='POSE')
for track in orig_armature.animation_data.nla_tracks:
print(track.name)
for s in track.strips:
action = s.action
print(action.name)
orig_armature.animation_data.action = action
new_armature.animation_data.action = None
bpy.context.view_layer.objects.active = new_armature
firstFrame = int(s.action_frame_start)
lastFrame = int(s.action_frame_end)
bpy.ops.nla.bake(frame_start=firstFrame, frame_end=lastFrame, step=5, only_selected=False, visual_keying=True, clear_constraints=False, clean_curves=True, use_current_action=False, bake_types={'POSE'})
aname = orig_armature.animation_data.action.name
orig_armature.animation_data.action.name = "bake_" + aname
new_armature.animation_data.action.name = aname
track = new_armature.animation_data.nla_tracks.new()
track.name = aname
track.strips.new(track.name, int(new_armature.animation_data.action.frame_range[0]), new_armature.animation_data.action)
track.mute = True
track.lock = True
print("Removing constraints...")
for b in new_armature.pose.bones:
for c in b.constraints:
b.constraints.remove(c)
new_armature.animation_data.action = bpy.data.actions[mapping.default_action]
bpy.context.view_layer.objects.active = new_armature
bpy.ops.object.mode_set(mode='OBJECT')
# track = new_armature.animation_data.nla_tracks.new()
# track.name = action.name
def clamp_angle_deg(angle, min_angle_deg, max_angle_deg):
min_angle = radians(min_angle_deg)
max_angle = radians(max_angle_deg)
if angle < min_angle:
angle = min_angle
if angle > max_angle:
angle = max_angle
return angle
def angle_to_linear(angle, divider):
if angle < 0.0:
return angle / divider
else:
return 0.0
def angle_to_linear_x(bone, angle):
skel = bpy.data.objects["skeleton_orig"]
left_base = "ctrl_base_upperleg.L.001"
right_base = "ctrl_base_upperleg.R.001"
base = ""
if base == "":
for e in ["_R", ".R"]:
if bone.name.endswith(e):
base = right_base
break
if base == "":
for e in ["_L", ".L"]:
if bone.name.endswith(e):
base = left_base
break
if base == "":
for e in ["_R.", ".R."]:
if bone.name.find(e) >= 0:
base = right_base
break
if base == "":
for e in ["_L.", ".L."]:
if bone.name.find(e) >= 0:
base = left_base
break
mul = skel.pose.bones[base]["to_linear_x_base"]
offset = skel.pose.bones[base]["angle_offset"]
# print("bone: ", bone.name, "base: ", base, "mul: ", mul)
# print("angle: ", angle, " angle_offset: ", offset, " angle_sum: ", angle + offset)
print("offset: ", mul * (angle + offset), "bone: ", base, "angle: ", angle)
return (angle + offset) * mul
def extra_linear(angle, offset):
ret = 0.0
offt = offset * angle * 2.0 / -radians(-90)
if angle * 2.0 < -radians(65):
if angle * 2.0 > -radians(65):
ret += offset
else:
ret += offt
return ret
mapping_blend_path = argv[0]
mapping_gltf_path = argv[1]
mapping_objects = argv[2]
mapping_armature_name = argv[3]
mapping_outfile = argv[4]
#for mapping in [ExportMappingFemale(), ExportMappingMale(), ExportMappingMaleBabyShape(), ExportMappingMaleEdited(), ExportMappingFemaleEdited(), ExportMappingMaleTestShapeEdited(), ExportMappingMaleBaseShapeEdited()]:
class CommandLineMapping:
blend_path = mapping_blend_path
gltf_path = mapping_gltf_path
# ogre_scene = "characters/female/vroid-normal-female.scene"
inner_path = "Object"
# objs = ["male", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
# objs = ["female", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
objs = []
armature_name = mapping_armature_name
outfile = mapping_outfile
default_action = 'default'
def __init__(self):
self.objs = [mapping_armature_name]
if len(mapping_objects) > 0:
self.objs += [o.otrip() for o in mapping_objects.split(";")]
self.files = []
for fobj in self.objs:
self.files.append({"name": fobj})
for mapping in[CommandLineMapping()]:
if not os.path.exists(mapping.blend_path):
print("Skipping mapping: " + mapping.blend_path)
continue
print("Processing mapping: from: " + mapping.blend_path + " to: " + mapping.gltf_path)
print("Initializing...")
bpy.ops.wm.read_homefile(use_empty=True)
print("Preparing driver setup...")
bpy.app.driver_namespace["clamp_angle_deg"] = clamp_angle_deg
bpy.app.driver_namespace["angle_to_linear"] = angle_to_linear
bpy.app.driver_namespace["extra_linear"] = extra_linear
bpy.app.driver_namespace["angle_to_linear_x"] = angle_to_linear_x
print("Driver setup done...")
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
directory=os.path.join(mapping.blend_path, mapping.inner_path),
files=mapping.files)
print("Append done...")
prepare_armature(mapping)
print("Armature done...")
print("Remove junk...")
for ob in bpy.data.objects:
if ob.name.startswith("cs_"):
bpy.data.objects.remove(ob)
elif ob.name.startswith("Face") and ob.name != "Face":
bpy.data.objects.remove(ob)
print("Removing original armature and actions...")
orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
bpy.data.objects.remove(orig_arm)
for act in bpy.data.actions:
if act.name.startswith("bake_"):
act.name = act.name + "-noimp"
for act in bpy.data.actions:
if act.name.startswith("bake_"):
bpy.data.actions.remove(act)
for act in bpy.data.actions:
if act.name.startswith("bake_"):
bpy.data.actions.remove(act)
for obj in bpy.data.objects:
if obj.type == 'MESH':
if not obj.name in mapping.objs and obj.parent is None:
if not obj.name.endswith("-noimp"):
obj.name = obj.name + "-noimp"
bpy.ops.wm.save_as_mainfile(filepath=(basepath + "/assets/blender/scripts/" + mapping.outfile))
os.makedirs(os.path.dirname(mapping.gltf_path), exist_ok=True)
bpy.ops.export_scene.gltf(filepath=mapping.gltf_path,
use_selection=False,
check_existing=False,
# export_format='GLTF_SEPARATE',
export_format='GLB',
export_texture_dir='textures', export_texcoords=True,
export_animation_mode='NLA_TRACKS',
export_normals=True,
export_tangents=True,
export_materials='EXPORT',
# export_all_vertex_colors=True,
# colors_type='SRGB',
# export_vertex_colors=True,
export_colors=True,
use_mesh_edges=False,
use_mesh_vertices=False,
export_cameras=False,
use_visible=False,
use_renderable=False,
export_yup=True,
export_animations=True,
export_force_sampling=True,
export_def_bones=False,
export_current_frame=False,
export_morph=True,
export_morph_animation=False,
export_morph_normal=True,
export_morph_tangent=True,
export_lights=False,
export_skins=True)
print("exported to: " + mapping.gltf_path)
bpy.ops.wm.read_homefile(use_empty=True)
time.sleep(2)
bpy.ops.wm.quit_blender()