diff --git a/assets/imported/animations/Disappointed.fbx b/assets/imported/animations/Disappointed.fbx new file mode 100644 index 0000000..ffa60d8 Binary files /dev/null and b/assets/imported/animations/Disappointed.fbx differ diff --git a/assets/imported/animations/Idle.fbx b/assets/imported/animations/Idle.fbx new file mode 100644 index 0000000..9cd726c Binary files /dev/null and b/assets/imported/animations/Idle.fbx differ diff --git a/assets/imported/animations/Jumping.fbx b/assets/imported/animations/Jumping.fbx new file mode 100644 index 0000000..e9ad892 Binary files /dev/null and b/assets/imported/animations/Jumping.fbx differ diff --git a/assets/imported/animations/Kneeling Down.fbx b/assets/imported/animations/Kneeling Down.fbx new file mode 100644 index 0000000..60b1ec3 Binary files /dev/null and b/assets/imported/animations/Kneeling Down.fbx differ diff --git a/assets/imported/animations/Kneeling.fbx b/assets/imported/animations/Kneeling.fbx new file mode 100644 index 0000000..7a8e945 Binary files /dev/null and b/assets/imported/animations/Kneeling.fbx differ diff --git a/assets/imported/animations/Lifting.fbx b/assets/imported/animations/Lifting.fbx new file mode 100644 index 0000000..0e5f041 Binary files /dev/null and b/assets/imported/animations/Lifting.fbx differ diff --git a/assets/imported/animations/Lying Down.fbx b/assets/imported/animations/Lying Down.fbx new file mode 100644 index 0000000..d27aeee Binary files /dev/null and b/assets/imported/animations/Lying Down.fbx differ diff --git a/assets/imported/animations/Male Crouch Pose.fbx b/assets/imported/animations/Male Crouch Pose.fbx new file mode 100644 index 0000000..ca03d24 Binary files /dev/null and b/assets/imported/animations/Male Crouch Pose.fbx differ diff --git a/assets/imported/animations/Male Laying Pose(1).fbx b/assets/imported/animations/Male Laying Pose(1).fbx new file mode 100644 index 0000000..528578a Binary files /dev/null and b/assets/imported/animations/Male Laying Pose(1).fbx differ diff --git a/assets/imported/animations/Male Laying Pose.fbx b/assets/imported/animations/Male Laying Pose.fbx new file mode 100644 index 0000000..8344478 Binary files /dev/null and b/assets/imported/animations/Male Laying Pose.fbx differ diff --git a/assets/imported/animations/Running Jump.fbx b/assets/imported/animations/Running Jump.fbx new file mode 100644 index 0000000..af6aadb Binary files /dev/null and b/assets/imported/animations/Running Jump.fbx differ diff --git a/assets/imported/animations/Running.fbx b/assets/imported/animations/Running.fbx new file mode 100644 index 0000000..b74e946 Binary files /dev/null and b/assets/imported/animations/Running.fbx differ diff --git a/assets/imported/animations/Sit To Stand.fbx b/assets/imported/animations/Sit To Stand.fbx new file mode 100644 index 0000000..72b5a52 Binary files /dev/null and b/assets/imported/animations/Sit To Stand.fbx differ diff --git a/assets/imported/animations/Sitting Idle(1).fbx b/assets/imported/animations/Sitting Idle(1).fbx new file mode 100644 index 0000000..84aa0ef Binary files /dev/null and b/assets/imported/animations/Sitting Idle(1).fbx differ diff --git a/assets/imported/animations/Sitting Idle.fbx b/assets/imported/animations/Sitting Idle.fbx new file mode 100644 index 0000000..876ceac Binary files /dev/null and b/assets/imported/animations/Sitting Idle.fbx differ diff --git a/assets/imported/animations/Sitting Talking(1).fbx b/assets/imported/animations/Sitting Talking(1).fbx new file mode 100644 index 0000000..22a6eae Binary files /dev/null and b/assets/imported/animations/Sitting Talking(1).fbx differ diff --git a/assets/imported/animations/Sitting Talking.fbx b/assets/imported/animations/Sitting Talking.fbx new file mode 100644 index 0000000..f191cc4 Binary files /dev/null and b/assets/imported/animations/Sitting Talking.fbx differ diff --git a/assets/imported/animations/Sitting.fbx b/assets/imported/animations/Sitting.fbx new file mode 100644 index 0000000..fdff456 Binary files /dev/null and b/assets/imported/animations/Sitting.fbx differ diff --git a/assets/imported/animations/Sneaking Forward.fbx b/assets/imported/animations/Sneaking Forward.fbx new file mode 100644 index 0000000..98633cd Binary files /dev/null and b/assets/imported/animations/Sneaking Forward.fbx differ diff --git a/assets/imported/animations/Sprint.fbx b/assets/imported/animations/Sprint.fbx new file mode 100644 index 0000000..0fd0086 Binary files /dev/null and b/assets/imported/animations/Sprint.fbx differ diff --git a/assets/imported/animations/Sprinting Forward Roll.fbx b/assets/imported/animations/Sprinting Forward Roll.fbx new file mode 100644 index 0000000..a122803 Binary files /dev/null and b/assets/imported/animations/Sprinting Forward Roll.fbx differ diff --git a/assets/imported/animations/Stand To Roll.fbx b/assets/imported/animations/Stand To Roll.fbx new file mode 100644 index 0000000..b38b7f4 Binary files /dev/null and b/assets/imported/animations/Stand To Roll.fbx differ diff --git a/assets/imported/animations/Stand To Sit.fbx b/assets/imported/animations/Stand To Sit.fbx new file mode 100644 index 0000000..2b03ad6 Binary files /dev/null and b/assets/imported/animations/Stand To Sit.fbx differ diff --git a/assets/imported/animations/Standing Up(1).fbx b/assets/imported/animations/Standing Up(1).fbx new file mode 100644 index 0000000..422f926 Binary files /dev/null and b/assets/imported/animations/Standing Up(1).fbx differ diff --git a/assets/imported/animations/Standing Up.fbx b/assets/imported/animations/Standing Up.fbx new file mode 100644 index 0000000..5ea7fab Binary files /dev/null and b/assets/imported/animations/Standing Up.fbx differ diff --git a/assets/imported/animations/Standing.fbx b/assets/imported/animations/Standing.fbx new file mode 100644 index 0000000..42a68a4 Binary files /dev/null and b/assets/imported/animations/Standing.fbx differ diff --git a/assets/imported/animations/T-Pose.fbx b/assets/imported/animations/T-Pose.fbx new file mode 100644 index 0000000..f5b5bb6 Binary files /dev/null and b/assets/imported/animations/T-Pose.fbx differ diff --git a/assets/imported/animations/Talking(1).fbx b/assets/imported/animations/Talking(1).fbx new file mode 100644 index 0000000..a2a07ee Binary files /dev/null and b/assets/imported/animations/Talking(1).fbx differ diff --git a/assets/imported/animations/Talking.fbx b/assets/imported/animations/Talking.fbx new file mode 100644 index 0000000..a5bd6c1 Binary files /dev/null and b/assets/imported/animations/Talking.fbx differ diff --git a/assets/imported/animations/Walking.fbx b/assets/imported/animations/Walking.fbx new file mode 100644 index 0000000..3730473 Binary files /dev/null and b/assets/imported/animations/Walking.fbx differ diff --git a/assets/imported/animations/Yelling.fbx b/assets/imported/animations/Yelling.fbx new file mode 100644 index 0000000..cad4092 Binary files /dev/null and b/assets/imported/animations/Yelling.fbx differ diff --git a/assets/imported/models/hero.fbx b/assets/imported/models/hero.fbx new file mode 100644 index 0000000..2df2e06 Binary files /dev/null and b/assets/imported/models/hero.fbx differ diff --git a/blight-assets/src/main/resources/Characters/Models/hero.j3o b/blight-assets/src/main/resources/Characters/Models/hero.j3o new file mode 100644 index 0000000..ba61881 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/Models/hero.j3o differ diff --git a/blight-assets/src/main/resources/Models/Chars/mainchar.j3o b/blight-assets/src/main/resources/Characters/Models/mainchar.j3o similarity index 100% rename from blight-assets/src/main/resources/Models/Chars/mainchar.j3o rename to blight-assets/src/main/resources/Characters/Models/mainchar.j3o diff --git a/blight-assets/src/main/resources/Models/Chars/silas.j3o b/blight-assets/src/main/resources/Characters/Models/silas.j3o similarity index 100% rename from blight-assets/src/main/resources/Models/Chars/silas.j3o rename to blight-assets/src/main/resources/Characters/Models/silas.j3o diff --git a/blight-assets/src/main/resources/Models/Chars/silas.j3o.meta b/blight-assets/src/main/resources/Characters/Models/silas.j3o.meta similarity index 100% rename from blight-assets/src/main/resources/Models/Chars/silas.j3o.meta rename to blight-assets/src/main/resources/Characters/Models/silas.j3o.meta diff --git a/blight-assets/src/main/resources/character/bjoernson.character b/blight-assets/src/main/resources/Characters/bjoernson.character similarity index 100% rename from blight-assets/src/main/resources/character/bjoernson.character rename to blight-assets/src/main/resources/Characters/bjoernson.character diff --git a/blight-assets/src/main/resources/Characters/clips/Disappointed.j3o b/blight-assets/src/main/resources/Characters/clips/Disappointed.j3o new file mode 100644 index 0000000..7c703c1 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Disappointed.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Idle.j3o b/blight-assets/src/main/resources/Characters/clips/Idle.j3o new file mode 100644 index 0000000..6ce8adb Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Idle.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Jumping.j3o b/blight-assets/src/main/resources/Characters/clips/Jumping.j3o new file mode 100644 index 0000000..1ffbf8f Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Jumping.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Kneeling Down.j3o b/blight-assets/src/main/resources/Characters/clips/Kneeling Down.j3o new file mode 100644 index 0000000..8d050a7 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Kneeling Down.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Kneeling.j3o b/blight-assets/src/main/resources/Characters/clips/Kneeling.j3o new file mode 100644 index 0000000..f1820e6 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Kneeling.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Lifting.j3o b/blight-assets/src/main/resources/Characters/clips/Lifting.j3o new file mode 100644 index 0000000..f8382a1 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Lifting.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Lying Down.j3o b/blight-assets/src/main/resources/Characters/clips/Lying Down.j3o new file mode 100644 index 0000000..4422c9a Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Lying Down.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Male Crouch Pose.j3o b/blight-assets/src/main/resources/Characters/clips/Male Crouch Pose.j3o new file mode 100644 index 0000000..3c6fe5f Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Male Crouch Pose.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Male Laying Pose(1).j3o b/blight-assets/src/main/resources/Characters/clips/Male Laying Pose(1).j3o new file mode 100644 index 0000000..934e0f6 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Male Laying Pose(1).j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Male Laying Pose.j3o b/blight-assets/src/main/resources/Characters/clips/Male Laying Pose.j3o new file mode 100644 index 0000000..705d7c8 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Male Laying Pose.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Running Jump.j3o b/blight-assets/src/main/resources/Characters/clips/Running Jump.j3o new file mode 100644 index 0000000..ee35998 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Running Jump.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Running.j3o b/blight-assets/src/main/resources/Characters/clips/Running.j3o new file mode 100644 index 0000000..3b4558a Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Running.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o b/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o new file mode 100644 index 0000000..15d4b18 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Idle(1).j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Idle(1).j3o new file mode 100644 index 0000000..9adff1d Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sitting Idle(1).j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o new file mode 100644 index 0000000..c9b4d27 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Talking(1).j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Talking(1).j3o new file mode 100644 index 0000000..7b2ce22 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sitting Talking(1).j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Talking.j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Talking.j3o new file mode 100644 index 0000000..13258df Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sitting Talking.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting.j3o b/blight-assets/src/main/resources/Characters/clips/Sitting.j3o new file mode 100644 index 0000000..adc437e Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sitting.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o b/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o new file mode 100644 index 0000000..6157ad5 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sprint.j3o b/blight-assets/src/main/resources/Characters/clips/Sprint.j3o new file mode 100644 index 0000000..bbea304 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sprint.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sprinting Forward Roll.j3o b/blight-assets/src/main/resources/Characters/clips/Sprinting Forward Roll.j3o new file mode 100644 index 0000000..f6ab96c Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sprinting Forward Roll.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Stand To Roll.j3o b/blight-assets/src/main/resources/Characters/clips/Stand To Roll.j3o new file mode 100644 index 0000000..7b980c3 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Stand To Roll.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o b/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o new file mode 100644 index 0000000..79b875e Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing Up(1).j3o b/blight-assets/src/main/resources/Characters/clips/Standing Up(1).j3o new file mode 100644 index 0000000..56f4e58 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Standing Up(1).j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing Up.j3o b/blight-assets/src/main/resources/Characters/clips/Standing Up.j3o new file mode 100644 index 0000000..b658032 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Standing Up.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing.j3o b/blight-assets/src/main/resources/Characters/clips/Standing.j3o new file mode 100644 index 0000000..451efcd Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Standing.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o b/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o new file mode 100644 index 0000000..577f606 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Talking(1).j3o b/blight-assets/src/main/resources/Characters/clips/Talking(1).j3o new file mode 100644 index 0000000..07114d2 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Talking(1).j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Talking.j3o b/blight-assets/src/main/resources/Characters/clips/Talking.j3o new file mode 100644 index 0000000..3fd1410 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Talking.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Walking.j3o b/blight-assets/src/main/resources/Characters/clips/Walking.j3o new file mode 100644 index 0000000..a3d66bd Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Walking.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Yelling.j3o b/blight-assets/src/main/resources/Characters/clips/Yelling.j3o new file mode 100644 index 0000000..37cc7a3 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Yelling.j3o differ diff --git a/blight-assets/src/main/resources/character/hero.character b/blight-assets/src/main/resources/Characters/hero.character similarity index 100% rename from blight-assets/src/main/resources/character/hero.character rename to blight-assets/src/main/resources/Characters/hero.character diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json new file mode 100644 index 0000000..7449c8b --- /dev/null +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -0,0 +1,56 @@ +{ + "clips": [ + "Disappointed", + "Idle", + "Jumping", + "Kneeling", + "Kneeling Down", + "Lifting", + "Lying Down", + "Male Crouch Pose", + "Male Laying Pose", + "Male Laying Pose(1)", + "Running", + "Running Jump", + "Sit To Stand", + "Sitting", + "Sitting Idle", + "Sitting Idle(1)", + "Sitting Talking", + "Sitting Talking(1)", + "Sneaking Forward", + "Sprint", + "Sprinting Forward Roll", + "Stand To Roll", + "Stand To Sit", + "Standing", + "Standing Up", + "Standing Up(1)", + "T-Pose", + "Talking", + "Talking(1)", + "Walking", + "Yelling" + ], + "actionMap": {}, + "previewModelPath": "Characters/Models/hero.j3o", + "animOffsets": { + "sitting": { + "tx": 0.0, + "ty": 0.0, + "tz": -0.5, + "rx": 0.0, + "ry": 0.0, + "rz": 0.0 + }, + "get_up_sitting": { + "tx": 0.0, + "ty": 0.0, + "tz": -0.5, + "rx": 0.0, + "ry": 0.0, + "rz": 0.0 + } + }, + "subClips": {} +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/character/silas.character b/blight-assets/src/main/resources/Characters/silas.character similarity index 100% rename from blight-assets/src/main/resources/character/silas.character rename to blight-assets/src/main/resources/Characters/silas.character diff --git a/blight-assets/src/main/resources/animations/clips/alive_again.j3o b/blight-assets/src/main/resources/animations/clips/alive_again.j3o deleted file mode 100644 index 33dcfcc..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/alive_again.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/disappointed.j3o b/blight-assets/src/main/resources/animations/clips/disappointed.j3o deleted file mode 100644 index 71bdf76..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/disappointed.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/idle.j3o b/blight-assets/src/main/resources/animations/clips/idle.j3o deleted file mode 100644 index aac8cab..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/idle.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/idle_jump.j3o b/blight-assets/src/main/resources/animations/clips/idle_jump.j3o deleted file mode 100644 index 9f99790..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/idle_jump.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/pickup.j3o b/blight-assets/src/main/resources/animations/clips/pickup.j3o deleted file mode 100644 index 394b217..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/pickup.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/running.j3o b/blight-assets/src/main/resources/animations/clips/running.j3o deleted file mode 100644 index 5a89ac2..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/running.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/running_jump.j3o b/blight-assets/src/main/resources/animations/clips/running_jump.j3o deleted file mode 100644 index 228a147..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/running_jump.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/sit_down.j3o b/blight-assets/src/main/resources/animations/clips/sit_down.j3o deleted file mode 100644 index 42aaaea..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/sit_down.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o b/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o deleted file mode 100644 index d6d0833..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/sitting_talking.j3o b/blight-assets/src/main/resources/animations/clips/sitting_talking.j3o deleted file mode 100644 index 432c04d..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/sitting_talking.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/sprinting.j3o b/blight-assets/src/main/resources/animations/clips/sprinting.j3o deleted file mode 100644 index d184823..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/sprinting.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/stand_up.j3o b/blight-assets/src/main/resources/animations/clips/stand_up.j3o deleted file mode 100644 index 1812335..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/stand_up.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/talking1.j3o b/blight-assets/src/main/resources/animations/clips/talking1.j3o deleted file mode 100644 index 9802e45..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/talking1.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/talking2.j3o b/blight-assets/src/main/resources/animations/clips/talking2.j3o deleted file mode 100644 index c0049c6..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/talking2.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/tpose.j3o b/blight-assets/src/main/resources/animations/clips/tpose.j3o deleted file mode 100644 index 8da9796..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/tpose.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/walking.j3o b/blight-assets/src/main/resources/animations/clips/walking.j3o deleted file mode 100644 index 1bdf353..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/walking.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/yelling.j3o b/blight-assets/src/main/resources/animations/clips/yelling.j3o deleted file mode 100644 index f9f1bbb..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/yelling.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/sets/human.animset.json b/blight-assets/src/main/resources/animations/sets/human.animset.json deleted file mode 100644 index cd39e4d..0000000 --- a/blight-assets/src/main/resources/animations/sets/human.animset.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "clips": [ - "alive_again", - "idle", - "idle_jump", - "pickup", - "running", - "running_jump", - "sit_down_bench", - "sitting_bench", - "sitting_floor", - "sprinting", - "stand_up", - "stand_up_bench", - "tpose", - "walking", - "sitting_talking" - ], - "actionMap": { - "DEFAULT": "tpose", - "IDLE": "idle", - "WALK": "walking", - "RUN": "running", - "SPRINT": "sprinting", - "JUMP": "idle_jump", - "RUNNING_JUMP": "running_jump", - "PICK_UP": "pickup", - "SIT_DOWN": "sit_down_bench", - "SIT_UP": "stand_up_bench", - "SITTING": "sitting_bench", - "REVIVE": "alive_again" - }, - "previewModelPath": "Models/Chars/mainchar.j3o", - "animOffsets": { - "sitting": { - "tx": 0.0, - "ty": 0.0, - "tz": -0.5, - "rx": 0.0, - "ry": 0.0, - "rz": 0.0 - }, - "get_up_sitting": { - "tx": 0.0, - "ty": 0.0, - "tz": -0.5, - "rx": 0.0, - "ry": 0.0, - "rz": 0.0 - } - }, - "subClips": {} -} \ No newline at end of file diff --git a/blight-editor/build.gradle b/blight-editor/build.gradle index 5ae8257..fe6a69d 100644 --- a/blight-editor/build.gradle +++ b/blight-editor/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}" implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}" implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}" + implementation 'com.github.stephengold:MonkeyWrench:1.0.0' implementation 'com.google.code.gson:gson:2.11.0' implementation 'org.slf4j:slf4j-api:2.0.17' implementation 'org.slf4j:jul-to-slf4j:2.0.17' diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 6147594..82d9ab1 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -190,7 +190,6 @@ public class EditorApp extends Application { private ListView animClipListView; private Label animPreviewStatusLabel; private ComboBox animPreviewModelCombo; - private ComboBox addAnimComboField; private double animPrevDragX, animPrevDragY; // Cache: Menge aller j3o-Pfade die ein Skelett besitzen (null = Scan noch nicht abgeschlossen) private java.util.Set skeletalModelPaths = null; @@ -413,6 +412,7 @@ public class EditorApp extends Application { private TreeItem jmeModelsNode; private TreeItem jmeTexturesNode; private TreeItem animationsNode; + private TreeItem charactersNode; private TreeItem itemsNode; // ── JavaFX Entry-Point ─────────────────────────────────────────────────── @@ -620,8 +620,8 @@ public class EditorApp extends Application { } if (input.animImportCompleted) { input.animImportCompleted = false; - refreshAddAnimCombo(addAnimComboField); - refreshCategoryNode(animationsNode, shouldPreserveExpansion()); + if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion()); + if (animationsNode != null) refreshCategoryNode(animationsNode, shouldPreserveExpansion()); } String animOp = input.animOpStatus; @@ -630,6 +630,33 @@ public class EditorApp extends Application { if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp); } + SharedInput.CharModelImportResult charImport = input.charModelImportResult.getAndSet(null); + if (charImport != null) { + if (charImport.error() != null) { + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR); + alert.initOwner(primaryStage); + alert.setTitle("Character-Import fehlgeschlagen"); + alert.setHeaderText("FBX konnte nicht als Character-Modell importiert werden"); + alert.setContentText(charImport.error()); + alert.showAndWait(); + if (charEditorStatusLabel != null) + charEditorStatusLabel.setText("Import fehlgeschlagen"); + } else { + String relPath = charImport.relPath(); + if (charModelCombo != null && !charModelCombo.getItems().contains(relPath)) { + charModelCombo.getItems().add(relPath); + } + if (charModelCombo != null) charModelCombo.setValue(relPath); + if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion()); + if (animSetModelCombo != null && !animSetModelCombo.getItems().contains(relPath)) + animSetModelCombo.getItems().add(relPath); + if (charEditorStatusLabel != null) + charEditorStatusLabel.setText("Modell importiert: " + relPath); + setStatus("Character-Modell importiert: " + relPath); + } + } + String embedStatus = input.animEmbedStatus; if (embedStatus != null) { input.animEmbedStatus = null; @@ -663,9 +690,9 @@ public class EditorApp extends Application { if (input.refreshAssets) { input.refreshAssets = false; boolean pe = shouldPreserveExpansion(); - refreshCategoryNode(modelsNode, pe); - refreshCategoryNode(animationsNode, pe); - refreshAddAnimCombo(addAnimComboField); + if (modelsNode != null) refreshCategoryNode(modelsNode, pe); + if (animationsNode != null) refreshCategoryNode(animationsNode, pe); + if (charactersNode != null) refreshCategoryNode(charactersNode, pe); } if (input.refreshTreeFolders) { @@ -1494,26 +1521,29 @@ public class EditorApp extends Application { MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…"); MenuItem importAudioItem = new MenuItem("Audio…"); MenuItem importAnimItem = new MenuItem("Animationen…"); - importModelLodItem.setOnAction(e -> openModelImport(primaryStage)); - importTexItem.setOnAction(e -> handleTextureImport(primaryStage)); - importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); - importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); - importAudioItem.setOnAction(e -> handleAudioImport(primaryStage)); - importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage)); + MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…"); + importModelLodItem.setOnAction(e -> openModelImport(primaryStage)); + importTexItem.setOnAction(e -> handleTextureImport(primaryStage)); + importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); + importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); + importAudioItem.setOnAction(e -> handleAudioImport(primaryStage)); + importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage)); + importCharModelItem.setOnAction(e -> handleCharacterModelImport(primaryStage)); importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem, - importTexSetItem, importAudioItem, importAnimItem); + importTexSetItem, importAudioItem, importAnimItem, + new javafx.scene.control.SeparatorMenuItem(), importCharModelItem); Menu toolsMenu = new Menu("Werkzeuge"); MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)"); MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)"); - MenuItem animPrevItem = new MenuItem("Animationseditor"); + MenuItem animPrevItem = new MenuItem("Character-Editor"); MenuItem objEditorItem = new MenuItem("Model Editor"); MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung"); vegetationsItem.setOnAction(e -> switchToVegetationGenerator()); ezTreeItem.setOnAction(e -> switchToEzTree()); tripoItem.setOnAction(e -> switchToTripo()); - animPrevItem.setOnAction(e -> switchToAnimPreview()); + animPrevItem.setOnAction(e -> openCharacterEditor()); objEditorItem.setOnAction(e -> openModelEditor(null, null)); thumbItem.setOnAction(e -> switchToThumbnailManager()); toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem, @@ -3410,7 +3440,7 @@ public class EditorApp extends Application { ComboBox animCombo = new ComboBox<>(); animCombo.setMaxWidth(Double.MAX_VALUE); animCombo.getItems().add("(keine)"); - Path animDir = ASSET_ROOT.resolve("animations"); + Path animDir = ASSET_ROOT.resolve("Characters").resolve("clips"); if (java.nio.file.Files.isDirectory(animDir)) { try (java.util.stream.Stream walk = java.nio.file.Files.walk(animDir)) { walk.filter(p -> p.toString().endsWith(".j3o")) @@ -5069,6 +5099,7 @@ public class EditorApp extends Application { assetTree = tree; tree.setShowRoot(false); VBox.setVgrow(tree, Priority.ALWAYS); + tree.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); tree.setCellFactory(tv -> buildAssetCell()); // F2 → Umbenennen @@ -5076,7 +5107,8 @@ public class EditorApp extends Application { if (e.getCode() != KeyCode.F2) return; TreeItem sel = tree.getSelectionModel().getSelectedItem(); if (sel == null || sel == modelsNode || sel == texturesNode - || sel == audioNode || sel == animationsNode || sel == itemsNode) return; + || sel == audioNode || sel == animationsNode || sel == itemsNode + || sel == charactersNode) return; renameAsset(sel); e.consume(); }); @@ -5116,9 +5148,9 @@ public class EditorApp extends Application { boolean isSkeletal = skeletalModelPaths == null /* noch nicht gescannt – zulassen */ || skeletalModelPaths.contains(relPath); - // Im Animationseditor: j3o aus Models/ oder animations/ direkt laden + // Im Animations Viewer: j3o direkt laden if ("animpreview".equals(currentTool) - && (cat == modelsNode || cat == animationsNode) + && (cat == modelsNode || cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o")) { if (!isSkeletal) { setStatus("Kein Skelett: " + relPath); return; } input.animPreviewLoadPath = relPath; @@ -5148,7 +5180,7 @@ public class EditorApp extends Application { if (objPlaceBtn != null) objPlaceBtn.setSelected(true); root.setRight(buildObjectPlacePanel()); setStatus("Textur: " + relPath + " | Wird auf platzierte Primitive angewendet"); - } else if (cat == animationsNode && relPath.endsWith(".j3o") && isSkeletal) { + } else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) { switchToAnimPreview(); input.animPreviewLoadPath = relPath; if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); @@ -5179,7 +5211,10 @@ public class EditorApp extends Application { miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow())); miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow())); miAnim.setOnAction(ev -> handleAnimationImport(importBtn.getScene().getWindow())); - popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim); + MenuItem miCharModel = new MenuItem("Character-Modell (FBX)…"); + miCharModel.setOnAction(ev -> handleCharacterModelImport(importBtn.getScene().getWindow())); + popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim, + new javafx.scene.control.SeparatorMenuItem(), miCharModel); popup.show(importBtn, javafx.geometry.Side.TOP, 0, 0); }); @@ -5396,7 +5431,7 @@ public class EditorApp extends Application { if (jmePaths.containsKey(item) || jmeFolderNodes.contains(item)) return; // Kategorie-Wurzeln (Models, Textures, …) nicht verschiebbar if (item == modelsNode || item == texturesNode || item == audioNode - || item == animationsNode || item == itemsNode) return; + || item == animationsNode || item == itemsNode || item == charactersNode) return; draggedItem = item; Dragboard db = cell.startDragAndDrop(TransferMode.MOVE); ClipboardContent cc = new ClipboardContent(); @@ -5477,6 +5512,53 @@ public class EditorApp extends Application { if (jmeFolderNodes.contains(item) || jmePaths.containsKey(item)) return; ContextMenu ctx = new ContextMenu(); + // ── Multi-Select: Auswahl löschen ──────────────────────────────── + var selectedItems = assetTree.getSelectionModel().getSelectedItems(); + java.util.List> deletableSelection = selectedItems.stream() + .filter(si -> si != null && !isAssetFolder(si) + && itemPaths.containsKey(si) + && !jmePaths.containsKey(si) + && !jmeFolderNodes.contains(si)) + .collect(java.util.stream.Collectors.toList()); + if (deletableSelection.size() > 1) { + MenuItem multiDel = new MenuItem("🗑 Auswahl löschen (" + deletableSelection.size() + " Dateien)"); + multiDel.setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;"); + multiDel.setOnAction(ev -> { + StringBuilder names = new StringBuilder(); + for (int i = 0; i < Math.min(5, deletableSelection.size()); i++) { + names.append(" ").append(deletableSelection.get(i).getValue()).append('\n'); + } + if (deletableSelection.size() > 5) + names.append(" … und ").append(deletableSelection.size() - 5).append(" weitere"); + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + deletableSelection.size() + " Dateien löschen?\n\n" + names, + ButtonType.OK, ButtonType.CANCEL); + confirm.initOwner(primaryStage); + confirm.setHeaderText(null); + confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { + int deleted = 0; + for (TreeItem si : new java.util.ArrayList<>(deletableSelection)) { + Path p = itemPaths.get(si); + if (p == null) continue; + try { + Files.deleteIfExists(p); + if (p.toString().endsWith(".j3o")) deleteJ3oSideFiles(p); + si.getParent().getChildren().remove(si); + itemPaths.remove(si); + deleted++; + } catch (IOException ex) { + setStatus("Fehler beim Löschen: " + p.getFileName()); + } + } + setStatus(deleted + " Datei(en) gelöscht"); + }); + }); + ctx.getItems().addAll(multiDel, new SeparatorMenuItem()); + ctx.show(cell, e.getScreenX(), e.getScreenY()); + e.consume(); + return; + } + // Texture-Set-Knoten: eigenes Menü if (textureSetNodes.contains(item)) { Path setDir = itemPaths.get(item); @@ -5527,7 +5609,8 @@ public class EditorApp extends Application { ctx.getItems().add(newSub); boolean isCatRoot = (item == modelsNode || item == texturesNode - || item == audioNode || item == animationsNode || item == itemsNode); + || item == audioNode || item == animationsNode || item == itemsNode + || item == charactersNode); Path dir = itemPaths.get(item); // Ordner unter Models: Zufällig-Platzieren-Eintrag @@ -5554,7 +5637,15 @@ public class EditorApp extends Application { ctx.getItems().add(importAnim); } - Path setsDir = ASSET_ROOT.resolve("animations").resolve("sets"); + if (item == charactersNode) { + MenuItem importChar = new MenuItem("⊕ Character-Modell importieren (FBX)…"); + importChar.setOnAction(ev -> handleCharacterModelImport(ctx.getOwnerWindow())); + MenuItem importAnim = new MenuItem("⊕ Animation hinzufügen…"); + importAnim.setOnAction(ev -> handleAnimationImport(ctx.getOwnerWindow())); + ctx.getItems().addAll(importChar, importAnim); + } + + Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets"); if (dir != null && setsDir.equals(dir)) { MenuItem newSet = new MenuItem("➕ Neues Set…"); newSet.setOnAction(ev -> createNewAnimSet()); @@ -5714,11 +5805,11 @@ public class EditorApp extends Application { return p != null && Files.isDirectory(p); } - /** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode). */ + /** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode / charactersNode). */ private TreeItem getCategoryRoot(TreeItem item) { for (TreeItem cur = item; cur != null; cur = cur.getParent()) if (cur == modelsNode || cur == texturesNode || cur == audioNode - || cur == animationsNode || cur == itemsNode) return cur; + || cur == animationsNode || cur == itemsNode || cur == charactersNode) return cur; return null; } @@ -6161,6 +6252,7 @@ public class EditorApp extends Application { private void populateAssetTree(TreeItem root) { modelsNode = null; texturesNode = null; audioNode = null; animationsNode = null; itemsNode = null; + charactersNode = null; jmeModelsNode = null; jmeTexturesNode = null; jmeFolderNodes.clear(); textureSetNodes.clear(); @@ -6203,7 +6295,8 @@ public class EditorApp extends Application { animationsNode = node; addAnimClipSubNodes(node); } - case "items" -> itemsNode = node; + case "characters" -> charactersNode = node; + case "items" -> itemsNode = node; } } } @@ -6279,7 +6372,7 @@ public class EditorApp extends Application { private void clearItemPathsFor(TreeItem item) { for (TreeItem child : item.getChildren()) clearItemPathsFor(child); - if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode) + if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode && item != charactersNode) itemPaths.remove(item); textureSetNodes.remove(item); } @@ -10256,6 +10349,63 @@ public class EditorApp extends Application { // ── AnimSet-Editor ──────────────────────────────────────────────────────── + private void openCharacterEditor() { + Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets"); + java.util.List available = new java.util.ArrayList<>(); + if (java.nio.file.Files.isDirectory(setsDir)) { + try (var walk = java.nio.file.Files.walk(setsDir, 1)) { + walk.filter(p -> p.toString().endsWith(".animset.json")) + .map(p -> p.getFileName().toString().replaceFirst("\\.animset\\.json$", "")) + .sorted() + .forEach(available::add); + } catch (IOException ignored) {} + } + + // Dialog: vorhandenes AnimSet wählen oder neues anlegen + javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); + dlg.setTitle("Character-Editor"); + dlg.setHeaderText("AnimSet auswählen oder neu anlegen"); + + ListView listView = new ListView<>(); + listView.getItems().addAll(available); + listView.setPrefHeight(200); + listView.setPrefWidth(320); + + ButtonType openType = new ButtonType("Öffnen", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); + ButtonType newType = new ButtonType("Neu…", javafx.scene.control.ButtonBar.ButtonData.OTHER); + ButtonType cancelType = ButtonType.CANCEL; + dlg.getDialogPane().getButtonTypes().addAll(openType, newType, cancelType); + + javafx.scene.Node openNode = dlg.getDialogPane().lookupButton(openType); + openNode.setDisable(true); + listView.getSelectionModel().selectedItemProperty() + .addListener((obs, ov, nv) -> openNode.setDisable(nv == null)); + listView.setOnMouseClicked(e -> { + if (e.getClickCount() == 2 && listView.getSelectionModel().getSelectedItem() != null) { + dlg.setResult(listView.getSelectionModel().getSelectedItem()); + dlg.close(); + } + }); + + dlg.getDialogPane().setContent(listView); + dlg.setResultConverter(bt -> { + if (bt == openType) return listView.getSelectionModel().getSelectedItem(); + if (bt == newType) return "__NEW__"; + return null; + }); + + dlg.showAndWait().ifPresent(result -> { + if ("__NEW__".equals(result)) { + createNewAnimSet(); + } else if (result != null) { + Path setFile = setsDir.resolve(result + ".animset.json"); + String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/'); + openAnimSetEditor(relPath, setFile); + } + }); + } + private void openAnimSetEditor(String relPath, Path absPath) { String setName = absPath.getFileName().toString().replaceFirst("\\.animset\\.json$", ""); Path setDir = absPath.getParent(); @@ -10327,15 +10477,16 @@ public class EditorApp extends Application { animSetModelCombo = new ComboBox<>(); animSetModelCombo.setMaxWidth(Double.MAX_VALUE); animSetModelCombo.setPromptText("j3o-Datei wählen…"); - for (String dir : new String[]{"Models", "animations"}) { - Path base = ASSET_ROOT.resolve(dir); - if (!java.nio.file.Files.isDirectory(base)) continue; - try (var walk = java.nio.file.Files.walk(base)) { - walk.filter(p -> p.toString().endsWith(".j3o")) - .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) - .sorted() - .forEach(animSetModelCombo.getItems()::add); - } catch (IOException ignored) {} + { + Path base = ASSET_ROOT.resolve("Characters").resolve("Models"); + if (java.nio.file.Files.isDirectory(base)) { + try (var walk = java.nio.file.Files.walk(base)) { + walk.filter(p -> p.toString().endsWith(".j3o")) + .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) + .sorted() + .forEach(animSetModelCombo.getItems()::add); + } catch (IOException ignored) {} + } } // Gespeicherten Modell-Pfad vorauswählen if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) { @@ -10352,7 +10503,10 @@ public class EditorApp extends Application { animSet.setPreviewModelPath(path); animSetDirty = true; }); - inner.getChildren().addAll(animSetModelCombo, loadModelBtn); + Button importCharBtn = new Button("Character-Modell importieren (FBX)…"); + importCharBtn.setMaxWidth(Double.MAX_VALUE); + importCharBtn.setOnAction(e -> handleCharacterModelImport(importCharBtn.getScene().getWindow())); + inner.getChildren().addAll(animSetModelCombo, loadModelBtn, importCharBtn); // Modell beim Öffnen automatisch laden, wenn Pfad bekannt if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) { @@ -10368,7 +10522,7 @@ public class EditorApp extends Application { animSetClipListView.setPrefHeight(180); editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips()); - Path animRootDir = ASSET_ROOT.resolve("animations"); + Path animRootDir = ASSET_ROOT.resolve("Characters"); Button addClipBtn = new Button("+ Hinzufügen…"); Button removeClipBtn = new Button("- Entfernen"); @@ -10411,9 +10565,10 @@ public class EditorApp extends Application { String name = cp.getFileName().toString(); boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb") || name.endsWith(".gltf") || name.endsWith(".fbx"); - boolean notSets = !animRootDir.relativize(cp).startsWith("sets"); - _log.debug("[ClipScan] {} | isAnim={} notSets={}", cp, isAnim, notSets); - return isAnim && notSets; + java.nio.file.Path rel = animRootDir.relativize(cp); + boolean onlyClips = rel.startsWith("clips"); + _log.debug("[ClipScan] {} | isAnim={} onlyClips={}", cp, isAnim, onlyClips); + return isAnim && onlyClips; }) .map(cp -> cp.getFileName().toString().replaceFirst("\\.[^.]+$", "")) .distinct() @@ -10486,7 +10641,12 @@ public class EditorApp extends Application { HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn); HBox.setHgrow(addClipBtn, Priority.ALWAYS); HBox.setHgrow(removeClipBtn, Priority.ALWAYS); - inner.getChildren().addAll(animSetClipListView, clipBtns); + + Button importAnimBtn = new Button("Animation hinzufügen…"); + importAnimBtn.setMaxWidth(Double.MAX_VALUE); + importAnimBtn.setOnAction(e -> handleAnimationImport(importAnimBtn.getScene().getWindow())); + + inner.getChildren().addAll(animSetClipListView, clipBtns, importAnimBtn); // ── Clip-Teile (Sub-Clips) ──────────────────────────────────────────── inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator()); @@ -10818,14 +10978,14 @@ public class EditorApp extends Application { } private String findAnimClipPath(String clipName) { - Path animDir = ASSET_ROOT.resolve("animations"); + Path charDir = ASSET_ROOT.resolve("Characters"); for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) { - if (java.nio.file.Files.exists(animDir.resolve("clips").resolve(clipName + ext))) - return "animations/clips/" + clipName + ext; - if (java.nio.file.Files.exists(animDir.resolve(clipName + ext))) - return "animations/" + clipName + ext; + if (java.nio.file.Files.exists(charDir.resolve("clips").resolve(clipName + ext))) + return "Characters/clips/" + clipName + ext; + if (java.nio.file.Files.exists(charDir.resolve(clipName + ext))) + return "Characters/" + clipName + ext; } - return "animations/clips/" + clipName + ".j3o"; + return "Characters/clips/" + clipName + ".j3o"; } private void showAddActionToSetDialog() { @@ -11041,12 +11201,12 @@ public class EditorApp extends Application { dlg.showAndWait().ifPresent(raw -> { String name = raw.trim(); if (name.isBlank()) return; - Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); + Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); Path setFile = setDir.resolve(name + ".animset.json"); try { Files.createDirectories(setDir); new de.blight.game.animation.AnimSet().save(setDir, name); - refreshCategoryNode(animationsNode, shouldPreserveExpansion()); + refreshCategoryNode(charactersNode, shouldPreserveExpansion()); String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/'); openAnimSetEditor(relPath, setFile); } catch (IOException ex) { @@ -11058,7 +11218,7 @@ public class EditorApp extends Application { private ToolBar buildAnimPreviewToolBar() { Button backBtn = new Button("← Welteneditor"); backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Animationseditor"); + Label label = new Label("Animations Viewer"); label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); ToolBar tb = new ToolBar(); tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); @@ -11069,39 +11229,8 @@ public class EditorApp extends Application { VBox inner = new VBox(8); inner.setPadding(new Insets(10)); - // ── Modell-Auswahl ──────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Modell"), new Separator()); - animPreviewModelCombo = new ComboBox<>(); - animPreviewModelCombo.setMaxWidth(Double.MAX_VALUE); - animPreviewModelCombo.setPromptText("j3o-Datei wählen…"); - for (String dir : new String[]{"Models", "animations"}) { - Path base = ASSET_ROOT.resolve(dir); - if (!java.nio.file.Files.isDirectory(base)) continue; - try (var walk = java.nio.file.Files.walk(base)) { - walk.filter(p -> p.toString().endsWith(".j3o")) - .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) - .sorted() - .forEach(animPreviewModelCombo.getItems()::add); - } catch (IOException ignored) {} - } - - Button loadBtn = new Button("Laden"); - loadBtn.setMaxWidth(Double.MAX_VALUE); - loadBtn.setStyle("-fx-font-weight: bold;"); - loadBtn.setOnAction(e -> { - String path = animPreviewModelCombo.getValue(); - if (path == null || path.isEmpty()) return; - input.animPreviewLoadPath = path; - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); - if (animClipListView != null) animClipListView.getItems().clear(); - }); - Button reimportBtn = new Button("Modell neu importieren (GLB/GLTF)…"); - reimportBtn.setMaxWidth(Double.MAX_VALUE); - reimportBtn.setOnAction(e -> reimportModelForPreview(reimportBtn.getScene().getWindow())); - inner.getChildren().addAll(new Label("Modell:"), animPreviewModelCombo, loadBtn, reimportBtn); - // ── Animationen ─────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Animationen"), new Separator()); + inner.getChildren().addAll(sectionTitle("Animationen"), new Separator()); animClipListView = new ListView<>(); animClipListView.setPrefHeight(200); animClipListView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.SINGLE); @@ -11116,7 +11245,6 @@ public class EditorApp extends Application { if (clip != null) input.animPreviewPlayClip = clip; }); stopBtn.setOnAction(e -> input.animPreviewPlayClip = ""); - // Doppelklick auf Clip → direkt abspielen animClipListView.setOnMouseClicked(e -> { if (e.getClickCount() == 2) { String clip = animClipListView.getSelectionModel().getSelectedItem(); @@ -11128,26 +11256,6 @@ public class EditorApp extends Application { HBox.setHgrow(stopBtn, Priority.ALWAYS); inner.getChildren().addAll(animClipListView, btnRow); - Button renameClipBtn = new Button("Clip umbenennen / exportieren…"); - renameClipBtn.setMaxWidth(Double.MAX_VALUE); - renameClipBtn.setOnAction(e -> { - String clip = animClipListView.getSelectionModel().getSelectedItem(); - if (clip == null) return; - javafx.scene.control.TextInputDialog dlg = new javafx.scene.control.TextInputDialog(clip); - dlg.setTitle("Clip umbenennen"); - dlg.setHeaderText("Clip '" + clip + "' unter neuem Namen speichern"); - dlg.setContentText("Neuer Name:"); - dlg.showAndWait().ifPresent(newName -> { - if (!newName.isBlank()) input.clipRenameRequest.set(new SharedInput.ClipRenameRequest(clip, newName.trim())); - }); - }); - - Button saveSetBtn = new Button("Als Animations-Set speichern…"); - saveSetBtn.setMaxWidth(Double.MAX_VALUE); - saveSetBtn.setStyle("-fx-font-weight: bold;"); - saveSetBtn.setOnAction(e -> showSaveAnimSetDialog()); - inner.getChildren().addAll(renameClipBtn, saveSetBtn); - // ── Steuerung ───────────────────────────────────────────────────────── inner.getChildren().addAll(new Separator(), sectionTitle("Steuerung"), new Separator()); @@ -11163,51 +11271,11 @@ public class EditorApp extends Application { speedSlider.valueProperty().addListener((obs, ov, nv) -> input.animPreviewSpeed = nv.floatValue()); inner.getChildren().addAll(loopCB, speedLbl, withField(speedSlider, "%.2f")); - // ── Clip importieren ────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Clip importieren"), new Separator()); - - addAnimComboField = new ComboBox<>(); - ComboBox addAnimCombo = addAnimComboField; - - // Beim Hinzufügen wird der Clip direkt in animations/clips/ gespeichert - Label animHint = new Label("Clip wird retargeted und direkt in animations/clips/ gespeichert. Mixamo: \"With Skin\" wählen."); - animHint.setWrapText(true); - animHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - Button importAnimBtn = new Button("Animation importieren (GLB/GLTF)…"); - importAnimBtn.setMaxWidth(Double.MAX_VALUE); - importAnimBtn.setOnAction(e -> - handleAnimationImport(importAnimBtn.getScene().getWindow())); - addAnimCombo.setMaxWidth(Double.MAX_VALUE); - addAnimCombo.setPromptText("Animation aus animations/ wählen…"); - refreshAddAnimCombo(addAnimCombo); - - Button addAnimBtn = new Button("Animation hinzufügen"); - addAnimBtn.setMaxWidth(Double.MAX_VALUE); - addAnimBtn.setStyle("-fx-font-weight: bold;"); - addAnimBtn.setOnAction(e -> { - String animPath = addAnimCombo.getValue(); - if (animPath == null || animPath.isEmpty()) { - if (animPreviewStatusLabel != null) - animPreviewStatusLabel.setText("Bitte eine Animation auswählen"); - return; - } - input.animImportQueue.offer(animPath); - if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Füge Clips hinzu…"); - }); - inner.getChildren().addAll(animHint, importAnimBtn, addAnimCombo, addAnimBtn); - - // ── Diagnose ────────────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Diagnose"), new Separator()); - Button dumpBtn = new Button("Skelett-Info ins Log dumpen"); - dumpBtn.setMaxWidth(Double.MAX_VALUE); - dumpBtn.setOnAction(e -> input.animDumpRequested = true); - inner.getChildren().add(dumpBtn); - // ── Status ──────────────────────────────────────────────────────────── inner.getChildren().addAll(new Separator()); - animPreviewStatusLabel = new Label("Kein Modell geladen"); + animPreviewStatusLabel = new Label(""); animPreviewStatusLabel.setWrapText(true); - animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #c00;"); inner.getChildren().add(animPreviewStatusLabel); ScrollPane scroll = new ScrollPane(inner); @@ -11221,24 +11289,6 @@ public class EditorApp extends Application { return panel; } - private void refreshAddAnimCombo(ComboBox combo) { - if (combo == null) return; - String current = combo.getValue(); - combo.getItems().clear(); - Path animBase = ASSET_ROOT.resolve("animations"); - if (java.nio.file.Files.isDirectory(animBase)) { - try (var walk = java.nio.file.Files.walk(animBase)) { - walk.filter(ap -> { - String s = ap.toString(); - return s.endsWith(".j3o") || s.endsWith(".glb") || s.endsWith(".gltf"); - }) - .map(ap -> ASSET_ROOT.relativize(ap).toString().replace('\\', '/')) - .sorted() - .forEach(combo.getItems()::add); - } catch (IOException ignored) {} - } - if (current != null && combo.getItems().contains(current)) combo.setValue(current); - } /** * Kopiert die Original-Quelldatei nach {@code /assets/imported//}. @@ -11259,9 +11309,9 @@ public class EditorApp extends Application { private void handleAnimationImport(javafx.stage.Window owner) { FileChooser fc = new FileChooser(); - fc.setTitle("Animation importieren (GLB/GLTF)"); + fc.setTitle("Animation importieren (FBX/GLB/GLTF)"); fc.getExtensionFilters().add( - new FileChooser.ExtensionFilter("Animationen (GLTF, GLB)", "*.gltf", "*.glb")); + new FileChooser.ExtensionFilter("Animationen (FBX, GLTF, GLB)", "*.fbx", "*.gltf", "*.glb")); var files = fc.showOpenMultipleDialog(owner); if (files == null) return; for (File file : files) { @@ -11273,6 +11323,20 @@ public class EditorApp extends Application { } } + private void handleCharacterModelImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Character-Modell importieren (Mixamo T-Pose FBX)"); + fc.getExtensionFilters().add( + new FileChooser.ExtensionFilter("FBX-Modelle", "*.fbx")); + File file = fc.showOpenDialog(owner); + if (file == null) return; + archiveOriginal(file, "models"); + input.charModelImportQueue.offer(file.getAbsolutePath()); + if (charEditorStatusLabel != null) + charEditorStatusLabel.setText("Importiere: " + file.getName() + " …"); + setStatus("Importiere Character-Modell: " + file.getName() + " …"); + } + private void reimportModelForPreview(javafx.stage.Window owner) { FileChooser fc = new FileChooser(); fc.setTitle("Modell (GLB/GLTF) neu importieren"); @@ -11370,7 +11434,7 @@ public class EditorApp extends Application { java.util.List allClips = animClipListView.getItems(); - Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); + Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); // Aktions-Zuweisung: Liste mit Add/Remove javafx.scene.control.ListView actionList = new javafx.scene.control.ListView<>(); @@ -11511,7 +11575,7 @@ public class EditorApp extends Application { topBar.getChildren().set(1, buildCharacterEditorToolBar()); dialogEditorView = new de.blight.editor.ui.DialogEditorView(); dialogEditorView.setDisable(true); - routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("character")); + routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("Characters")); routineEditorView.setDisable(true); monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); @@ -11690,7 +11754,7 @@ public class EditorApp extends Application { } private ScrollPane buildCharacterSettingsPane() { - Path charDir = ASSET_ROOT.resolve("character"); + Path charDir = ASSET_ROOT.resolve("Characters"); VBox inner = new VBox(8); inner.setPadding(new Insets(10)); @@ -11698,7 +11762,13 @@ public class EditorApp extends Application { Button newCharBtn = new Button("Neuer Charakter"); newCharBtn.setMaxWidth(Double.MAX_VALUE); newCharBtn.setOnAction(e -> clearCharacterForm()); - inner.getChildren().add(newCharBtn); + + Button importCharModelBtn = new Button("Character-Modell importieren (FBX)…"); + importCharModelBtn.setMaxWidth(Double.MAX_VALUE); + importCharModelBtn.setStyle("-fx-font-weight: bold;"); + importCharModelBtn.setOnAction(e -> handleCharacterModelImport(importCharModelBtn.getScene().getWindow())); + + inner.getChildren().addAll(newCharBtn, importCharModelBtn); charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen"); charEditorStatusLabel.setWrapText(true); @@ -11891,7 +11961,7 @@ public class EditorApp extends Application { if (charAnimSetCombo == null) return; String cur = charAnimSetCombo.getValue(); charAnimSetCombo.getItems().clear(); - Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); + Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); if (java.nio.file.Files.isDirectory(setDir)) { try (var walk = java.nio.file.Files.walk(setDir)) { walk.filter(p -> p.toString().endsWith(".animset.json")) @@ -11912,7 +11982,7 @@ public class EditorApp extends Application { charActionLabelsBox.getChildren().add(hint); return; } - Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); + Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); de.blight.game.animation.AnimSet animSet; try { animSet = de.blight.game.animation.AnimSet.load(setDir, setName); @@ -12695,7 +12765,7 @@ public class EditorApp extends Application { } private void deleteCharacterById(String id) { - Path charFile = ASSET_ROOT.resolve("character").resolve(id + ".character"); + Path charFile = ASSET_ROOT.resolve("Characters").resolve(id + ".character"); try { java.nio.file.Files.deleteIfExists(charFile); } catch (IOException ex) { @@ -12728,7 +12798,7 @@ public class EditorApp extends Application { dlg.setContentText("ID:"); dlg.showAndWait().ifPresent(newId -> { if (newId == null || newId.isBlank()) return; - Path charDir = ASSET_ROOT.resolve("character"); + Path charDir = ASSET_ROOT.resolve("Characters"); Path src = charDir.resolve(sourceId + ".character"); Path dest = charDir.resolve(newId.trim() + ".character"); try { @@ -12763,7 +12833,7 @@ public class EditorApp extends Application { contentTreeUpdating = true; try { contentCharNode.getChildren().clear(); - Path charDir = ASSET_ROOT.resolve("character"); + Path charDir = ASSET_ROOT.resolve("Characters"); if (!java.nio.file.Files.isDirectory(charDir)) return; java.util.List> items = new java.util.ArrayList<>(); try (var walk = java.nio.file.Files.list(charDir)) { @@ -12780,7 +12850,7 @@ public class EditorApp extends Application { private void loadCharacterById(String id) { if (id == null || id.isBlank()) return; - Path charDir = ASSET_ROOT.resolve("character"); + Path charDir = ASSET_ROOT.resolve("Characters"); try { de.blight.common.model.GameCharacter c = de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index cccc16f..3178348 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -619,6 +619,12 @@ public class SharedInput { public volatile String animPreviewPlayClip = null; /** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */ public final ConcurrentLinkedQueue animImportQueue = new ConcurrentLinkedQueue<>(); + /** JavaFX → JME3: Absoluter FBX-Pfad für Character-Modell-Import (nach Models/Chars/). */ + public final ConcurrentLinkedQueue charModelImportQueue = new ConcurrentLinkedQueue<>(); + /** JME3 → JavaFX: Ergebnis des Character-Modell-Imports. relPath=null wenn Fehler. */ + public record CharModelImportResult(String relPath, String error) {} + public final java.util.concurrent.atomic.AtomicReference + charModelImportResult = new java.util.concurrent.atomic.AtomicReference<>(); /** JavaFX → JME3: Clip-Name zum Entfernen aus dem geladenen Modell. null = kein Auftrag. */ public volatile String animPreviewRemoveClip = null; /** JavaFX → JME3: Scan aller j3o auf Skelett-Controls anstoßen. */ diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index fbd2999..4f9748c 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -85,6 +85,8 @@ public class AnimPreviewState extends BaseAppState { protected void initialize(Application app) { this.app = (SimpleApplication) app; this.assets = app.getAssetManager(); + assets.registerLoader(com.github.stephengold.wrench.LwjglAssetLoader.class, + "fbx", "dae", "bvh"); previewFB = buildFrameBuffer(PREVIEW_SIZE, PREVIEW_SIZE); @@ -153,6 +155,11 @@ public class AnimPreviewState extends BaseAppState { addAnimation(addAnimPath); } + String charModelPath = input.charModelImportQueue.poll(); + if (charModelPath != null) { + importFbxAsCharacterModel(charModelPath); + } + String removeClip = input.animPreviewRemoveClip; if (removeClip != null) { input.animPreviewRemoveClip = null; @@ -193,7 +200,8 @@ public class AnimPreviewState extends BaseAppState { double length = currentAction.getLength(); double time = ac.getTime(); if (length <= 0) { - LOG.warn("[AnimPreview] Loop-Check: length=0, Clip hat keine Dauer!"); + LOG.warn("[AnimPreview] Clip '{}' hat keine Dauer, stoppe.", currentClipName); + stopAll(); } else if (time >= length - 0.02) { LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length); if (input.animPreviewLoop) { @@ -267,7 +275,7 @@ public class AnimPreviewState extends BaseAppState { // Alle Clips in-place snappen (verhindert Drift im Preview) AnimComposer previewAC = findControl(model, AnimComposer.class); SkinningControl previewSC = findControl(model, SkinningControl.class); - LOG.info("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}", + LOG.debug("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}", previewAC != null ? "gefunden" : "NULL", previewSC != null ? "gefunden" : "NULL", previewAC != null ? previewAC.getAnimClips().size() : "n/a"); @@ -365,7 +373,10 @@ public class AnimPreviewState extends BaseAppState { private void collectClips(Spatial s, List out) { AnimComposer ac = s.getControl(AnimComposer.class); if (ac != null) { - List names = new ArrayList<>(ac.getAnimClipsNames()); + List names = new ArrayList<>(); + for (String n : ac.getAnimClipsNames()) { + if (!n.startsWith("__")) names.add(n); + } Collections.sort(names); out.addAll(names); } @@ -395,7 +406,38 @@ public class AnimPreviewState extends BaseAppState { if (currentAction != null) { currentAction.setSpeed(input.animPreviewSpeed); input.animPreviewCurrentClipDuration = (float) currentAction.getLength(); - LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength()); + // DIAG: Clip-Inhalt + aktueller Hips-Zustand + AnimClip diagClip = ac.getAnimClip(clipName); + if (diagClip != null) { + LOG.info("[DIAG] Play '{}' length={} tracks={}", clipName, + currentAction.getLength(), diagClip.getTracks().length); + for (com.jme3.anim.AnimTrack t : diagClip.getTracks()) { + if (!(t instanceof com.jme3.anim.TransformTrack tt)) continue; + if (!(tt.getTarget() instanceof com.jme3.anim.Joint j)) continue; + if (!j.getName().contains("Hips")) continue; + com.jme3.math.Vector3f[] tr = tt.getTranslations(); + com.jme3.math.Quaternion[] rr = tt.getRotations(); + String tStr = (tr != null && tr.length > 0) ? tr[0].toString() : "null"; + String rStr = (rr != null && rr.length > 0) ? rr[0].toString() : "null"; + LOG.info("[DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={}", + clipName, j.getName(), tt.getTimes().length, tStr, rStr); + } + } + // DIAG: Modell-Hips localTransform + IBM + SkinningControl diaSC = findControl(currentModel, SkinningControl.class); + if (diaSC != null) { + com.jme3.anim.Armature diaArm = diaSC.getArmature(); + for (int di = 0; di < diaArm.getJointCount(); di++) { + com.jme3.anim.Joint dj = diaArm.getJoint(di); + if (dj.getParent() == null) { + com.jme3.math.Vector3f lt = dj.getLocalTransform().getTranslation(); + com.jme3.math.Matrix4f ibm = dj.getInverseModelBindMatrix(); + LOG.info("[DIAG] Model-Root '{}': localT=({},{},{}) IBM_transl=({},{},{})", + dj.getName(), lt.x, lt.y, lt.z, + ibm != null ? ibm.m30 : 0, ibm != null ? ibm.m31 : 0, ibm != null ? ibm.m32 : 0); + } + } + } } } catch (Exception e) { input.animPreviewStatus = "Abspielen fehlgeschlagen: " + e.getMessage(); @@ -499,40 +541,39 @@ public class AnimPreviewState extends BaseAppState { com.jme3.anim.Armature srcArm = sourceSC != null ? sourceSC.getArmature() : null; com.jme3.anim.Armature dstArm = targetSC != null ? targetSC.getArmature() : null; - if (srcArm != null) { - LOG.trace("[Retarget] Quell-Knochen ({}):", srcArm.getJointCount()); - for (var j : srcArm.getJointList()) LOG.trace(" src: {}", j.getName()); - } else { - LOG.warn("[Retarget] Keine SkinningControl in Quelle!"); - } - if (dstArm != null) { - LOG.trace("[Retarget] Ziel-Knochen ({}):", dstArm.getJointCount()); - for (var j : dstArm.getJointList()) LOG.trace(" dst: {}", j.getName()); - } else { - LOG.warn("[Retarget] Keine SkinningControl im Modell!"); + if (srcArm == null) { + LOG.debug("[Retarget] Quelle hat keine SkinningControl (j3o-Clip oder reines Anim-FBX)."); + } else if (dstArm == null) { + LOG.debug("[Retarget] Kein Charakter-Modell geladen – Clip wird ohne Retargeting gespeichert."); } - boolean retarget = srcArm != null && dstArm != null && srcArm != dstArm; - if (retarget) { - var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(srcArm, dstArm); - LOG.trace("[Retarget] Mapping ({} Treffer): {}", mapping.size(), mapping); - } + // Retarget wenn Ziel-Armature bekannt UND entweder andere Quelle-Armature + // oder Quelle hat gar keine Armature (z.B. j3o-Clip-Wrapper ohne SkinningControl). + // Ohne Retarget würden die Track-Targets auf fremde (Standalone-)Joints zeigen + // und die Modell-Joints gar nicht bewegen. + boolean retarget = dstArm != null && (srcArm == null || srcArm != dstArm); java.util.Set srcNames = new java.util.HashSet<>(); for (AnimClip c : sourceAC.getAnimClips()) srcNames.add(c.getName()); - java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips"); + java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips"); java.nio.file.Files.createDirectories(clipsDir); - // Blender exportiert GLB-Dateien oft mit internem Namen "Action" statt dem Dateinamen. - // Bei einer GLB/GLTF-Datei mit genau einem Clip: Dateinamen als Clip-Namen verwenden. - boolean isSingleClipGlb = animAssetPath.matches(".*\\.(glb|gltf)$") + // GLB/FBX mit genau einem Clip: Dateinamen als Clip-Namen verwenden. + // Mixamo FBX nennt alle Clips "mixamo.com" unabhängig vom Inhalt. + boolean isSingleClipSource = animAssetPath.matches("(?i).*\\.(glb|gltf|fbx)$") && sourceAC.getAnimClips().size() == 1; - String fileBaseName = isSingleClipGlb + boolean isSingleClipGlb = isSingleClipSource; // Kompatibilität mit unterem Delete-Block + String fileBaseName = isSingleClipSource ? java.nio.file.Paths.get(animAssetPath).getFileName().toString() - .replaceFirst("\\.(glb|gltf)$", "") + .replaceFirst("(?i)\\.(glb|gltf|fbx)$", "") : null; + // j3o-Quelle: Clip bereits gespeichert – nur Preview, kein erneutes Speichern. + // snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary. + boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$"); + com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm; + int saved = 0; for (AnimClip clip : sourceAC.getAnimClips()) { String name = clip.getName(); @@ -543,9 +584,18 @@ public class AnimPreviewState extends BaseAppState { continue; } } - AnimClip result = retarget - ? de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm) - : clip; + AnimClip result; + if (!retarget) { + result = clip; + } else { + // Immer erst redirect-by-name wie FbxTestApp: mappt alle passenden Joints, + // droppt nur die wenigen fehlenden ($AssimpFbx$-Varianten die im Model nicht existieren). + result = redirectAnimClipByName(clip, dstArm); + if (result == null) { + LOG.warn("[AnimPreview] redirectByName: keine Joints gemappt, versuche Retargeting"); + result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm); + } + } if (result == null) continue; String saveName = (fileBaseName != null) ? fileBaseName : name; @@ -556,18 +606,16 @@ public class AnimPreviewState extends BaseAppState { LOG.info("[AnimPreview] Clip '{}' als '{}' gespeichert (Dateiname-Alias)", name, saveName); } - // XZ-Drift einfrieren bevor gespeichert wird – Clip-Dateien bleiben immer sauber - com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm; - toSave = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(toSave, snapArm); - - // Direkt in die Clip-Bibliothek speichern – das Modell wird nicht modifiziert - saveClipToFile(toSave, snapArm, clipsDir.resolve(saveName + ".j3o")); - // Für den aktuellen Preview-Session auch auf das Modell anwenden (wenn geladen) + if (!isJ3oSource) { + // Direkt in die Clip-Bibliothek speichern – snap passiert nur im Spiel zur Laufzeit + saveClipToFile(toSave, snapArm, clipsDir.resolve(saveName + ".j3o")); + saved++; + } + // Für den aktuellen Preview auch auf das Modell anwenden (wenn geladen) if (targetAC != null) targetAC.addAnimClip(toSave); - saved++; } - // Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien) - if (saved > 0 && animAssetPath.matches(".*\\.(glb|gltf)$") + // Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien, nie FBX) + if (saved > 0 && animAssetPath.matches("(?i).*\\.(glb|gltf)$") && !Path.of(animAssetPath).isAbsolute()) { Path srcGlb = ASSET_ROOT.resolve(animAssetPath.replace('/', java.io.File.separatorChar)); try { @@ -578,9 +626,13 @@ public class AnimPreviewState extends BaseAppState { List clips = new ArrayList<>(); if (currentModel != null) collectClips(currentModel, clips); input.animPreviewClips.set(Collections.unmodifiableList(clips)); - input.animPreviewStatus = saved + " Clip(s) in animations/clips/ gespeichert" - + (retarget ? " (retargeted)" : " (direkt)"); - if (saved > 0) input.animImportCompleted = true; + if (isJ3oSource) { + input.animPreviewStatus = "Clip geladen (Preview)"; + } else { + input.animPreviewStatus = saved + " Clip(s) in Characters/clips/ gespeichert" + + (retarget ? " (retargeted)" : " (direkt)"); + if (saved > 0) input.animImportCompleted = true; + } } catch (Exception e) { LOG.error("[AnimPreview] Fehler beim Importieren von {}", animAssetPath, e); input.animPreviewStatus = "Fehler beim Hinzufügen: " + e.getMessage(); @@ -897,10 +949,10 @@ public class AnimPreviewState extends BaseAppState { ac.addAnimClip(renamed); // kein saveModel() – Quell-Modell bleibt unverändert - // Als eigenständige .j3o nach animations/clips/ exportieren + // Als eigenständige .j3o nach Characters/clips/ exportieren try { SkinningControl sc = findControl(currentModel, SkinningControl.class); - java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips"); + java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips"); saveClipToFile(renamed, sc != null ? sc.getArmature() : null, clipsDir.resolve(req.newName() + ".j3o")); java.util.List clips = new java.util.ArrayList<>(); @@ -916,7 +968,7 @@ public class AnimPreviewState extends BaseAppState { private void executeAnimSetSave(SharedInput.AnimSetSaveRequest req) { try { - java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); + java.nio.file.Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); java.nio.file.Files.createDirectories(setDir); de.blight.game.animation.AnimSet animSet = new de.blight.game.animation.AnimSet(); animSet.setClips(req.clips()); @@ -929,8 +981,8 @@ public class AnimPreviewState extends BaseAppState { } private void executeAnimEmbed(SharedInput.AnimEmbedRequest req) { - java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); - java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips"); + java.nio.file.Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); + java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips"); de.blight.game.animation.AnimSet set; try { @@ -1096,15 +1148,461 @@ public class AnimPreviewState extends BaseAppState { BinaryExporter.getInstance().save(holder, outFile.toFile()); } - /** Returns "animations/clips/." for the first matching file, or null if not found. */ + // ── FBX-Import ─────────────────────────────────────────────────────────── + + /** + * Importiert eine FBX-Datei als Character-Modell nach Models/Chars/. + * Prüft vorab ob ein Mixamo-Skelett (mixamorig:-Präfix) vorhanden ist. + * Ergebnis wird via charModelImportResult zurück an die UI gemeldet. + */ + private void importFbxAsCharacterModel(String absolutePath) { + java.io.File file = new java.io.File(absolutePath); + if (!file.exists()) { + input.charModelImportResult.set(new SharedInput.CharModelImportResult( + null, "Datei nicht gefunden: " + absolutePath)); + return; + } + String basename = file.getName() + .replaceFirst("(?i)\\.[^.]+$", "") + .replaceAll("[^a-zA-Z0-9_\\-]", "_"); + + try { + assets.registerLocator(file.getParent(), com.jme3.asset.plugins.FileLocator.class); + String fbmBase = file.getName().replaceAll("\\.[^.]+$", ""); + java.io.File fbmDir = new java.io.File(file.getParentFile(), fbmBase + ".fbm"); + if (fbmDir.isDirectory()) { + assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); + } + + com.github.stephengold.wrench.LwjglAssetKey key = + new com.github.stephengold.wrench.LwjglAssetKey(file.getName()); + key.setVerboseLogging(true); + Spatial model = (Spatial) assets.loadAsset(key); + + // Mixamo-Skelett prüfen + if (!hasMixamoSkeleton(model)) { + input.charModelImportResult.set(new SharedInput.CharModelImportResult( + null, "Kein Mixamo-Skelett gefunden (kein 'mixamorig:'-Joint).\n" + + "Bitte Mixamo-FBX mit 'With Skin' exportieren.")); + return; + } + + java.io.File embeddedDir = extractEmbeddedTextures(file, model); + if (embeddedDir != null) { + assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); + } + + fixFbxMaterials(model); + fixFbxMeshAndSkeleton(model); + fixBrokenTexturePaths(model, file.getParentFile()); + + // Textur-Keys entfernen → BinaryExporter bettet Image-Daten direkt in die j3o ein. + // Filename-only Keys ("character.png") wären nach dem nächsten Editor-Start nicht + // mehr auflösbar (kein Temp-Dir mehr registriert) → gelbe Fallback-Textur. + embedTexturesInModel(model); + + model.setLocalTranslation(Vector3f.ZERO); + model.updateGeometricState(); + float scale = 0.01f; + if (model.getWorldBound() instanceof com.jme3.bounding.BoundingBox bb) { + float h = bb.getYExtent() * 2f; + if (h > 0.01f) scale = 2.0f / h; + } + model.setLocalScale(scale); + model.updateGeometricState(); + + // Animationsclips entfernen + AnimComposer ac = findControl(model, AnimComposer.class); + if (ac != null) { + for (AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) { + ac.removeAnimClip(c); + } + } + + java.nio.file.Path destJ3o = ASSET_ROOT.resolve("Characters").resolve("Models").resolve(basename + ".j3o"); + java.nio.file.Files.createDirectories(destJ3o.getParent()); + com.jme3.export.binary.BinaryExporter.getInstance().save(model, destJ3o.toFile()); + + String relStr = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/'); + for (String root : new String[]{"blight-assets/bin/main", "blight-assets/build/resources/main"}) { + java.nio.file.Path mirror = java.nio.file.Paths.get(root).resolve( + ASSET_ROOT.relativize(destJ3o)); + if (java.nio.file.Files.exists(mirror)) { + try { + java.nio.file.Files.copy(destJ3o, mirror, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (Exception ignored) {} + } + } + assets.deleteFromCache(new com.jme3.asset.ModelKey(relStr)); + LOG.info("[CharImport] Character-Modell gespeichert: {} (scale={})", destJ3o, scale); + input.charModelImportResult.set(new SharedInput.CharModelImportResult(relStr, null)); + + } catch (Exception e) { + LOG.error("[CharImport] Fehler bei {}", absolutePath, e); + input.charModelImportResult.set(new SharedInput.CharModelImportResult( + null, "Import-Fehler: " + e.getMessage())); + } + } + + /** + * Entfernt den AssetKey aller Texturen im Baum, so dass BinaryExporter die Image-Daten direkt + * in die j3o-Datei einbettet (kein externer Textur-Verweis → funktioniert nach Editor-Restart). + * JME3: Texture mit key != null → AssetLink-Referenz gespeichert; key == null → Image inline. + */ + private void embedTexturesInModel(Spatial s) { + if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) { + com.jme3.material.Material mat = geo.getMaterial(); + for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) { + if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue; + if (tex.getImage() == null) continue; + com.jme3.texture.Texture2D embedded = new com.jme3.texture.Texture2D(tex.getImage()); + embedded.setMinFilter(tex.getMinFilter()); + embedded.setMagFilter(tex.getMagFilter()); + embedded.setWrap(com.jme3.texture.Texture.WrapAxis.S, + tex.getWrap(com.jme3.texture.Texture.WrapAxis.S)); + embedded.setWrap(com.jme3.texture.Texture.WrapAxis.T, + tex.getWrap(com.jme3.texture.Texture.WrapAxis.T)); + mat.setTexture(mp.getName(), embedded); + } + } + if (s instanceof Node n) { + for (Spatial child : n.getChildren()) embedTexturesInModel(child); + } + } + + /** + * Kopiert Textur-Dateien in ein permanentes Asset-Verzeichnis und setzt die Material-Referenzen + * auf asset-relative Pfade um, damit das j3o auch nach dem nächsten Editor-Start funktioniert. + */ + private void relinkTexturesAsAssets(Spatial s, + java.util.List srcDirs, + java.nio.file.Path texDestDir, + String texRelBase) { + if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) { + com.jme3.material.Material mat = geo.getMaterial(); + for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) { + if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue; + com.jme3.asset.AssetKey key = tex.getKey(); + if (key == null) continue; + String filename = new java.io.File(key.getName()).getName(); + if (filename.isEmpty()) continue; + if (key.getName().contains("/") || key.getName().contains("\\")) continue; // bereits relativ + + // Quelldatei suchen + java.io.File src = null; + for (java.io.File dir : srcDirs) { + java.io.File c = new java.io.File(dir, filename); + if (c.exists()) { src = c; break; } + } + if (src == null) { + LOG.warn("[CharImport] Textur-Quelle nicht gefunden: {}", filename); + continue; + } + + // In Asset-Verzeichnis kopieren + java.nio.file.Path dest = texDestDir.resolve(filename); + try { + java.nio.file.Files.copy(src.toPath(), dest, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (Exception ex) { + LOG.warn("[CharImport] Kopieren fehlgeschlagen ({}): {}", filename, ex.getMessage()); + continue; + } + + // Material mit asset-relativem Key neu laden und setzen + String relPath = texRelBase + "/" + filename; + try { + com.jme3.asset.TextureKey newKey = new com.jme3.asset.TextureKey(relPath, true); + newKey.setGenerateMips(true); + assets.deleteFromCache(newKey); + com.jme3.texture.Texture reloaded = assets.loadTexture(newKey); + mat.setTexture(mp.getName(), reloaded); + LOG.info("[CharImport] Textur → {}", relPath); + } catch (Exception ex) { + LOG.warn("[CharImport] Textur-Reload fehlgeschlagen ({}): {}", relPath, ex.getMessage()); + } + } + } + if (s instanceof Node n) { + for (Spatial child : n.getChildren()) + relinkTexturesAsAssets(child, srcDirs, texDestDir, texRelBase); + } + } + + private boolean hasMixamoSkeleton(Spatial model) { + SkinningControl sc = findControl(model, SkinningControl.class); + if (sc == null) return false; + com.jme3.anim.Armature arm = sc.getArmature(); + for (int i = 0; i < arm.getJointCount(); i++) { + if (arm.getJoint(i).getName().startsWith("mixamorig:")) return true; + } + return false; + } + + // ── FBX-Hilfsroutinen (portiert aus FbxTestApp) ─────────────────────────── + + /** + * Bereinigt FBX-Mesh-Knoten und Skelett nach MonkeyWrench-Import. + * Assimp konvertiert Animations-Tracks zu Y-up, aber NICHT die Bind-Pose-Translation + * des Root-Joints. Der Root-Joint liegt in Z-up vor: t=(x,y,z) wird zu t=(x,z,-y). + * Danach werden die inversen Bind-Matrizen aller Joints neu berechnet. + */ + private void fixFbxMeshAndSkeleton(Spatial model) { + fixFbxZeroMeshRotations(model); + SkinningControl skinCtrl = findControl(model, SkinningControl.class); + if (skinCtrl == null) return; + com.jme3.anim.Armature arm = skinCtrl.getArmature(); + // Assimp konvertiert Mesh-Vertices zu Y-up, aber NICHT die Bind-Pose-Translation + // des Root-Joints – der liegt noch in Z-up vor: (x,y,z) → (x,z,-y). + boolean fixed = false; + for (int i = 0; i < arm.getJointCount(); i++) { + com.jme3.anim.Joint j = arm.getJoint(i); + if (j.getParent() == null) { + com.jme3.math.Vector3f t = j.getLocalTransform().getTranslation(); + float ox = t.x, oy = t.y, oz = t.z; + t.set(ox, oz, -oy); + LOG.debug("[fixSkeleton] Root-Joint '{}': ({},{},{}) -> ({},{},{})", + j.getName(), ox, oy, oz, t.x, t.y, t.z); + fixed = true; + } + } + if (!fixed) return; + // Inverse Bind-Matrizen aus den korrigierten lokalen Transforms neu berechnen. + java.util.HashMap modelMats = new java.util.HashMap<>(); + for (int i = 0; i < arm.getJointCount(); i++) { + buildFbxJointModelMatrix(arm.getJoint(i), modelMats); + } + for (int i = 0; i < arm.getJointCount(); i++) { + com.jme3.anim.Joint j = arm.getJoint(i); + com.jme3.math.Matrix4f ibm = j.getInverseModelBindMatrix(); + if (ibm != null) ibm.set(modelMats.get(j).invert()); + } + arm.saveBindPose(); + arm.saveInitialPose(); + } + + private com.jme3.math.Matrix4f buildFbxJointModelMatrix( + com.jme3.anim.Joint j, + java.util.HashMap cache) { + com.jme3.math.Matrix4f cached = cache.get(j); + if (cached != null) return cached; + com.jme3.math.Matrix4f local = j.getLocalTransform().toTransformMatrix(); + com.jme3.math.Matrix4f result = (j.getParent() != null) + ? buildFbxJointModelMatrix(j.getParent(), cache).mult(local) + : local; + cache.put(j, result); + return result; + } + + private void fixFbxZeroMeshRotations(Spatial s) { + if (!(s instanceof Node n)) return; + boolean hasSkeleton = n.getControl(AnimComposer.class) != null + || n.getControl(SkinningControl.class) != null; + if (!hasSkeleton) { + com.jme3.math.Quaternion rot = n.getLocalRotation(); + boolean isIdentity = Math.abs(rot.getW() - 1f) < 0.01f && Math.abs(rot.getX()) < 0.01f + && Math.abs(rot.getY()) < 0.01f && Math.abs(rot.getZ()) < 0.01f; + if (!isIdentity) n.setLocalRotation(com.jme3.math.Quaternion.IDENTITY); + } + for (Spatial child : n.getChildren()) fixFbxZeroMeshRotations(child); + } + + /** + * Konvertiert PBR-Materialien in Lighting.j3md (Editor hat keine LightProbe). + * Vorhandene Lighting.j3md-Materialien werden nur um DiffuseColor ergänzt. + */ + private void fixFbxMaterials(Spatial s) { + if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) { + com.jme3.material.Material mat = geo.getMaterial(); + String defName = mat.getMaterialDef().getName(); + if (defName.contains("Lighting")) { + com.jme3.material.MatParam dm = mat.getParam("DiffuseMap"); + if (dm != null && dm.getValue() instanceof com.jme3.texture.Texture) { + mat.setColor("Diffuse", com.jme3.math.ColorRGBA.White.clone()); + mat.setColor("Ambient", new com.jme3.math.ColorRGBA(0.45f, 0.45f, 0.45f, 1f)); + } + return; + } + com.jme3.texture.Texture diffuseTex = null; + com.jme3.texture.Texture normalTex = null; + com.jme3.math.ColorRGBA baseColor = null; + for (String nm : new String[]{"BaseColorMap", "DiffuseMap", "Albedo", "ColorMap"}) { + com.jme3.material.MatParam p = mat.getParam(nm); + if (p != null && p.getValue() instanceof com.jme3.texture.Texture t) { diffuseTex = t; break; } + } + for (String nm : new String[]{"NormalMap", "NormalCamera"}) { + com.jme3.material.MatParam p = mat.getParam(nm); + if (p != null && p.getValue() instanceof com.jme3.texture.Texture t) { normalTex = t; break; } + } + for (String nm : new String[]{"BaseColor", "Diffuse"}) { + com.jme3.material.MatParam p = mat.getParam(nm); + if (p != null && p.getValue() instanceof com.jme3.math.ColorRGBA c) { baseColor = c.clone(); break; } + } + com.jme3.material.Material lit = new com.jme3.material.Material(assets, "Common/MatDefs/Light/Lighting.j3md"); + lit.setBoolean("UseMaterialColors", true); + if (diffuseTex != null) { + lit.setTexture("DiffuseMap", diffuseTex); + lit.setColor("Diffuse", com.jme3.math.ColorRGBA.White.clone()); + lit.setColor("Ambient", new com.jme3.math.ColorRGBA(0.45f, 0.45f, 0.45f, 1f)); + } else { + com.jme3.math.ColorRGBA col = baseColor != null ? baseColor : com.jme3.math.ColorRGBA.LightGray.clone(); + lit.setColor("Diffuse", col); + lit.setColor("Ambient", col.mult(0.4f).setAlpha(1f)); + } + if (normalTex != null) lit.setTexture("NormalMap", normalTex); + lit.setColor("Specular", new com.jme3.math.ColorRGBA(0.1f, 0.1f, 0.1f, 1f)); + lit.setFloat("Shininess", 24f); + geo.setMaterial(lit); + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) fixFbxMaterials(child); + } + } + + /** Sammelt alle Dateinamen aus Textur-Params eines Spatial-Baums. */ + private java.util.Set collectTextureFilenames(Spatial s) { + java.util.Set names = new java.util.LinkedHashSet<>(); + if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) { + for (com.jme3.material.MatParam p : geo.getMaterial().getParams()) { + if (p.getValue() instanceof com.jme3.texture.Texture tex && tex.getKey() != null) { + String name = new java.io.File(tex.getKey().getName()).getName(); + if (!name.isEmpty()) names.add(name); + } + } + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) names.addAll(collectTextureFilenames(child)); + } + return names; + } + + /** + * Sucht eingebettete PNG-Texturen im FBX-Binär (Dateiname → PNG-Magic → IEND) + * und schreibt sie in ein temporäres Verzeichnis. + */ + private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) { + java.util.Set filenames = collectTextureFilenames(model); + if (filenames.isEmpty()) return null; + try { + byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath()); + java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"), + "fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode())); + tempDir.mkdirs(); + byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}; + byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82}; + boolean any = false; + for (String filename : filenames) { + byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + int namePos = binaryIndexOf(data, nameBytes, 0); + if (namePos < 0) continue; + int pngPos = binaryIndexOf(data, pngMagic, namePos); + if (pngPos < 0 || pngPos - namePos > 65536) continue; + int endPos = binaryIndexOf(data, iend, pngPos + 8); + if (endPos < 0) continue; + byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length); + java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png); + LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length); + any = true; + } + return any ? tempDir : null; + } catch (Exception e) { + LOG.warn("[FbxImport] Embedded-Extraktion fehlgeschlagen: {}", e.getMessage()); + return null; + } + } + + private static int binaryIndexOf(byte[] data, byte[] pattern, int from) { + outer: + for (int i = from, max = data.length - pattern.length; i <= max; i++) { + for (int j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) continue outer; + } + return i; + } + return -1; + } + + /** Ersetzt kaputte Textur-Pfade (z.B. Mixamo-Server-Pfade) durch Reload per Dateiname. */ + private void fixBrokenTexturePaths(Spatial s, java.io.File baseDir) { + if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) { + com.jme3.material.Material mat = geo.getMaterial(); + for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) { + if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue; + com.jme3.asset.AssetKey key = tex.getKey(); + if (key == null) continue; + String filename = new java.io.File(key.getName()).getName(); + if (filename.isEmpty()) continue; + try { + com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true); + texKey.setGenerateMips(true); + com.jme3.texture.Texture reloaded = assets.loadTexture(texKey); + mat.setTexture(mp.getName(), reloaded); + continue; + } catch (Exception ignored) {} + try { + final String fn = filename; + java.nio.file.Path found = java.nio.file.Files.walk(baseDir.toPath(), 3) + .filter(p -> p.getFileName().toString().equalsIgnoreCase(fn)) + .findFirst().orElse(null); + if (found != null) { + assets.registerLocator(found.getParent().toAbsolutePath().toString(), + com.jme3.asset.plugins.FileLocator.class); + com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true); + texKey.setGenerateMips(true); + mat.setTexture(mp.getName(), assets.loadTexture(texKey)); + } + } catch (Exception ignored) {} + } + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) fixBrokenTexturePaths(child, baseDir); + } + } + + /** Returns "Characters/clips/." for the first matching file, or null if not found. */ private static String resolveClipFile(java.nio.file.Path clipsDir, String clipName) { for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) { if (Files.exists(clipsDir.resolve(clipName + ext))) - return "animations/clips/" + clipName + ext; + return "Characters/clips/" + clipName + ext; } return null; } + /** Leitet Clip-Tracks per Gelenk-Name auf die Ziel-Armatur um (FbxTestApp-Ansatz für Same-Rig). */ + private static AnimClip redirectAnimClipByName(AnimClip src, com.jme3.anim.Armature targetArm) { + List> newTracks = new ArrayList<>(); + for (com.jme3.anim.AnimTrack track : src.getTracks()) { + if (!(track instanceof com.jme3.anim.TransformTrack tt)) continue; + if (!(tt.getTarget() instanceof com.jme3.anim.Joint srcJoint)) continue; + com.jme3.anim.Joint dstJoint = targetArm.getJoint(srcJoint.getName()); + if (dstJoint == null) continue; + + // Bind-Pose-Delta: "Without Skin"-FBX kann dieselbe Gesamt-Translation + // anders auf den Base-Joint und $AssimpFbx$-Helper aufteilen als "With Skin". + // Alle Translation-Keyframes um die Differenz (dstInit - srcInit) verschieben, + // damit die Modell-Bind-Pose korrekt abgebildet wird. + com.jme3.math.Vector3f[] translations = tt.getTranslations(); + if (translations != null && translations.length > 0) { + com.jme3.math.Vector3f srcInitT = srcJoint.getInitialTransform().getTranslation(); + com.jme3.math.Vector3f dstInitT = dstJoint.getInitialTransform().getTranslation(); + com.jme3.math.Vector3f delta = dstInitT.subtract(srcInitT); + if (delta.lengthSquared() > 1e-6f) { + com.jme3.math.Vector3f[] retargeted = new com.jme3.math.Vector3f[translations.length]; + for (int i = 0; i < translations.length; i++) { + retargeted[i] = translations[i].add(delta); + } + translations = retargeted; + } + } + + newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(), + translations, tt.getRotations(), tt.getScales())); + } + if (newTracks.isEmpty()) return null; + AnimClip result = new AnimClip(src.getName()); + result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0])); + LOG.debug("[AnimPreview] redirectByName '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength()); + return result; + } + private static boolean haveSameBoneNames(com.jme3.anim.Armature a, com.jme3.anim.Armature b) { if (a.getJointCount() != b.getJointCount()) return false; java.util.Set namesA = new java.util.HashSet<>(); diff --git a/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java index 6ac767a..af5593c 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java @@ -1362,7 +1362,7 @@ public class DialogEditorView extends BorderPane { odt.normal("Exportiert: " + LocalDate.now()); odt.empty(); - Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character"); + Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "Characters"); for (GameCharacter gc : CharacterIO.loadAll(charDir)) { if (!(gc instanceof NPC npc)) continue; if (npc.getDialogOptions() == null || npc.getDialogOptions().isEmpty()) continue; diff --git a/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java b/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java index 3bddd7e..3935484 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java @@ -290,7 +290,7 @@ public class TtsGeneratorDialog extends Dialog { private List collectHeroKeys() { List keys = new ArrayList<>(); - Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character"); + Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "Characters"); for (GameCharacter gc : CharacterIO.loadAll(charDir)) { if (!(gc instanceof NPC npc) || npc.getDialogOptions() == null) continue; for (Map.Entry e : npc.getDialogOptions().entrySet()) { diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java index 9dba1c8..5673c72 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java @@ -192,7 +192,7 @@ public class AnimationLibrary extends BaseAppState { * @return Clip-Name oder {@code null} */ public static String getClipForAction(Path assetRoot, String setName, AnimationAction action) { - Path setDir = assetRoot.resolve("animations").resolve("sets"); + Path setDir = assetRoot.resolve("Characters").resolve("sets"); try { AnimSet set = AnimSet.load(setDir, setName); return set.getActionMap().get(action.name()); @@ -223,7 +223,7 @@ public class AnimationLibrary extends BaseAppState { log.info("[AnimLib] {} Clips geladen: {}", clips.size(), clips.keySet()); } - Path setDir = findAssetRoot().resolve("animations").resolve("sets"); + Path setDir = findAssetRoot().resolve("Characters").resolve("sets"); try { AnimSet animSet = AnimSet.load(setDir, "human"); createSubClips(animSet); @@ -293,7 +293,7 @@ public class AnimationLibrary extends BaseAppState { private void loadClipFromFile(Path file) { String fileName = file.getFileName().toString(); String clipName = fileName.replaceFirst("\\.j3o$", ""); - String assetKey = "animations/clips/" + fileName; + String assetKey = "Characters/clips/" + fileName; try { Spatial loaded = assetManager.loadModel(assetKey); @@ -319,18 +319,18 @@ public class AnimationLibrary extends BaseAppState { } } - /** Gibt den Asset-Wurzelpfad zurück (erstes Verzeichnis mit {@code animations/}-Unterordner). */ + /** Gibt den Asset-Wurzelpfad zurück (erstes Verzeichnis mit {@code Characters/}-Unterordner). */ public static Path findAssetRoot() { for (String base : ASSET_BASES) { Path p = Paths.get(base); - if (Files.isDirectory(p.resolve("animations"))) return p; + if (Files.isDirectory(p.resolve("Characters"))) return p; } return Paths.get(ASSET_BASES[0]); } private static Path findClipsDir() { for (String base : ASSET_BASES) { - Path p = Paths.get(base, "animations", "clips"); + Path p = Paths.get(base, "Characters", "clips"); if (Files.isDirectory(p)) return p; } return null; @@ -367,7 +367,7 @@ public class AnimationLibrary extends BaseAppState { } if (minDepth == Integer.MAX_VALUE) return clip; - boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName()); + boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName().toLowerCase(java.util.Locale.ROOT)); List> newTracks = new ArrayList<>(); boolean modified = false; diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index fd6765b..e40bd89 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -550,7 +550,7 @@ public class WorldScene extends BaseAppState { animLib.getClipKeys()); // AnimSet-ActionMap ausgeben if (setName != null) { - java.nio.file.Path setDir = AnimationLibrary.findAssetRoot().resolve("animations").resolve("sets"); + java.nio.file.Path setDir = AnimationLibrary.findAssetRoot().resolve("Characters").resolve("sets"); try { de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, setName); log.info("[AnimCtx] AnimSet '{}' actionMap: {}", setName, set.getActionMap()); @@ -736,7 +736,7 @@ public class WorldScene extends BaseAppState { } private MainCharacter findMainCharacter() { - java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character"); + java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("Characters"); for (GameCharacter c : CharacterIO.loadAll(charDir)) { if (c instanceof MainCharacter mc) return mc; } diff --git a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java index d4382b8..caa43c9 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java @@ -668,7 +668,7 @@ public class WorldNpcsState extends BaseAppState { && saveState.getSave().character.positionSaved && SaveGameIO.exists(); try { - java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character"); + java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("Characters"); for (GameCharacter gc : CharacterIO.loadAll(charDir)) { if (gc instanceof NPC npc) { java.util.List savedState = useSaved diff --git a/com/jme3/anim/TransformTrack.java b/com/jme3/anim/TransformTrack.java new file mode 100644 index 0000000..39b66d5 --- /dev/null +++ b/com/jme3/anim/TransformTrack.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2009-2023 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.anim; + +import com.jme3.anim.interpolator.FrameInterpolator; +import com.jme3.anim.util.HasLocalTransform; +import com.jme3.animation.CompactQuaternionArray; +import com.jme3.animation.CompactVector3Array; +import com.jme3.export.*; +import com.jme3.math.*; +import com.jme3.util.clone.Cloner; + +import java.io.IOException; + +/** + * An AnimTrack that transforms a Joint or Spatial + * using a list of transforms and times for keyframes. + * + * @author Rémy Bouquet + */ +public class TransformTrack implements AnimTrack { + + private double length; + /** + * The interpolator to use, or null to always use the default interpolator + * of the current thread. + */ + private FrameInterpolator interpolator = null; + private HasLocalTransform target; + + /** + * Transforms and times for keyframes. + */ + private CompactVector3Array translations; + private CompactQuaternionArray rotations; + private CompactVector3Array scales; + private float[] times; + + /** + * Serialization-only. Do not use. + */ + protected TransformTrack() { + } + + /** + * Creates a transform track for the given target. + * + * @param target the target Joint or Spatial of the new track + * @param times the time for each keyframe, or null for none + * @param translations the translation of the target for each keyframe + * (same length as times) or null for no translation + * @param rotations the rotation of the target for each keyframe + * (same length as times) or null for no rotation + * @param scales the scale of the target for each keyframe + * (same length as times) or null for no scaling + */ + public TransformTrack(HasLocalTransform target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + this.target = target; + this.setKeyframes(times, translations, rotations, scales); + } + + /** + * Copies the rotations. + * + * @return a new array, or null if no rotations + */ + public Quaternion[] getRotations() { + return rotations == null ? null : rotations.toObjectArray(); + } + + /** + * Copies the scales. + * + * @return a new array, or null if no scales + */ + public Vector3f[] getScales() { + return scales == null ? null : scales.toObjectArray(); + } + + /** + * Gives access to the keyframe times. + * + * @return the pre-existing array + */ + public float[] getTimes() { + return times; + } + + /** + * Copies the translations. + * + * @return a new array, or null if no translations + */ + public Vector3f[] getTranslations() { + return translations == null ? null : translations.toObjectArray(); + } + + + /** + * Sets the keyframe times. + * + * @param times the desired keyframe times (alias created, not null, not empty) + */ + public void setTimes(float[] times) { + if (times == null || times.length == 0) { + throw new IllegalArgumentException( + "No keyframe times were provided."); + } + this.times = times; + length = times[times.length - 1] - times[0]; + } + + /** + * Sets the translations. + * + * @param translations the desired translation of the target for each + * keyframe (not null, same length as "times") + */ + public void setKeyframesTranslation(Vector3f[] translations) { + if (times == null) { + throw new IllegalStateException( + "TransformTrack lacks keyframe times. " + + "Please invoke setTimes() first."); + } + if (translations == null || translations.length == 0) { + throw new IllegalArgumentException( + "No translations were provided."); + } + this.translations = new CompactVector3Array(); + this.translations.add(translations); + this.translations.freeze(); + + assert times != null && times.length == translations.length; + } + + /** + * Sets the scales. + * + * @param scales the desired scale of the target for each keyframe (not + * null, same length as "times") + */ + public void setKeyframesScale(Vector3f[] scales) { + if (times == null) { + throw new IllegalStateException( + "TransformTrack lacks keyframe times. " + + "Please invoke setTimes() first."); + } + if (scales == null || scales.length == 0) { + throw new IllegalArgumentException( + "No scale vectors were provided."); + } + this.scales = new CompactVector3Array(); + this.scales.add(scales); + this.scales.freeze(); + + assert times != null && times.length == scales.length; + } + + /** + * Sets the rotations. + * + * @param rotations the desired rotation of the target for each keyframe + * (not null, same length as "times") + */ + public void setKeyframesRotation(Quaternion[] rotations) { + if (times == null) { + throw new IllegalStateException( + "TransformTrack lacks keyframe times. " + + "Please invoke setTimes() first."); + } + if (rotations == null || rotations.length == 0) { + throw new IllegalArgumentException( + "No rotations were provided."); + } + this.rotations = new CompactQuaternionArray(); + this.rotations.add(rotations); + this.rotations.freeze(); + + assert times != null && times.length == rotations.length; + } + + + /** + * Sets the translations, rotations, and/or scales. + * + * @param times the desired time for each keyframe, + * or null to leave the times unchanged + * @param translations the desired translation of the target for each + * keyframe (same length as times) + * or null to leave the translations unchanged + * @param rotations the desired rotation of the target for each keyframe + * (same length as times) + * or null to leave the rotations unchanged + * @param scales the desired scale of the target for each keyframe + * (same length as times) + * or null to leave the scales unchanged + */ + public void setKeyframes(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) { + if (times != null) { + setTimes(times); + } + if (translations != null) { + setKeyframesTranslation(translations); + } + if (rotations != null) { + setKeyframesRotation(rotations); + } + if (scales != null) { + setKeyframesScale(scales); + } + } + + @Override + public double getLength() { + return length; + } + + @Override + public void getDataAtTime(double t, Transform transform) { + float time = (float) t; + + int lastFrame = times.length - 1; + if (time < 0 || lastFrame == 0) { + if (translations != null) { + translations.get(0, transform.getTranslation()); + } + if (rotations != null) { + rotations.get(0, transform.getRotation()); + } + if (scales != null) { + scales.get(0, transform.getScale()); + } + return; + } + + int startFrame = 0; + int endFrame = 1; + float blend = 0; + if (time >= times[lastFrame]) { + // extrapolate beyond the final keyframe of the animation + startFrame = lastFrame; + + float inferredInterval = times[lastFrame] - times[lastFrame - 1]; + if (inferredInterval > 0f) { + blend = (time - times[startFrame]) / inferredInterval; + } + + } else { + // use lastFrame so we never overflow the array + int i; + for (i = 0; i < lastFrame && times[i] < time; i++) { + startFrame = i; + endFrame = i + 1; + } + blend = (time - times[startFrame]) + / (times[endFrame] - times[startFrame]); + } + + FrameInterpolator fi = (interpolator == null) + ? FrameInterpolator.getThreadDefault() : interpolator; + Transform interpolated = fi.interpolate( + blend, startFrame, translations, rotations, scales, times); + + if (translations != null) { + transform.setTranslation(interpolated.getTranslation()); + } + if (rotations != null) { + transform.setRotation(interpolated.getRotation()); + } + if (scales != null) { + transform.setScale(interpolated.getScale()); + } + } + + /** + * Access the FrameInterpolator. + * + * @return the pre-existing instance or null + */ + public FrameInterpolator getFrameInterpolator() { + return interpolator; + } + + /** + * Replaces the frame interpolator. + * + * @param interpolator the interpolator to use (alias created) + */ + public void setFrameInterpolator(FrameInterpolator interpolator) { + this.interpolator = interpolator; + } + + /** + * Gives access to the target, which might be a Joint or a Spatial. + * + * @return the pre-existing instance + */ + public HasLocalTransform getTarget() { + return target; + } + + /** + * Replaces the target, which might be a Joint or a Spatial. + * + * @param target the target to use (alias created) + */ + public void setTarget(HasLocalTransform target) { + this.target = target; + } + + /** + * Serializes this track to the specified exporter, for example when + * saving to a J3O file. + * + * @param ex the exporter to write to (not null) + * @throws IOException from the exporter + */ + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(translations, "translations", null); + oc.write(rotations, "rotations", null); + oc.write(times, "times", null); + oc.write(scales, "scales", null); + oc.write(target, "target", null); + } + + /** + * De-serializes this track from the specified importer, for example when + * loading from a J3O file. + * + * @param im the importer to read from (not null) + * @throws IOException from the importer + */ + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + translations = (CompactVector3Array) ic.readSavable("translations", null); + rotations = (CompactQuaternionArray) ic.readSavable("rotations", null); + times = ic.readFloatArray("times", null); + scales = (CompactVector3Array) ic.readSavable("scales", null); + target = (HasLocalTransform) ic.readSavable("target", null); + setTimes(times); + } + + @Override + public TransformTrack jmeClone() { + try { + TransformTrack clone = (TransformTrack) super.clone(); + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + this.target = cloner.clone(target); + } +}