Gap filling, improvements for character pipeline
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 -- <blend_file> <output_file>
|
||||
"""
|
||||
|
||||
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 -- <input.blend> <output.blend>")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 -- <input.blend> <output.blend>")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<AnimationTreeComponent>())
|
||||
e.get_mut<AnimationTreeComponent>().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<ushort>(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)
|
||||
|
||||
@@ -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 <OgreEntity.h>
|
||||
#undef private
|
||||
|
||||
void prepareEntityTempBlendBuffers(Ogre::Entity *ent)
|
||||
{
|
||||
ent->prepareTempBlendBuffers();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef OGRE_ENTITY_HACK_HPP
|
||||
#define OGRE_ENTITY_HACK_HPP
|
||||
|
||||
#include <OgrePrerequisites.h>
|
||||
|
||||
namespace Ogre {
|
||||
class Entity;
|
||||
}
|
||||
|
||||
void prepareEntityTempBlendBuffers(Ogre::Entity *ent);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user