wow_alchemy_adt/
normal_map.rs

1// normal_map.rs - Extract normal maps from ADT files
2
3use crate::Adt;
4use crate::error::Result;
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::Path;
8
9/// Format for normal map export
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum NormalMapFormat {
12    /// Raw data format (just the values)
13    Raw,
14    /// PNG format (requires image feature)
15    PNG,
16    // /// DirectDraw Surface format (requires dds_encoder feature)
17    // DDS,
18}
19
20/// Options for normal map extraction
21#[derive(Debug, Clone)]
22pub struct NormalMapOptions {
23    /// Output format
24    pub format: NormalMapFormat,
25    /// Whether to invert Y axis
26    pub invert_y: bool,
27    /// Whether to use tangent space normals (vs object space)
28    pub tangent_space: bool,
29    /// Whether to flip the Y axis in the image
30    pub flip_y: bool,
31    /// Whether to flip the X axis in the image
32    pub flip_x: bool,
33    /// Normal map channel encoding mode
34    pub encoding: NormalChannelEncoding,
35}
36
37/// Normal map channel encoding
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum NormalChannelEncoding {
40    /// Standard RGB encoding where R=X, G=Y, B=Z
41    RGB,
42    /// OpenGL normal map format (Y flipped)
43    OpenGL,
44    /// DirectX normal map format (Y and Z flipped)
45    DirectX,
46}
47
48impl Default for NormalMapOptions {
49    fn default() -> Self {
50        Self {
51            format: NormalMapFormat::PNG,
52            invert_y: false,
53            tangent_space: true,
54            flip_y: false,
55            flip_x: false,
56            encoding: NormalChannelEncoding::RGB,
57        }
58    }
59}
60
61/// Extract a normal map from an ADT file
62pub fn extract_normal_map<P: AsRef<Path>>(
63    adt: &Adt,
64    output_path: P,
65    options: NormalMapOptions,
66) -> Result<()> {
67    match options.format {
68        NormalMapFormat::Raw => extract_raw_normal_map(adt, output_path, &options),
69        NormalMapFormat::PNG => {
70            #[cfg(feature = "image")]
71            {
72                extract_png_normal_map(adt, output_path, &options)
73            }
74            #[cfg(not(feature = "image"))]
75            {
76                Err(crate::error::AdtError::NotImplemented(
77                    "PNG export requires the 'image' feature to be enabled".to_string(),
78                ))
79            }
80        } // NormalMapFormat::DDS => {
81          //     #[cfg(feature = "dds_encoder")]
82          //     {
83          //         extract_dds_normal_map(adt, output_path, &options)
84          //     }
85          //     #[cfg(not(feature = "dds_encoder"))]
86          //     {
87          //         Err(crate::error::AdtError::NotImplemented(
88          //             "DDS export requires the 'dds_encoder' feature to be enabled".to_string(),
89          //         ))
90          //     }
91          // }
92    }
93}
94
95/// Extract a raw normal map (just the values)
96fn extract_raw_normal_map<P: AsRef<Path>>(
97    adt: &Adt,
98    output_path: P,
99    options: &NormalMapOptions,
100) -> Result<()> {
101    let file = File::create(output_path)?;
102    let mut writer = BufWriter::new(file);
103
104    // The normal map is a grid of normal vectors for each vertex in the heightmap
105
106    // For each MCNK in the grid
107    for y in 0..16 {
108        for x in 0..16 {
109            let chunk_index = y * 16 + x;
110
111            if chunk_index < adt.mcnk_chunks.len() {
112                let chunk = &adt.mcnk_chunks[chunk_index];
113
114                // Get the normal values from this chunk
115                for normal in &chunk.normals {
116                    // Convert normal from [u8; 3] format to normalized [-1, 1] floats
117                    let nx = (normal[0] as f32) / 127.0;
118                    let mut ny = (normal[1] as f32) / 127.0;
119                    let mut nz = (normal[2] as f32) / 127.0;
120
121                    // Apply options
122                    if options.invert_y {
123                        ny = -ny;
124                    }
125
126                    if options.encoding == NormalChannelEncoding::DirectX {
127                        ny = -ny;
128                        nz = -nz;
129                    } else if options.encoding == NormalChannelEncoding::OpenGL {
130                        ny = -ny;
131                    }
132
133                    // Write the normal components
134                    writer.write_all(&nx.to_le_bytes())?;
135                    writer.write_all(&ny.to_le_bytes())?;
136                    writer.write_all(&nz.to_le_bytes())?;
137                }
138            }
139        }
140    }
141
142    Ok(())
143}
144
145#[cfg(feature = "image")]
146fn extract_png_normal_map<P: AsRef<Path>>(
147    adt: &Adt,
148    output_path: P,
149    options: &NormalMapOptions,
150) -> Result<()> {
151    use image::{ImageBuffer, Rgb};
152
153    // Calculate final image dimensions
154    // Each MCNK contains a 9x9 grid of vertices
155    let width = 145; // 9*16 + 1 (with overlap)
156    let height = 145; // 9*16 + 1 (with overlap)
157
158    // Create a new RGB image
159    let mut img = ImageBuffer::<Rgb<u8>, Vec<u8>>::new(width, height);
160
161    // Collect all normals first
162    let mut normals = vec![[0u8; 3]; width as usize * height as usize];
163
164    // Process each MCNK to extract normals
165    for y in 0..16 {
166        for x in 0..16 {
167            let chunk_index = y * 16 + x;
168
169            if chunk_index < adt.mcnk_chunks.len() {
170                let chunk = &adt.mcnk_chunks[chunk_index];
171
172                // Map the chunk to the image grid
173                let chunk_x = x * 9;
174                let chunk_y = y * 9;
175
176                // Place normals on the grid
177                for i in 0..9 {
178                    for j in 0..9 {
179                        let normal_index = i * 9 + j;
180                        let pos_x = chunk_x + j;
181                        let pos_y = if options.flip_y {
182                            height as usize - 1 - (chunk_y + i)
183                        } else {
184                            chunk_y + i
185                        };
186
187                        if normal_index < chunk.normals.len() {
188                            let normal = chunk.normals[normal_index];
189
190                            // Store normal
191                            let combined_index = pos_y * width as usize + pos_x;
192                            if combined_index < normals.len() {
193                                normals[combined_index] = normal;
194                            }
195                        }
196                    }
197                }
198            }
199        }
200    }
201
202    // Convert normals to RGB image
203    for (i, normal) in normals.iter().enumerate() {
204        let x = (i % width as usize) as u32;
205        let y = (i / width as usize) as u32;
206
207        // Convert from [-127, 127] to [0, 255]
208        // Note: ADT normals are actually stored as signed bytes (-127 to 127)
209
210        // Get normal components - convert from u8 to i8 first
211        let mut nx = normal[0] as i8;
212        let mut ny = normal[1] as i8;
213        let nz = normal[2] as i8;
214
215        // Apply options
216        if options.invert_y {
217            ny = -ny;
218        }
219
220        if options.flip_x {
221            nx = -nx;
222        }
223
224        // Convert to [0, 255] range (127 is 0, 255 is 1.0, 0 is -1.0)
225        let r = ((nx + 127) as u8).clamp(0, 255);
226        let g = ((ny + 127) as u8).clamp(0, 255);
227        let b = ((nz + 127) as u8).clamp(0, 255);
228
229        // Set the pixel
230        img.put_pixel(x, y, Rgb([r, g, b]));
231    }
232
233    // Save the image
234    img.save(output_path).map_err(|e| {
235        crate::error::AdtError::Io(std::io::Error::other(format!(
236            "Failed to save PNG image: {e}"
237        )))
238    })?;
239
240    Ok(())
241}
242
243// #[cfg(feature = "dds_encoder")]
244// fn extract_dds_normal_map<P: AsRef<Path>>(
245//     _adt: &Adt,
246//     _output_path: P,
247//     _options: &NormalMapOptions,
248// ) -> Result<()> {
249//     // Implementation would use a DDS encoder library to save the normal map
250//     // For now, return a not implemented error
251//     Err(crate::error::AdtError::NotImplemented(
252//         "DDS export is not yet implemented".to_string(),
253//     ))
254// }
255
256/// Generate a normal map from a height map
257#[allow(dead_code)]
258pub fn generate_normal_map_from_heightmap(
259    heightmap: &[f32],
260    width: usize,
261    height: usize,
262    scale: f32,
263) -> Vec<[i8; 3]> {
264    let mut normals = vec![[0i8; 3]; width * height];
265
266    // For each vertex in the height map
267    for y in 0..height {
268        for x in 0..width {
269            // Get surrounding heights (with bounds checking)
270            let h_center = heightmap[y * width + x];
271
272            let h_left = if x > 0 {
273                heightmap[y * width + (x - 1)]
274            } else {
275                h_center
276            };
277
278            let h_right = if x < width - 1 {
279                heightmap[y * width + (x + 1)]
280            } else {
281                h_center
282            };
283
284            let h_up = if y > 0 {
285                heightmap[(y - 1) * width + x]
286            } else {
287                h_center
288            };
289
290            let h_down = if y < height - 1 {
291                heightmap[(y + 1) * width + x]
292            } else {
293                h_center
294            };
295
296            // Calculate derivatives
297            let dx = (h_right - h_left) * scale;
298            let dy = (h_down - h_up) * scale;
299
300            // Calculate normal vector using cross product
301            let nx = -dx;
302            let ny = -dy;
303            let nz = 2.0; // Fixed step size
304
305            // Normalize
306            let length = (nx * nx + ny * ny + nz * nz).sqrt();
307
308            // Convert to -127 to 127 range for ADT normals
309            let nx_scaled = ((nx / length) * 127.0) as i8;
310            let ny_scaled = ((ny / length) * 127.0) as i8;
311            let nz_scaled = ((nz / length) * 127.0) as i8;
312
313            // Store normal
314            normals[y * width + x] = [nx_scaled, ny_scaled, nz_scaled];
315        }
316    }
317
318    normals
319}