1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// Character-model schema: a body conforming to a CharacterSchema.
use crate::{CharacterCapsule, MaterialHandle, de_opt_material_handle};
use alloc::string::String;
use alloc::vec::Vec;
/// A character body that conforms to a [CharacterSchema](#characterschema).
///
/// The build validates the source against the schema (joint names and
/// parentage, complete `+` / `-` pairs for bipolar keys), imports it,
/// generates the schema's synthesized targets, and emits one
/// [SkinnedMesh](#skinnedmesh) under this asset's name. A
/// [CharacterShape](#charactershape) or [Animation](#animation) targets the
/// model by this name exactly as it would a `SkinnedMesh`. Lower levels of
/// detail come from `lod_levels`, as on a `SkinnedMesh`.
///
/// The source's extra shape keys (ones the schema does not list) are
/// imported and appear under the editor panel's "Other" section.
///
/// ```rust
/// # use concinnity_asset::CharacterModel;
/// CharacterModel {
/// schema: "builtin:humanoid".into(),
/// source: "./base_humanoid.glb".into(),
/// lod_levels: 3,
/// ..Default::default()
/// };
/// ```
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct CharacterModel {
/// The [CharacterSchema](#characterschema) the source conforms to, by
/// asset name or the reserved `builtin:humanoid`.
pub schema: String,
/// Path to the `.glb` / `.gltf` body.
pub source: String,
/// Which skinned mesh of `source` to import, in file order.
pub skin_index: u32,
/// [Material](#material) of the emitted mesh.
#[serde(deserialize_with = "de_opt_material_handle")]
pub material: Option<MaterialHandle>,
/// World-space position.
pub position: [f32; 3],
/// World rotation, Euler degrees [pitch, yaw, roll], YXZ order.
pub rotation_deg: [f32; 3],
/// World scale.
pub scale: [f32; 3],
/// Number of level-of-detail versions to generate, including the
/// original. `1` (the default) generates none; values are clamped to
/// `[1, 8]`.
pub lod_levels: u32,
/// Camera distances at which to switch to each lower-detail version.
/// When non-empty, must have exactly `lod_levels - 1` entries; empty
/// lets the build choose defaults.
pub lod_distances: Vec<f32>,
/// Runtime copies the mesh may spawn beyond the authored one.
pub max_instances: u32,
/// Character capsule of the emitted mesh.
pub capsule: Option<CharacterCapsule>,
}
impl Default for CharacterModel {
fn default() -> Self {
Self {
schema: String::from("builtin:humanoid"),
source: String::new(),
skin_index: 0,
material: None,
position: [0.0, 0.0, 0.0],
rotation_deg: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0],
lod_levels: 1,
lod_distances: Vec::new(),
max_instances: 0,
capsule: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_blank_model_names_the_builtin_schema_and_no_source() {
let m = CharacterModel::default();
assert_eq!(m.schema, "builtin:humanoid");
assert!(m.source.is_empty());
assert_eq!(m.lod_levels, 1);
assert_eq!(m.scale, [1.0, 1.0, 1.0]);
assert!(m.capsule.is_none());
}
#[test]
fn a_model_round_trips_through_postcard() {
crate::test_support::install_resolvers();
let m: CharacterModel = serde_json::from_str(
r#"{"schema":"humanoid","material":"skin","source":"hero.glb","skin_index":1,
"position":[0,0.1,0],"lod_levels":3,"lod_distances":[6,12],"max_instances":2,
"capsule":{"half_height":0.9,"radius":0.3}}"#,
)
.unwrap();
assert_eq!(m.material, Some(MaterialHandle(4)));
let bytes = postcard::to_allocvec(&m).unwrap();
let back: CharacterModel = postcard::from_bytes(&bytes).unwrap();
assert_eq!(back.schema, "humanoid");
assert_eq!(back.source, "hero.glb");
assert_eq!(back.skin_index, 1);
assert_eq!(back.lod_levels, 3);
assert_eq!(back.lod_distances, [6.0, 12.0]);
assert_eq!(back.max_instances, 2);
assert_eq!(back.capsule.unwrap().half_height, 0.9);
}
}