# 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>** - Clean ownership model with components as data in hashmaps - **Component Storages** owned by single `World` struct ## Building and Running ### Build Commands ```bash 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::standard` → `standard` 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) ```rust 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 ```rust pub struct TransformStorage { pub components: HashMap, } impl TransformStorage { pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option 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 ```bash cd /home/jonas/projects/snow_trail_sdl cargo run ``` ### Building for Release ```bash cargo build --release ``` ### Formatting Code ```bash 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::standard` → `standard` artifact ### Adding New Components 1. Define component struct (pure data, no Rc) 2. Add storage to `world.rs` (HashMap) 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)