diff --git a/src/blend_asset.rs b/src/blend_asset.rs index 035160a..10fc7e7 100644 --- a/src/blend_asset.rs +++ b/src/blend_asset.rs @@ -17,6 +17,7 @@ use bevy::{ }; use serde::Deserialize; use std::{ + collections::HashMap, fs, io, path::{Path, PathBuf}, process::Command, @@ -38,6 +39,11 @@ struct ExportManifest { dependencies: Vec, } +#[derive(Debug, Deserialize)] +struct AnimationManifest { + animations: HashMap, +} + impl AssetLoader for BlendSourceLoader { type Asset = BlendSource; type Settings = (); @@ -63,6 +69,12 @@ impl AssetLoader for BlendSourceLoader { }; let output = temp.path().join("export.gltf"); let script = temp.path().join("export.py"); + let actions = referenced_actions(load_context, &asset_root()).await?; + let actions_path = temp.path().join("actions.json"); + fs::write( + &actions_path, + serde_json::to_vec(&actions).map_err(io::Error::other)?, + )?; fs::write(&script, EXPORT_SCRIPT)?; let result = Command::new(std::env::var_os("BLENDER").unwrap_or_else(|| "blender".into())) @@ -70,9 +82,10 @@ impl AssetLoader for BlendSourceLoader { .arg(&blend_path) .arg("--python") .arg(&script) - .arg("--") - .arg(&output) - .output() + .arg("--") + .arg(&output) + .arg(&actions_path) + .output() .map_err(|error| io::Error::other(format!("failed to start Blender: {error}")))?; if !result.status.success() { return Err(io::Error::other(format!( @@ -127,6 +140,30 @@ impl AssetLoader for BlendSourceLoader { } } +async fn referenced_actions( + load_context: &mut LoadContext<'_>, + root: &Path, +) -> io::Result> { + let model_path = load_context.path().path(); + let Some(stem) = model_path.file_stem().and_then(|stem| stem.to_str()) else { + return Ok(Vec::new()); + }; + let manifest_path = model_path + .parent() + .unwrap_or_else(|| Path::new("")) + .join(format!("{stem}.fighter.ron")); + if !root.join(&manifest_path).is_file() { + return Ok(Vec::new()); + } + let bytes = load_context + .read_asset_bytes(manifest_path) + .await + .map_err(|error| io::Error::other(error.to_string()))?; + let manifest: AnimationManifest = ron::de::from_bytes(&bytes) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + Ok(manifest.animations.into_values().collect()) +} + #[derive(Asset, TypePath)] struct ProcessedBlend { bytes: Vec, diff --git a/src/blend_asset/export.py b/src/blend_asset/export.py index 703fad7..7a2c2ea 100644 --- a/src/blend_asset/export.py +++ b/src/blend_asset/export.py @@ -6,6 +6,41 @@ import sys def main(): output = sys.argv[sys.argv.index("--") + 1] + actions_path = sys.argv[sys.argv.index("--") + 2] + with open(actions_path) as file: + actions_to_keep = set(json.load(file)) + + for obj in bpy.data.objects: + if not obj.animation_data: + continue + for track in list(obj.animation_data.nla_tracks): + if track.name.startswith("[Action Stash]"): + obj.animation_data.nla_tracks.remove(track) + + nla_tracks = [ + track + for obj in bpy.data.objects + if obj.animation_data + for track in obj.animation_data.nla_tracks + ] + use_nla_tracks = bool(nla_tracks) + + # For blends without NLA tracks, use the actions referenced by the fighter + # manifest instead of sampling every source action. + if not use_nla_tracks and actions_to_keep: + from io_scene_gltf2 import GLTF2_filter_action + + if not hasattr(bpy.types.Scene, "gltf_action_filter"): + bpy.types.Scene.gltf_action_filter = bpy.props.CollectionProperty( + type=GLTF2_filter_action + ) + scene = bpy.context.scene + scene.gltf_action_filter.clear() + for action in bpy.data.actions: + item = scene.gltf_action_filter.add() + item.action = action + item.keep = action.name in actions_to_keep + bpy.ops.export_scene.gltf( filepath=output, export_format="GLTF_SEPARATE", @@ -15,6 +50,8 @@ def main(): export_skins=True, export_morph=True, export_yup=True, + export_action_filter=False, + export_animation_mode="NLA_TRACKS" if use_nla_tracks else "ACTIONS", ) dependencies = []