Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rendering System

AstraWeave’s rendering system is built on top of WGPU, providing a modern, high-performance graphics pipeline with support for physically-based rendering (PBR), advanced lighting techniques, and post-processing effects.

Architecture Overview

The rendering system follows a modular architecture with clear separation of concerns:

graph TD
    A[Render Graph] --> B[Material System]
    A --> C[Lighting System]
    A --> D[Post-Processing]
    B --> E[Texture Streaming]
    C --> F[Clustered Forward]
    D --> G[Bloom/Tonemap]
    E --> H[GPU Upload]
    F --> I[Light Culling]

Key Components

  • Render Graph: Declarative frame composition with automatic resource barriers
  • Material System: PBR materials with metallic-roughness workflow
  • Lighting: Clustered forward rendering supporting thousands of dynamic lights
  • Texture Streaming: Virtual texture system with priority-based loading
  • Post-Processing: Modular effects stack (bloom, tonemap, FXAA, etc.)
The rendering system is designed to scale from integrated graphics to high-end GPUs, with automatic quality adaptation based on detected hardware capabilities.

WGPU-Based Renderer

AstraWeave uses WGPU as its graphics abstraction layer, providing cross-platform support for Vulkan, Metal, DirectX 12, and WebGPU.

Initialization

#![allow(unused)]
fn main() {
use astraweave_render::{RenderContext, RenderConfig};

// Initialize the renderer
let config = RenderConfig {
    width: 1920,
    height: 1080,
    vsync: true,
    msaa_samples: 4,
    ..Default::default()
};

let render_ctx = RenderContext::new(&window, config).await?;
}

Device and Queue Management

The render context manages the WGPU device and queue lifecycle:

#![allow(unused)]
fn main() {
// Access device for resource creation
let device = render_ctx.device();
let queue = render_ctx.queue();

// Create a texture
let texture = device.create_texture(&wgpu::TextureDescriptor {
    label: Some("game_texture"),
    size: wgpu::Extent3d {
        width: 1024,
        height: 1024,
        depth_or_array_layers: 1,
    },
    mip_level_count: 1,
    sample_count: 1,
    dimension: wgpu::TextureDimension::D2,
    format: wgpu::TextureFormat::Rgba8UnormSrgb,
    usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
    view_formats: &[],
});
}

PBR Materials and Lighting

AstraWeave implements a complete PBR pipeline based on the metallic-roughness workflow, closely following theglTF 2.0 specification.

Material Definition

#![allow(unused)]
fn main() {
use astraweave_render::material::{Material, PbrMaterial};

let material = PbrMaterial {
    base_color: [1.0, 1.0, 1.0, 1.0],
    base_color_texture: Some(texture_handle),
    metallic: 0.0,
    roughness: 0.5,
    metallic_roughness_texture: Some(mr_texture),
    normal_texture: Some(normal_map),
    emissive: [0.0, 0.0, 0.0],
    emissive_texture: None,
    occlusion_texture: Some(ao_map),
    alpha_mode: AlphaMode::Opaque,
    double_sided: false,
};

// Register material with renderer
let material_id = render_ctx.register_material(material);
}

Material Properties

PropertyTypeDescription
base_color[f32; 4]RGB color with alpha channel
metallicf32Metallic factor (0 = dielectric, 1 = metal)
roughnessf32Surface roughness (0 = smooth, 1 = rough)
emissive[f32; 3]Emissive color (HDR)
normal_textureOption<Handle>Tangent-space normal map
occlusion_textureOption<Handle>Ambient occlusion map
Use texture packing to reduce memory bandwidth: combine metallic (B channel), roughness (G channel), and occlusion (R channel) into a single texture.

Lighting Model

The PBR shader implements a Cook-Torrance BRDF with:

  • Diffuse: Lambertian diffuse term
  • Specular: GGX/Trowbridge-Reitz normal distribution
  • Fresnel: Schlick’s approximation
  • Geometry: Smith’s shadowing-masking function
#![allow(unused)]
fn main() {
// BRDF calculation (shader pseudo-code)
fn pbr_brdf(
    N: vec3<f32>,  // Normal
    V: vec3<f32>,  // View direction
    L: vec3<f32>,  // Light direction
    F0: vec3<f32>, // Fresnel reflectance at 0°
    roughness: f32,
    metallic: f32,
) -> vec3<f32> {
    let H = normalize(V + L);
    let NdotH = max(dot(N, H), 0.0);
    let NdotV = max(dot(N, V), 0.0);
    let NdotL = max(dot(N, L), 0.0);
    
    // Distribution term (GGX)
    let D = distribution_ggx(NdotH, roughness);
    
    // Geometry term (Smith)
    let G = geometry_smith(NdotV, NdotL, roughness);
    
    // Fresnel term (Schlick)
    let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
    
    // Cook-Torrance BRDF
    let specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.0001);
    
    let kD = (vec3(1.0) - F) * (1.0 - metallic);
    let diffuse = kD * base_color.rgb / PI;
    
    return (diffuse + specular) * radiance * NdotL;
}
}

Clustered Forward Rendering

Clustered forward rendering divides the view frustum into a 3D grid of clusters, assigning lights to clusters for efficient culling.

Cluster Configuration

#![allow(unused)]
fn main() {
use astraweave_render::lighting::{ClusteredLightingConfig, LightCluster};

let cluster_config = ClusteredLightingConfig {
    tile_size_x: 16,
    tile_size_y: 16,
    z_slices: 24,
    max_lights_per_cluster: 256,
};

render_ctx.configure_clustered_lighting(cluster_config);
}

Cluster Grid

graph LR
    A[View Frustum] --> B[XY Tiling]
    B --> C[Z Slicing]
    C --> D[Cluster Grid]
    D --> E[Light Assignment]
    E --> F[Per-Cluster Light Lists]

The frustum is divided into:

  • XY tiles: Screen-space subdivision (typically 16x16 pixels)
  • Z slices: Exponential depth slices for better near-field distribution

Adding Lights

#![allow(unused)]
fn main() {
use astraweave_render::lighting::{PointLight, SpotLight, DirectionalLight};

// Point light
let point_light = PointLight {
    position: [0.0, 5.0, 0.0],
    color: [1.0, 0.9, 0.8],
    intensity: 100.0,
    radius: 10.0,
    falloff: 2.0, // Inverse square falloff
};

render_ctx.add_light(point_light);

// Directional light (sun)
let sun = DirectionalLight {
    direction: [-0.3, -1.0, -0.5],
    color: [1.0, 0.95, 0.9],
    intensity: 5.0,
    cast_shadows: true,
    shadow_cascade_count: 4,
};

render_ctx.set_directional_light(sun);
}

Light Culling

The system performs GPU-based light culling using a compute shader:

#![allow(unused)]
fn main() {
// Compute shader for light culling (WGSL)
@compute @workgroup_size(16, 16, 1)
fn cull_lights(
    @builtin(global_invocation_id) global_id: vec3<u32>,
) {
    let cluster_id = compute_cluster_id(global_id);
    let cluster_aabb = get_cluster_bounds(cluster_id);
    
    var light_count: u32 = 0u;
    for (var i = 0u; i < light_buffer.count; i++) {
        let light = light_buffer.lights[i];
        if (sphere_aabb_intersection(light.position, light.radius, cluster_aabb)) {
            light_indices[cluster_id * MAX_LIGHTS + light_count] = i;
            light_count++;
        }
    }
    
    cluster_light_counts[cluster_id] = light_count;
}
}
Clustered forward rendering requires compute shader support. Fallback to traditional forward rendering is provided for older hardware.

Post-Processing Effects

AstraWeave provides a flexible post-processing stack with composable effects.

Effect Pipeline

#![allow(unused)]
fn main() {
use astraweave_render::post::{PostProcessStack, BloomEffect, TonemapEffect, FxaaEffect};

let mut post_stack = PostProcessStack::new();

// Add bloom
post_stack.add_effect(BloomEffect {
    threshold: 1.0,
    intensity: 0.5,
    blur_passes: 5,
});

// Add tonemapping
post_stack.add_effect(TonemapEffect {
    exposure: 1.0,
    operator: TonemapOperator::AcesFilmic,
});

// Add anti-aliasing
post_stack.add_effect(FxaaEffect {
    quality: FxaaQuality::High,
});

render_ctx.set_post_processing(post_stack);
}

Available Effects

EffectDescriptionPerformance Impact
BloomHDR glow effectMedium
TonemapHDR to LDR mappingLow
FXAAFast approximate anti-aliasingLow
SMAASubpixel morphological anti-aliasingMedium
Depth of FieldBokeh blur based on depthHigh
Motion BlurVelocity-based blurMedium
Color GradingLUT-based color correctionLow

Custom Post-Processing Effects

#![allow(unused)]
fn main() {
use astraweave_render::post::{PostEffect, PostEffectContext};

struct CustomVignetteEffect {
    intensity: f32,
    radius: f32,
}

impl PostEffect for CustomVignetteEffect {
    fn apply(&self, ctx: &mut PostEffectContext) -> Result<()> {
        let shader = ctx.load_shader("vignette.wgsl")?;
        
        ctx.bind_uniform("intensity", &self.intensity);
        ctx.bind_uniform("radius", &self.radius);
        ctx.bind_texture(0, ctx.input_texture());
        
        ctx.dispatch_fullscreen(shader)?;
        
        Ok(())
    }
}
}

Texture Streaming

The texture streaming system manages GPU memory efficiently by loading textures on-demand with priority-based eviction.

Virtual Texture System

#![allow(unused)]
fn main() {
use astraweave_render::texture::{TextureStreamingConfig, VirtualTexture};

let streaming_config = TextureStreamingConfig {
    budget_mb: 512,
    min_mip_level: 0,
    max_mip_level: 12,
    load_bias: 0.0,
    anisotropy: 16,
};

render_ctx.configure_texture_streaming(streaming_config);
}

Loading Textures

#![allow(unused)]
fn main() {
use astraweave_render::texture::TextureLoader;

// Load texture with streaming
let texture = TextureLoader::new()
    .with_streaming(true)
    .with_mip_levels(8)
    .with_compression(TextureCompression::BC7)
    .load("assets/textures/ground_albedo.png")?;

// Set priority (higher = load sooner)
texture.set_priority(1.0);
}

Mip Streaming

The system automatically selects mip levels based on:

  • Distance to camera
  • Screen coverage
  • Available GPU memory
  • User-defined priority
graph TD
    A[Texture Request] --> B{In Budget?}
    B -->|Yes| C[Load Full Resolution]
    B -->|No| D[Calculate Required Mip]
    D --> E[Load Mip Level]
    E --> F{Budget Exceeded?}
    F -->|Yes| G[Evict Low Priority]
    F -->|No| H[Complete]
    G --> H
    C --> H
Pre-generate mip chains offline and compress textures to reduce load times. AstraWeave supports BC1-BC7, ASTC, and ETC2 compression formats.

Performance Optimization

GPU Profiling

#![allow(unused)]
fn main() {
use astraweave_render::profiling::GpuProfiler;

let profiler = GpuProfiler::new(&render_ctx);

profiler.begin_frame();
{
    profiler.begin_scope("Shadow Pass");
    render_shadows(&mut render_ctx);
    profiler.end_scope();
    
    profiler.begin_scope("Main Pass");
    render_main_scene(&mut render_ctx);
    profiler.end_scope();
    
    profiler.begin_scope("Post Processing");
    apply_post_effects(&mut render_ctx);
    profiler.end_scope();
}
profiler.end_frame();

// Print timing report
println!("{}", profiler.report());
}

Instancing

Use GPU instancing for rendering many identical meshes:

#![allow(unused)]
fn main() {
use astraweave_render::mesh::InstancedMesh;

let instances: Vec<InstanceData> = trees
    .iter()
    .map(|tree| InstanceData {
        transform: tree.transform.matrix(),
        color: tree.color,
    })
    .collect();

render_ctx.draw_instanced(tree_mesh, &instances);
}

Occlusion Culling

#![allow(unused)]
fn main() {
use astraweave_render::culling::OcclusionCulling;

let mut occlusion = OcclusionCulling::new(&render_ctx);

// First pass: render occluders
occlusion.render_occluders(&occluder_meshes);

// Second pass: test visibility
let visible_objects = occlusion.test_visibility(&all_objects);

// Third pass: render only visible objects
render_objects(&visible_objects);
}

Code Examples

Complete Rendering Loop

use astraweave_render::{RenderContext, Camera, Scene};
use winit::event_loop::EventLoop;

fn main() -> Result<()> {
    let event_loop = EventLoop::new();
    let window = Window::new(&event_loop)?;
    
    let mut render_ctx = RenderContext::new(&window, Default::default()).await?;
    let mut scene = Scene::new();
    let mut camera = Camera::perspective(60.0, 16.0 / 9.0, 0.1, 1000.0);
    
    event_loop.run(move |event, _, control_flow| {
        match event {
            Event::RedrawRequested(_) => {
                // Update camera
                camera.update(&input_state);
                
                // Begin frame
                let mut frame = render_ctx.begin_frame().unwrap();
                
                // Render scene
                frame.render_scene(&scene, &camera);
                
                // Present
                frame.present();
            }
            _ => {}
        }
    });
}

Dynamic Material Updates

#![allow(unused)]
fn main() {
// Update material properties at runtime
let material = render_ctx.get_material_mut(material_id)?;
material.base_color = [1.0, 0.0, 0.0, 1.0]; // Change to red
material.roughness = 0.8; // Make rougher
render_ctx.mark_material_dirty(material_id);
}

API Reference

For complete API documentation, see: