advanced example
This commit is contained in:
1 parent
8c0a639fba
commit
6fe57915e8
99 files changed
+1329
-56
No files matched your search
+22
-1
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user