wow_alchemy_adt/
normal_map.rs1use crate::Adt;
4use crate::error::Result;
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::Path;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum NormalMapFormat {
12 Raw,
14 PNG,
16 }
19
20#[derive(Debug, Clone)]
22pub struct NormalMapOptions {
23 pub format: NormalMapFormat,
25 pub invert_y: bool,
27 pub tangent_space: bool,
29 pub flip_y: bool,
31 pub flip_x: bool,
33 pub encoding: NormalChannelEncoding,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum NormalChannelEncoding {
40 RGB,
42 OpenGL,
44 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
61pub 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 } }
93}
94
95fn 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 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 for normal in &chunk.normals {
116 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 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 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 let width = 145; let height = 145; let mut img = ImageBuffer::<Rgb<u8>, Vec<u8>>::new(width, height);
160
161 let mut normals = vec![[0u8; 3]; width as usize * height as usize];
163
164 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 let chunk_x = x * 9;
174 let chunk_y = y * 9;
175
176 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 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 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 let mut nx = normal[0] as i8;
212 let mut ny = normal[1] as i8;
213 let nz = normal[2] as i8;
214
215 if options.invert_y {
217 ny = -ny;
218 }
219
220 if options.flip_x {
221 nx = -nx;
222 }
223
224 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 img.put_pixel(x, y, Rgb([r, g, b]));
231 }
232
233 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#[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 y in 0..height {
268 for x in 0..width {
269 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 let dx = (h_right - h_left) * scale;
298 let dy = (h_down - h_up) * scale;
299
300 let nx = -dx;
302 let ny = -dy;
303 let nz = 2.0; let length = (nx * nx + ny * ny + nz * nz).sqrt();
307
308 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 normals[y * width + x] = [nx_scaled, ny_scaled, nz_scaled];
315 }
316 }
317
318 normals
319}