Skip to main content

pfx_load/scene/
anim.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use pfx_core::anim::{Atlas, ClipEvent, FrameEvent, Playback, Rig, SpriteClip, SpriteSheet};
5use pfx_scene::Entry;
6use pfx_scene::types as file;
7
8use super::build::{Places, Reader, Step, content_from, inner};
9use super::{Content, Matrix, Object, SceneError, SceneMesh};
10
11pub const SPRITE_CLIP: &str = "";
12
13#[derive(Clone, Debug, Default, PartialEq)]
14pub struct Posed {
15    pub time: f32,
16    pub models: Vec<Matrix>,
17    pub palettes: BTreeMap<usize, Vec<Matrix>>,
18    pub frames: BTreeMap<usize, [f32; 4]>,
19}
20
21impl Posed {
22    pub fn at(time: f32) -> Self {
23        Self {
24            time,
25            ..Self::default()
26        }
27    }
28}
29
30#[derive(Clone, Debug, PartialEq)]
31pub struct Animation {
32    pub clip: Option<String>,
33    pub blend: Vec<(String, f32)>,
34    pub speed: f32,
35    pub playback: Playback,
36    pub start: f32,
37    pub rig: Option<Arc<Rig>>,
38    pub sprite: Option<Sprite>,
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct Sprite {
43    pub sheet: Arc<SpriteSheet>,
44    pub content: String,
45}
46
47impl Animation {
48    pub fn plays(&self) -> bool {
49        self.clip.is_some() || !self.blend.is_empty()
50    }
51
52    pub fn still_frame(&self) -> Option<[f32; 4]> {
53        let sprite = self.sprite.as_ref()?;
54        let sheet = &sprite.sheet;
55        let frame = self
56            .clip
57            .as_deref()
58            .and_then(|clip| sheet.clip(clip))
59            .map(|clip| {
60                let clip = &sheet.clips()[clip];
61                let step = (self.start as f64 * clip.fps() as f64).floor() as u64;
62                clip.frames()[clip.position(step)]
63            })
64            .unwrap_or(0);
65        sheet.atlas().uv(frame)
66    }
67}
68
69pub fn surface(frame: [f32; 4]) -> ([f32; 2], [f32; 2], [f32; 4]) {
70    (
71        [frame[0], frame[1]],
72        [frame[2] - frame[0], frame[3] - frame[1]],
73        frame,
74    )
75}
76
77fn playback(repeat: Option<file::Repeat>) -> Option<Playback> {
78    repeat.map(|repeat| match repeat {
79        file::Repeat::Loop => Playback::Loop,
80        file::Repeat::Once => Playback::Once,
81        file::Repeat::PingPong => Playback::PingPong,
82    })
83}
84
85pub fn content_key(object: &str) -> String {
86    format!("sprite:{object}")
87}
88
89pub(super) fn build_animations(
90    places: &Places,
91    entries: &[Entry<file::Object>],
92    objects: &mut [Object],
93    meshes: &BTreeMap<String, SceneMesh>,
94    contents: &mut BTreeMap<String, Content>,
95    reader: &mut Reader,
96) -> Result<BTreeMap<String, Animation>, SceneError> {
97    let mut out = BTreeMap::new();
98    for (entry, object) in entries.iter().zip(objects.iter_mut()) {
99        let Some(found) = &entry.value.animation else {
100            continue;
101        };
102        let name = entry.key.as_str();
103        let refuse = |key: Option<&str>, message: String| {
104            let mut at = vec![Step::Named("object", inner(name)), Step::Key("animation")];
105            if let Some(key) = key {
106                at.push(Step::Key(key));
107            }
108            places.refusal(&entry.file, &at, format!("object {name}: {message}"))
109        };
110        let looped = playback(found.looping).unwrap_or_default();
111        let mut animation = Animation {
112            clip: found.clip.clone(),
113            blend: found
114                .blend
115                .iter()
116                .flatten()
117                .map(|blend| {
118                    (
119                        blend.clip.clone(),
120                        blend.weight.unwrap_or(file::BlendClip::WEIGHT),
121                    )
122                })
123                .collect(),
124            speed: found.speed.unwrap_or(file::Animation::SPEED),
125            playback: looped,
126            start: found.start.unwrap_or(file::Animation::START),
127            rig: None,
128            sprite: None,
129        };
130        if found.is_sprite() {
131            let atlas_path = found.atlas.as_deref().unwrap_or_default();
132            let path = places.root.join(atlas_path);
133            let bytes = reader
134                .read(&path)
135                .map_err(|message| refuse(Some("atlas"), message))?;
136            let content =
137                content_from(path, &bytes).map_err(|message| refuse(Some("atlas"), message))?;
138            if let Some(other) = &object.content {
139                return Err(refuse(
140                    None,
141                    format!("it shows content {other} and a sprite; give it one"),
142                ));
143            }
144            let size = [content.width, content.height];
145            let atlas = match (&found.grid, &found.frames) {
146                (Some([columns, rows]), _) => Atlas::grid(
147                    size,
148                    [size[0] / columns.max(&1), size[1] / rows.max(&1)],
149                    Some((columns * rows) as usize),
150                ),
151                (None, Some(frames)) => Atlas::rects(size, frames.clone()),
152                (None, None) => Atlas::grid(size, size, None),
153            }
154            .map_err(|error| {
155                refuse(
156                    Some(if found.grid.is_some() {
157                        "grid"
158                    } else {
159                        "frames"
160                    }),
161                    error.to_string(),
162                )
163            })?;
164            let fps = found.fps.unwrap_or(file::Animation::FPS);
165            let mut clips: Vec<SpriteClip> = Vec::new();
166            if found.clips.is_empty() {
167                clips.push(
168                    SpriteClip::new(SPRITE_CLIP, (0..atlas.len()).collect(), fps, looped)
169                        .map_err(|error| refuse(None, error.to_string()))?,
170                );
171                if animation.clip.is_none() {
172                    animation.clip = Some(SPRITE_CLIP.to_string());
173                }
174            }
175            for (clip, range) in &found.clips {
176                if range.to as usize >= atlas.len() {
177                    return Err(refuse(
178                        Some("clips"),
179                        format!(
180                            "sprite clip {clip} runs to frame {}, past the atlas's last frame {}",
181                            range.to,
182                            atlas.len().saturating_sub(1)
183                        ),
184                    ));
185                }
186                clips.push(
187                    SpriteClip::new(
188                        clip.clone(),
189                        (range.from as usize..=range.to as usize).collect(),
190                        range.fps.unwrap_or(fps),
191                        playback(range.looping).unwrap_or(looped),
192                    )
193                    .map_err(|error| refuse(Some("clips"), error.to_string()))?,
194                );
195            }
196            let mut marks: BTreeMap<String, Vec<FrameEvent>> = BTreeMap::new();
197            for event in &found.events {
198                let clip = event
199                    .clip
200                    .clone()
201                    .or_else(|| animation.clip.clone())
202                    .unwrap_or_default();
203                let Some(index) = clips.iter().position(|candidate| candidate.name() == clip)
204                else {
205                    return Err(refuse(
206                        Some("events"),
207                        format!(
208                            "event {} marks clip {clip}, which the sprite has not",
209                            event.name
210                        ),
211                    ));
212                };
213                let target = &clips[index];
214                let frame = match (event.frame, event.time) {
215                    (Some(frame), _) => frame as usize,
216                    (None, Some(time)) => (time as f64 * target.fps() as f64).floor() as usize,
217                    (None, None) => continue,
218                };
219                if frame < target.frames().len() {
220                    marks
221                        .entry(clip)
222                        .or_default()
223                        .push(FrameEvent::new(frame, event.name.clone()));
224                }
225            }
226            let clips = clips
227                .into_iter()
228                .map(|clip| match marks.remove(clip.name()) {
229                    Some(events) => clip.with_events(events),
230                    None => Ok(clip),
231                })
232                .collect::<Result<Vec<_>, _>>()
233                .map_err(|error| refuse(Some("events"), error.to_string()))?;
234            let sheet =
235                SpriteSheet::new(atlas, clips).map_err(|error| refuse(None, error.to_string()))?;
236            if let Some(clip) = &animation.clip
237                && sheet.clip(clip).is_none()
238            {
239                return Err(refuse(
240                    Some("clip"),
241                    format!("the sprite has no clip {clip}"),
242                ));
243            }
244            let key = content_key(name);
245            contents.insert(key.clone(), content);
246            object.content = Some(key.clone());
247            animation.sprite = Some(Sprite {
248                sheet: Arc::new(sheet),
249                content: key,
250            });
251        } else {
252            let Some(rig) = meshes.get(&object.mesh).and_then(|mesh| mesh.rig.clone()) else {
253                return Err(refuse(
254                    None,
255                    format!("mesh {} holds no animation clips", object.mesh),
256                ));
257            };
258            let known = |clip: &str| rig.clip(clip).is_some();
259            if let Some(clip) = &animation.clip
260                && !known(clip)
261            {
262                return Err(refuse(
263                    Some("clip"),
264                    format!("mesh {} has no clip {clip}", object.mesh),
265                ));
266            }
267            if let Some((clip, _)) = animation.blend.iter().find(|(clip, _)| !known(clip)) {
268                return Err(refuse(
269                    Some("blend"),
270                    format!("mesh {} has no clip {clip}", object.mesh),
271                ));
272            }
273            let mut marks: BTreeMap<String, Vec<ClipEvent>> = BTreeMap::new();
274            for event in &found.events {
275                let Some(clip) = event.clip.clone().or_else(|| animation.clip.clone()) else {
276                    return Err(refuse(
277                        Some("events"),
278                        format!("event {} names no clip", event.name),
279                    ));
280                };
281                let Some(index) = rig.clip(&clip) else {
282                    return Err(refuse(
283                        Some("events"),
284                        format!(
285                            "event {} marks clip {clip}, which mesh {} has not",
286                            event.name, object.mesh
287                        ),
288                    ));
289                };
290                let Some(time) = event.time else {
291                    return Err(refuse(
292                        Some("events"),
293                        format!("event {} of a glTF clip takes a time", event.name),
294                    ));
295                };
296                if time <= rig.clips()[index].duration() {
297                    marks
298                        .entry(clip)
299                        .or_default()
300                        .push(ClipEvent::new(time, event.name.clone()));
301                }
302            }
303            let mut rig = Rig::clone(&rig);
304            for (clip, events) in marks {
305                rig = rig
306                    .with_events(&clip, events)
307                    .map_err(|error| refuse(Some("events"), error.to_string()))?;
308            }
309            animation.rig = Some(Arc::new(rig));
310        }
311        out.insert(name.to_string(), animation);
312    }
313    Ok(out)
314}