WMO (World Map Objects) are large static structures in World of Warcraft such as
buildings, dungeons, and cities. Unlike smaller decorative objects (M2 models),
WMOs have complex interior/exterior structures, multiple groups, portals, and
advanced lighting. This guide covers loading, processing, and rendering WMO files
using warcraft-rs.
Before working with WMO files, ensure you have:
- Understanding of 3D graphics and scene graphs
- Knowledge of BSP trees and portal rendering
warcraft-rsinstalled with thewmofeature enabled- Graphics API experience (OpenGL/Vulkan/DirectX/WebGPU)
- Familiarity with occlusion culling techniques
WMO files consist of:
- Root file (
.wmo): Contains general information, materials, portals - Group files (
_000.wmo,_001.wmo, etc.): Individual building sections - Portal system: Visibility determination between rooms
- Doodad sets: Furniture and decorative object placements
- Lighting: Pre-baked vertex lighting and light definitions
- Groups: Self-contained mesh sections (rooms, floors)
- Portals: Openings between groups for visibility culling
- BSP Tree: Binary space partitioning for collision detection
- Batches: Render batches with material assignments
- MOCVs: Vertex colors for pre-baked lighting
- Liquid: Water planes within buildings
use wow_wmo::{WmoRoot, WmoGroup, WmoParser, WmoGroupParser};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
struct LoadedWmo {
root: WmoRoot,
groups: Vec<WmoGroup>,
}
fn load_wmo(root_path: &str) -> Result<LoadedWmo, Box<dyn std::error::Error>> {
// Load root WMO file
let file = File::open(root_path)?;
let mut reader = BufReader::new(file);
let root = WmoParser::new().parse_root(&mut reader)?;
println!("Groups: {}", root.groups.len());
println!("Portals: {}", root.portals.len());
println!("Materials: {}", root.materials.len());
println!("Doodad Sets: {}", root.doodad_sets.len());
// Load group files
let mut groups = Vec::new();
let base_path = root_path.trim_end_matches(".wmo");
for i in 0..root.header.n_groups {
let group_path = format!("{}_{:03}.wmo", base_path, i);
if Path::new(&group_path).exists() {
let group_file = File::open(&group_path)?;
let mut group_reader = BufReader::new(group_file);
let group = WmoGroupParser::new().parse_group(&mut group_reader, i)?;
groups.push(group);
}
}
Ok(LoadedWmo { root, groups })
}
// Load with LOD support
fn load_wmo_with_lod(root_path: &str) -> Result<Vec<LoadedWmo>, Box<dyn std::error::Error>> {
let mut lods = Vec::new();
// Try to load LOD versions
for lod_level in 0..3 {
let lod_path = if lod_level == 0 {
root_path.to_string()
} else {
root_path.replace(".wmo", &format!("_lod{}.wmo", lod_level))
};
if Path::new(&lod_path).exists() {
let wmo = load_wmo(&lod_path)?;
lods.push(wmo);
}
}
Ok(lods)
}use wow_wmo::{WmoGroup, WmoGroupFlags, BoundingBox, WmoBatch};
#[derive(Debug, Clone)]
struct ProcessedGroup {
vertices: Vec<GpuVertex>,
indices: Vec<u32>,
batches: Vec<WmoBatch>,
bounding_box: BoundingBox,
is_indoor: bool,
}
#[derive(Debug, Clone, Copy)]
#[repr(C)]
struct GpuVertex {
position: [f32; 3],
normal: [f32; 3],
texcoord: [f32; 2],
vertex_color: [f32; 4],
}
fn process_wmo_group(group: &WmoGroup) -> ProcessedGroup {
let mut vertices = Vec::with_capacity(group.vertices.len());
// Process vertices
for (i, vertex) in group.vertices.iter().enumerate() {
let vertex_color = if let Some(ref colors) = group.vertex_colors {
if i < colors.len() {
let color = &colors[i];
[
color.r as f32 / 255.0,
color.g as f32 / 255.0,
color.b as f32 / 255.0,
color.a as f32 / 255.0,
]
} else {
[1.0, 1.0, 1.0, 1.0]
}
} else {
[1.0, 1.0, 1.0, 1.0]
};
let normal = if i < group.normals.len() {
[group.normals[i].x, group.normals[i].y, group.normals[i].z]
} else {
[0.0, 0.0, 1.0]
};
let texcoord = if i < group.tex_coords.len() {
[group.tex_coords[i].u, group.tex_coords[i].v]
} else {
[0.0, 0.0]
};
vertices.push(GpuVertex {
position: [vertex.x, vertex.y, vertex.z],
normal,
texcoord,
vertex_color,
});
}
ProcessedGroup {
vertices,
indices: group.indices.iter().map(|&i| i as u32).collect(),
batches: group.batches.clone(),
bounding_box: group.header.bounding_box,
is_indoor: group.header.flags.contains(WmoGroupFlags::INDOOR),
}
}use wow_wmo::{WmoPortal, WmoPortalReference, Vec3};
use std::collections::HashSet;
struct PortalSystem {
portals: Vec<WmoPortal>,
relations: Vec<WmoPortalReference>,
group_visibility: Vec<bool>,
}
impl PortalSystem {
fn new(root: &WmoRoot) -> Self {
Self {
portals: root.portals.clone(),
relations: root.portal_references.clone(),
group_visibility: vec![false; root.header.n_groups as usize],
}
}
fn update_visibility(&mut self, camera_pos: Vec3, camera_group: usize) {
// Reset visibility
self.group_visibility.fill(false);
// Current group is always visible
self.group_visibility[camera_group] = true;
// Flood fill through portals
let mut to_check = vec![camera_group];
let mut checked = HashSet::new();
while let Some(current_group) = to_check.pop() {
if !checked.insert(current_group) {
continue;
}
// Find portals connected to this group
for relation in &self.relations {
if relation.group_index == current_group as u16 {
let portal = &self.portals[relation.portal_index as usize];
// Check if camera can see through portal
if self.is_portal_visible(portal, camera_pos) {
// Note: WmoPortalReference doesn't have target_group
// This would need to be determined from portal geometry
// For now, mark all connected groups as visible
// In a real implementation, you'd determine the other group
}
}
}
}
}
fn is_portal_visible(&self, portal: &WmoPortal, camera_pos: Vec3) -> bool {
// Simple visibility check - can be enhanced with frustum culling
let portal_center = Vec3 {
x: portal.vertices.iter().map(|v| v.x).sum::<f32>() / portal.vertices.len() as f32,
y: portal.vertices.iter().map(|v| v.y).sum::<f32>() / portal.vertices.len() as f32,
z: portal.vertices.iter().map(|v| v.z).sum::<f32>() / portal.vertices.len() as f32,
};
// Check if camera is on the positive side of portal plane
let to_portal = Vec3 {
x: portal_center.x - camera_pos.x,
y: portal_center.y - camera_pos.y,
z: portal_center.z - camera_pos.z,
};
let dot = to_portal.x * portal.normal.x +
to_portal.y * portal.normal.y +
to_portal.z * portal.normal.z;
dot > 0.0
}
}use wow_wmo::{WmoMaterial, WmoMaterialFlags, WmoRoot};
// Mock types for rendering (would be defined by your graphics library)
type TextureId = u32;
type BlendMode = u32;
type CullMode = u32;
struct WmoMaterialSet {
materials: Vec<GpuMaterial>,
textures: Vec<TextureId>,
}
struct GpuMaterial {
diffuse_texture: TextureId,
blend_mode: BlendMode,
cull_mode: CullMode,
flags: WmoMaterialFlags,
shader_id: u32,
}
// Mock texture manager for example
struct TextureManager;
impl TextureManager {
fn load_texture(&mut self, path: &str) -> Result<TextureId, Box<dyn std::error::Error>> {
// Mock implementation
Ok(0)
}
}
fn load_wmo_materials(
root: &WmoRoot,
texture_manager: &mut TextureManager,
) -> Result<WmoMaterialSet, Box<dyn std::error::Error>> {
let mut materials = Vec::new();
let mut textures = Vec::new();
for (i, wmo_material) in root.materials.iter().enumerate() {
// Load diffuse texture using texture index
let texture_path = if wmo_material.texture1 as usize < root.textures.len() {
&root.textures[wmo_material.texture1 as usize]
} else {
"default.blp" // fallback
};
let texture_id = texture_manager.load_texture(texture_path)?;
textures.push(texture_id);
// Determine blend mode
let blend_mode = if wmo_material.flags.contains(WmoMaterialFlags::UNLIT) {
0 // Opaque
} else if wmo_material.blend_mode == 1 {
1 // AlphaBlend
} else {
0 // Opaque
};
// Determine cull mode
let cull_mode = if wmo_material.flags.contains(WmoMaterialFlags::TWO_SIDED) {
0 // None
} else {
1 // Back
};
materials.push(GpuMaterial {
diffuse_texture: texture_id,
blend_mode,
cull_mode,
flags: wmo_material.flags,
shader_id: wmo_material.shader,
});
}
Ok(WmoMaterialSet { materials, textures })
}
// Mock shader variant enum
enum ShaderVariant {
Unlit,
Window,
Diffuse,
Specular,
Standard,
}
// Shader selection based on material properties
fn select_shader_for_material(material: &GpuMaterial) -> ShaderVariant {
if material.flags.contains(WmoMaterialFlags::UNLIT) {
ShaderVariant::Unlit
} else if material.flags.contains(WmoMaterialFlags::WINDOW_LIGHT) {
ShaderVariant::Window
} else if material.shader_id == 1 {
ShaderVariant::Diffuse
} else if material.shader_id == 2 {
ShaderVariant::Specular
} else {
ShaderVariant::Standard
}
}use wow_wmo::{WmoDoodadSet, WmoDoodadDef};
use std::collections::HashMap;
use std::sync::Arc;
// Mock M2 model type
struct M2Model;
struct WmoDoodadManager {
doodad_sets: Vec<WmoDoodadSet>,
instances: Vec<WmoDoodadDef>,
models: HashMap<String, Arc<M2Model>>,
}
// Mock types for example
type Matrix4<T> = [[T; 4]; 4];
type Vector3<T> = [T; 3];
type Vector4<T> = [T; 4];
type Quaternion<T> = [T; 4];
struct M2ModelManager;
impl M2ModelManager {
fn load_model(&mut self, _path: &str) -> Result<M2Model, Box<dyn std::error::Error>> {
Ok(M2Model)
}
}
impl WmoDoodadManager {
fn new(root: &WmoRoot) -> Self {
Self {
doodad_sets: root.doodad_sets.clone(),
instances: root.doodad_defs.clone(),
models: HashMap::new(),
}
}
fn load_doodad_set(
&mut self,
set_index: usize,
model_manager: &mut M2ModelManager,
doodad_names: &[String], // Would come from parsing MODN chunk
) -> Result<Vec<PlacedDoodad>, Box<dyn std::error::Error>> {
let set = &self.doodad_sets[set_index];
let mut placed_doodads = Vec::new();
for i in set.start_doodad..(set.start_doodad + set.n_doodads) {
let instance = &self.instances[i as usize];
// Get filename from name offset (simplified)
let filename = if instance.name_offset as usize < doodad_names.len() {
&doodad_names[instance.name_offset as usize]
} else {
"unknown.m2"
};
// Load model if not cached
let model = if let Some(cached) = self.models.get(filename) {
cached.clone()
} else {
let model = Arc::new(model_manager.load_model(filename)?);
self.models.insert(filename.to_string(), model.clone());
model
};
// Create transform matrix
let transform = create_doodad_transform(
[instance.position.x, instance.position.y, instance.position.z],
instance.orientation,
instance.scale,
);
placed_doodads.push(PlacedDoodad {
model,
transform,
color: [instance.color.r as f32 / 255.0, instance.color.g as f32 / 255.0,
instance.color.b as f32 / 255.0, instance.color.a as f32 / 255.0],
});
}
Ok(placed_doodads)
}
}
fn create_doodad_transform(
position: Vector3<f32>,
rotation: Quaternion<f32>,
scale: f32,
) -> Matrix4<f32> {
// Simplified transform creation - in a real implementation
// you'd use a proper math library like nalgebra or glam
let mut transform = [[0.0f32; 4]; 4];
// Identity matrix with scale
transform[0][0] = scale;
transform[1][1] = scale;
transform[2][2] = scale;
transform[3][3] = 1.0;
// Set translation
transform[3][0] = position[0];
transform[3][1] = position[1];
transform[3][2] = position[2];
// Note: rotation quaternion conversion omitted for brevity
transform
}
struct PlacedDoodad {
model: Arc<M2Model>,
transform: Matrix4<f32>,
color: Vector4<f32>,
}// Mock GPU types for example (would be from wgpu/vulkan/etc)
struct Device;
struct Queue;
struct RenderPipeline;
struct Buffer;
pub struct WmoRenderer {
group_buffers: Vec<GroupGpuData>,
material_set: WmoMaterialSet,
portal_system: PortalSystem,
}
struct GroupGpuData {
vertex_buffer: Buffer,
index_buffer: Buffer,
batches: Vec<WmoBatch>,
}
impl WmoRenderer {
pub fn new(wmo: &LoadedWmo) -> Result<Self, Box<dyn std::error::Error>> {
// Process groups
let mut group_buffers = Vec::new();
for group in &wmo.groups {
let processed = process_wmo_group(group);
// In a real implementation, you'd create GPU buffers here
group_buffers.push(GroupGpuData {
vertex_buffer: Buffer, // Mock buffer
index_buffer: Buffer, // Mock buffer
batches: processed.batches,
});
}
// Load materials
let mut texture_manager = TextureManager;
let material_set = load_wmo_materials(&wmo.root, &mut texture_manager)?;
// Initialize portal system
let portal_system = PortalSystem::new(&wmo.root);
Ok(Self {
group_buffers,
material_set,
portal_system,
})
}
pub fn render(
&mut self,
wmo: &LoadedWmo,
camera_pos: Vec3,
) {
// Update portal visibility
let camera_group = self.find_camera_group(camera_pos, wmo);
self.portal_system.update_visibility(camera_pos, camera_group);
// Render visible groups
println!("Rendering WMO with {} groups", wmo.groups.len());
for (group_idx, group) in wmo.groups.iter().enumerate() {
if !self.portal_system.group_visibility[group_idx] {
continue;
}
println!("Rendering group {}", group_idx);
// In a real renderer, you'd submit draw calls here
}
}
fn render_group(
&self,
render_pass: &mut RenderPass,
group_idx: usize,
pipeline: &RenderPipeline,
camera: &Camera,
) {
let group_data = &self.group_buffers[group_idx];
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, &camera.bind_group, &[]);
render_pass.set_vertex_buffer(0, group_data.vertex_buffer.slice(..));
render_pass.set_index_buffer(group_data.index_buffer.slice(..), IndexFormat::Uint32);
// Render each batch with its material
for batch in &group_data.batches {
let material = &self.material_set.materials[batch.material_id as usize];
// Set material bind group
render_pass.set_bind_group(1, &material.bind_group, &[]);
// Apply render state based on material
self.apply_material_state(render_pass, material);
// Draw
render_pass.draw_indexed(
batch.start_index..(batch.start_index + batch.index_count),
0,
0..1,
);
}
}
fn find_camera_group(&self, camera_pos: Vec3, wmo: &LoadedWmo) -> usize {
// Find which group contains the camera
for (idx, group) in wmo.groups.iter().enumerate() {
let bbox = &group.header.bounding_box;
if camera_pos.x >= bbox.min.x && camera_pos.x <= bbox.max.x &&
camera_pos.y >= bbox.min.y && camera_pos.y <= bbox.max.y &&
camera_pos.z >= bbox.min.z && camera_pos.z <= bbox.max.z {
return idx;
}
}
// Default to first group
0
}
}use warcraft_rs::wmo::*;
use std::sync::Arc;
pub struct WmoSceneManager {
loaded_wmos: HashMap<String, Arc<LoadedWmo>>,
instances: Vec<WmoInstance>,
renderer: WmoRenderer,
doodad_renderer: M2Renderer,
}
pub struct LoadedWmo {
wmo: Wmo,
gpu_data: WmoGpuData,
doodad_sets: Vec<Vec<PlacedDoodad>>,
collision_mesh: CollisionMesh,
}
pub struct WmoInstance {
wmo: Arc<LoadedWmo>,
transform: Matrix4<f32>,
doodad_set: usize,
tint_color: Vector4<f32>,
}
impl WmoSceneManager {
pub fn new(device: Device, queue: Queue) -> Self {
Self {
loaded_wmos: HashMap::new(),
instances: Vec::new(),
renderer: WmoRenderer::new(device.clone(), queue.clone()),
doodad_renderer: M2Renderer::new(device, queue),
}
}
pub async fn load_wmo(&mut self, path: &str) -> Result<Arc<LoadedWmo>, Box<dyn std::error::Error>> {
if let Some(cached) = self.loaded_wmos.get(path) {
return Ok(cached.clone());
}
// Load WMO files
let wmo = load_wmo(path)?;
// Create GPU resources
let gpu_data = self.renderer.create_gpu_data(&wmo)?;
// Load doodad sets
let mut doodad_sets = Vec::new();
for i in 0..wmo.root.doodad_sets.len() {
let doodads = load_doodad_set(&wmo, i)?;
doodad_sets.push(doodads);
}
// Generate collision mesh
let collision_mesh = generate_collision_mesh(&wmo)?;
let loaded = Arc::new(LoadedWmo {
wmo,
gpu_data,
doodad_sets,
collision_mesh,
});
self.loaded_wmos.insert(path.to_string(), loaded.clone());
Ok(loaded)
}
pub fn add_instance(&mut self, wmo: Arc<LoadedWmo>, transform: Matrix4<f32>, doodad_set: usize) {
self.instances.push(WmoInstance {
wmo,
transform,
doodad_set,
tint_color: Vector4::new(1.0, 1.0, 1.0, 1.0),
});
}
pub fn render_all(
&mut self,
encoder: &mut CommandEncoder,
view: &TextureView,
camera: &Camera,
) {
// Group instances by WMO for efficient rendering
let mut instance_groups: HashMap<Arc<LoadedWmo>, Vec<&WmoInstance>> = HashMap::new();
for instance in &self.instances {
instance_groups
.entry(instance.wmo.clone())
.or_insert_with(Vec::new)
.push(instance);
}
// Render each WMO type
for (wmo, instances) in instance_groups {
for instance in instances {
// Set instance transform
self.renderer.set_instance_transform(&instance.transform);
// Render WMO
self.renderer.render(encoder, view, camera, &wmo.wmo);
// Render doodads
if instance.doodad_set < wmo.doodad_sets.len() {
for doodad in &wmo.doodad_sets[instance.doodad_set] {
let world_transform = instance.transform * doodad.transform;
self.doodad_renderer.render_model(
encoder,
view,
&doodad.model,
&world_transform,
camera,
);
}
}
}
}
}
}use ncollide3d::shape::TriMesh;
use ncollide3d::query::{Ray, RayCast};
pub struct WmoCollisionSystem {
meshes: HashMap<String, CollisionMesh>,
}
pub struct CollisionMesh {
trimesh: TriMesh<f32>,
groups: Vec<GroupCollisionData>,
}
struct GroupCollisionData {
trimesh: TriMesh<f32>,
is_indoor: bool,
material_flags: Vec<MaterialFlags>,
}
impl WmoCollisionSystem {
pub fn build_collision_mesh(wmo: &Wmo) -> CollisionMesh {
let mut all_vertices = Vec::new();
let mut all_indices = Vec::new();
let mut groups = Vec::new();
for (group_idx, group) in wmo.groups.iter().enumerate() {
let vertex_offset = all_vertices.len();
// Collect collision vertices
let mut group_vertices = Vec::new();
let mut group_indices = Vec::new();
for vertex in &group.vertices {
let point = Point3::new(vertex.position.x, vertex.position.y, vertex.position.z);
all_vertices.push(point);
group_vertices.push(point);
}
// Collect collision triangles
for batch in &group.batches {
let material = &wmo.root.materials[batch.material_id as usize];
// Skip non-collidable materials
if material.flags.contains(MaterialFlags::NO_COLLISION) {
continue;
}
for i in (batch.start_index..(batch.start_index + batch.index_count)).step_by(3) {
let i0 = group.indices[i as usize] as usize;
let i1 = group.indices[(i + 1) as usize] as usize;
let i2 = group.indices[(i + 2) as usize] as usize;
all_indices.push([
vertex_offset + i0,
vertex_offset + i1,
vertex_offset + i2,
]);
group_indices.push([i0, i1, i2]);
}
}
// Create group collision mesh
let group_mesh = TriMesh::new(
group_vertices,
group_indices,
None,
);
groups.push(GroupCollisionData {
trimesh: group_mesh,
is_indoor: group.flags.contains(GroupFlags::INDOOR),
material_flags: Vec::new(), // Populate with actual material flags
});
}
// Create overall collision mesh
let trimesh = TriMesh::new(all_vertices, all_indices, None);
CollisionMesh { trimesh, groups }
}
pub fn raycast(
&self,
wmo_instance: &WmoInstance,
ray: &Ray<f32>,
) -> Option<RaycastHit> {
let mesh = &wmo_instance.wmo.collision_mesh;
// Transform ray to WMO local space
let inv_transform = wmo_instance.transform.try_inverse()?;
let local_ray = Ray::new(
inv_transform.transform_point(&ray.origin),
inv_transform.transform_vector(&ray.dir),
);
// Perform raycast
if let Some(toi) = mesh.trimesh.toi_with_ray(&Isometry3::identity(), &local_ray, f32::MAX, true) {
// Transform hit back to world space
let local_point = local_ray.point_at(toi);
let world_point = wmo_instance.transform.transform_point(&local_point);
Some(RaycastHit {
point: world_point,
distance: toi,
normal: Vector3::y(), // Calculate actual normal
material_flags: MaterialFlags::empty(),
})
} else {
None
}
}
}
#[derive(Debug)]
pub struct RaycastHit {
pub point: Point3<f32>,
pub distance: f32,
pub normal: Vector3<f32>,
pub material_flags: MaterialFlags,
}// wmo_shader.wgsl
struct Camera {
view_proj: mat4x4<f32>,
view: mat4x4<f32>,
position: vec3<f32>,
time: f32,
}
struct Instance {
transform: mat4x4<f32>,
tint_color: vec4<f32>,
}
struct Material {
flags: u32,
blend_mode: u32,
shader_id: u32,
_padding: u32,
}
@group(0) @binding(0)
var<uniform> camera: Camera;
@group(1) @binding(0)
var<uniform> instance: Instance;
@group(2) @binding(0)
var<uniform> material: Material;
@group(2) @binding(1)
var diffuse_texture: texture_2d<f32>;
@group(2) @binding(2)
var texture_sampler: sampler;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) texcoord: vec2<f32>,
@location(3) vertex_color: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) texcoord: vec2<f32>,
@location(3) vertex_color: vec4<f32>,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Transform position
let world_pos = instance.transform * vec4<f32>(input.position, 1.0);
out.world_position = world_pos.xyz;
out.clip_position = camera.view_proj * world_pos;
// Transform normal
let normal_matrix = mat3x3<f32>(
instance.transform[0].xyz,
instance.transform[1].xyz,
instance.transform[2].xyz,
);
out.normal = normalize(normal_matrix * input.normal);
out.texcoord = input.texcoord;
out.vertex_color = input.vertex_color * instance.tint_color;
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
// Sample texture
var color = textureSample(diffuse_texture, texture_sampler, input.texcoord);
// Apply vertex color (pre-baked lighting)
color = color * input.vertex_color * 2.0;
// Check material flags
let is_unlit = (material.flags & 0x1u) != 0u;
let is_window = (material.flags & 0x8u) != 0u;
if (!is_unlit) {
// Simple ambient + directional light
let light_dir = normalize(vec3<f32>(0.5, 1.0, 0.3));
let n_dot_l = max(dot(input.normal, light_dir), 0.0);
let ambient = vec3<f32>(0.4, 0.4, 0.5);
color.xyz = color.xyz * (ambient + n_dot_l * 0.6);
}
// Window material effect
if (is_window) {
// Add some transparency and reflectivity
color.w = color.w * 0.8;
}
return color;
}pub struct WmoLodSelector {
distance_thresholds: Vec<f32>,
}
impl WmoLodSelector {
pub fn new() -> Self {
Self {
distance_thresholds: vec![100.0, 300.0, 600.0],
}
}
pub fn select_lod(
&self,
wmo_lods: &[Wmo],
camera_pos: Point3<f32>,
wmo_center: Point3<f32>,
) -> usize {
let distance = (camera_pos - wmo_center).norm();
for (i, threshold) in self.distance_thresholds.iter().enumerate() {
if distance < *threshold && i < wmo_lods.len() {
return i;
}
}
// Return lowest detail LOD
wmo_lods.len() - 1
}
pub fn select_group_detail(
&self,
group: &WmoGroup,
camera_pos: Point3<f32>,
) -> RenderDetail {
let distance = group.bounding_box.distance_to_point(&camera_pos);
if distance < 50.0 {
RenderDetail::Full
} else if distance < 200.0 {
RenderDetail::Simplified
} else {
RenderDetail::BoundingBox
}
}
}
enum RenderDetail {
Full,
Simplified,
BoundingBox,
}pub struct WmoBatcher {
instance_data: HashMap<String, Vec<InstanceData>>,
instance_buffers: HashMap<String, Buffer>,
}
impl WmoBatcher {
pub fn add_instance(&mut self, wmo_path: &str, transform: Matrix4<f32>, color: Vector4<f32>) {
self.instance_data
.entry(wmo_path.to_string())
.or_insert_with(Vec::new)
.push(InstanceData {
transform: transform.into(),
color: color.into(),
});
}
pub fn update_buffers(&mut self, device: &Device, queue: &Queue) {
for (path, instances) in &self.instance_data {
let buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("WMO Instance Buffer"),
contents: bytemuck::cast_slice(instances),
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
});
self.instance_buffers.insert(path.clone(), buffer);
}
}
pub fn render_batched(
&self,
render_pass: &mut RenderPass,
wmo_path: &str,
base_vertices: u32,
) {
if let Some(instance_buffer) = self.instance_buffers.get(wmo_path) {
let instance_count = self.instance_data[wmo_path].len() as u32;
render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
render_pass.draw(0..base_vertices, 0..instance_count);
}
}
}pub struct OcclusionCuller {
query_pool: QuerySet,
visibility_buffer: Buffer,
}
impl OcclusionCuller {
pub fn test_group_visibility(
&mut self,
encoder: &mut CommandEncoder,
group_bounds: &[BoundingBox],
camera: &Camera,
) -> Vec<bool> {
// Render bounding boxes with occlusion queries
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Occlusion Test Pass"),
color_attachments: &[],
depth_stencil_attachment: Some(/* depth only */),
});
for (i, bounds) in group_bounds.iter().enumerate() {
render_pass.begin_occlusion_query(i as u32);
self.render_bounding_box(&mut render_pass, bounds, camera);
render_pass.end_occlusion_query();
}
drop(render_pass);
// Read back results
self.read_visibility_results(encoder, group_bounds.len())
}
fn render_bounding_box(
&self,
render_pass: &mut RenderPass,
bounds: &BoundingBox,
camera: &Camera,
) {
// Render conservative bounding box
// Implementation details...
}
}Problem: Flickering at group boundaries.
Solution:
// Add small offset between groups
fn adjust_group_boundaries(groups: &mut [WmoGroup]) {
const EPSILON: f32 = 0.001;
for (i, group) in groups.iter_mut().enumerate() {
// Slightly shrink each group's geometry
for vertex in &mut group.vertices {
let to_center = group.bounding_box.center() - vertex.position;
vertex.position += to_center.normalize() * EPSILON;
}
}
}Problem: Groups appearing/disappearing incorrectly.
Solution:
// Enhanced portal visibility with margin
fn is_portal_visible_conservative(
portal: &Portal,
camera_pos: Point3<f32>,
camera_frustum: &Frustum,
) -> bool {
// Add margin to portal bounds
let expanded_bounds = portal.bounding_box.expanded(1.0);
// Check frustum intersection
if !camera_frustum.intersects_aabb(&expanded_bounds) {
return false;
}
// Check if camera can see through portal
let portal_normal = calculate_portal_normal(&portal.vertices);
let to_portal = portal.center() - camera_pos;
// Add angle threshold for conservative culling
let dot = to_portal.normalize().dot(&portal_normal);
dot > -0.1 // Slightly visible from behind
}Problem: Indoor areas too bright or outdoor areas too dark.
Solution:
// Separate lighting for indoor/outdoor
struct LightingSettings {
outdoor_ambient: Vector3<f32>,
outdoor_diffuse: Vector3<f32>,
indoor_ambient: Vector3<f32>,
indoor_diffuse: Vector3<f32>,
}
fn apply_group_lighting(
group: &WmoGroup,
settings: &LightingSettings,
) -> GroupLighting {
if group.flags.contains(GroupFlags::OUTDOOR) {
GroupLighting {
ambient: settings.outdoor_ambient,
diffuse: settings.outdoor_diffuse,
use_vertex_color: true,
}
} else {
GroupLighting {
ambient: settings.indoor_ambient,
diffuse: settings.indoor_diffuse,
use_vertex_color: true,
}
}
}pub struct HierarchicalCuller {
octree: Octree<usize>,
}
impl HierarchicalCuller {
pub fn build(wmo: &Wmo) -> Self {
let mut octree = Octree::new(wmo.root.bounding_box.clone());
for (i, group) in wmo.groups.iter().enumerate() {
octree.insert(i, &group.bounding_box);
}
Self { octree }
}
pub fn get_visible_groups(&self, frustum: &Frustum) -> Vec<usize> {
self.octree.query_frustum(frustum)
}
}pub struct WmoTextureAtlas {
atlas: TextureAtlas,
material_mappings: HashMap<u32, AtlasRegion>,
}
impl WmoTextureAtlas {
pub fn build_for_wmo(
wmo: &Wmo,
texture_manager: &TextureManager,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut atlas = TextureAtlas::new(4096);
let mut mappings = HashMap::new();
// Collect unique textures
let mut textures = HashSet::new();
for material in &wmo.root.materials {
textures.insert(&material.texture);
}
// Pack into atlas
for (i, texture_path) in textures.iter().enumerate() {
let texture = texture_manager.load_texture(texture_path)?;
let region = atlas.add_texture(texture_path, &texture)?;
mappings.insert(i as u32, region);
}
Ok(Self {
atlas,
material_mappings: mappings,
})
}
}pub async fn load_wmo_async(
path: String,
) -> Result<Wmo, Box<dyn std::error::Error>> {
// Load root file
let root_data = tokio::fs::read(&path).await?;
let root = tokio::task::spawn_blocking(move || {
WmoRoot::from_bytes(&root_data)
}).await??;
// Load groups in parallel
let base_path = path.trim_end_matches(".wmo");
let mut group_futures = Vec::new();
for i in 0..root.group_count {
let group_path = format!("{}_{:03}.wmo", base_path, i);
group_futures.push(tokio::fs::read(group_path));
}
let group_data = futures::future::join_all(group_futures).await;
// Parse groups
let mut groups = Vec::new();
for data in group_data {
if let Ok(data) = data {
let group = WmoGroup::from_bytes(&data)?;
groups.push(group);
}
}
Ok(Wmo { root, groups })
}- 📦 Working with MPQ Archives - Extract WMO files from archives
- 🖼️ Texture Loading Guide - Load BLP textures for WMOs
- 🎭 Loading M2 Models - Load doodads placed in WMOs
- 🌍 Rendering ADT Terrain - Integrate WMOs with terrain
- 📊 LOD System Guide - Implement LOD for large structures
- WMO Format Documentation - Complete WMO format specification
- Portal Rendering - Understanding portal-based visibility
- BSP Trees - Binary space partitioning for WMOs
- WoW Model Viewer - Reference WMO implementation