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
// src/components/camera3d.rs
//
// Runtime 3D camera component. Its authored args and controller config live in
// the schema crate (concinnity_asset::camera3d).
use concinnity_asset::cook;
use crate::components::CameraController;
use crate::ecs::Component;
/// Declares the 3D camera. One per scene.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Camera3D {
/// Vertical field of view in degrees.
pub fov_y_degrees: f32,
/// Near clip distance in world units.
pub near: f32,
/// Far clip distance in world units.
pub far: f32,
/// Current view matrix, written each step by the active camera system.
/// Column-major, matching the GLSL mat4 convention.
pub view_matrix: [[f32; 4]; 4],
/// Current world-space eye position, kept in sync with view_matrix.
pub position: [f32; 3],
/// Current yaw in radians.
pub yaw: f32,
/// Current pitch in radians.
pub pitch: f32,
/// World-space horizontal movement intent (units/second). Written by
/// Camera3DSystem each frame, consumed by PhysicsSystem. Runtime-only.
pub desired_move: [f32; 3],
/// Set for one frame when the jump key is pressed. Runtime-only.
pub jump_requested: bool,
/// Set for one frame when the interact key is pressed. Runtime-only.
pub interact_requested: bool,
/// Controller settings, or `None` for an uncontrolled (cutscene) camera.
/// Read once by the internal camera controller at init.
pub controller: Option<CameraController>,
}
impl Camera3D {
/// Translate the authored args into the runtime camera: compose the initial
/// view matrix and zero the runtime state. Run by cook at build time (the
/// baked blob record carries the result) and by tests that need a camera.
pub fn bake(args: cook::Camera3D) -> Self {
Self {
fov_y_degrees: args.fov_y_degrees,
near: args.near,
far: args.far,
view_matrix: crate::gfx::camera::view_matrix(args.position, args.yaw, args.pitch),
position: args.position,
yaw: args.yaw,
pitch: args.pitch,
desired_move: [0.0; 3],
jump_requested: false,
interact_requested: false,
controller: args.controller,
}
}
}
impl Component for Camera3D {
const NAME: &'static str = "Camera3D";
fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
Ok(crate::blob::decode_exact(bytes)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::FollowDrive;
use crate::ecs::SkinnedMeshHandle;
#[test]
fn follow_block_deserializes_names_and_defaults() {
crate::test_support::reset_interner();
crate::test_support::intern_all(&["hero"]);
let args: cook::Camera3D = serde_json::from_value(serde_json::json!({
"controller": {"follow": {"target": "hero", "drive": "direct"}}
}))
.unwrap();
let follow = args.controller.unwrap().follow.unwrap();
// "hero" interns to id 0, and with no SkinnedMesh handle resolver installed
// the reference falls back to that interned value as its handle.
assert_eq!(follow.target, Some(SkinnedMeshHandle(0)));
assert_eq!(follow.drive, FollowDrive::Direct);
// Omitted fields keep the documented defaults.
assert_eq!(follow.speed_parameter, "speed");
assert!((follow.distance - 4.0).abs() < 1e-6);
assert!((follow.height - 1.5).abs() < 1e-6);
assert_eq!(follow.jump_height, 0.0);
// No follow block keeps the first-person modes.
let bare: cook::Camera3D =
serde_json::from_value(serde_json::json!({"controller": {}})).unwrap();
assert!(bare.controller.unwrap().follow.is_none());
}
}