From eea50adfcbda0a776655b1afb8f7d57362f02593 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 25 May 2026 01:42:28 +0300 Subject: [PATCH] Gap filling, improvements for character pipeline --- assets/blender/characters/CMakeLists.txt | 30 +++- .../characters/add_missing_shape_keys.py | 139 ++++++++++++++++++ assets/blender/characters/combine_clothes.py | 7 +- .../characters/propagate_shape_keys.py | 91 ++++++++++++ .../blender/characters/transfer_shape_keys.py | 134 +++++++++++++++-- assets/blender/scripts/export_models2.py | 26 +++- src/features/editScene/CMakeLists.txt | 1 + .../editScene/systems/CharacterSlotSystem.cpp | 80 +++++++++- .../editScene/systems/OgreEntityHack.cpp | 13 ++ .../editScene/systems/OgreEntityHack.hpp | 12 ++ 10 files changed, 507 insertions(+), 26 deletions(-) create mode 100644 assets/blender/characters/add_missing_shape_keys.py create mode 100644 assets/blender/characters/propagate_shape_keys.py create mode 100644 src/features/editScene/systems/OgreEntityHack.cpp create mode 100644 src/features/editScene/systems/OgreEntityHack.hpp diff --git a/assets/blender/characters/CMakeLists.txt b/assets/blender/characters/CMakeLists.txt index af922ff..b942b50 100644 --- a/assets/blender/characters/CMakeLists.txt +++ b/assets/blender/characters/CMakeLists.txt @@ -425,9 +425,35 @@ weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-top.blend) weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend) weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-feet.blend) +# Propagate missing shape keys (like "fat") from Body_shapes to base body parts. +# This ensures all body part meshes have consistent shape keys, preventing seams. +function(add_shape_key_propagation INPUT_BLEND OUTPUT_BLEND) + add_custom_command( + OUTPUT ${OUTPUT_BLEND} + DEPENDS ${INPUT_BLEND} + ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py + ${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py + ${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed + COMMAND ${CMAKE_COMMAND} -E copy ${INPUT_BLEND} ${OUTPUT_BLEND} + COMMAND ${BLENDER} -b -Y ${OUTPUT_BLEND} + -P ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py -- + ${OUTPUT_BLEND} ${OUTPUT_BLEND} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) +endfunction() + +add_shape_key_propagation( + ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend + ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend +) +add_shape_key_propagation( + ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend + ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend +) + # male add_clothes_pipeline( - "${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND + "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend" # INPUT_BLEND "${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # BOTTOM_OUTPUT_BLEND ) @@ -446,7 +472,7 @@ add_clothes_pipeline( # female add_clothes_pipeline( - "${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND + "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend" # INPUT_BLEND "${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # FINAL_OUTPUT_BLEND ) diff --git a/assets/blender/characters/add_missing_shape_keys.py b/assets/blender/characters/add_missing_shape_keys.py new file mode 100644 index 0000000..44e138c --- /dev/null +++ b/assets/blender/characters/add_missing_shape_keys.py @@ -0,0 +1,139 @@ +""" +Add missing shape keys from Body_shapes to body part objects. +Preserves existing shape keys. +Usage: blender -b --python add_missing_shape_keys.py -- +""" + +import bpy +import sys +import os +import mathutils +from mathutils.bvhtree import BVHTree + +# Import functions from transfer_shape_keys.py +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from transfer_shape_keys import ( + compute_robust_surface_mapping, + compute_side_preserving_position, + smooth_boundary_areas, + smooth_penetration_areas, + create_bvh_for_source, + load_source_data, + fix_seams_across_objects +) + +def get_source_object_name(target_obj_info): + if target_obj_info.get('ref_shapes'): + return target_obj_info['ref_shapes'] + return None + +def add_missing_shape_keys(source_data, target_obj): + """Add only missing shape keys to target object, preserving existing ones.""" + if not target_obj.data.shape_keys: + target_obj.shape_key_add(name='Basis', from_mix=False) + + existing_names = {kb.name for kb in target_obj.data.shape_keys.key_blocks} + missing_names = [n for n in source_data['names'] if n != 'Basis' and n not in existing_names] + + if not missing_names: + print(f" {target_obj.name}: all shape keys present, skipping") + return + + print(f" {target_obj.name}: adding missing keys: {missing_names}") + + # Create missing shape keys + for sk_name in missing_names: + target_obj.shape_key_add(name=sk_name, from_mix=False) + + target_obj.data.shape_keys.use_relative = True + + # Compute mapping + mapping = compute_robust_surface_mapping(target_obj, source_data) + + # Set values for missing shape keys + for sk_name in missing_names: + print(f" Setting {sk_name}...") + sk = target_obj.data.shape_keys.key_blocks[sk_name] + + target_obj.data.shape_keys.use_relative = False + for i, v in enumerate(sk.data): + if i < len(mapping['target_verts']): + pos = compute_side_preserving_position(i, sk_name, mapping, source_data, 1.0) + v.co = pos + + sk.data.update() + target_obj.data.update_tag() + bpy.context.view_layer.update() + + target_obj.data.shape_keys.use_relative = True + target_obj.data.update_tag() + bpy.context.view_layer.update() + + smooth_boundary_areas(target_obj, sk_name, mapping, source_data) + smooth_penetration_areas(target_obj, sk_name, mapping, source_data) + + print(f" ✓ Added {sk_name}") + +def process_blend(blend_path, output_path): + print(f"Processing: {blend_path}") + bpy.ops.wm.open_mainfile(filepath=blend_path) + + # Find source object + source_obj = None + for name in ['Body_shapes', 'Body']: + if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH': + obj = bpy.data.objects[name] + if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1: + source_obj = obj + print(f"Found source: {name}") + break + + if not source_obj: + print("ERROR: No source object with shape keys found") + return False + + # Build source data + source_data = { + 'names': [kb.name for kb in source_obj.data.shape_keys.key_blocks], + 'vertex_positions': {}, + 'polygons': [], + 'is_relative': source_obj.data.shape_keys.use_relative, + 'source_obj_name': source_obj.name + } + for poly in source_obj.data.polygons: + source_data['polygons'].append([v for v in poly.vertices]) + for sk in source_obj.data.shape_keys.key_blocks: + source_data['vertex_positions'][sk.name] = [(v.co.x, v.co.y, v.co.z) for v in sk.data] + + print(f"Source shape keys: {source_data['names']}") + + # Process body part objects + required_props = {'age', 'sex', 'slot'} + modified = False + for obj in bpy.data.objects: + if obj.type == 'MESH' and all(p in obj for p in required_props): + if obj == source_obj: + continue + add_missing_shape_keys(source_data, obj) + modified = True + + # Fix seams: synchronize shape key offsets for vertices sharing + # the same basis position across body parts. + fix_seams_across_objects() + + bpy.ops.wm.save_as_mainfile(filepath=output_path) + print(f"Saved: {output_path}") + return True + +if __name__ == "__main__": + try: + args = sys.argv[sys.argv.index("--") + 1:] + if len(args) >= 2: + process_blend(args[0], args[1]) + else: + print("Usage: blender -b -P script.py -- ") + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/assets/blender/characters/combine_clothes.py b/assets/blender/characters/combine_clothes.py index 874df4f..6a4d176 100644 --- a/assets/blender/characters/combine_clothes.py +++ b/assets/blender/characters/combine_clothes.py @@ -66,8 +66,11 @@ def raycast_and_adjust_vertices(target_body, bvh_cloth, out_ray_length=0.15): new_co = m_body_inv @ (v_world + offset) v.co = new_co - # Update shape keys if they exist - if has_shape_keys: + # Update shape keys if they exist. + # In relative mode, Blender automatically uses v.co as basis, + # so shape key offsets don't need updating. Only update if + # the shape keys are in absolute mode (which we don't use). + if has_shape_keys and not target_body.data.shape_keys.use_relative: for kb in target_body.data.shape_keys.key_blocks: kb.data[i].co = new_co diff --git a/assets/blender/characters/propagate_shape_keys.py b/assets/blender/characters/propagate_shape_keys.py new file mode 100644 index 0000000..a1b315c --- /dev/null +++ b/assets/blender/characters/propagate_shape_keys.py @@ -0,0 +1,91 @@ +import bpy +import sys +import os + +def propagate_shape_keys(blend_path, output_path): + """Copy missing shape keys from Body_shapes to all body part objects.""" + print(f"Propagating shape keys from {blend_path} to {output_path}") + + bpy.ops.wm.open_mainfile(filepath=blend_path) + + # Find source object with all shape keys + source_obj = None + for name in ['Body_shapes', 'Body']: + if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH': + obj = bpy.data.objects[name] + if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1: + source_obj = obj + print(f"Found source object: {name}") + break + + if not source_obj: + print("ERROR: No source object with shape keys found") + return False + + source_keys = source_obj.data.shape_keys.key_blocks + source_basis = source_keys['Basis'].data + + modified = False + for obj in bpy.data.objects: + if obj.type != 'MESH': + continue + # Only process body part objects + if not all(p in obj for p in ['age', 'sex', 'slot']): + continue + # Skip the source object itself + if obj == source_obj: + continue + + if not obj.data.shape_keys: + # Create basis if missing + obj.shape_key_add(name='Basis', from_mix=False) + + existing_names = {kb.name for kb in obj.data.shape_keys.key_blocks} + + for sk in source_keys: + if sk.name == 'Basis': + continue + if sk.name in existing_names: + print(f" {obj.name}: already has '{sk.name}', skipping") + continue + + # Copy shape key + print(f" {obj.name}: adding '{sk.name}' from {source_obj.name}") + new_sk = obj.shape_key_add(name=sk.name, from_mix=False) + + # Transfer vertex positions relative to basis + # We need to map source vertices to target vertices + # For now, assume same topology (body parts were separated from same mesh) + num_verts = min(len(new_sk.data), len(sk.data), len(source_basis)) + for i in range(num_verts): + # The shape key position in source + src_pos = sk.data[i].co + src_basis_pos = source_basis[i].co + # Offset from basis + offset = src_pos - src_basis_pos + # Apply to target using target's basis position + new_sk.data[i].co = obj.data.shape_keys.key_blocks['Basis'].data[i].co + offset + + new_sk.data.update() + modified = True + + if modified: + obj.data.update_tag() + + bpy.context.view_layer.update() + bpy.ops.wm.save_as_mainfile(filepath=output_path) + print(f"Saved: {output_path}") + return True + +if __name__ == "__main__": + try: + args = sys.argv[sys.argv.index("--") + 1:] + if len(args) >= 2: + propagate_shape_keys(args[0], args[1]) + else: + print("Usage: blender -b -P script.py -- ") + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/assets/blender/characters/transfer_shape_keys.py b/assets/blender/characters/transfer_shape_keys.py index 32b9ae1..883baee 100644 --- a/assets/blender/characters/transfer_shape_keys.py +++ b/assets/blender/characters/transfer_shape_keys.py @@ -243,12 +243,16 @@ def compute_signed_distance_and_direction(bvh, point, reference_normal=None): if location is None: return None, None, None, None - # Determine sign using reference normal + # Determine sign using reference normal. + # For points outside (in the direction of the normal), signed_distance + # must be positive. For points inside, negative. if reference_normal is not None: to_surface = location - point if to_surface.length > 0: dot = to_surface.normalized().dot(reference_normal) - signed_distance = distance if dot > 0 else -distance + # Inverted relative to the no-reference-normal branch because + # to_surface here points *toward* the surface, not away. + signed_distance = -distance if dot > 0 else distance else: signed_distance = 0 else: @@ -743,15 +747,21 @@ def smooth_penetration_areas(target_obj, sk_name, mapping, source_data, threshol problem_vertices = set() bvh_deformed, _ = create_bvh_for_source(source_data, sk_name) + # Shape keys are in relative mode here, so current_positions are offsets. + # The BVH is built from absolute source positions, so we must convert + # relative offsets to absolute positions before querying it. + basis_positions = mapping['target_verts'] + for i, pos in enumerate(current_positions): if i < len(mapping['surface_points']) and i < len(mapping['side_flags']): surface_point = mapping['surface_points'][i] target_side = mapping['side_flags'][i] - if target_side != 0: - location, _, _, _ = bvh_deformed.find_nearest(pos) + if target_side != 0 and i < len(basis_positions): + abs_pos = basis_positions[i] + pos + location, _, _, _ = bvh_deformed.find_nearest(abs_pos) if location and i < len(mapping['direction_vectors']): - to_surface = pos - location + to_surface = abs_pos - location current_side = 1 if to_surface.dot(mapping['direction_vectors'][i]) > 0 else -1 if current_side != target_side: @@ -1009,16 +1019,109 @@ def process_target_object(target_obj_info, source_data, current_file_path): mapping = transfer_shape_keys(source_data, target_obj) verify_transfer(source_data['names'], target_obj) - # Test quality of 'fat' shape key if it exists - for sk_name in source_data['names']: - if sk_name == 'fat': - test_shape_key_quality(target_obj, sk_name, mapping, source_data) - break - print(f" {'=' * 40}") print(f" ✓ Completed") return True +def get_body_part_objects(): + """Find all mesh objects that are body parts (have age/sex/slot)""" + result = [] + required_props = {'age', 'sex', 'slot'} + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.data and all(p in obj for p in required_props): + result.append(obj) + return result + +def round_pos(co, tolerance=0.0001): + """Round a coordinate to tolerance-based buckets for spatial hashing""" + return (round(co.x / tolerance), + round(co.y / tolerance), + round(co.z / tolerance)) + +def fix_seams_across_objects(tolerance=0.0001): + """ + Post-process shape keys to ensure vertices sharing the same basis + position (within tolerance) get identical offsets. This fixes seams + between body parts and UV seams within the same part. + """ + body_parts = get_body_part_objects() + if not body_parts: + print("No body part objects found for seam fixing") + return + + print(f"\n{'=' * 60}") + print("SEAM FIX: Synchronizing shape key offsets across body parts") + print(f"{'=' * 60}") + print(f"Found {len(body_parts)} body part objects") + + # Collect shape key names from first object that has them + shape_key_names = [] + for obj in body_parts: + if obj.data.shape_keys and obj.data.shape_keys.key_blocks: + for kb in obj.data.shape_keys.key_blocks: + if kb.name != 'Basis': + shape_key_names.append(kb.name) + break + + if not shape_key_names: + print("No shape keys found, skipping seam fix") + return + + print(f"Synchronizing {len(shape_key_names)} shape key(s): {', '.join(shape_key_names)}") + + fixed_vertices = 0 + fixed_groups = 0 + + for sk_name in shape_key_names: + # Build spatial hash: position_key -> list of (obj, vert_idx, offset) + pos_map = {} + + for obj in body_parts: + if not obj.data.shape_keys or sk_name not in obj.data.shape_keys.key_blocks: + continue + + sk = obj.data.shape_keys.key_blocks[sk_name] + for i, v in enumerate(obj.data.vertices): + if i >= len(sk.data): + continue + pos_key = round_pos(v.co, tolerance) + offset = sk.data[i].co.copy() + + if pos_key not in pos_map: + pos_map[pos_key] = [] + pos_map[pos_key].append((obj, i, offset)) + + # Average offsets for each position group with multiple vertices + for pos_key, entries in pos_map.items(): + if len(entries) < 2: + continue + + # Compute average offset + avg_offset = mathutils.Vector((0.0, 0.0, 0.0)) + for obj, idx, offset in entries: + avg_offset += offset + avg_offset /= len(entries) + + # Apply averaged offset to all vertices in this group + for obj, idx, offset in entries: + sk = obj.data.shape_keys.key_blocks[sk_name] + sk.data[idx].co = avg_offset.copy() + fixed_vertices += 1 + + fixed_groups += 1 + + # Update all meshes + for obj in body_parts: + if obj.data.shape_keys: + for kb in obj.data.shape_keys.key_blocks: + kb.data.update() + obj.data.update_tag() + + bpy.context.view_layer.update() + + print(f"Fixed {fixed_vertices} vertices across {fixed_groups} position groups") + print(f"Seam fix complete") + def main(): print("=" * 60) print("Blender Shape Key Transfer Script - Boundary Velocity Limiting") @@ -1077,6 +1180,15 @@ def main(): print(f"\nProgress saved to: {temp_progress_file}") + # Post-process: fix seams by synchronizing offsets across body parts + print(f"\nLoading final working file for seam fix...") + bpy.ops.wm.open_mainfile(filepath=working_file) + fix_seams_across_objects() + + # Save after seam fix + print(f"\nSaving after seam fix...") + bpy.ops.wm.save_as_mainfile(filepath=working_file) + # Copy the final working file to the output location print(f"\nCopying final result to: {output_file}") shutil.copy2(working_file, output_file) diff --git a/assets/blender/scripts/export_models2.py b/assets/blender/scripts/export_models2.py index 08e6a29..26de564 100644 --- a/assets/blender/scripts/export_models2.py +++ b/assets/blender/scripts/export_models2.py @@ -268,6 +268,17 @@ for mapping in[CommandLineMapping()]: elif mapping.auto_discover and ob.type == 'MESH' and ob.name not in mapping.objs: if not ob.name.endswith("-noimp"): ob.name = ob.name + "-noimp" + + # Remove .001 suffixed duplicates that may have leaked from consolidate step + # These are objects that have the same base name as an object in mapping.objs + # but with a .001 suffix (e.g., BodyBottom.001 when BodyBottom is in mapping.objs) + if mapping.auto_discover: + for ob in list(bpy.data.objects): + if ob.type == 'MESH' and ob.name.endswith('.001'): + base_name = ob.name[:-4] + if base_name in mapping.objs: + print(f"Removing duplicate '{ob.name}' (base '{base_name}' already in export list)") + bpy.data.objects.remove(ob, do_unlink=True) print("Removing original armature and actions...") orig_arm = bpy.data.objects[mapping.armature_name + '_orig'] @@ -327,9 +338,18 @@ for mapping in[CommandLineMapping()]: obj = bpy.data.objects.get(name) if obj and obj.type == 'MESH': - # 1. Rename Mesh Data - if not obj.data.name.startswith(prefix): - obj.data.name = prefix + obj.data.name + # 1. Rename Mesh Data to match object name (not mesh data name) + # This prevents .001 suffixed mesh data names (e.g., BodyBottom.001) + # from becoming male_BodyBottom.001 and clashing with male_BodyBottom. + new_mesh_name = prefix + name + if obj.data.name != new_mesh_name: + # Check if another mesh data already has this name + if new_mesh_name in bpy.data.meshes and bpy.data.meshes[new_mesh_name] != obj.data: + old = bpy.data.meshes[new_mesh_name] + old.name = new_mesh_name + "_old" + print(f"Renamed conflicting mesh data '{old.name}' for object '{name}'") + obj.data.name = new_mesh_name + print(f"Renamed mesh data from '{obj.data.name}' to '{new_mesh_name}' for object '{name}'") # 2. Iterate through all Material Slots on the object for slot in obj.material_slots: diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 8cf8171..28137a9 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -43,6 +43,7 @@ set(EDITSCENE_SOURCES systems/SaveLoadDialog.cpp systems/PlayerControllerSystem.cpp systems/CharacterSlotSystem.cpp + systems/OgreEntityHack.cpp systems/CharacterRegistry.cpp systems/MarkovNameGenerator.cpp systems/PregnancySystem.cpp diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index 463e245..8f91ba5 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -1,5 +1,6 @@ #include "CharacterSlotSystem.hpp" #include "CharacterRegistry.hpp" +#include "OgreEntityHack.hpp" #include "../components/Transform.hpp" #include "../components/AnimationTree.hpp" #include "../components/CharacterIdentity.hpp" @@ -540,6 +541,15 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, findCatalogEntry(age, cs.sex, masterSlot, masterMesh); applyShapeKeys(e, masterEnt, entry); + /* Re-prepare temp buffers after enabling vertex animation. + * OGRE's Entity::_initialise calls prepareTempBlendBuffers() + * before our animation state is enabled. If no skeletal + * animation was active, mSoftwareVertexAnimVertexData was + * never created, causing pose animation to corrupt the + * mesh's shared vertex buffer directly. + */ + prepareEntityTempBlendBuffers(masterEnt); + /* Notify AnimationTreeSystem that entity changed */ if (e.has()) e.get_mut().dirty = true; @@ -575,6 +585,14 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, const nlohmann::json *entry = findCatalogEntry(age, cs.sex, slot, mesh); applyShapeKeys(e, partEnt, entry); + + /* Re-prepare temp buffers after skeleton sharing and + * enabling vertex animation. shareSkeletonInstanceWith + * replaces the part's AnimationStateSet but does not + * recreate temp blend buffers, which are needed for + * proper per-entity pose animation. + */ + prepareEntityTempBlendBuffers(partEnt); } catch (const Ogre::Exception &ex) { Ogre::LogManager::getSingleton().logMessage( "[CharacterSlotSystem] buildCharacter: FAILED to load part '" + @@ -585,7 +603,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e, } void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, - const nlohmann::json *entry) + const nlohmann::json *entry) { if (!ent || !entry) return; @@ -600,6 +618,21 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, anim = mesh->getAnimation("ShapeKeys"); } catch (...) { anim = mesh->createAnimation("ShapeKeys", 1.0f); + } + + /* Ensure the animation has at least one pose track. + * We create tracks for ALL vertex data that has poses, + * not just handle 0, to handle meshes with mixed shared/dedicated data. + */ + bool hasTrack = false; + for (unsigned short i = 0; i < anim->getNumVertexTracks(); ++i) { + if (anim->getVertexTrack(i)->getAnimationType() == + Ogre::VAT_POSE) { + hasTrack = true; + break; + } + } + if (!hasTrack) { Ogre::VertexAnimationTrack *track = anim->createVertexTrack(0, Ogre::VAT_POSE); Ogre::VertexPoseKeyFrame *kf = @@ -608,9 +641,34 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, kf->addPoseReference(static_cast(i), 0.0f); } - if (!ent->hasAnimationState("ShapeKeys")) - return; - Ogre::AnimationState *as = ent->getAnimationState("ShapeKeys"); + /* Ensure the entity has an animation state for ShapeKeys. + * shareSkeletonInstanceWith() replaces the part entity's + * AnimationStateSet with the master's. If the master doesn't + * have ShapeKeys, the part entity won't have it either. + * We work around this by creating the state on the shared + * AnimationStateSet when needed. + */ + Ogre::AnimationState *as = nullptr; + if (ent->hasAnimationState("ShapeKeys")) { + as = ent->getAnimationState("ShapeKeys"); + } else { + /* Create the state on the entity's current AnimationStateSet. + * After shareSkeletonInstanceWith(), this is the master's set, + * so all parts sharing the skeleton will see it. + */ + Ogre::AnimationStateSet *stateSet = + ent->getAllAnimationStates(); + if (stateSet) { + as = stateSet->createAnimationState( + "ShapeKeys", 0.0, 1.0, 1.0, false); + } else { + Ogre::LogManager::getSingleton().logMessage( + "[CharacterSlotSystem] applyShapeKeys: entity '" + + ent->getName() + "' mesh '" + mesh->getName() + + "' has no AnimationStateSet"); + return; + } + } as->setEnabled(true); as->setLoop(false); @@ -636,10 +694,16 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent, continue; if (it->second >= mesh->getPoseCount()) continue; - /* Update the keyframe's pose reference influence */ - Ogre::VertexAnimationTrack *track = - anim->getVertexTrack(0); - if (track) { + /* Update the keyframe's pose reference influence + * on ALL pose tracks in the animation, not just track 0. + */ + for (unsigned short t = 0; + t < anim->getNumVertexTracks(); ++t) { + Ogre::VertexAnimationTrack *track = + anim->getVertexTrack(t); + if (!track || track->getAnimationType() != + Ogre::VAT_POSE) + continue; Ogre::VertexPoseKeyFrame *kf = track->getVertexPoseKeyFrame(0); if (kf) diff --git a/src/features/editScene/systems/OgreEntityHack.cpp b/src/features/editScene/systems/OgreEntityHack.cpp new file mode 100644 index 0000000..42c9d19 --- /dev/null +++ b/src/features/editScene/systems/OgreEntityHack.cpp @@ -0,0 +1,13 @@ +/* + * Workaround: OGRE's Entity::prepareTempBlendBuffers() is private, but we need + * to call it after shareSkeletonInstanceWith() to ensure per-entity temporary + * vertex buffers are created for pose animation. + */ +#define private public +#include +#undef private + +void prepareEntityTempBlendBuffers(Ogre::Entity *ent) +{ + ent->prepareTempBlendBuffers(); +} diff --git a/src/features/editScene/systems/OgreEntityHack.hpp b/src/features/editScene/systems/OgreEntityHack.hpp new file mode 100644 index 0000000..8e18e98 --- /dev/null +++ b/src/features/editScene/systems/OgreEntityHack.hpp @@ -0,0 +1,12 @@ +#ifndef OGRE_ENTITY_HACK_HPP +#define OGRE_ENTITY_HACK_HPP + +#include + +namespace Ogre { + class Entity; +} + +void prepareEntityTempBlendBuffers(Ogre::Entity *ent); + +#endif