352 lines
14 KiB
Markdown
352 lines
14 KiB
Markdown
# 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
|
||
```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<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
|
||
```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<RefCell>)
|
||
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)
|