🎨 Model Rendering Guide
Overview
Rendering World of Warcraft models efficiently requires understanding of
GPU techniques, shader programming, and optimization strategies. This guide covers
advanced rendering techniques for M2 models and WMOs using warcraft-rs, including
materials, lighting, effects, and performance optimization.
Prerequisites
Before implementing model rendering, ensure you have:
- Strong understanding of graphics APIs (wgpu, Vulkan, OpenGL)
- Knowledge of shader programming (WGSL, GLSL, HLSL)
- Understanding of rendering pipelines and GPU architecture
- Familiarity with PBR (Physically Based Rendering) concepts
- Experience with graphics debugging tools
Understanding WoW Rendering
Rendering Features
- Multi-pass Rendering: Opaque, transparent, particle passes
- Material System: Diffuse, specular, emissive, environment maps
- Lighting: Vertex lighting, dynamic lights, ambient lighting
- Effects: Glow, transparency, billboarding, UV animation
- Shadows: Shadow mapping, cascaded shadows
- Post-processing: Bloom, fog, color grading
Rendering Challenges
- Draw Call Optimization: Thousands of objects per frame
- Transparency Sorting: Proper alpha blending order
- Texture Management: Efficient texture binding
- State Changes: Minimizing GPU state switches
- Memory Bandwidth: Vertex data optimization
Step-by-Step Instructions
1. Setting Up the Rendering Pipeline
#![allow(unused)]
fn main() {
use wgpu::*;
use bytemuck::{Pod, Zeroable};
pub struct ModelRenderer {
device: Device,
queue: Queue,
pipelines: RenderPipelineCache,
bind_group_layouts: BindGroupLayouts,
shader_cache: ShaderCache,
}
pub struct RenderPipelineCache {
opaque: HashMap<PipelineKey, RenderPipeline>,
transparent: HashMap<PipelineKey, RenderPipeline>,
particle: RenderPipeline,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct PipelineKey {
vertex_layout: VertexLayoutType,
material_flags: MaterialFlags,
blend_mode: BlendMode,
cull_mode: CullMode,
}
pub struct BindGroupLayouts {
camera: BindGroupLayout,
model: BindGroupLayout,
material: BindGroupLayout,
lighting: BindGroupLayout,
}
impl ModelRenderer {
pub fn new(device: Device, queue: Queue) -> Self {
let shader_cache = ShaderCache::new(&device);
let bind_group_layouts = Self::create_bind_group_layouts(&device);
let pipelines = Self::create_pipelines(&device, &shader_cache, &bind_group_layouts);
Self {
device,
queue,
pipelines,
bind_group_layouts,
shader_cache,
}
}
fn create_bind_group_layouts(device: &Device) -> BindGroupLayouts {
// Camera bind group layout
let camera = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("Camera Bind Group Layout"),
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(NonZeroU64::new(256).unwrap()),
},
count: None,
},
],
});
// Model bind group layout (per-instance data)
let model = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("Model Bind Group Layout"),
entries: &[
// Model transform
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(NonZeroU64::new(64).unwrap()),
},
count: None,
},
// Bone matrices
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: Some(NonZeroU64::new(64 * 256).unwrap()),
},
count: None,
},
],
});
// Material bind group layout
let material = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("Material Bind Group Layout"),
entries: &[
// Material properties
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(NonZeroU64::new(64).unwrap()),
},
count: None,
},
// Diffuse texture
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
view_dimension: TextureViewDimension::D2,
sample_type: TextureSampleType::Float { filterable: true },
},
count: None,
},
// Texture sampler
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
},
],
});
// Lighting bind group layout
let lighting = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
label: Some("Lighting Bind Group Layout"),
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: Some(NonZeroU64::new(32 * 128).unwrap()),
},
count: None,
},
],
});
BindGroupLayouts {
camera,
model,
material,
lighting,
}
}
}
}
2. Material System Implementation
#![allow(unused)]
fn main() {
use bitflags::bitflags;
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct GpuMaterial {
pub base_color: [f32; 4],
pub emissive: [f32; 3],
pub metallic: f32,
pub roughness: f32,
pub flags: u32,
pub blend_mode: u32,
pub _padding: u32,
}
bitflags! {
pub struct MaterialFlags: u32 {
const UNLIT = 0x01;
const UNFOGGED = 0x02;
const TWO_SIDED = 0x04;
const DEPTH_TEST = 0x08;
const DEPTH_WRITE = 0x10;
const ALPHA_TEST = 0x20;
const ADDITIVE = 0x40;
const ENVIRONMENT_MAP = 0x80;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
Opaque,
AlphaBlend,
Additive,
Multiply,
AlphaKey,
}
pub struct MaterialManager {
materials: HashMap<u32, Material>,
bind_groups: HashMap<u32, BindGroup>,
default_material: Material,
}
#[derive(Debug, Clone)]
pub struct Material {
pub gpu_data: GpuMaterial,
pub textures: MaterialTextures,
pub render_flags: RenderFlags,
}
#[derive(Debug, Clone)]
pub struct MaterialTextures {
pub diffuse: Option<TextureId>,
pub normal: Option<TextureId>,
pub specular: Option<TextureId>,
pub emissive: Option<TextureId>,
pub environment: Option<TextureId>,
}
impl MaterialManager {
pub fn create_material_from_m2(
&mut self,
m2_material: &M2Material,
texture_manager: &TextureManager,
) -> u32 {
let material = Material {
gpu_data: GpuMaterial {
base_color: [1.0, 1.0, 1.0, 1.0],
emissive: [0.0, 0.0, 0.0],
metallic: 0.0,
roughness: 0.8,
flags: m2_material.flags.bits(),
blend_mode: m2_material.blend_mode as u32,
_padding: 0,
},
textures: MaterialTextures {
diffuse: texture_manager.get_texture(m2_material.texture_id),
normal: None,
specular: None,
emissive: None,
environment: None,
},
render_flags: self.determine_render_flags(m2_material),
};
let material_id = self.materials.len() as u32;
self.materials.insert(material_id, material);
// Create bind group
self.create_material_bind_group(material_id);
material_id
}
fn determine_render_flags(&self, m2_material: &M2Material) -> RenderFlags {
let mut flags = RenderFlags::empty();
if m2_material.flags.contains(MaterialFlags::UNLIT) {
flags |= RenderFlags::DISABLE_LIGHTING;
}
if m2_material.flags.contains(MaterialFlags::TWO_SIDED) {
flags |= RenderFlags::DISABLE_CULLING;
}
match m2_material.blend_mode {
1 => flags |= RenderFlags::ALPHA_BLEND,
2 => flags |= RenderFlags::ADDITIVE_BLEND,
_ => {}
}
flags
}
}
}
3. Advanced Shader System
// model_shader.wgsl
// Vertex shader structures
struct CameraUniforms {
view: mat4x4<f32>,
proj: mat4x4<f32>,
view_proj: mat4x4<f32>,
eye_pos: vec3<f32>,
time: f32,
}
struct ModelUniforms {
model: mat4x4<f32>,
normal_matrix: mat3x3<f32>,
color: vec4<f32>,
}
struct MaterialUniforms {
base_color: vec4<f32>,
emissive: vec3<f32>,
metallic: f32,
roughness: f32,
flags: u32,
blend_mode: u32,
_padding: u32,
}
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(1) @binding(1) var<storage, read> bone_matrices: array<mat4x4<f32>>;
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
@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) tangent: vec4<f32>,
@location(3) texcoord0: vec2<f32>,
@location(4) texcoord1: vec2<f32>,
@location(5) color: vec4<f32>,
@location(6) bone_indices: vec4<u32>,
@location(7) bone_weights: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_pos: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) tangent: vec3<f32>,
@location(3) bitangent: vec3<f32>,
@location(4) texcoord0: vec2<f32>,
@location(5) texcoord1: vec2<f32>,
@location(6) vertex_color: vec4<f32>,
@location(7) view_dir: vec3<f32>,
}
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Skeletal animation
var skinned_pos = vec4<f32>(0.0);
var skinned_normal = vec3<f32>(0.0);
var skinned_tangent = vec3<f32>(0.0);
for (var i = 0u; i < 4u; i++) {
let bone_idx = in.bone_indices[i];
let weight = in.bone_weights[i];
if (weight > 0.0) {
let bone_matrix = bone_matrices[bone_idx];
skinned_pos += bone_matrix * vec4<f32>(in.position, 1.0) * weight;
skinned_normal += (bone_matrix * vec4<f32>(in.normal, 0.0)).xyz * weight;
skinned_tangent += (bone_matrix * vec4<f32>(in.tangent.xyz, 0.0)).xyz * weight;
}
}
// Transform to world space
let world_pos = model.model * skinned_pos;
out.world_pos = world_pos.xyz;
out.clip_position = camera.view_proj * world_pos;
// Transform normals
out.normal = normalize(model.normal_matrix * skinned_normal);
out.tangent = normalize(model.normal_matrix * skinned_tangent);
out.bitangent = cross(out.normal, out.tangent) * in.tangent.w;
// Pass through vertex attributes
out.texcoord0 = in.texcoord0;
out.texcoord1 = in.texcoord1;
out.vertex_color = in.color * model.color;
// Calculate view direction
out.view_dir = normalize(camera.eye_pos - out.world_pos);
return out;
}
// Fragment shader with PBR lighting
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Sample textures
var base_color = textureSample(diffuse_texture, texture_sampler, in.texcoord0);
base_color *= material.base_color * in.vertex_color;
// Alpha test
if ((material.flags & 0x20u) != 0u && base_color.a < 0.5) {
discard;
}
// Check if unlit
if ((material.flags & 0x01u) != 0u) {
return vec4<f32>(base_color.rgb + material.emissive, base_color.a);
}
// Normal mapping (if available)
var N = normalize(in.normal);
// PBR calculations
let V = normalize(in.view_dir);
let NdotV = max(dot(N, V), 0.0);
// Lighting accumulation
var Lo = vec3<f32>(0.0);
// Directional light (sun)
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
let H = normalize(V + L);
let NdotL = max(dot(N, L), 0.0);
let NdotH = max(dot(N, H), 0.0);
let VdotH = max(dot(V, H), 0.0);
// BRDF
let D = distribution_ggx(NdotH, material.roughness);
let G = geometry_smith(NdotV, NdotL, material.roughness);
let F = fresnel_schlick(VdotH, vec3<f32>(0.04));
let numerator = D * G * F;
let denominator = 4.0 * NdotV * NdotL + 0.001;
let specular = numerator / denominator;
let kS = F;
let kD = (vec3<f32>(1.0) - kS) * (1.0 - material.metallic);
let radiance = vec3<f32>(2.0);
Lo += (kD * base_color.rgb / 3.14159265 + specular) * radiance * NdotL;
// Ambient
let ambient = vec3<f32>(0.3) * base_color.rgb;
// Final color
var color = ambient + Lo + material.emissive;
// Apply fog (if enabled)
if ((material.flags & 0x02u) == 0u) {
let fog_distance = length(in.world_pos - camera.eye_pos);
let fog_factor = exp(-fog_distance * 0.001);
color = mix(vec3<f32>(0.5, 0.6, 0.7), color, fog_factor);
}
return vec4<f32>(color, base_color.a);
}
// PBR helper functions
fn distribution_ggx(NdotH: f32, roughness: f32) -> f32 {
let a = roughness * roughness;
let a2 = a * a;
let NdotH2 = NdotH * NdotH;
let numerator = a2;
let denominator = NdotH2 * (a2 - 1.0) + 1.0;
let denominator2 = 3.14159265 * denominator * denominator;
return numerator / denominator2;
}
fn geometry_schlick_ggx(NdotV: f32, roughness: f32) -> f32 {
let r = roughness + 1.0;
let k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
fn geometry_smith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 {
let ggx1 = geometry_schlick_ggx(NdotV, roughness);
let ggx2 = geometry_schlick_ggx(NdotL, roughness);
return ggx1 * ggx2;
}
fn fresnel_schlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
4. Batch Rendering System
#![allow(unused)]
fn main() {
use std::sync::Arc;
pub struct BatchRenderer {
batches: HashMap<BatchKey, RenderBatch>,
instance_data: Vec<InstanceData>,
instance_buffer: Buffer,
draw_commands: Vec<DrawCommand>,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct BatchKey {
mesh_id: u64,
material_id: u32,
pipeline_key: PipelineKey,
}
pub struct RenderBatch {
instances: Vec<u32>, // indices into instance_data
vertex_buffer: Arc<Buffer>,
index_buffer: Arc<Buffer>,
index_count: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
pub struct InstanceData {
model_matrix: [[f32; 4]; 4],
normal_matrix: [[f32; 3]; 3],
color: [f32; 4],
texture_transform: [f32; 4], // scale_x, scale_y, offset_x, offset_y
}
pub struct DrawCommand {
batch_key: BatchKey,
instance_start: u32,
instance_count: u32,
}
impl BatchRenderer {
pub fn prepare_frame(&mut self, models: &[ModelInstance]) {
self.batches.clear();
self.instance_data.clear();
self.draw_commands.clear();
// Group models by batch key
for model in models {
let batch_key = BatchKey {
mesh_id: model.mesh.id(),
material_id: model.material_id,
pipeline_key: model.pipeline_key(),
};
let instance_idx = self.instance_data.len() as u32;
self.instance_data.push(model.instance_data());
self.batches
.entry(batch_key)
.or_insert_with(|| RenderBatch {
instances: Vec::new(),
vertex_buffer: model.mesh.vertex_buffer.clone(),
index_buffer: model.mesh.index_buffer.clone(),
index_count: model.mesh.index_count,
})
.instances
.push(instance_idx);
}
// Generate draw commands
for (batch_key, batch) in &self.batches {
if !batch.instances.is_empty() {
self.draw_commands.push(DrawCommand {
batch_key: batch_key.clone(),
instance_start: batch.instances[0],
instance_count: batch.instances.len() as u32,
});
}
}
// Sort draw commands for optimal rendering
self.sort_draw_commands();
// Update instance buffer
self.update_instance_buffer();
}
fn sort_draw_commands(&mut self) {
self.draw_commands.sort_by(|a, b| {
// Sort by pipeline first (minimize state changes)
match a.batch_key.pipeline_key.cmp(&b.batch_key.pipeline_key) {
std::cmp::Ordering::Equal => {
// Then by material (minimize texture bindings)
match a.batch_key.material_id.cmp(&b.batch_key.material_id) {
std::cmp::Ordering::Equal => {
// Finally by mesh
a.batch_key.mesh_id.cmp(&b.batch_key.mesh_id)
}
other => other,
}
}
other => other,
}
});
}
pub fn render(
&self,
render_pass: &mut RenderPass,
pipeline_cache: &RenderPipelineCache,
material_manager: &MaterialManager,
) {
let mut current_pipeline: Option<PipelineKey> = None;
let mut current_material: Option<u32> = None;
for command in &self.draw_commands {
let batch = &self.batches[&command.batch_key];
// Set pipeline if changed
if current_pipeline != Some(command.batch_key.pipeline_key.clone()) {
let pipeline = pipeline_cache.get(&command.batch_key.pipeline_key);
render_pass.set_pipeline(pipeline);
current_pipeline = Some(command.batch_key.pipeline_key.clone());
}
// Set material if changed
if current_material != Some(command.batch_key.material_id) {
let material_bind_group = material_manager.get_bind_group(command.batch_key.material_id);
render_pass.set_bind_group(2, material_bind_group, &[]);
current_material = Some(command.batch_key.material_id);
}
// Set mesh buffers
render_pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
render_pass.set_index_buffer(batch.index_buffer.slice(..), IndexFormat::Uint32);
// Draw instanced
render_pass.draw_indexed(
0..batch.index_count,
0,
command.instance_start..(command.instance_start + command.instance_count),
);
}
}
}
}
5. Effect Rendering System
#![allow(unused)]
fn main() {
pub struct EffectRenderer {
glow_pipeline: RenderPipeline,
particle_pipeline: RenderPipeline,
ribbon_pipeline: RenderPipeline,
screen_effect_pipeline: RenderPipeline,
}
pub struct GlowEffect {
intensity: f32,
color: Vector3<f32>,
size: f32,
}
pub struct ParticleSystem {
emitters: Vec<ParticleEmitter>,
particles: Vec<Particle>,
vertex_buffer: Buffer,
instance_buffer: Buffer,
}
impl EffectRenderer {
pub fn render_glow_effects(
&self,
encoder: &mut CommandEncoder,
models: &[ModelWithGlow],
glow_texture: &TextureView,
depth_texture: &TextureView,
) {
// First pass: Render glowing parts to separate texture
{
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Glow Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: glow_texture,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(Color::BLACK),
store: true,
},
})],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: depth_texture,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: false,
}),
stencil_ops: None,
}),
});
render_pass.set_pipeline(&self.glow_pipeline);
for model in models {
if model.has_glow() {
self.render_model_glow(&mut render_pass, model);
}
}
}
// Second pass: Blur glow texture
self.blur_texture(encoder, glow_texture);
// Third pass: Composite with main scene
self.composite_glow(encoder, glow_texture);
}
pub fn render_particles(
&self,
render_pass: &mut RenderPass,
particle_system: &ParticleSystem,
camera: &Camera,
) {
render_pass.set_pipeline(&self.particle_pipeline);
// Update particle vertices (billboarding)
let vertices = self.generate_particle_vertices(particle_system, camera);
particle_system.vertex_buffer.write(&vertices);
// Sort particles by distance for proper blending
let sorted_indices = self.sort_particles_by_distance(particle_system, camera);
render_pass.set_vertex_buffer(0, particle_system.vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, particle_system.instance_buffer.slice(..));
for emitter in &particle_system.emitters {
render_pass.set_bind_group(2, &emitter.material_bind_group, &[]);
let particle_range = emitter.particle_range();
render_pass.draw(
0..4, // Quad vertices
particle_range.start as u32..particle_range.end as u32,
);
}
}
fn generate_particle_vertices(
&self,
system: &ParticleSystem,
camera: &Camera,
) -> Vec<ParticleVertex> {
let mut vertices = Vec::new();
let right = camera.right();
let up = camera.up();
for particle in &system.particles {
let size = particle.size * 0.5;
// Billboard corners
let corners = [
particle.position - right * size - up * size,
particle.position + right * size - up * size,
particle.position + right * size + up * size,
particle.position - right * size + up * size,
];
for (i, corner) in corners.iter().enumerate() {
vertices.push(ParticleVertex {
position: corner.into(),
texcoord: match i {
0 => [0.0, 1.0],
1 => [1.0, 1.0],
2 => [1.0, 0.0],
3 => [0.0, 0.0],
_ => unreachable!(),
},
color: particle.color.into(),
});
}
}
vertices
}
}
}
6. Shadow Mapping
#![allow(unused)]
fn main() {
pub struct ShadowRenderer {
shadow_maps: Vec<ShadowMap>,
shadow_pipeline: RenderPipeline,
cascade_data: CascadeData,
}
pub struct ShadowMap {
texture: Texture,
view: TextureView,
size: u32,
view_proj: Matrix4<f32>,
}
pub struct CascadeData {
splits: Vec<f32>,
matrices: Vec<Matrix4<f32>>,
}
impl ShadowRenderer {
pub fn render_shadows(
&mut self,
encoder: &mut CommandEncoder,
models: &[ModelInstance],
light_direction: Vector3<f32>,
camera: &Camera,
) {
// Calculate cascade splits
self.update_cascades(camera, light_direction);
// Render each cascade
for (cascade_idx, shadow_map) in self.shadow_maps.iter_mut().enumerate() {
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(&format!("Shadow Pass Cascade {}", cascade_idx)),
color_attachments: &[],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &shadow_map.view,
depth_ops: Some(Operations {
load: LoadOp::Clear(1.0),
store: true,
}),
stencil_ops: None,
}),
});
render_pass.set_pipeline(&self.shadow_pipeline);
// Set cascade view-projection matrix
let cascade_uniform = CascadeUniform {
view_proj: shadow_map.view_proj.into(),
};
render_pass.set_bind_group(0, &cascade_uniform.bind_group, &[]);
// Render models
for model in models {
if self.is_in_cascade_frustum(model, cascade_idx) {
self.render_model_depth_only(&mut render_pass, model);
}
}
}
}
fn update_cascades(&mut self, camera: &Camera, light_dir: Vector3<f32>) {
let view_matrix = camera.view_matrix();
let proj_matrix = camera.projection_matrix();
let inv_view_proj = (proj_matrix * view_matrix).try_inverse().unwrap();
for (i, split_distance) in self.cascade_data.splits.iter().enumerate() {
let near = if i == 0 {
camera.near()
} else {
self.cascade_data.splits[i - 1]
};
let far = *split_distance;
// Calculate cascade frustum corners
let frustum_corners = self.calculate_frustum_corners(near, far, &inv_view_proj);
// Calculate light view matrix
let center = frustum_corners.iter().sum::<Vector3<f32>>() / 8.0;
let light_view = Matrix4::look_at_rh(
&Point3::from(center + light_dir * 100.0),
&Point3::from(center),
&Vector3::y(),
);
// Calculate light projection matrix
let mut min = Vector3::new(f32::MAX, f32::MAX, f32::MAX);
let mut max = Vector3::new(f32::MIN, f32::MIN, f32::MIN);
for corner in &frustum_corners {
let view_space = light_view.transform_point(&Point3::from(*corner));
min = min.zip_map(&view_space.coords, f32::min);
max = max.zip_map(&view_space.coords, f32::max);
}
// Snap to texel grid to reduce shimmer
let texel_size = (max.x - min.x) / self.shadow_maps[i].size as f32;
min.x = (min.x / texel_size).floor() * texel_size;
max.x = (max.x / texel_size).ceil() * texel_size;
min.y = (min.y / texel_size).floor() * texel_size;
max.y = (max.y / texel_size).ceil() * texel_size;
let light_proj = Matrix4::new_orthographic(
min.x, max.x,
min.y, max.y,
min.z - 50.0, max.z + 50.0,
);
self.shadow_maps[i].view_proj = light_proj * light_view;
self.cascade_data.matrices[i] = self.shadow_maps[i].view_proj;
}
}
}
}
Code Examples
Complete Render Frame
#![allow(unused)]
fn main() {
pub struct RenderFrame {
models: Vec<ModelInstance>,
transparent_models: Vec<ModelInstance>,
particles: Vec<ParticleSystem>,
lights: Vec<Light>,
shadow_casters: Vec<ModelInstance>,
}
impl ModelRenderer {
pub fn render_frame(
&mut self,
encoder: &mut CommandEncoder,
frame: &RenderFrame,
camera: &Camera,
output_view: &TextureView,
) {
// Update per-frame uniforms
self.update_camera_uniforms(camera);
self.update_lighting_uniforms(&frame.lights);
// Shadow pass
if !frame.shadow_casters.is_empty() {
self.shadow_renderer.render_shadows(
encoder,
&frame.shadow_casters,
self.sun_direction,
camera,
);
}
// Depth pre-pass for opaque objects
self.render_depth_prepass(encoder, &frame.models);
// Main render pass
{
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Main Render Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: output_view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: true,
},
})],
depth_stencil_attachment: Some(self.depth_attachment()),
});
// Render opaque models
render_pass.push_debug_group("Opaque Models");
self.batch_renderer.prepare_frame(&frame.models);
self.batch_renderer.render(
&mut render_pass,
&self.pipelines,
&self.material_manager,
);
render_pass.pop_debug_group();
// Render transparent models (sorted back-to-front)
render_pass.push_debug_group("Transparent Models");
let sorted_transparent = self.sort_transparent_models(&frame.transparent_models, camera);
for model in sorted_transparent {
self.render_transparent_model(&mut render_pass, model);
}
render_pass.pop_debug_group();
// Render particles
render_pass.push_debug_group("Particles");
for particle_system in &frame.particles {
self.effect_renderer.render_particles(
&mut render_pass,
particle_system,
camera,
);
}
render_pass.pop_debug_group();
}
// Post-processing
self.render_post_processing(encoder, output_view);
}
fn sort_transparent_models<'a>(
&self,
models: &'a [ModelInstance],
camera: &Camera,
) -> Vec<&'a ModelInstance> {
let mut sorted: Vec<_> = models.iter().collect();
let camera_pos = camera.position();
sorted.sort_by(|a, b| {
let dist_a = (a.position() - camera_pos).magnitude_squared();
let dist_b = (b.position() - camera_pos).magnitude_squared();
dist_b.partial_cmp(&dist_a).unwrap()
});
sorted
}
}
}
GPU-Driven Rendering
#![allow(unused)]
fn main() {
pub struct GpuDrivenRenderer {
indirect_buffer: Buffer,
visibility_buffer: Buffer,
draw_commands_buffer: Buffer,
cull_shader: ComputePipeline,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
struct DrawIndirectCommand {
vertex_count: u32,
instance_count: u32,
first_vertex: u32,
first_instance: u32,
}
impl GpuDrivenRenderer {
pub fn cull_and_render(
&mut self,
encoder: &mut CommandEncoder,
models: &[ModelInstance],
camera: &Camera,
) {
// Upload model data
let model_data: Vec<GpuModelData> = models
.iter()
.map(|m| GpuModelData {
bounding_sphere: m.bounding_sphere(),
lod_distances: m.lod_distances(),
instance_data: m.instance_data(),
})
.collect();
self.model_buffer.write(&model_data);
// Frustum culling compute pass
{
let mut compute_pass = encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some("GPU Culling Pass"),
});
compute_pass.set_pipeline(&self.cull_shader);
compute_pass.set_bind_group(0, &self.cull_bind_group, &[]);
let dispatch_size = (models.len() as u32 + 63) / 64;
compute_pass.dispatch_workgroups(dispatch_size, 1, 1);
}
// Render using indirect draw
{
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("GPU-Driven Render Pass"),
// ... attachments
});
render_pass.set_pipeline(&self.render_pipeline);
// Multi-draw indirect
render_pass.multi_draw_indirect(
&self.indirect_buffer,
0,
models.len() as u32,
);
}
}
}
}
Best Practices
1. Render State Management
#![allow(unused)]
fn main() {
pub struct RenderStateManager {
current_state: RenderState,
state_cache: HashMap<RenderStateKey, RenderPipeline>,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct RenderState {
blend_mode: BlendMode,
cull_mode: CullMode,
depth_test: bool,
depth_write: bool,
stencil_test: bool,
}
impl RenderStateManager {
pub fn set_state(
&mut self,
render_pass: &mut RenderPass,
new_state: &RenderState,
) {
if self.current_state != *new_state {
// Get or create pipeline for this state
let pipeline = self.get_or_create_pipeline(new_state);
render_pass.set_pipeline(&pipeline);
self.current_state = new_state.clone();
}
}
fn get_or_create_pipeline(&mut self, state: &RenderState) -> &RenderPipeline {
let key = RenderStateKey::from(state);
self.state_cache.entry(key).or_insert_with(|| {
self.create_pipeline_for_state(state)
})
}
}
}
2. Texture Binding Optimization
#![allow(unused)]
fn main() {
pub struct TextureBindingOptimizer {
texture_arrays: HashMap<TextureFormat, TextureArray>,
bindless_heap: Option<BindlessTextureHeap>,
}
impl TextureBindingOptimizer {
pub fn optimize_texture_bindings(&mut self, materials: &[Material]) {
// Group textures by format and size
let mut texture_groups: HashMap<(TextureFormat, u32, u32), Vec<TextureId>> = HashMap::new();
for material in materials {
if let Some(texture_id) = material.textures.diffuse {
let info = self.get_texture_info(texture_id);
texture_groups
.entry((info.format, info.width, info.height))
.or_insert_with(Vec::new)
.push(texture_id);
}
}
// Create texture arrays for groups
for ((format, width, height), textures) in texture_groups {
if textures.len() > 4 {
self.create_texture_array(format, width, height, &textures);
}
}
}
}
}
3. Draw Call Merging
#![allow(unused)]
fn main() {
pub struct DrawCallMerger {
merge_distance: f32,
max_vertices_per_buffer: u32,
}
impl DrawCallMerger {
pub fn merge_static_geometry(
&self,
static_models: &[StaticModel],
) -> Vec<MergedMesh> {
let mut merged_meshes = Vec::new();
let mut spatial_hash = SpatialHash::new(self.merge_distance);
// Group models by material and spatial proximity
for model in static_models {
spatial_hash.insert(model.position, model);
}
// Merge nearby models with same material
for cell in spatial_hash.cells() {
let mut groups: HashMap<u32, Vec<&StaticModel>> = HashMap::new();
for model in cell {
groups
.entry(model.material_id)
.or_insert_with(Vec::new)
.push(model);
}
for (material_id, models) in groups {
if let Some(merged) = self.merge_models(&models) {
merged_meshes.push(merged);
}
}
}
merged_meshes
}
}
}
Common Issues and Solutions
Issue: Z-Fighting
Problem: Flickering between overlapping surfaces.
Solution:
#![allow(unused)]
fn main() {
// Use polygon offset for decals
let decal_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
// ... other settings
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: CompareFunction::LessEqual,
stencil: StencilState::default(),
bias: DepthBiasState {
constant: -1,
slope_scale: -1.0,
clamp: 0.0,
},
}),
});
}
Issue: Transparency Sorting
Problem: Incorrect rendering order for transparent objects.
Solution:
#![allow(unused)]
fn main() {
pub struct TransparencyManager {
oit_buffers: OitBuffers, // Order-Independent Transparency
}
impl TransparencyManager {
pub fn render_transparent_oit(
&mut self,
render_pass: &mut RenderPass,
transparent_objects: &[TransparentObject],
) {
// Use per-pixel linked lists or weighted blended OIT
render_pass.set_pipeline(&self.oit_accumulation_pipeline);
for object in transparent_objects {
// Render to OIT buffers without sorting
self.render_to_oit_buffer(render_pass, object);
}
// Resolve OIT buffer to final image
self.resolve_oit(render_pass);
}
}
}
Issue: Shader Compilation Stutter
Problem: Frame drops when new shaders compile.
Solution:
#![allow(unused)]
fn main() {
pub struct ShaderPrecompiler {
compile_queue: Arc<Mutex<VecDeque<ShaderVariant>>>,
worker_thread: Option<JoinHandle<()>>,
}
impl ShaderPrecompiler {
pub fn precompile_variants(&mut self, materials: &[Material]) {
// Collect all possible shader variants
let mut variants = HashSet::new();
for material in materials {
variants.insert(ShaderVariant::from_material(material));
}
// Queue for background compilation
let mut queue = self.compile_queue.lock().unwrap();
for variant in variants {
queue.push_back(variant);
}
}
}
}
Performance Tips
1. GPU Instancing
#![allow(unused)]
fn main() {
pub fn optimize_instancing(models: &[ModelInstance]) -> Vec<InstancedDraw> {
let mut instanced_draws: HashMap<MeshId, Vec<InstanceData>> = HashMap::new();
for model in models {
instanced_draws
.entry(model.mesh_id)
.or_insert_with(Vec::new)
.push(model.instance_data());
}
instanced_draws
.into_iter()
.filter(|(_, instances)| instances.len() > 1)
.map(|(mesh_id, instances)| InstancedDraw {
mesh_id,
instances,
})
.collect()
}
}
2. Occlusion Culling
#![allow(unused)]
fn main() {
pub struct HiZOcclusionCuller {
hi_z_buffer: Texture,
hi_z_levels: Vec<TextureView>,
}
impl HiZOcclusionCuller {
pub fn test_visibility(
&self,
bounding_boxes: &[BoundingBox],
camera: &Camera,
) -> Vec<bool> {
// Use hierarchical Z-buffer for fast occlusion tests
let mut visibility = vec![true; bounding_boxes.len()];
for (i, bbox) in bounding_boxes.iter().enumerate() {
let screen_rect = self.project_to_screen(bbox, camera);
let mip_level = self.select_mip_level(screen_rect);
visibility[i] = self.test_rect_visibility(screen_rect, mip_level);
}
visibility
}
}
}
3. Mesh LOD Selection
#![allow(unused)]
fn main() {
pub struct LodSelector {
screen_space_error: f32,
}
impl LodSelector {
pub fn select_lod(
&self,
model: &Model,
camera: &Camera,
screen_height: f32,
) -> usize {
let distance = (model.center - camera.position()).magnitude();
let screen_size = (model.radius / distance) * screen_height;
// Select LOD based on screen coverage
for (i, lod) in model.lods.iter().enumerate() {
if screen_size * lod.error_metric < self.screen_space_error {
return i;
}
}
model.lods.len() - 1
}
}
}
Related Guides
- 🎭 Loading M2 Models - Load models for rendering
- 🏛️ WMO Rendering Guide - Render world objects
- 🖼️ Texture Loading Guide - Texture management
- 🎬 Animation System Guide - Animate rendered models
- 📊 LOD System Guide - Level-of-detail rendering
References
- Real-Time Rendering - Rendering techniques reference
- GPU Gems - Advanced GPU programming
- Learn OpenGL - OpenGL techniques
- GPU-Driven Rendering - GPU-driven techniques