Skip to main content

optic_render/asset/attr/
attr.rs

1use optic_core::ATTRType;
2
3use crate::asset::attr::DataType;
4
5/// Discriminated name for a vertex or instance attribute.
6///
7/// Built-in names cover the standard attributes used by Optic's mesh and instance
8/// system. The [`Custom`](ATTRName::Custom) variant allows user-defined names for
9/// shader-specific data.
10#[derive(Clone, Debug, PartialEq)]
11pub enum ATTRName {
12    Custom(String),
13    Pos2D,
14    Pos3D,
15    Col,
16    UVM,
17    Nrm,
18    Ind,
19    Rot3D,
20    Rot2D,
21    Scale3D,
22    Scale2D,
23}
24
25impl ATTRName {
26    /// Returns a human-readable label for this attribute name.
27    pub fn as_string(&self) -> String {
28        match self {
29            ATTRName::Pos2D => "pos2d".into(),
30            ATTRName::Pos3D => "pos3d".into(),
31            ATTRName::Col => "color".into(),
32            ATTRName::UVM => "uv map".into(),
33            ATTRName::Nrm => "normals".into(),
34            ATTRName::Ind => "indices".into(),
35            ATTRName::Rot3D => "rotation 3d".into(),
36            ATTRName::Rot2D => "rotation 2d".into(),
37            ATTRName::Scale3D => "scale 3d".into(),
38            ATTRName::Scale2D => "scale 2d".into(),
39            ATTRName::Custom(n) => format!("{n}(custom)"),
40        }
41    }
42}
43
44/// Describes a single vertex or instance attribute: name, GL type, byte size, element count.
45///
46/// Used internally to configure VAO attribute pointers and to validate type matches
47/// at runtime (e.g. in [`MeshHandle::update_vertex`](crate::handles::MeshHandle::update_vertex)).
48#[derive(Clone, Debug)]
49pub struct ATTRInfo {
50    pub name: ATTRName,
51    pub typ: ATTRType,
52    pub byte_count: usize,
53    pub elem_count: usize,
54}
55
56impl ATTRInfo {
57    /// Creates an empty descriptor with default name `Pos3D` and zero sizes.
58    pub fn empty() -> Self {
59        Self {
60            name: ATTRName::Pos3D,
61            typ: ATTRType::F32,
62            byte_count: 0,
63            elem_count: 0,
64        }
65    }
66
67    /// Returns a human-readable type string like `"f32"` or `"[f32;3]"`.
68    pub fn fmt_as_string(&self) -> String {
69        let typ_str = match self.typ {
70            ATTRType::U8 => "u8",
71            ATTRType::I8 => "i8",
72            ATTRType::U16 => "u16",
73            ATTRType::I16 => "i16",
74            ATTRType::U32 => "u32",
75            ATTRType::I32 => "i32",
76            ATTRType::F32 => "f32",
77            ATTRType::F64 => "f64",
78        };
79        if self.elem_count == 1 {
80            typ_str.to_string()
81        } else {
82            format!("[{typ_str};{}]", self.elem_count)
83        }
84    }
85}
86
87macro_rules! attr {
88    ($(#[$meta:meta])* $attr:ident, $typ:ty, $name:expr) => {
89        $(#[$meta])*
90        #[derive(Debug, Clone)]
91        pub struct $attr {
92            pub data: Vec<$typ>,
93            pub info: ATTRInfo,
94        }
95
96        impl $attr {
97            pub fn empty() -> Self {
98                let mut info = ATTRInfo::empty();
99                info.typ = <$typ>::ATTR_FORMAT;
100                info.byte_count = <$typ>::BYTE_COUNT;
101                info.elem_count = <$typ>::ELEM_COUNT;
102                info.name = $name;
103                Self { data: Vec::new(), info }
104            }
105
106            pub fn from(vec: Vec<$typ>) -> Self {
107                let mut attr = Self::empty();
108                for elem in vec {
109                    attr.data.push(elem);
110                }
111                attr
112            }
113
114            pub fn from_array(array: &[$typ]) -> Self {
115                Self::from(Vec::from(array))
116            }
117
118            pub fn push(&mut self, elem: $typ) {
119                self.data.push(elem);
120            }
121
122            pub fn is_empty(&self) -> bool {
123                self.data.is_empty()
124            }
125        }
126    };
127}
128
129// 3D position attribute (`[f32; 3]` per vertex).
130attr!(Pos3DATTR, [f32; 3], ATTRName::Pos3D);
131// 2D position attribute (`[f32; 2]` per vertex).
132attr!(Pos2DATTR, [f32; 2], ATTRName::Pos2D);
133// RGBA colour attribute (`[f32; 4]` per vertex).
134attr!(ColATTR, [f32; 4], ATTRName::Col);
135// UV / texture coordinate attribute (`[f32; 2]` per vertex).
136attr!(UVMATTR, [f32; 2], ATTRName::UVM);
137// Normal vector attribute (`[f32; 3]` per vertex).
138attr!(NrmATTR, [f32; 3], ATTRName::Nrm);
139// Index attribute (`u32` per element).
140attr!(IndATTR, u32, ATTRName::Ind);
141// 3D rotation as a quaternion (`[f32; 4]` per instance).
142attr!(Rot3DATTR, [f32; 4], ATTRName::Rot3D);
143// 2D rotation as a single angle in degrees (`f32` per instance).
144attr!(Rot2DATTR, f32, ATTRName::Rot2D);
145// 3D scale (`[f32; 3]` per instance).
146attr!(Scale3DATTR, [f32; 3], ATTRName::Scale3D);
147// 2D scale (`[f32; 2]` per instance).
148attr!(Scale2DATTR, [f32; 2], ATTRName::Scale2D);
149
150/// User-defined vertex or instance attribute with arbitrary byte data.
151///
152/// Create via [`CustomATTR::empty`] or [`CustomATTR::from`] parameterised by a
153/// [`DataType`], then push elements with [`CustomATTR::push`].
154///
155/// # Example
156///
157/// ```
158/// use optic_render::asset::attr::CustomATTR;
159/// let mut attr = CustomATTR::empty::<u32>("bone_ids");
160/// attr.push(0u32);
161/// attr.push(1u32);
162/// ```
163#[derive(Debug)]
164pub struct CustomATTR {
165    pub data: Vec<u8>,
166    pub info: ATTRInfo,
167}
168
169impl CustomATTR {
170    /// Creates an empty custom attribute with the given name and data type.
171    pub fn empty<D: DataType>(name: &str) -> Self {
172        let mut info = ATTRInfo::empty();
173        info.typ = D::ATTR_FORMAT;
174        info.byte_count = D::BYTE_COUNT;
175        info.elem_count = D::ELEM_COUNT;
176        info.name = ATTRName::Custom(name.to_string());
177        Self { data: Vec::new(), info }
178    }
179
180    /// Creates a custom attribute from a `Vec` of typed elements.
181    pub fn from<D: DataType>(name: &str, vec: Vec<D>) -> Self {
182        let mut attr = Self::empty::<D>(name);
183        for elem in vec {
184            let bytes = elem.u8ify();
185            attr.data.extend_from_slice(&bytes);
186        }
187        attr
188    }
189
190    /// Creates a custom attribute from a slice of typed elements.
191    pub fn from_array<D: DataType + Clone>(name: &str, array: &[D]) -> Self {
192        Self::from(name, Vec::from(array))
193    }
194
195    /// Appends one typed element (serialised to raw bytes).
196    pub fn push<D: DataType>(&mut self, elem: D) {
197        let bytes = elem.u8ify();
198        self.data.extend_from_slice(&bytes);
199    }
200
201    /// Returns `true` when no elements have been pushed.
202    pub fn is_empty(&self) -> bool {
203        self.data.is_empty()
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn attr_info_empty() {
213        let info = ATTRInfo::empty();
214        assert_eq!(info.byte_count, 0);
215        assert_eq!(info.elem_count, 0);
216    }
217
218    #[test]
219    fn attr_info_fmt_as_string() {
220        let mut info = ATTRInfo::empty();
221        info.typ = ATTRType::F32;
222        info.elem_count = 3;
223        assert_eq!(info.fmt_as_string(), "[f32;3]");
224        info.elem_count = 1;
225        assert_eq!(info.fmt_as_string(), "f32");
226    }
227
228    #[test]
229    fn attr_name_as_string() {
230        assert_eq!(ATTRName::Pos2D.as_string(), "pos2d");
231        assert_eq!(ATTRName::Pos3D.as_string(), "pos3d");
232        assert_eq!(ATTRName::Col.as_string(), "color");
233        assert_eq!(ATTRName::UVM.as_string(), "uv map");
234        assert_eq!(ATTRName::Nrm.as_string(), "normals");
235        assert_eq!(ATTRName::Ind.as_string(), "indices");
236        assert_eq!(ATTRName::Rot3D.as_string(), "rotation 3d");
237        assert_eq!(ATTRName::Rot2D.as_string(), "rotation 2d");
238        assert_eq!(ATTRName::Scale3D.as_string(), "scale 3d");
239        assert_eq!(ATTRName::Scale2D.as_string(), "scale 2d");
240        let custom = ATTRName::Custom("user_data".into());
241        assert_eq!(custom.as_string(), "user_data(custom)");
242    }
243
244    #[test]
245    fn pos3d_attr() {
246        let mut attr = Pos3DATTR::empty();
247        assert!(attr.is_empty());
248        attr.push([1.0, 2.0, 3.0]);
249        assert!(!attr.is_empty());
250        assert_eq!(attr.data.len(), 1);
251        assert_eq!(attr.data[0], [1.0, 2.0, 3.0]);
252    }
253
254    #[test]
255    fn pos3d_from_array() {
256        let attr = Pos3DATTR::from_array(&[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]);
257        assert_eq!(attr.data.len(), 2);
258        assert_eq!(attr.info.name.as_string(), "pos3d");
259    }
260
261    #[test]
262    fn pos2d_attr() {
263        let mut attr = Pos2DATTR::empty();
264        attr.push([0.5, 0.5]);
265        assert_eq!(attr.data[0], [0.5, 0.5]);
266    }
267
268    #[test]
269    fn color_attr() {
270        let mut attr = ColATTR::empty();
271        attr.push([1.0, 0.0, 0.0, 1.0]);
272        assert_eq!(attr.data[0], [1.0, 0.0, 0.0, 1.0]);
273    }
274
275    #[test]
276    fn uvm_attr() {
277        let attr = UVMATTR::from_array(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
278        assert_eq!(attr.data.len(), 4);
279    }
280
281    #[test]
282    fn nrm_attr() {
283        let attr = NrmATTR::from_array(&[[0.0, 1.0, 0.0]]);
284        assert_eq!(attr.data[0], [0.0, 1.0, 0.0]);
285    }
286
287    #[test]
288    fn ind_attr() {
289        let mut attr = IndATTR::empty();
290        attr.push(0);
291        attr.push(1);
292        attr.push(2);
293        assert_eq!(attr.data, vec![0, 1, 2]);
294    }
295
296    #[test]
297    fn rot3d_attr() {
298        let mut attr = Rot3DATTR::empty();
299        attr.push([0.0, 0.0, 0.0, 1.0]);
300        assert_eq!(attr.data[0], [0.0, 0.0, 0.0, 1.0]);
301        assert_eq!(attr.info.elem_count, 4);
302        assert_eq!(attr.info.byte_count, 4);
303    }
304
305    #[test]
306    fn rot2d_attr() {
307        let mut attr = Rot2DATTR::empty();
308        attr.push(1.5708);
309        assert!((attr.data[0] - 1.5708).abs() < 0.001);
310        assert_eq!(attr.info.elem_count, 1);
311    }
312
313    #[test]
314    fn scale3d_attr() {
315        let attr = Scale3DATTR::from_array(&[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]);
316        assert_eq!(attr.data.len(), 2);
317        assert_eq!(attr.info.name.as_string(), "scale 3d");
318    }
319
320    #[test]
321    fn custom_attr_empty() {
322        let attr = CustomATTR::empty::<[f32; 3]>("weights");
323        assert!(attr.is_empty());
324        assert_eq!(attr.info.name.as_string(), "weights(custom)");
325        assert_eq!(attr.info.typ, ATTRType::F32);
326        assert_eq!(attr.info.elem_count, 3);
327    }
328
329    #[test]
330    fn custom_attr_from_array() {
331        let attr = CustomATTR::from_array::<[f32; 3]>("bone_weights", &[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
332        assert!(!attr.is_empty());
333        assert_eq!(attr.data.len(), 24); // 2 * 3 * 4 bytes
334    }
335
336    #[test]
337    fn custom_attr_push() {
338        let mut attr = CustomATTR::empty::<u32>("ids");
339        attr.push(42u32);
340        attr.push(99u32);
341        assert_eq!(attr.data.len(), 8); // 2 * 4 bytes
342    }
343}