Limit Blender animation export to authored clips
This commit is contained in:
@@ -17,6 +17,7 @@ use bevy::{
|
|||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs, io,
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
@@ -38,6 +39,11 @@ struct ExportManifest {
|
|||||||
dependencies: Vec<String>,
|
dependencies: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AnimationManifest {
|
||||||
|
animations: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AssetLoader for BlendSourceLoader {
|
impl AssetLoader for BlendSourceLoader {
|
||||||
type Asset = BlendSource;
|
type Asset = BlendSource;
|
||||||
type Settings = ();
|
type Settings = ();
|
||||||
@@ -63,6 +69,12 @@ impl AssetLoader for BlendSourceLoader {
|
|||||||
};
|
};
|
||||||
let output = temp.path().join("export.gltf");
|
let output = temp.path().join("export.gltf");
|
||||||
let script = temp.path().join("export.py");
|
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)?;
|
fs::write(&script, EXPORT_SCRIPT)?;
|
||||||
|
|
||||||
let result = Command::new(std::env::var_os("BLENDER").unwrap_or_else(|| "blender".into()))
|
let result = Command::new(std::env::var_os("BLENDER").unwrap_or_else(|| "blender".into()))
|
||||||
@@ -72,6 +84,7 @@ impl AssetLoader for BlendSourceLoader {
|
|||||||
.arg(&script)
|
.arg(&script)
|
||||||
.arg("--")
|
.arg("--")
|
||||||
.arg(&output)
|
.arg(&output)
|
||||||
|
.arg(&actions_path)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|error| io::Error::other(format!("failed to start Blender: {error}")))?;
|
.map_err(|error| io::Error::other(format!("failed to start Blender: {error}")))?;
|
||||||
if !result.status.success() {
|
if !result.status.success() {
|
||||||
@@ -127,6 +140,30 @@ impl AssetLoader for BlendSourceLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn referenced_actions(
|
||||||
|
load_context: &mut LoadContext<'_>,
|
||||||
|
root: &Path,
|
||||||
|
) -> io::Result<Vec<String>> {
|
||||||
|
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)]
|
#[derive(Asset, TypePath)]
|
||||||
struct ProcessedBlend {
|
struct ProcessedBlend {
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
|
|||||||
@@ -6,6 +6,41 @@ import sys
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
output = sys.argv[sys.argv.index("--") + 1]
|
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(
|
bpy.ops.export_scene.gltf(
|
||||||
filepath=output,
|
filepath=output,
|
||||||
export_format="GLTF_SEPARATE",
|
export_format="GLTF_SEPARATE",
|
||||||
@@ -15,6 +50,8 @@ def main():
|
|||||||
export_skins=True,
|
export_skins=True,
|
||||||
export_morph=True,
|
export_morph=True,
|
||||||
export_yup=True,
|
export_yup=True,
|
||||||
|
export_action_filter=False,
|
||||||
|
export_animation_mode="NLA_TRACKS" if use_nla_tracks else "ACTIONS",
|
||||||
)
|
)
|
||||||
|
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
Reference in New Issue
Block a user