14 KiB
Snow Trail SDL Project
Model Usage Guide
When interacting with this codebase, I follow a step-by-step, concise approach:
- Start with exploration: Read files to understand context before making changes
- Build incrementally: Make small, targeted changes and verify them
- Test after changes: Run
cargo checkand relevant tests - Keep explanations brief: Code should speak for itself; comments only for complex logic
- 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_progresswhen starting - Mark tasks as
completedimmediately 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 byEntityManager - 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
Worldstruct
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:
standardshader 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
usestatements must be at the file level (module top), not inside function bodies - NO inline paths - always add
usestatements 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)
- SDL Events → InputState: Poll events, handle raw input
- InputState → InputComponent:
player_input_system()converts raw input to gameplay commands - State Machine Update:
state_machine_physics_system()andstate_machine_system() - Physics Simulation: Fixed timestep physics step
- Physics → Transforms:
physics_sync_system()syncs physics bodies to transforms - Rendering:
render_system()generates DrawCalls, renderer executes pipeline - Cleanup: Clear just-pressed states
ECS Component Storages
All storages are owned by the World struct:
TransformStorage- Position, rotation, scaleMeshStorage- Mesh data + render pipelinePhysicsStorage- Rapier3d rigidbody/collider handlesMovementStorage- Movement config + stateJumpStorage- Jump mechanics stateInputStorage- Gameplay input commandsPlayerTagStorage- Marker for player entitiesStateMachineStorage- Behavior state machinesCameraStorage- Camera componentsCameraFollowStorage- 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 readsInputState
Input Flow: SDL Events → InputState → InputComponent → Movement Systems
Current Controls:
W/A/S/D: MovementSpace: JumpShift: Speed boost (noclip mode)I: Toggle mouse captureEscape: Quit gameN: 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 usingweslcrate- Shader compilation:
package::standard→standardartifact - 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
- glTF:
GIMP (gimp/)
- Dither patterns:
dither_patterns.xcf(Bayer matrix patterns)
Export Process
- Model terrain in Blender 5.0
- Export as glTF with baked height values
- Export same terrain as EXR heightmap
- Both files represent same data (visual/physics sync guaranteed)
Future Development
Next Steps (from CLAUDE.md)
- Implement
movement_systemto applyInputComponentto physics velocities - Configure player state machine transitions (idle → walking → jumping → falling)
- Implement ground detection (raycasting with QueryPipeline)
- Add camera follow system (tracks player entity)
- Integrate snow deformation compute shaders
- Implement debug UI system for parameter tweaking
Testing Strategy
- Systems are pure functions (easy to test)
- Create multiple
Worldinstances 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 Worldfor 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()andemit()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
.weslfiles insrc/shaders/ - Changes take effect on application restart
- Build script compiles
package::standard→standardartifact
Adding New Components
- Define component struct (pure data, no Rc)
- Add storage to
world.rs(HashMap<EntityHandle, Component>) - Add storage to
Worldstruct - Update
World::despawn()to clean up component - Create systems that query and modify the component
Adding New Systems
- Add function in
systems/directory - Import at top of
main.rs - Add to system execution order in game loop
- Systems receive
&mut World(or&Worldfor read-only)