wow_wmo/
visualizer.rs

1//! WMO visualization and 3D export functionality
2//!
3//! This module provides utilities for exporting WMO data to common 3D formats
4//! like OBJ/MTL for use in 3D modeling applications.
5
6use crate::types::{Color, Vec3};
7use crate::wmo_group_types::*;
8use crate::wmo_types::*;
9
10/// A simple structure to export mesh data in a format suitable for 3D rendering
11#[derive(Debug, Clone)]
12pub struct WmoMesh {
13    /// Vertex positions
14    pub positions: Vec<[f32; 3]>,
15
16    /// Vertex normals
17    pub normals: Vec<[f32; 3]>,
18
19    /// Vertex texture coordinates
20    pub tex_coords: Vec<[f32; 2]>,
21
22    /// Vertex colors
23    pub colors: Vec<[u8; 4]>,
24
25    /// Indices (triangles)
26    pub indices: Vec<u32>,
27
28    /// Submeshes grouped by material
29    pub submeshes: Vec<WmoSubmesh>,
30}
31
32/// A submesh of a WMO mesh, containing a subset of the triangles with the same material
33#[derive(Debug, Clone)]
34pub struct WmoSubmesh {
35    /// Material index
36    pub material_index: u16,
37
38    /// Start index in the indices array
39    pub start_index: u32,
40
41    /// Number of indices
42    pub index_count: u32,
43
44    /// Texture filename
45    pub texture_filename: Option<String>,
46}
47
48/// Helper for visualizing WMO files
49pub struct WmoVisualizer;
50
51impl Default for WmoVisualizer {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl WmoVisualizer {
58    /// Create a new WMO visualizer
59    pub fn new() -> Self {
60        Self
61    }
62
63    /// Convert a WMO model into a mesh suitable for 3D rendering
64    pub fn create_mesh(&self, root: &WmoRoot, groups: &[WmoGroup]) -> WmoMesh {
65        let mut mesh = WmoMesh {
66            positions: Vec::new(),
67            normals: Vec::new(),
68            tex_coords: Vec::new(),
69            colors: Vec::new(),
70            indices: Vec::new(),
71            submeshes: Vec::new(),
72        };
73
74        let mut global_index_offset = 0;
75
76        // Process each group
77        for group in groups {
78            // Calculate vertex offset for this group
79            let vertex_offset = mesh.positions.len() as u32;
80
81            // Add vertices
82            for vertex in &group.vertices {
83                mesh.positions.push([vertex.x, vertex.y, vertex.z]);
84            }
85
86            // Add normals
87            if !group.normals.is_empty() {
88                for normal in &group.normals {
89                    mesh.normals.push([normal.x, normal.y, normal.z]);
90                }
91            } else {
92                // If no normals, add default up normals
93                for _ in 0..group.vertices.len() {
94                    mesh.normals.push([0.0, 0.0, 1.0]);
95                }
96            }
97
98            // Add texture coordinates
99            if !group.tex_coords.is_empty() {
100                for tex_coord in &group.tex_coords {
101                    mesh.tex_coords.push([tex_coord.u, tex_coord.v]);
102                }
103            } else {
104                // If no texture coordinates, add default ones
105                for _ in 0..group.vertices.len() {
106                    mesh.tex_coords.push([0.0, 0.0]);
107                }
108            }
109
110            // Add colors
111            if let Some(colors) = &group.vertex_colors {
112                for color in colors {
113                    mesh.colors.push([color.r, color.g, color.b, color.a]);
114                }
115            } else {
116                // If no colors, add default white
117                for _ in 0..group.vertices.len() {
118                    mesh.colors.push([255, 255, 255, 255]);
119                }
120            }
121
122            // Process batches
123            for batch in &group.batches {
124                let material_index = batch.material_id;
125                let start_index = batch.start_index;
126                let index_count = batch.count as u32;
127
128                // Get texture filename if available
129                let texture_filename = if material_index < root.materials.len() as u16 {
130                    let material = &root.materials[material_index as usize];
131                    if material.texture1 < root.textures.len() as u32 {
132                        Some(root.textures[material.texture1 as usize].clone())
133                    } else {
134                        None
135                    }
136                } else {
137                    None
138                };
139
140                // Add indices (remapped to global index space)
141                let submesh_start = mesh.indices.len() as u32;
142
143                for i in 0..index_count {
144                    let idx = global_index_offset + start_index + i;
145                    if idx < group.indices.len() as u32 {
146                        let vertex_index = group.indices[idx as usize] as u32;
147                        mesh.indices.push(vertex_offset + vertex_index);
148                    }
149                }
150
151                let submesh_count = mesh.indices.len() as u32 - submesh_start;
152
153                // Add submesh
154                mesh.submeshes.push(WmoSubmesh {
155                    material_index,
156                    start_index: submesh_start,
157                    index_count: submesh_count,
158                    texture_filename,
159                });
160            }
161
162            global_index_offset += group.indices.len() as u32;
163        }
164
165        mesh
166    }
167
168    /// Extract doodad placement information for visualization
169    pub fn extract_doodads(&self, root: &WmoRoot) -> Vec<WmoDoodadPlacement> {
170        let mut placements = Vec::new();
171
172        for (i, doodad) in root.doodad_defs.iter().enumerate() {
173            // Find which set(s) this doodad belongs to
174            let mut set_names = Vec::new();
175
176            for set in &root.doodad_sets {
177                let start = set.start_doodad as usize;
178                let end = start + set.n_doodads as usize;
179
180                if i >= start && i < end {
181                    set_names.push(set.name.clone());
182                }
183            }
184
185            // Get name reference
186            // In a real implementation, you'd look up the name from doodad_names
187            let name = format!("Doodad_{}", doodad.name_offset);
188
189            placements.push(WmoDoodadPlacement {
190                index: i,
191                name,
192                position: doodad.position,
193                orientation: doodad.orientation,
194                scale: doodad.scale,
195                color: doodad.color,
196                set_indices: set_names,
197            });
198        }
199
200        placements
201    }
202
203    /// Generate a list of triangles for each group
204    pub fn generate_triangles(&self, groups: &[WmoGroup]) -> Vec<Vec<WmoTriangle>> {
205        let mut result = Vec::with_capacity(groups.len());
206
207        for group in groups {
208            let mut triangles = Vec::new();
209
210            // Process each batch
211            for batch in &group.batches {
212                let material_id = batch.material_id;
213
214                // Convert indices to triangles
215                for i in 0..(batch.count / 3) {
216                    let idx_base = (batch.start_index + i as u32 * 3) as usize;
217
218                    if idx_base + 2 < group.indices.len() {
219                        let idx1 = group.indices[idx_base] as usize;
220                        let idx2 = group.indices[idx_base + 1] as usize;
221                        let idx3 = group.indices[idx_base + 2] as usize;
222
223                        if idx1 < group.vertices.len()
224                            && idx2 < group.vertices.len()
225                            && idx3 < group.vertices.len()
226                        {
227                            triangles.push(WmoTriangle {
228                                vertices: [
229                                    group.vertices[idx1],
230                                    group.vertices[idx2],
231                                    group.vertices[idx3],
232                                ],
233                                normals: if !group.normals.is_empty() {
234                                    Some([
235                                        group.normals.get(idx1).cloned().unwrap_or_default(),
236                                        group.normals.get(idx2).cloned().unwrap_or_default(),
237                                        group.normals.get(idx3).cloned().unwrap_or_default(),
238                                    ])
239                                } else {
240                                    None
241                                },
242                                tex_coords: if !group.tex_coords.is_empty() {
243                                    Some([
244                                        group.tex_coords.get(idx1).cloned().unwrap_or_default(),
245                                        group.tex_coords.get(idx2).cloned().unwrap_or_default(),
246                                        group.tex_coords.get(idx3).cloned().unwrap_or_default(),
247                                    ])
248                                } else {
249                                    None
250                                },
251                                colors: group.vertex_colors.as_ref().map(|colors| {
252                                    [
253                                        colors.get(idx1).cloned().unwrap_or_default(),
254                                        colors.get(idx2).cloned().unwrap_or_default(),
255                                        colors.get(idx3).cloned().unwrap_or_default(),
256                                    ]
257                                }),
258                                material_id,
259                            });
260                        }
261                    }
262                }
263            }
264
265            result.push(triangles);
266        }
267
268        result
269    }
270
271    /// Export to OBJ format (simple)
272    pub fn export_to_obj(&self, root: &WmoRoot, groups: &[WmoGroup]) -> String {
273        let mut obj = String::new();
274
275        // Write header
276        obj.push_str("# WMO Model exported from wow_wmo\n");
277        obj.push_str(&format!("# Version: {}\n", root.version.to_raw()));
278        obj.push_str(&format!("# Groups: {}\n\n", groups.len()));
279
280        let mut global_vertex_offset = 1; // OBJ indices start at 1
281        let mut global_normal_offset = 1;
282        let mut global_texcoord_offset = 1;
283
284        // Process each group
285        for (group_idx, group) in groups.iter().enumerate() {
286            obj.push_str(&format!("g Group_{group_idx}\n"));
287
288            // Write vertices
289            for v in &group.vertices {
290                obj.push_str(&format!("v {} {} {}\n", v.x, v.y, v.z));
291            }
292
293            // Write texture coordinates
294            for t in &group.tex_coords {
295                obj.push_str(&format!("vt {} {}\n", t.u, t.v));
296            }
297
298            // Write normals
299            for n in &group.normals {
300                obj.push_str(&format!("vn {} {} {}\n", n.x, n.y, n.z));
301            }
302
303            // Process batches
304            for batch in &group.batches {
305                let material_id = batch.material_id;
306                obj.push_str(&format!("usemtl Material_{material_id}\n"));
307
308                // Write faces
309                for i in 0..(batch.count / 3) {
310                    let idx_base = (batch.start_index + i as u32 * 3) as usize;
311
312                    if idx_base + 2 < group.indices.len() {
313                        let idx1 = group.indices[idx_base] as usize + global_vertex_offset;
314                        let idx2 = group.indices[idx_base + 1] as usize + global_vertex_offset;
315                        let idx3 = group.indices[idx_base + 2] as usize + global_vertex_offset;
316
317                        let tex1 = idx1 - global_vertex_offset + global_texcoord_offset;
318                        let tex2 = idx2 - global_vertex_offset + global_texcoord_offset;
319                        let tex3 = idx3 - global_vertex_offset + global_texcoord_offset;
320
321                        let norm1 = idx1 - global_vertex_offset + global_normal_offset;
322                        let norm2 = idx2 - global_vertex_offset + global_normal_offset;
323                        let norm3 = idx3 - global_vertex_offset + global_normal_offset;
324
325                        if !group.tex_coords.is_empty() && !group.normals.is_empty() {
326                            obj.push_str(&format!(
327                                "f {idx1}/{tex1}/{norm1} {idx2}/{tex2}/{norm2} {idx3}/{tex3}/{norm3}\n"
328                            ));
329                        } else if !group.tex_coords.is_empty() {
330                            obj.push_str(&format!("f {idx1}/{tex1} {idx2}/{tex2} {idx3}/{tex3}\n"));
331                        } else if !group.normals.is_empty() {
332                            obj.push_str(&format!(
333                                "f {idx1}//{norm1} {idx2}//{norm2} {idx3}//{norm3}\n"
334                            ));
335                        } else {
336                            obj.push_str(&format!("f {idx1} {idx2} {idx3}\n"));
337                        }
338                    }
339                }
340            }
341
342            // Update global offsets
343            global_vertex_offset += group.vertices.len();
344            global_texcoord_offset += group.tex_coords.len();
345            global_normal_offset += group.normals.len();
346
347            obj.push('\n');
348        }
349
350        // Write material library reference
351        obj.push_str("mtllib materials.mtl\n");
352
353        obj
354    }
355
356    /// Export materials to MTL format
357    pub fn export_to_mtl(&self, root: &WmoRoot) -> String {
358        let mut mtl = String::new();
359
360        // Write header
361        mtl.push_str("# WMO Materials exported from wow_wmo\n");
362
363        // Write each material
364        for (i, material) in root.materials.iter().enumerate() {
365            mtl.push_str(&format!("newmtl Material_{i}\n"));
366
367            // Convert material properties to MTL format
368            let diffuse = &material.diffuse_color;
369            let ambient = &material.sidn_color;
370            let emissive = &material.emissive_color;
371
372            mtl.push_str(&format!(
373                "Ka {} {} {}\n",
374                ambient.r as f32 / 255.0,
375                ambient.g as f32 / 255.0,
376                ambient.b as f32 / 255.0
377            ));
378
379            mtl.push_str(&format!(
380                "Kd {} {} {}\n",
381                diffuse.r as f32 / 255.0,
382                diffuse.g as f32 / 255.0,
383                diffuse.b as f32 / 255.0
384            ));
385
386            mtl.push_str(&format!(
387                "Ke {} {} {}\n",
388                emissive.r as f32 / 255.0,
389                emissive.g as f32 / 255.0,
390                emissive.b as f32 / 255.0
391            ));
392
393            // Add texture if available
394            if material.texture1 < root.textures.len() as u32 {
395                let texture = &root.textures[material.texture1 as usize];
396                mtl.push_str(&format!("map_Kd {texture}\n"));
397            }
398
399            // Add alpha if material has transparency
400            if material.flags.contains(WmoMaterialFlags::TWO_SIDED) {
401                mtl.push_str("d 0.5\n");
402            } else {
403                mtl.push_str("d 1.0\n");
404            }
405
406            mtl.push('\n');
407        }
408
409        mtl
410    }
411}
412
413/// A visualization-friendly doodad placement
414#[derive(Debug, Clone)]
415pub struct WmoDoodadPlacement {
416    /// Doodad index
417    pub index: usize,
418
419    /// Doodad name (typically M2 model path)
420    pub name: String,
421
422    /// Position
423    pub position: Vec3,
424
425    /// Orientation (quaternion)
426    pub orientation: [f32; 4],
427
428    /// Scale
429    pub scale: f32,
430
431    /// Color
432    pub color: Color,
433
434    /// Doodad set names that include this doodad
435    pub set_indices: Vec<String>,
436}
437
438/// A single triangle from a WMO group
439#[derive(Debug, Clone)]
440pub struct WmoTriangle {
441    /// Vertex positions
442    pub vertices: [Vec3; 3],
443
444    /// Vertex normals (if available)
445    pub normals: Option<[Vec3; 3]>,
446
447    /// Texture coordinates (if available)
448    pub tex_coords: Option<[TexCoord; 3]>,
449
450    /// Vertex colors (if available)
451    pub colors: Option<[Color; 3]>,
452
453    /// Material ID
454    pub material_id: u16,
455}