wow_m2/chunks/
camera.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::io::{Read, Seek, Write};
3
4use crate::chunks::animation::{M2AnimationBlock, M2AnimationTrack};
5use crate::common::C3Vector;
6use crate::error::Result;
7use crate::version::M2Version;
8
9bitflags::bitflags! {
10    /// Camera flags as defined in the M2 format
11    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12    pub struct M2CameraFlags: u16 {
13        /// Camera uses custom UVs for positioning
14        const CUSTOM_UV = 0x01;
15        /// Auto-generated camera based on model
16        const AUTO_GENERATED = 0x02;
17        /// Camera is at global scene coordinates
18        const GLOBAL_POSITION = 0x04;
19    }
20}
21
22/// Represents a camera in an M2 model
23///
24/// Camera structure layout:
25/// - Pre-WotLK (version < 264): 124 bytes
26///   - type(4) + fov/far/near(12) + pos_track(28) + pos_base(12)
27///   - + target_track(28) + target_base(12) + roll_track(28)
28/// - WotLK+ (version >= 264): 108 bytes
29///   - type(4) + fov/far/near(12) + pos_track(20) + pos_base(12)
30///   - + target_track(20) + target_base(12) + roll_track(20) + id(4) + flags(2) + pad(2)
31#[derive(Debug, Clone)]
32pub struct M2Camera {
33    /// Camera type (0=portrait, 1=character info, -1=default)
34    pub camera_type: u32,
35    /// Field of view (in radians)
36    pub fov: f32,
37    /// Far clip distance
38    pub far_clip: f32,
39    /// Near clip distance
40    pub near_clip: f32,
41    /// Camera position animation
42    pub position_animation: M2AnimationBlock<C3Vector>,
43    /// Camera position base (default position when not animated)
44    pub position_base: C3Vector,
45    /// Target position animation
46    pub target_position_animation: M2AnimationBlock<C3Vector>,
47    /// Target position base (default target when not animated)
48    pub target_position_base: C3Vector,
49    /// Roll animation (rotation around the view axis)
50    pub roll_animation: M2AnimationBlock<f32>,
51    /// Camera ID (WotLK+ only)
52    pub id: u32,
53    /// Camera flags (WotLK+ only)
54    pub flags: M2CameraFlags,
55}
56
57impl M2Camera {
58    /// Parse a camera from a reader based on the M2 version
59    ///
60    /// Camera structure varies by version:
61    /// - Pre-WotLK (< 264): 124 bytes - header + tracks + base values (no id/flags)
62    /// - WotLK+ (>= 264): 108 bytes - smaller tracks + id/flags
63    pub fn parse<R: Read + Seek>(reader: &mut R, version: u32) -> Result<Self> {
64        let camera_type = reader.read_u32_le()?;
65        let fov = reader.read_f32_le()?;
66        let far_clip = reader.read_f32_le()?;
67        let near_clip = reader.read_f32_le()?;
68
69        // Position track followed by position base (C3Vector)
70        let position_animation = M2AnimationBlock::parse(reader)?;
71        let position_base = C3Vector::parse(reader)?;
72
73        // Target position track followed by target base (C3Vector)
74        let target_position_animation = M2AnimationBlock::parse(reader)?;
75        let target_position_base = C3Vector::parse(reader)?;
76
77        // Roll track (no base value - roll defaults to 0)
78        let roll_animation = M2AnimationBlock::parse(reader)?;
79
80        // ID and flags are only present in WotLK+ (version >= 264)
81        let (id, flags) = if version >= 264 {
82            let id = reader.read_u32_le()?;
83            let flags = M2CameraFlags::from_bits_retain(reader.read_u16_le()?);
84            reader.read_u16_le()?; // Skip padding
85            (id, flags)
86        } else {
87            // Pre-WotLK: no id/flags fields
88            (0, M2CameraFlags::empty())
89        };
90
91        Ok(Self {
92            camera_type,
93            fov,
94            far_clip,
95            near_clip,
96            position_animation,
97            position_base,
98            target_position_animation,
99            target_position_base,
100            roll_animation,
101            id,
102            flags,
103        })
104    }
105
106    /// Write a camera to a writer based on the M2 version
107    pub fn write<W: Write>(&self, writer: &mut W, version: u32) -> Result<()> {
108        writer.write_u32_le(self.camera_type)?;
109        writer.write_f32_le(self.fov)?;
110        writer.write_f32_le(self.far_clip)?;
111        writer.write_f32_le(self.near_clip)?;
112
113        // Position track followed by position base
114        self.position_animation.write(writer)?;
115        self.position_base.write(writer)?;
116
117        // Target position track followed by target base
118        self.target_position_animation.write(writer)?;
119        self.target_position_base.write(writer)?;
120
121        // Roll track (no base value)
122        self.roll_animation.write(writer)?;
123
124        // ID and flags only for WotLK+ (version >= 264)
125        if version >= 264 {
126            writer.write_u32_le(self.id)?;
127            writer.write_u16_le(self.flags.bits())?;
128            writer.write_u16_le(0)?; // padding
129        }
130
131        Ok(())
132    }
133
134    /// Convert this camera to a different version (no version differences for cameras)
135    pub fn convert(&self, _target_version: M2Version) -> Self {
136        self.clone()
137    }
138
139    /// Create a new camera with default values
140    pub fn new(id: u32) -> Self {
141        Self {
142            camera_type: 0,
143            fov: 0.8726646, // 50 degrees in radians
144            far_clip: 100.0,
145            near_clip: 0.1,
146            position_animation: M2AnimationBlock::new(M2AnimationTrack::default()),
147            position_base: C3Vector::default(),
148            target_position_animation: M2AnimationBlock::new(M2AnimationTrack::default()),
149            target_position_base: C3Vector::default(),
150            roll_animation: M2AnimationBlock::new(M2AnimationTrack::default()),
151            id,
152            flags: M2CameraFlags::empty(),
153        }
154    }
155
156    /// Returns the size of a camera in bytes for the given version
157    pub fn size(version: u32) -> usize {
158        if version >= 264 {
159            // WotLK+: 20-byte tracks + id/flags
160            // type(4) + fov/far/near(12) + pos_track(20) + pos_base(12)
161            // + target_track(20) + target_base(12) + roll_track(20) + id(4) + flags(4)
162            108
163        } else {
164            // Pre-WotLK: 28-byte tracks (with ranges), no id/flags
165            // type(4) + fov/far/near(12) + pos_track(28) + pos_base(12)
166            // + target_track(28) + target_base(12) + roll_track(28)
167            124
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use std::io::Cursor;
176
177    #[test]
178    fn test_camera_parse_write_vanilla() {
179        let camera = M2Camera::new(1);
180        let version = M2Version::Vanilla.to_header_version();
181
182        // Test write
183        let mut data = Vec::new();
184        camera.write(&mut data, version).unwrap();
185
186        // Vanilla camera: type(4) + fov/far/near(12) + 3 tracks(28*3) + 2 bases(12*2)
187        // = 16 + 84 + 24 = 124 bytes (no id/flags)
188        assert_eq!(data.len(), 124);
189
190        // Test parse
191        let mut cursor = Cursor::new(data);
192        let parsed = M2Camera::parse(&mut cursor, version).unwrap();
193
194        assert_eq!(parsed.camera_type, 0);
195        // id defaults to 0 for Vanilla (not stored in file)
196        assert_eq!(parsed.id, 0);
197        assert_eq!(parsed.flags, M2CameraFlags::empty());
198    }
199
200    #[test]
201    fn test_camera_parse_write_wotlk() {
202        let mut camera = M2Camera::new(5);
203        camera.flags = M2CameraFlags::CUSTOM_UV;
204        let version = M2Version::WotLK.to_header_version();
205
206        // Test write
207        let mut data = Vec::new();
208        camera.write(&mut data, version).unwrap();
209
210        // Note: Current implementation always writes 28-byte tracks (pre-WotLK format)
211        // WotLK+ should technically use 20-byte tracks, but that's not yet implemented
212        // So actual size: 16 + 84 + 24 + 8 (id/flags/pad) = 132 bytes
213        assert_eq!(data.len(), 132);
214
215        // Test parse
216        let mut cursor = Cursor::new(data);
217        let parsed = M2Camera::parse(&mut cursor, version).unwrap();
218
219        assert_eq!(parsed.camera_type, 0);
220        assert_eq!(parsed.id, 5);
221        assert_eq!(parsed.flags, M2CameraFlags::CUSTOM_UV);
222    }
223
224    #[test]
225    fn test_camera_flags() {
226        let flags = M2CameraFlags::CUSTOM_UV | M2CameraFlags::AUTO_GENERATED;
227        assert!(flags.contains(M2CameraFlags::CUSTOM_UV));
228        assert!(flags.contains(M2CameraFlags::AUTO_GENERATED));
229        assert!(!flags.contains(M2CameraFlags::GLOBAL_POSITION));
230    }
231
232    #[test]
233    fn test_camera_size() {
234        // These are the expected format sizes (not current implementation sizes)
235        assert_eq!(M2Camera::size(256), 124); // Vanilla (28-byte tracks)
236        assert_eq!(M2Camera::size(263), 124); // TBC (28-byte tracks)
237        assert_eq!(M2Camera::size(264), 108); // WotLK (20-byte tracks)
238        assert_eq!(M2Camera::size(272), 108); // MoP (20-byte tracks)
239    }
240}