Files
snow_trail/QWEN.md
2026-02-08 14:06:35 +01:00

14 KiB
Raw Permalink Blame History

Snow Trail SDL Project

Model Usage Guide

When interacting with this codebase, I follow a step-by-step, concise approach:

  1. Start with exploration: Read files to understand context before making changes
  2. Build incrementally: Make small, targeted changes and verify them
  3. Test after changes: Run cargo check and relevant tests
  4. Keep explanations brief: Code should speak for itself; comments only for complex logic
  5. Follow existing patterns: Mimic the style, structure, and conventions in the codebase

For task management, I use the todo list to track multi-step work:

  • Mark tasks as in_progress when starting
  • Mark tasks as completed immediately after finishing
  • Add new tasks when scope expands
  • Never batch multiple completions

Project Overview

This is a pure Rust game engine implementation (not a game yet) that serves as a migration from a Godot-based project. It's a 3D game using SDL3 for windowing/input, wgpu for rendering, rapier3d for physics, and features a low-res retro aesthetic with dithering.

The project implements an ECS (Entity Component System) architecture without engine dependencies, providing core systems for rendering, physics, input handling, and entity management.

Key Technologies

  • SDL3: Windowing and input handling (latest stable bindings)
  • wgpu: Modern GPU API abstraction for rendering (Vulkan/Metal/DX12/OpenGL backends)
  • rapier3d: Fast 3D physics engine
  • glam: Fast vector/matrix math library
  • gltf: Loading 3D models in glTF format
  • exr: Loading EXR heightmap files for physics colliders
  • kurbo: Bezier curve evaluation for movement acceleration curves

Architecture: Pure ECS

  • Entities: Just IDs (EntityHandle = u64), managed by EntityManager
  • Components: Pure data structures stored in component storages (HashMap)
  • Systems: Functions that query entities with specific component combinations
  • No Rc<RefCell<>> - Clean ownership model with components as data in hashmaps
  • Component Storages owned by single World struct

Building and Running

Build Commands

cargo build
cargo build --release
cargo check
cargo test
cargo run
cargo fmt

Shader Compilation

The project uses a custom shader compilation system via the wesl crate:

  • WGSL/WESL shaders are compiled at build time via build.rs
  • Shaders are loaded at runtime via std::fs::read_to_string(), allowing hot-reloading by restarting the application
  • Build artifact: standard shader package

Runtime Behavior

  • Window resolution: 800×600 (resizable)
  • Rendering resolution: Low-res framebuffer (160×120) upscaled to window
  • Target FPS: 60 FPS with fixed timestep physics (1/60s)
  • Default mode: Noclip camera active
  • Toggle modes: Press 'N' to toggle noclip/follow modes

Development Conventions

Code Style (from CLAUDE.md)

  • NO inline comments unless ABSOLUTELY necessary - Code must be self-documenting
  • Doc comments (///) only for public APIs and complex algorithms
  • All use statements must be at the file level (module top), not inside function bodies
  • NO inline paths - always add use statements at the top of files
  • Formatting: brace_style = "AlwaysNextLine", control_brace_style = "AlwaysNextLine"

File Structure

src/
├── main.rs              - SDL3 event loop, game loop orchestration, system execution order
├── entity.rs            - EntityManager for entity lifecycle (spawn/despawn/query)
├── world.rs             - World struct owning all component storages
├── camera.rs            - 3D camera with rotation and follow behavior
├── physics.rs           - PhysicsManager singleton (rapier3d world)
├── player.rs            - Player entity spawning function
├── terrain.rs           - Terrain entity spawning, glTF loading, EXR heightmap loading
├── render.rs            - wgpu renderer, pipelines, bind groups, DrawCall execution
├── postprocess.rs       - Low-res framebuffer and blit shader for upscaling
├── mesh.rs              - Vertex/Mesh structs, plane/cube mesh generation, glTF loading
├── shader.rs            - Standard mesh shader (WGSL) with diffuse+ambient lighting
├── state.rs             - Generic StateMachine implementation
├── event.rs             - Type-safe event bus (complementary to ECS)
├── picking.rs           - Ray casting for mouse picking (unused currently)
├── heightmap.rs         - EXR heightmap loading utilities
├── draw.rs              - DrawManager (legacy, kept for compatibility)
├── texture_loader.rs    - Texture loading utilities
└── systems/             - ECS systems (input, state_machine, physics_sync, render, camera)
    ├── input.rs
    ├── state_machine.rs
    ├── physics_sync.rs
    ├── render.rs
    ├── camera_follow.rs
    ├── camera_input.rs
    └── camera_noclip.rs
├── components/          - ECS component definitions
    ├── input.rs
    ├── mesh.rs
    ├── movement.rs
    ├── physics.rs
    ├── player_tag.rs
    ├── jump.rs
    ├── camera.rs
    └── camera_follow.rs
├── utility/             - Utility modules
    ├── input.rs         - InputState (raw SDL input handling)
    ├── time.rs          - Time singleton (game time tracking)
    └── transform.rs     - Transform struct (position/rotation/scale data type)
├── debug/               - Debug utilities
    ├── noclip.rs        - Noclip camera controller
    └── render_collider_debug.rs
└── shaders/             - WGSL/WESL shader files
    ├── shared.wesl      - Shared shader utilities
    ├── standard.wesl    - Standard mesh rendering with directional lighting
    ├── terrain.wesl     - Terrain rendering with shadow mapping
    └── blit.wgsl        - Fullscreen blit for upscaling low-res framebuffer

System Execution Order (main.rs game loop)

  1. SDL Events → InputState: Poll events, handle raw input
  2. InputState → InputComponent: player_input_system() converts raw input to gameplay commands
  3. State Machine Update: state_machine_physics_system() and state_machine_system()
  4. Physics Simulation: Fixed timestep physics step
  5. Physics → Transforms: physics_sync_system() syncs physics bodies to transforms
  6. Rendering: render_system() generates DrawCalls, renderer executes pipeline
  7. Cleanup: Clear just-pressed states

ECS Component Storages

All storages are owned by the World struct:

  • TransformStorage - Position, rotation, scale
  • MeshStorage - Mesh data + render pipeline
  • PhysicsStorage - Rapier3d rigidbody/collider handles
  • MovementStorage - Movement config + state
  • JumpStorage - Jump mechanics state
  • InputStorage - Gameplay input commands
  • PlayerTagStorage - Marker for player entities
  • StateMachineStorage - Behavior state machines
  • CameraStorage - Camera components
  • CameraFollowStorage - Camera follow behavior

Input Handling (Two-Layer Pipeline)

Layer 1: Raw Input (utility/input.rs - InputState):

  • Global singleton for SDL event handling
  • Tracks raw hardware state (W/A/S/D pressed, mouse delta, etc.)
  • Handles SDL events via handle_event() method

Layer 2: Gameplay Commands (components/input.rs - InputComponent):

  • Per-entity ECS component
  • Stores processed gameplay commands (move_direction, jump_pressed)
  • Filled by player_input_system() which reads InputState

Input Flow: SDL Events → InputState → InputComponent → Movement Systems

Current Controls:

  • W/A/S/D: Movement
  • Space: Jump
  • Shift: Speed boost (noclip mode)
  • I: Toggle mouse capture
  • Escape: Quit game
  • N: Toggle noclip/follow mode
  • Mouse motion: Camera look (yaw/pitch)

Rendering Pipeline

  • Low-res framebuffer (160×120) renders to texture
  • Bayer 8×8 dithering reduces colors to 3-bit RGB (8 colors)
  • Final blit pass upscales framebuffer to window using nearest-neighbor sampling
  • Depth buffer for 3D rendering with proper occlusion
  • Multiple render pipelines: Standard mesh and terrain pipelines
  • Directional lighting: Diffuse + ambient (basic Phong model)

Terrain System

  • glTF mesh exported from Blender 5.0 with baked height values in vertices
  • EXR heightmap loaded for physics colliders (single-channel R32Float format)
  • Heightfield collider created directly from EXR data
  • No runtime displacement - vertices rendered directly
  • Separate terrain pipeline for terrain-specific rendering

Build System

  • build.rs: Custom build script using wesl crate
  • Shader compilation: package::standardstandard artifact
  • No external dependencies needed for shader compilation at build time

Dependencies Rationale

  • sdl3: Modern SDL3 bindings for future-proofing
  • wgpu: Modern GPU API with cross-platform support
  • rapier3d: Fast physics engine with good Rust integration
  • gltf: Standard 3D model format for asset pipeline
  • exr: High-dynamic-range heightmap loading for physics
  • kurbo: Bezier curves for smooth movement acceleration
  • bytemuck: Safe byte casting for GPU buffer uploads

Current Implementation Status

Implemented Features

Full ECS architecture (entities, components, systems) SDL3 windowing and input handling wgpu rendering with low-res framebuffer Multiple render pipelines (standard mesh + terrain) Bayer dithering for retro aesthetic glTF mesh loading EXR heightmap loading for physics rapier3d physics integration State machine system (generic implementation) Event bus (complementary to ECS) Camera system (free look + follow modes) Noclip mode for development Two-layer input pipeline

In Progress

⚠️ Movement system (apply InputComponent → physics velocities) ⚠️ Ground detection and collision response ⚠️ Player state machine configuration (transitions not yet set up) ⚠️ Camera follow behavior (partial implementation) ⚠️ Snow deformation compute shaders ⚠️ Debug UI system

Known Limitations

  • Player entity spawns but is inactive (noclip camera used for testing)
  • Movement physics not yet connected to input
  • Ground detection not implemented
  • State machine transitions not configured
  • Camera follow needs refinement

Content Creation Workflow

Blender 5.0 (blender/)

  • Terrain modeling: terrain.blend
  • Player character: player_mesh.blend
  • Export formats:
    • glTF: meshes/ for rendering (baked heights in vertices)
    • EXR: textures/ single-channel float heightmap for physics

GIMP (gimp/)

  • Dither patterns: dither_patterns.xcf (Bayer matrix patterns)

Export Process

  1. Model terrain in Blender 5.0
  2. Export as glTF with baked height values
  3. Export same terrain as EXR heightmap
  4. Both files represent same data (visual/physics sync guaranteed)

Future Development

Next Steps (from CLAUDE.md)

  1. Implement movement_system to apply InputComponent to physics velocities
  2. Configure player state machine transitions (idle → walking → jumping → falling)
  3. Implement ground detection (raycasting with QueryPipeline)
  4. Add camera follow system (tracks player entity)
  5. Integrate snow deformation compute shaders
  6. Implement debug UI system for parameter tweaking

Testing Strategy

  • Systems are pure functions (easy to test)
  • Create multiple World instances for isolation
  • Query patterns are predictable
  • State machine transitions are testable

Technical Notes

EXR Heightmap Loading (Physics Only)

use exr::prelude::{ReadChannels, ReadLayers};

let builder = exr::Image::new("heightmap.exr")
    .no_deep_data()
    .largest_resolution_level()
    .all_channels()
    .all_layers()
    .all_attributes();

Component Storage Pattern

pub struct TransformStorage {
    pub components: HashMap<EntityHandle, Transform>,
}

impl TransformStorage {
    pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
    where
        F: FnOnce(&mut Transform) -> R,
    {
        self.components.get_mut(&entity).map(f)
    }
}

State Machine Integration

  • TypeId-based state identification
  • Transitions as closures (can capture entity ID)
  • State callbacks receive &mut World for component access
  • Safe pattern: Remove → Update → Insert to avoid borrow conflicts

Event System (Complementary to ECS)

  • Handles irregular, one-time occurrences
  • Cross-system messaging without tight coupling
  • Global add_listener() and emit() functions
  • Example: FootstepEvent for snow deformation

Shader Hot-Reloading

  • Shaders loaded at runtime via std::fs::read_to_string()
  • Restart application to reload shaders
  • No recompilation needed

Quick Reference

Running the Project

cd /home/jonas/projects/snow_trail_sdl
cargo run

Building for Release

cargo build --release

Formatting Code

cargo fmt

Toggling Modes at Runtime

  • Press N to toggle between noclip and follow modes
  • Press I to toggle mouse capture
  • Press Escape to quit

Working with Shaders

  • Edit .wesl files in src/shaders/
  • Changes take effect on application restart
  • Build script compiles package::standardstandard artifact

Adding New Components

  1. Define component struct (pure data, no Rc)
  2. Add storage to world.rs (HashMap<EntityHandle, Component>)
  3. Add storage to World struct
  4. Update World::despawn() to clean up component
  5. Create systems that query and modify the component

Adding New Systems

  1. Add function in systems/ directory
  2. Import at top of main.rs
  3. Add to system execution order in game loop
  4. Systems receive &mut World (or &World for read-only)