Skip to main content

concinnity_asset/
instanced_prop.rs

1// Instanced-prop schema.
2
3use crate::{
4    AssetId, MaterialHandle, MeshHandle, TextureHandle, de_opt_material_handle, de_opt_mesh_handle,
5    de_opt_texture_handle,
6};
7use alloc::vec::Vec;
8
9/// Per-instance transform within an `InstancedProp`.
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11#[serde(default)]
12pub struct InstanceTransform {
13    /// World-space position `[x, y, z]`.
14    pub position: [f32; 3],
15    /// Euler rotation in degrees `[pitch, yaw, roll]`, applied in YXZ order.
16    pub rotation_deg: [f32; 3],
17    /// Non-uniform scale `[x, y, z]`.
18    pub scale: [f32; 3],
19}
20
21impl Default for InstanceTransform {
22    fn default() -> Self {
23        Self {
24            position: [0.0, 0.0, 0.0],
25            rotation_deg: [0.0, 0.0, 0.0],
26            scale: [1.0, 1.0, 1.0],
27        }
28    }
29}
30
31/// A single mesh + material drawn at many world-space transforms.
32///
33/// Use for foliage, debris, projectiles, or any content that repeats the same
34/// shape with varied placement. Each instance gets its own world transform and
35/// culling without the overhead of declaring many separate [Prop](#prop)s.
36///
37/// Each `instances` entry has the shape `{"position":[x,y,z], "rotation_deg":[p,y,r], "scale":[sx,sy,sz]}`.
38/// `rotation_deg` and `scale` may be omitted (defaults `[0,0,0]` and `[1,1,1]`).
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40#[serde(default)]
41pub struct InstancedProp {
42    /// Asset identity; injected via `inject_name`. Not part of `args`.
43    #[serde(skip)]
44    pub asset_id: AssetId,
45    /// A [Mesh](#mesh), [ProceduralMesh](#proceduralmesh),
46    /// [VoxelChunk](#voxelchunk), or mesh-kind [File](#file) asset.
47    #[serde(deserialize_with = "de_opt_mesh_handle")]
48    pub mesh: Option<MeshHandle>,
49    /// A [Material](#material); takes precedence over `texture` when set.
50    #[serde(deserialize_with = "de_opt_material_handle")]
51    pub material: Option<MaterialHandle>,
52    /// Older texture-only reference; ignored when `material` is set.
53    #[serde(deserialize_with = "de_opt_texture_handle")]
54    pub texture: Option<TextureHandle>,
55    /// Per-instance transforms. Empty list renders nothing.
56    pub instances: Vec<InstanceTransform>,
57    /// View-distance cutoff in world units per instance. 0 = always draw.
58    pub cull_distance: f32,
59}
60
61impl Default for InstancedProp {
62    fn default() -> Self {
63        Self {
64            asset_id: AssetId::default(),
65            mesh: None,
66            material: None,
67            texture: None,
68            instances: Vec::new(),
69            cull_distance: 0.0,
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn a_blank_instance_is_an_untransformed_copy() {
80        let t = InstanceTransform::default();
81        assert_eq!(t.position, [0.0, 0.0, 0.0]);
82        assert_eq!(t.rotation_deg, [0.0, 0.0, 0.0]);
83        // Unit scale, not zero: an omitted scale must not collapse the copy.
84        assert_eq!(t.scale, [1.0, 1.0, 1.0]);
85    }
86
87    #[test]
88    fn a_blank_prop_draws_nothing_and_never_culls() {
89        let p = InstancedProp::default();
90        assert!(p.instances.is_empty());
91        assert!(p.mesh.is_none());
92        assert!(p.material.is_none());
93        assert!(p.texture.is_none());
94        // Zero means "no distance cull", not "cull everything".
95        assert_eq!(p.cull_distance, 0.0);
96    }
97
98    #[test]
99    fn an_authored_instance_list_parses_and_round_trips_through_postcard() {
100        crate::test_support::install_resolvers();
101        let p: InstancedProp = serde_json::from_str(
102            r#"{"mesh":"tree_mesh","material":"bark","texture":"bark_tex","cull_distance":120,
103                "instances":[{"position":[1,0,2]},{"position":[3,0,4],"scale":[2,2,2]}]}"#,
104        )
105        .unwrap();
106        assert_eq!(p.mesh, Some(MeshHandle(9)));
107        assert_eq!(p.material, Some(MaterialHandle(4)));
108        assert_eq!(p.texture, Some(TextureHandle(8)));
109        assert_eq!(p.instances.len(), 2);
110        // A copy that mentions only its position keeps unit scale.
111        assert_eq!(p.instances[0].scale, [1.0, 1.0, 1.0]);
112        assert_eq!(p.instances[1].scale, [2.0, 2.0, 2.0]);
113
114        let bytes = postcard::to_allocvec(&p).unwrap();
115        let back: InstancedProp = postcard::from_bytes(&bytes).unwrap();
116        assert_eq!(back.instances.len(), 2);
117        assert_eq!(back.instances[1].position, [3.0, 0.0, 4.0]);
118        assert_eq!(back.cull_distance, 120.0);
119        assert_eq!(back.asset_id, AssetId::default());
120    }
121}