veloren_voxygen/scene/terrain/
sprite.rs

1use std::ops::Range;
2
3use super::SPRITE_LOD_LEVELS;
4use common::{
5    assets,
6    terrain::{
7        Block, SpriteKind,
8        sprite::{self, RelativeNeighborPosition},
9    },
10};
11use hashbrown::HashMap;
12use serde::Deserialize;
13use vek::*;
14
15#[derive(Deserialize, Debug)]
16/// Configuration data for an individual sprite model.
17#[serde(deny_unknown_fields)]
18pub(super) struct SpriteModelConfig {
19    /// Data for the .vox model associated with this sprite.
20    pub model: String,
21    /// Sprite model center (as an offset from 0 in the .vox file).
22    pub offset: (f32, f32, f32),
23    /// LOD axes (how LOD gets applied along each axis, when we switch
24    /// to an LOD model).
25    pub lod_axes: (f32, f32, f32),
26}
27
28macro_rules! impl_sprite_attribute_filter {
29    (
30        $($attr:ident $field_name:ident = |$filter_arg:ident: $filter_ty:ty, $value_arg:ident| $filter:block),+ $(,)?
31    ) => {
32        // TODO: depending on what types of filters we end up with an enum may end up being more suitable.
33        #[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq, Hash)]
34        #[serde(default, deny_unknown_fields)]
35        pub struct SpriteAttributeFilters {
36            $(
37                pub $field_name: Option<$filter_ty>,
38            )+
39        }
40
41        impl SpriteAttributeFilters {
42            fn matches_filter(&self, block: &Block) -> bool {
43                $(
44                    self.$field_name.as_ref().map_or(true, |$filter_arg| {
45                        block
46                            .get_attr::<sprite::$attr>()
47                            .map_or(false, |$value_arg| $filter)
48                    })
49                )&&+
50            }
51
52            #[cfg(test)]
53            fn is_valid_for_category(&self, category: sprite::Category) -> Result<(), &'static str> {
54                $(if self.$field_name.is_some() && !category.has_attr::<sprite::$attr>() {
55                    return Err(::std::any::type_name::<sprite::$attr>());
56                })*
57                Ok(())
58            }
59
60            fn no_filters(&self) -> bool {
61                true $(&& self.$field_name.is_none())+
62            }
63        }
64    };
65}
66
67impl_sprite_attribute_filter!(
68    Growth growth_stage = |filter: Range<u8>, growth| { filter.contains(&growth.0) },
69    LightEnabled light_enabled = |filter: bool, light_enabled| { *filter == light_enabled.0 },
70    Collectable collectable = |filter: bool, collectable| { *filter == collectable.0 },
71    Damage damage = |filter: Range<u8>, damage| { filter.contains(&damage.0) },
72    AdjacentType adjacent_type = |filter: RelativeNeighborPosition, adjacent_type| { (*filter as u8) == adjacent_type.0 },
73    SnowCovered snow_covered = |filter: bool, snow_covered| { *filter == snow_covered.0 },
74);
75
76/// Configuration data for a group of sprites (currently associated with a
77/// particular SpriteKind).
78#[derive(Deserialize, Debug)]
79#[serde(deny_unknown_fields)]
80struct SpriteConfig {
81    /// Filter for selecting what config to use based on sprite attributes.
82    #[serde(default)]
83    filter: SpriteAttributeFilters,
84    /// All possible model variations for this sprite.
85    // NOTE: Could make constant per sprite type, but eliminating this indirection and
86    // allocation is probably not that important considering how sprites are used.
87    #[serde(default)]
88    variations: Vec<SpriteModelConfig>,
89    /// The extent to which the sprite sways in the wind.
90    ///
91    /// 0.0 is normal.
92    #[serde(default)]
93    wind_sway: f32,
94}
95
96#[serde_with::serde_as]
97#[derive(Deserialize)]
98struct SpriteSpecRaw(
99    #[serde_as(as = "serde_with::MapPreventDuplicates<_, _>")]
100    HashMap<SpriteKind, Vec<SpriteConfig>>,
101);
102
103/// Configuration data for all sprite models.
104///
105/// NOTE: Model is an asset path to the appropriate sprite .vox model.
106#[derive(Deserialize)]
107#[serde(try_from = "SpriteSpecRaw")]
108pub struct SpriteSpec(HashMap<SpriteKind, Vec<SpriteConfig>>);
109
110/// Conversion of [`SpriteSpec`] from a hashmap failed because some sprite kinds
111/// were missing.
112struct SpritesMissing(Vec<SpriteKind>);
113
114impl core::fmt::Display for SpritesMissing {
115    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116        writeln!(
117            f,
118            "Missing entries in the sprite manifest for these sprites: {:?}",
119            &self.0,
120        )
121    }
122}
123
124impl TryFrom<SpriteSpecRaw> for SpriteSpec {
125    type Error = SpritesMissing;
126
127    fn try_from(SpriteSpecRaw(map): SpriteSpecRaw) -> Result<Self, Self::Error> {
128        let sprites_missing = SpriteKind::all()
129            .iter()
130            .copied()
131            .filter(|kind| !map.contains_key(kind))
132            .collect::<Vec<_>>();
133
134        if sprites_missing.is_empty() {
135            Ok(Self(map))
136        } else {
137            Err(SpritesMissing(sprites_missing))
138        }
139    }
140}
141
142impl assets::Asset for SpriteSpec {
143    type Loader = assets::RonLoader;
144
145    const EXTENSION: &'static str = "ron";
146}
147
148impl SpriteSpec {
149    pub fn map_to_data(
150        &self,
151        mut map_variation: impl FnMut(&SpriteModelConfig) -> [SpriteModelData; super::SPRITE_LOD_LEVELS],
152    ) -> HashMap<SpriteKind, FilteredSpriteData> {
153        let mut to_sprite_data = |config: &SpriteConfig| SpriteData {
154            variations: config.variations.iter().map(&mut map_variation).collect(),
155            wind_sway: config.wind_sway,
156        };
157
158        // Note, the returned datastructure can potentially be optimized further from a
159        // HashMap, a phf could be used or if we can rely on the sprite kind
160        // discriminants in each sprite category being packed fairly densely, we
161        // could just have an offset per sprite catagory used to
162        // convert a sprite kind into a flat index.
163        self.0
164            .iter()
165            .map(|(kind, config)| {
166                let filtered_data = match config.as_slice() {
167                    [config] if config.filter.no_filters() => {
168                        FilteredSpriteData::Unfiltered(to_sprite_data(config))
169                    },
170                    // Note, we have a test that checks if this is completely empty. That should be
171                    // represented by an entry with no variantions instead of having an empty
172                    // top-level list.
173                    filtered_configs => {
174                        let list = filtered_configs
175                            .iter()
176                            .map(|config| (config.filter.clone(), to_sprite_data(config)))
177                            .collect::<Box<[_]>>();
178                        FilteredSpriteData::Filtered(list)
179                    },
180                };
181                (*kind, filtered_data)
182            })
183            .collect()
184    }
185}
186
187pub(in crate::scene) struct SpriteModelData {
188    // Sprite vert page ranges that need to be drawn
189    pub vert_pages: core::ops::Range<u32>,
190    // Scale
191    pub scale: Vec3<f32>,
192    // Offset
193    pub offset: Vec3<f32>,
194}
195
196pub(in crate::scene) struct SpriteData {
197    pub variations: Box<[[SpriteModelData; SPRITE_LOD_LEVELS]]>,
198    /// See [`SpriteConfig::wind_sway`].
199    pub wind_sway: f32,
200}
201
202pub(in crate::scene) enum FilteredSpriteData {
203    // Special case when there is only one entry with the an empty filter since this is most
204    // cases, and it will reduce indirection.
205    Unfiltered(SpriteData),
206    Filtered(Box<[(SpriteAttributeFilters, SpriteData)]>),
207}
208
209impl FilteredSpriteData {
210    /// Gets sprite data for the filter that matches the provided block.
211    ///
212    /// This only returns `None` if no filters matches the provided block (i.e.
213    /// the set of filters does not cover all values). A "missing"
214    /// placeholder model can be displayed in this case in this case.
215    pub fn for_block(&self, block: &Block) -> Option<&SpriteData> {
216        match self {
217            Self::Unfiltered(data) => Some(data),
218            Self::Filtered(multiple) => multiple
219                .iter()
220                .find_map(|(filter, data)| filter.matches_filter(block).then_some(data)),
221        }
222    }
223}
224
225#[cfg(test)]
226mod test {
227    use super::SpriteSpec;
228    use common::assets::AssetExt;
229
230    #[test]
231    fn test_sprite_spec_valid() {
232        let spec = SpriteSpec::load_expect("voxygen.voxel.sprite_manifest").read();
233
234        // Test that filters are relevant for the particular sprite kind.
235        for (sprite, filter) in spec.0.iter().flat_map(|(&sprite, configs)| {
236            configs.iter().map(move |config| (sprite, &config.filter))
237        }) {
238            if let Err(invalid_attribute) = filter.is_valid_for_category(sprite.category()) {
239                panic!(
240                    "Sprite category '{:?}' does not have attribute '{}' (in sprite config for \
241                     {:?})",
242                    sprite.category(),
243                    invalid_attribute,
244                    sprite,
245                );
246            }
247        }
248
249        // Test that there is at least one entry per sprite. An empty variations list in
250        // an entry is used to represent a sprite that doesn't have a model.
251        let mut empty_config = Vec::new();
252        for (kind, configs) in &spec.0 {
253            if configs.is_empty() {
254                empty_config.push(kind)
255            }
256        }
257        assert!(
258            empty_config.is_empty(),
259            "Sprite config(s) with no entries, if these sprite(s) are intended to have no models \
260             use an explicit entry with an empty `variations` list instead: {empty_config:?}",
261        );
262    }
263}