advanced example

This commit is contained in:
Vysyr committed 2026-10-31 20:16:32 +01:00
1 parent 8c0a639fba
commit 6fe57915e8
99 files changed
+1329 -56

No files matched your search

+22 -1
View File
@@ -1,4 +1,4 @@
"""Executed by Blender, never by Unreal. Export an evaluated, joined static mesh."""
"""Executed by Blender: static GLB or skinned FBX with the active animation."""
import argparse
from pathlib import Path
import sys
@@ -10,6 +10,7 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output", required=True)
parser.add_argument("--collection", default="Character")
parser.add_argument("--skeletal", action="store_true")
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:])
collection = bpy.data.collections.get(args.collection)
if collection is None:
@@ -18,6 +19,26 @@ def main():
if not meshes:
raise RuntimeError("Kolekcja Character nie zawiera siatek.")
bpy.ops.object.select_all(action="DESELECT")
if args.skeletal:
armatures = {modifier.object for obj in meshes for modifier in obj.modifiers
if modifier.type == "ARMATURE" and modifier.object is not None}
if len(armatures) != 1:
raise RuntimeError("Eksport szkieletowy wymaga jednego Armature przypisanego do siatek.")
armature = next(iter(armatures))
for obj in [*meshes, armature]:
obj.hide_set(False)
obj.select_set(True)
bpy.context.view_layer.objects.active = armature
output = Path(args.output).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.export_scene.fbx(
filepath=str(output), use_selection=True, object_types={"MESH", "ARMATURE"},
add_leaf_bones=False, use_armature_deform_only=False, bake_anim=True,
bake_anim_use_all_actions=False, bake_anim_use_nla_strips=False,
bake_anim_force_startend_keying=True, bake_anim_simplify_factor=0.0,
axis_forward="-Y", axis_up="Z", path_mode="COPY", embed_textures=True)
print("WORKSHOP_EXPORTED=" + str(output))
return
depsgraph = bpy.context.evaluated_depsgraph_get()
copies = []
for obj in meshes:
+38
View File
@@ -0,0 +1,38 @@
"""Run in Blender: edit a copy of the Mixamo fighter and save an animated .blend."""
from pathlib import Path
import bpy
root = Path(__file__).resolve().parents[1]
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
bpy.ops.import_scene.fbx(filepath=str(root / "build/SkeletalFixture/fighter.fbx"))
meshes = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"]
rig = next(obj for obj in bpy.context.scene.objects if obj.type == "ARMATURE")
original_objects = set(bpy.context.scene.objects)
bpy.ops.import_scene.fbx(filepath=str(root / "build/SkeletalFixture/walk.fbx"))
animation_rig = next(obj for obj in bpy.context.scene.objects
if obj not in original_objects and obj.type == "ARMATURE")
rig.animation_data_create()
rig.animation_data.action = animation_rig.animation_data.action
if hasattr(animation_rig.animation_data, "action_slot"):
rig.animation_data.action_slot = animation_rig.animation_data.action_slot
for obj in list(bpy.context.scene.objects):
if obj not in original_objects:
bpy.data.objects.remove(obj, do_unlink=True)
collection = bpy.data.collections.new("Character")
bpy.context.scene.collection.children.link(collection)
for obj in [*meshes, rig]:
if obj.name not in collection.objects:
collection.objects.link(obj)
# A small mesh edit while retaining the imported weights/armature modifier.
for vertex in meshes[0].data.vertices:
vertex.co.x *= 1.02
action = rig.animation_data.action
bpy.context.scene.frame_start = int(action.frame_range[0])
bpy.context.scene.frame_end = int(action.frame_range[1])
bpy.context.scene.frame_set(bpy.context.scene.frame_start)
output = root / "build/SkeletalFixture/edited_fighter.blend"
output.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=str(output))
print("WORKSHOP_SKELETAL_FIXTURE=" + str(output))
+36
View File
@@ -0,0 +1,36 @@
"""Real UE test of Python-authored crossfades using existing character assets."""
import json
import unreal
from ue_workshop import Scene, Sequence, Renderer, camera, lighting
from ue_workshop.characters import CREATURE_MODEL
from ue_workshop.paths import PROJECT
scene = Scene("BlendingSmoke")
character = scene.skeletal_model(
CREATURE_MODEL, name="Creature", rotation=(0, 0, 180), reimport=False)
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2))
lighting.studio(scene)
cam = camera.create(scene, location=(450, -550, 300), fov=45)
shot = Sequence(scene, frames=144, fps=24)
shot.camera(cam)
first = shot.animation(character, "mutant_idle", start=0, end=60)
middle = shot.animation(character, "mutant_swiping", start=48, end=108)
last = shot.animation(character, "mutant_walking", start=96, end=144)
def keys(section):
return [(key.get_time().frame_number.value, key.get_value())
for key in section.get_all_channels()[0].get_keys()]
assert keys(first) == [(0, 1.0), (48, 1.0), (60, 0.0)]
assert keys(middle) == [(48, 0.0), (60, 1.0), (96, 1.0), (108, 0.0)]
assert keys(last) == [(96, 0.0), (108, 1.0)]
shot.transform_key(character, 48, location=(0, 0, 0))
shot.transform_key(character, 107, location=(100, 0, 0))
shot.save(open_editor=False)
unreal.LevelSequenceEditorBlueprintLibrary.open_level_sequence(shot.asset)
unreal.LevelSequenceEditorBlueprintLibrary.set_current_time(54)
unreal.LevelSequenceEditorBlueprintLibrary.refresh_current_level_sequence()
(PROJECT / "Saved/Workshop/blending_smoke.json").write_text(
json.dumps({"blend_display_frames": 12, "clips": 3}), encoding="utf-8")
renderer = Renderer(width=640, height=360, samples=1, warmup=0, mp4=False)
+4
View File
@@ -22,6 +22,10 @@ assert len(second["shot"].asset.get_bindings()) == 2
assert len(second["shot"].asset.find_tracks_by_type(unreal.MovieSceneCameraCutTrack)) == 1
short = Sequence(second["scene"], "Smoke", fps=24, frames=2)
short.camera(second["cam"])
if second["character"].get_path_name() in second["scene"]._skeletal_models:
clip = second["ANIMATION"] if second["MODEL"] in ("fighter", "creature") else (
"custom" if second["ANIMATION_FILE"] else None)
short.animation(second["character"], clip)
short.transform_key(second["character"], 0, rotation=(0, 0, 0))
short.transform_key(second["character"], 1, rotation=(0, 0, 30))
render.start(short, mp4=True)
+69
View File
@@ -0,0 +1,69 @@
"""Real UE check: both Mixamo skeletons, animation-only FBXs and Sequencer poses."""
import json
import unreal
from ue_workshop import Scene, Sequence, camera, lighting, Renderer
from ue_workshop.paths import PROJECT
scene = Scene("SkeletalSmoke")
base = PROJECT / "source_assets/character"
characters = []
for name, model, walk, idle, attack, position in (
("FighterSkeletalSmoke", "fighter/Maria WProp J J Ong.fbx", "fighter/great sword walk.fbx",
"fighter/great sword idle.fbx", "fighter/great sword attack.fbx", (-160, 0, 0)),
("CreatureSkeletalSmoke", "creature/Maw J Laygo.fbx", "creature/mutant walking.fbx",
"creature/mutant idle.fbx", "creature/mutant swiping.fbx", (160, 0, 0)),
):
actor = scene.skeletal_model(base / model, name=name, location=position,
animations={"walk": base / walk, "idle": base / idle, "attack": base / attack})
child, imported = scene._skeletal_models[actor.get_path_name()]
assert isinstance(child, unreal.SkeletalMeshActor)
assert isinstance(imported.mesh, unreal.SkeletalMesh)
for alias in ("walk", "idle", "attack"):
clip = imported.animations[alias]
assert clip.get_editor_property("skeleton") == imported.mesh.get_editor_property("skeleton")
assert clip.get_editor_property("sequence_length") > 0
if alias == "walk":
tracks = unreal.AnimationLibrary.get_animation_track_names(clip)
assert tracks, "Walk has no bone tracks"
poses_differ = False
for bone in tracks:
first = unreal.AnimationLibrary.get_bone_pose_for_frame(clip, bone, 0, False)
later = unreal.AnimationLibrary.get_bone_pose_for_frame(clip, bone, 10, False)
if not first.rotation.equals(later.rotation, 0.001):
poses_differ = True
break
assert poses_differ, "Walk has no changing bone rotations"
characters.append(actor)
if name == "FighterSkeletalSmoke":
# Bundled sources are FBX 6.1, too old for Blender's FBX importer.
# Export modern FBXs for the Blender round-trip fixture.
folder = PROJECT / "build/SkeletalFixture"
folder.mkdir(parents=True, exist_ok=True)
for obj, filename in ((imported.mesh, "fighter.fbx"), (imported.animations["walk"], "walk.fbx")):
task = unreal.AssetExportTask()
task.set_editor_property("object", obj)
task.set_editor_property("filename", str(folder / filename))
task.set_editor_property("automated", True)
task.set_editor_property("prompt", False)
task.set_editor_property("replace_identical", True)
task.set_editor_property("options", unreal.FbxExportOption())
assert unreal.Exporter.run_asset_export_task(task)
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2))
lighting.studio(scene)
cam = camera.create(scene, location=(600, -850, 400), target=(0, 0, 100), fov=55)
shot = Sequence(scene, frames=48)
shot.camera(cam)
for actor in characters:
first = shot.animation(actor, "walk", start=0, end=16)
second = shot.animation(actor, "idle", start=16, end=32, speed=0.75)
shot.animation(actor, "attack", start=32, end=48)
assert first.get_editor_property("params").animation is not None
assert abs(second.get_editor_property("params").play_rate.to_fixed_play_rate() - 0.75) < 1e-6
shot.transform_key(actor, 0, rotation=(0, 0, 0))
shot.transform_key(actor, 47, rotation=(0, 0, 90))
shot.save(open_editor=False)
summary = {"characters": [actor.get_actor_label() for actor in characters], "frames": shot.frames}
(PROJECT / "Saved/Workshop/skeletal_smoke.json").write_text(json.dumps(summary), encoding="utf-8")
renderer = Renderer(width=640, height=360, samples=1, warmup=0, mp4=False)
+21
View File
@@ -0,0 +1,21 @@
"""UE integration test after tools/create_skeletal_fixture.py was run in Blender."""
import unreal
from ue_workshop import Scene, Sequence, Renderer, camera, lighting
from ue_workshop.paths import PROJECT
scene = Scene("SkeletalBlenderSmoke")
actor = scene.skeletal_model(PROJECT / "build/SkeletalFixture/edited_fighter.blend",
name="EditedFighterSmoke", animations={})
child, imported = scene._skeletal_models[actor.get_path_name()]
assert isinstance(imported.mesh, unreal.SkeletalMesh)
assert len(imported.animations) == 1
assert next(iter(imported.animations.values())).get_editor_property("sequence_length") > 0
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2))
lighting.studio(scene)
cam = camera.create(scene)
shot = Sequence(scene, frames=24)
shot.camera(cam)
shot.animation(actor)
shot.save(open_editor=False)
renderer = Renderer(width=640, height=360, samples=1, warmup=0, mp4=False)
+42
View File
@@ -0,0 +1,42 @@
"""UE integration: teaching orbit, two-clip starter and complete animation catalogs."""
import json
import math
import runpy
from ue_workshop import Renderer
from ue_workshop.paths import PROJECT
from ue_workshop.characters import FIGHTER_MODEL, CREATURE_MODEL, _clips
fighter_clips, creature_clips = _clips(FIGHTER_MODEL), _clips(CREATURE_MODEL)
basics = runpy.run_path(str(PROJECT / "exercises/basics.py"))
shot = basics["shot"]
assert shot.frames == 192
channels = shot._transforms[basics["cam"].get_path_name()]
x = [key.get_value() for key in channels[0].get_keys()]
y = [key.get_value() for key in channels[1].get_keys()]
yaw = [key.get_value() for key in channels[5].get_keys()]
assert len(x) == shot.frames == len(yaw)
assert abs(yaw[-1] - yaw[0] - 360) < 0.001
assert all(b > a for a, b in zip(yaw, yaw[1:])), "Camera reverses direction"
assert abs(x[0] - x[-1]) < 0.001 and abs(y[0] - y[-1]) < 0.001
assert all(abs(math.hypot(px, py) - basics["radius"]) < 0.01 for px, py in zip(x, y))
starter = runpy.run_path(str(PROJECT / "exercises/animation_starter.py"))
assert starter["shot"].frames == 360
models = starter["scene"]._skeletal_models
assert len(models) == 1
assert set(next(iter(models.values()))[1].animations) == {"mutant_idle", "mutant_swiping"}
example = runpy.run_path(str(PROJECT / "examples/mixamo_characters.py"))
for actor_name, clips in (("fighter", fighter_clips), ("creature", creature_clips)):
actor = example[actor_name]
child, imported = example["scene"]._skeletal_models[actor.get_path_name()]
assert set(imported.animations) == set(clips)
assert all(clip.get_editor_property("sequence_length") > 0 for clip in imported.animations.values())
summary = {"fighter_clips": len(fighter_clips), "creature_clips": len(creature_clips),
"orbit_frames": basics["shot"].frames, "starter_frames": starter["shot"].frames}
(PROJECT / "Saved/Workshop/workshop_files_smoke.json").write_text(json.dumps(summary), encoding="utf-8")
shot = example["shot"]
renderer = Renderer(width=640, height=360, samples=1, warmup=0, mp4=False)