Files
snow_trail/CLAUDE.md
2026-01-21 11:04:55 +01:00

664 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
## Project Overview
This is a pure Rust game project using SDL3 for windowing/input, wgpu for rendering, rapier3d for physics, and a low-res retro aesthetic with dithering. This is a migration from the Godot-based snow_trail project, implementing the same snow deformation system and character controller without engine dependencies.
**Content Creation:** Blender 5.0 is used for terrain modeling and asset export (glTF meshes + EXR heightmaps).
## Code Style
**Code Documentation Guidelines**:
- **NO inline comments unless ABSOLUTELY necessary**
- Code must be self-documenting through clear naming and structure
- Use doc comments (`///`) only for public APIs and complex algorithms
- Avoid obvious comments that restate what the code does
- Let the code speak for itself
**Formatting:**
- All code must follow the project's `rustfmt.toml` configuration
- Always run `cargo fmt` before committing to ensure consistent formatting
- Current rustfmt settings: brace_style = "AlwaysNextLine", control_brace_style = "AlwaysNextLine"
- **NO inline paths** - always add `use` statements at the top of files (e.g., `use std::rc::Rc;` instead of `std::rc::Rc` inline in code)
- **NO inline `use` statements in functions** - all `use` statements must be at the file level (module top), not inside function bodies or impl blocks
## Architecture Decisions
### ECS Architecture
The project uses a **pure ECS (Entity Component System)** architecture:
**Entities:**
- Just IDs (`EntityHandle = u64`)
- Managed by `EntityManager` (spawn/despawn)
- No data themselves - just containers for components
**Components:**
- Pure data structures stored in component storages
- Each storage is a `HashMap<EntityHandle, ComponentData>`
- No `Rc<RefCell<>>` - clean ownership model
- Components: Transform, Mesh, Physics, Movement, Input, PlayerTag, StateMachine
**Systems:**
- Functions that query entities with specific component combinations
- Run each frame in defined order
- Read from and write to component storages
- Examples: `player_input_system`, `state_machine_system`, `physics_sync_system`, `render_system`
**Component Storages (World-owned):**
- `TransformStorage` - Position, rotation, scale
- `MeshStorage` - Mesh data + render pipeline
- `PhysicsStorage` - Rapier3d rigidbody/collider handles
- `MovementStorage` - Movement config + state
- `InputStorage` - Gameplay input commands
- `PlayerTagStorage` - Marker for player entities
- `StateMachineStorage` - Behavior state machines
- All storages owned by single `World` struct for clean ownership
**Key Benefits:**
- No `Rc<RefCell<>>` needed - components are just data
- Clear data flow through systems
- Easy to add/remove components at runtime
- Testable - systems are pure functions
- StateMachine integrates as a component for complex behaviors
- EventBus remains for irregular events and cross-system messaging
### SDL3 vs SDL2
We are using SDL3 (latest stable bindings) rather than SDL2. SDL3 provides:
- Modern GPU API integration
- Better input handling
- Active development and future-proofing
As of December 2025, SDL3 Rust bindings are usable but still maturing:
- `sdl3` crate: v0.16.2 (high-level bindings)
- `sdl3-sys` crate: v0.5.11 (low-level FFI)
- Some features may be incomplete, but core functionality is stable
### wgpu for Rendering
**Using wgpu instead of OpenGL:**
- Modern GPU API abstraction (Vulkan/Metal/DX12/OpenGL backends)
- Better cross-platform support
- WGSL shader language (WebGPU Shading Language)
- Type-safe API with explicit resource management
- Low-res framebuffer rendering with 3-bit RGB dithering (retro aesthetic)
**Rendering Architecture:**
- wgpu for 3D mesh rendering with custom shaders
- Low-resolution framebuffer (160×120) upscaled to window size
- Bayer 8×8 dithering for 3-bit RGB color (8 colors total)
- Multiple rendering pipelines: standard meshes and terrain
- Separate bind groups for different material types
### Future: Debug UI
- Debug UI system not yet implemented
- Will be used for real-time parameter tweaking (replacing Godot's exported properties)
- Current debugging relies on println! and recompilation
## Physics Integration
Using rapier3d for 3D physics:
- Character controller implemented manually (no built-in CharacterBody equivalent)
- Ground detection via raycasting with QueryPipeline
- Manual rigidbody velocity application
- State machine for movement states (Idle, Walking, Jumping, Falling)
## Input Handling
**Two-Layer Input 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
- Manages global state (mouse capture, quit request, noclip mode)
- Lives in main event loop
**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`
- Used by movement systems to control entities
- Decouples input source from entity control
**Input Flow:**
```
SDL Events → InputState → player_input_system() → InputComponent → movement_system()
```
**Current Input Layout:**
- `W/A/S/D`: Movement (converted to Vec3 direction in InputComponent)
- `Space`: Jump (sets jump_pressed in InputComponent)
- `Shift`: Speed boost (for noclip camera)
- `I`: Toggle mouse capture (lock/unlock cursor)
- `Escape`: Quit game
- `N`: Toggle noclip mode
- Mouse motion: Camera look (yaw/pitch)
## Rendering Pipeline
**wgpu Rendering System:**
- 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
**Terrain Rendering:**
- glTF mesh exported from Blender 5.0 with baked height values in vertices
- No runtime displacement in shader - vertices rendered directly
- Separate terrain pipeline for terrain-specific rendering
- Terrain mesh has heights pre-baked during export for optimal performance
**Terrain Physics:**
- EXR heightmap files loaded via `exr` crate (single-channel R32Float format)
- Heightmap loaded directly into rapier3d heightfield collider
- No runtime sampling or computation - instant loading
- Both glTF and EXR exported from same Blender terrain, guaranteed to match
**Lighting Model:**
- Directional light (like Godot's DirectionalLight3D)
- Diffuse + ambient lighting (basic Phong model, no specular)
- Light direction is uniform across entire scene
- No attenuation or distance falloff
- Dithering applied after lighting calculations
## Migration from Godot
This project ports the snow_trail Godot project (located at `~/shared/projects/snow_trail`) to pure Rust:
**What carries over:**
- Snow deformation compute shader logic (GLSL can be reused with minor adjustments)
- Character controller state machine architecture
- Movement physics parameters
- Camera follow behavior
**What changes:**
- No `Base<Node3D>` pattern → **Pure ECS with EntityHandle + Components**
- No Godot scene tree → **Entity-Component-System architecture**
- No exported properties → Components with data (debug UI planned for future)
- rapier3d RigidBodyHandle in PhysicsComponent instead of Gd<RigidBody3D>
- Manual ground detection instead of CharacterBody3D.is_on_floor()
- **Component storages** (TransformStorage, MeshStorage, etc.) instead of Godot nodes
- **Systems** (player_input_system, state_machine_system, etc.) instead of _process()
- **No `Rc<RefCell<>>`** - components are just data in hashmaps
- Event bus implemented from scratch (complementary to systems)
- State machine implemented from scratch (integrates as ECS component)
## Build Commands
```bash
cargo build
cargo build --release
cargo check
cargo test
cargo run
cargo fmt
```
## Shader Files
WGSL shaders are stored in the `shaders/` directory:
- `shaders/standard.wgsl` - Standard mesh rendering with directional lighting
- `shaders/terrain.wgsl` - Terrain rendering with shadow mapping (no displacement)
- `shaders/blit.wgsl` - Fullscreen blit for upscaling low-res framebuffer
Shaders are loaded at runtime via `std::fs::read_to_string()`, allowing hot-reloading by restarting the application.
## Module Structure
**Core:**
- `main.rs` - SDL3 event loop, game loop orchestration, system execution order
- `entity.rs` - EntityManager for entity lifecycle (spawn/despawn/query)
- `world.rs` - World struct that owns all component storages and EntityManager
**ECS Components (`components/`):**
- `input.rs` - InputComponent (gameplay commands)
- `mesh.rs` - MeshComponent (mesh + pipeline)
- `movement.rs` - MovementComponent (movement config/state)
- `physics.rs` - PhysicsComponent (rigidbody/collider handles)
- `player_tag.rs` - PlayerTag marker component
- `state_machine.rs` - (empty, StateMachine defined in state.rs)
- Note: Component *storages* are defined in `world.rs`, not in component files
**ECS Systems (`systems/`):**
- `input.rs` - player_input_system (InputState → InputComponent)
- `state_machine.rs` - state_machine_system (updates all state machines)
- `physics_sync.rs` - physics_sync_system (physics → transforms)
- `render.rs` - render_system (queries entities, generates DrawCalls)
**Rendering:**
- `render.rs` - wgpu renderer, pipelines, bind groups, DrawCall execution
- `shader.rs` - Standard mesh shader (WGSL) with diffuse+ambient lighting
- `terrain.rs` - Terrain entity spawning, glTF loading, EXR heightmap → physics collider
- `postprocess.rs` - Low-res framebuffer and blit shader for upscaling
- `mesh.rs` - Vertex/Mesh structs, plane/cube mesh generation, glTF loading
- `draw.rs` - DrawManager (legacy, kept for compatibility)
**Game Logic:**
- `player.rs` - Player entity spawning function
- `camera.rs` - 3D camera with rotation and follow behavior
- `movement.rs` - Movement configuration and state structs
- `state.rs` - Generic StateMachine implementation
- `physics.rs` - PhysicsManager singleton (rapier3d world)
**Utilities:**
- `utility/input.rs` - InputState (raw SDL input handling)
- `utility/time.rs` - Time singleton (game time tracking)
- `utility/transform.rs` - Transform struct (position/rotation/scale data type)
**Debug:**
- `debug/noclip.rs` - Noclip camera controller for development
**Other:**
- `event.rs` - Type-safe event bus (complementary to ECS for irregular events)
- `picking.rs` - Ray casting for mouse picking (unused currently)
## Dependencies Rationale
- **sdl3**: Windowing, input events, and platform integration
- **wgpu**: Modern GPU API abstraction for rendering (Vulkan/Metal/DX12 backends)
- **pollster**: Simple blocking executor for async wgpu initialization
- **rapier3d**: Fast physics engine with good Rust integration
- **glam**: Fast vector/matrix math library (vec3, mat4, quaternions)
- **nalgebra**: Linear algebra for rapier3d integration (Isometry3 conversions)
- **bytemuck**: Safe byte casting for GPU buffer uploads (Pod/Zeroable for vertex data)
- **anyhow**: Ergonomic error handling
- **gltf**: Loading 3D models in glTF format
- **exr**: Loading EXR heightmap files (single-channel float data for physics colliders)
- **kurbo**: Bezier curve evaluation for movement acceleration curves
## Technical Notes
### EXR Heightmap Loading (Physics Only)
When loading EXR files with the `exr` crate for physics colliders:
- Must import traits: `use exr::prelude::{ReadChannels, ReadLayers};`
- Use builder pattern: `.no_deep_data().largest_resolution_level().all_channels().all_layers().all_attributes().from_file(path)`
- Extract float data: `channel.sample_data.values_as_f32().collect()`
- Convert to nalgebra DMatrix for rapier3d heightfield collider
- No GPU texture creation - physics data only
### Blender Export Workflow
**Using Blender 5.0** for terrain creation and export:
- Export terrain as **glTF** with baked height values in mesh vertices
- Export same terrain as **EXR** heightmap (single-channel R32Float)
- Both files represent the same terrain data, guaranteeing visual/physics sync
- glTF used for rendering (vertices rendered directly, no shader displacement)
- EXR used for physics (loaded into rapier3d heightfield collider)
### Multiple Render Pipelines
- `Pipeline` enum determines which pipeline to use per DrawCall
- Different pipelines can have different shaders, bind group layouts, uniforms
- Terrain pipeline: shadow-mapped rendering with line hatching shading
- Standard pipeline: basic mesh rendering with diffuse lighting
- Each pipeline writes to its own uniform buffer before rendering
### ECS Component Storages
**Pattern:**
All component storages are owned by the `World` struct:
```rust
pub struct World {
pub entities: EntityManager,
pub transforms: TransformStorage,
pub meshes: MeshStorage,
pub physics: PhysicsStorage,
pub movements: MovementStorage,
pub inputs: InputStorage,
pub player_tags: PlayerTagStorage,
pub state_machines: StateMachineStorage,
}
pub struct TransformStorage {
pub components: HashMap<EntityHandle, Transform>,
}
impl TransformStorage {
pub fn insert(&mut self, entity: EntityHandle, component: Transform) { }
pub fn get(&self, entity: EntityHandle) -> Option<&Transform> { }
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R> { }
pub fn remove(&mut self, entity: EntityHandle) { }
pub fn all(&self) -> Vec<EntityHandle> { }
}
```
**Key Features:**
- No `Rc<RefCell<>>` needed - clean ownership model
- World owns all component data - explicit ownership
- Instance methods instead of static methods
- Systems receive `&mut World` - clear data dependencies
- Easy to test - can create multiple worlds
- Safe lookups return `Option`
**Example Usage:**
```rust
// Create world and entity
let mut world = World::new();
let entity = world.spawn();
// Insert components via world
world.transforms.insert(entity, Transform::IDENTITY);
world.meshes.insert(entity, MeshComponent { ... });
// Query and update via world
world.transforms.with_mut(entity, |transform| {
transform.position += velocity * delta;
});
// Systems receive world
pub fn my_system(world: &mut World) {
for entity in world.player_tags.all() {
if let Some(input) = world.inputs.get(entity) {
// Process input...
}
}
}
// Cleanup
world.despawn(entity); // Removes from all storages
```
### State Machine as ECS Component
StateMachine integrates into ECS as a component for complex entity behaviors:
**State Machine Pattern:**
- StateMachine owns all states via `HashMap<TypeId, Box<dyn State>>`
- TypeId-based state identification
- Transition conditions are simple closures (can capture entity ID)
- State callbacks receive `&mut World` and can access any component
- Updated by `state_machine_system()` each frame using safe remove/insert pattern
**Integration Example:**
```rust
// Create state machine for entity
let mut sm = StateMachine::new(Box::new(IdleState { entity }));
sm.add_state(WalkingState { entity });
// Transitions can capture entity for checking
sm.add_transition::<IdleState, WalkingState>(move || {
// Note: transitions run before update, so they don't access world
false // Placeholder - implement proper transition logic
});
// Insert into world
world.state_machines.insert(entity, sm);
// States receive world when updated
impl State for IdleState {
fn on_state_update(&mut self, world: &mut World, delta: f32) {
// States can access any component via world
if let Some(input) = world.inputs.get(self.entity) {
// React to input...
}
}
}
```
**State Machine System (Safe Pattern):**
```rust
pub fn state_machine_system(world: &mut World, delta: f32) {
let entities: Vec<_> = world.state_machines.all();
for entity in entities {
// Temporarily remove state machine to avoid borrow conflicts
if let Some(mut state_machine) = world.state_machines.components.remove(&entity) {
state_machine.update(world, delta); // States can now safely access world
world.state_machines.components.insert(entity, state_machine);
}
}
}
```
### Event System (event.rs)
**Complementary to ECS:**
- Events handle irregular, one-time occurrences
- Systems handle regular per-frame updates
- Events enable cross-system messaging without tight coupling
**Event Bus Features:**
- No `Clone` requirement on events (fire-and-forget)
- `FnMut` handlers allow stateful callbacks
- Global `add_listener()` and `emit()` functions
- Handlers can access ECS components directly via storages
**ECS Integration:**
```rust
#[derive(Debug)]
struct FootstepEvent { position: Vec3, force: f32 }
impl Event for FootstepEvent {}
// System emits event
pub fn foot_contact_system(world: &World) {
for player in world.player_tags.all() {
if is_on_ground(player) {
let pos = world.transforms.get(player).unwrap().position;
emit(&FootstepEvent { position: pos, force: 10.0 });
}
}
}
// Event handler (global listener, not part of World)
add_listener(|event: &FootstepEvent| {
snow_terrain::deform_at_position(event.position, event.force);
});
```
**When to use:**
- ✅ One-time events (collision, death, pickup)
- ✅ Cross-system communication (audio, VFX triggers)
- ✅ Spawning/despawning entities
- ❌ Regular updates (use systems instead)
### ECS Systems
**System Execution Order (main.rs game loop):**
```rust
let mut world = World::new();
'running: loop {
// 1. SDL Events → InputState
for event in event_pump.poll_iter() {
input_state.handle_event(&event);
}
// 2. InputState → InputComponent (player_input_system)
player_input_system(&mut world, &input_state);
// 3. Update state machines (state_machine_system)
state_machine_system(&mut world, delta);
// 4. Simulate physics (PhysicsManager)
PhysicsManager::physics_step();
// 5. Sync physics → transforms (physics_sync_system)
physics_sync_system(&mut world);
// 6. Render (render_system)
let draw_calls = render_system(&world);
render::render(&camera, &draw_calls, time);
// 7. Cleanup
input_state.clear_just_pressed();
}
```
**System Patterns:**
**Query Pattern:**
```rust
pub fn my_system(world: &mut World) {
let entities = world.my_storage.all(); // All entities with this component
for entity in entities {
world.my_storage.with_mut(entity, |component| {
// Process component
});
}
}
```
**Multi-Component Query:**
```rust
pub fn movement_system(world: &mut World) {
for entity in world.player_tags.all() {
if let Some(input) = world.inputs.get(entity) {
if let Some(movement) = world.movements.get(entity) {
world.transforms.with_mut(entity, |transform| {
// Update position based on input + movement
});
}
}
}
}
```
### Movement System (movement.rs)
Configuration and state for character movement physics:
**Horizontal Movement:**
- `HorizontalMovementConfig`: Parameters for ground movement (acceleration, damping, speed limits)
- `HorizontalMovementState`: Runtime state (input direction, flooring status, surface normal)
- Uses Bezier curves (kurbo::CubicBez) for smooth acceleration ramps
- Separate damping for walking vs idle states
**Vertical Movement:**
- `VerticalMovementConfig`: Jump parameters (height, duration, air control)
- `VerticalMovementState`: Jump execution tracking (progress, peak detection, abort state)
- Bezier curves for jump height progression over time
- Peak detection allows early jump termination with smooth falloff
**Key Features:**
- Physics parameters tuned to match Godot prototype
- Curve-based interpolation for responsive feel
- State tracking for ground detection and jump execution
- Configurable air control and momentum limits
- Integration with Time singleton for execution timing
**Usage Pattern:**
```rust
let config = HorizontalMovementConfig::new();
let mut state = HorizontalMovementState::new();
state.move_input = Vec3::new(input.x, 0.0, input.z);
state.forward_direction = camera.forward();
state.is_floored = ground_check.is_grounded;
// Apply movement physics using config + state
```
### Time System (utility/time.rs)
Global game time tracking using OnceLock singleton:
**Implementation:**
```rust
static GAME_START: OnceLock<Instant> = OnceLock::new();
pub struct Time;
impl Time {
pub fn init() { GAME_START.get_or_init(Instant::now); }
pub fn get_time_elapsed() -> f32 { /* ... */ }
}
```
**Key Features:**
- Thread-safe singleton using std::sync::OnceLock
- Single initialization point (call Time::init() at startup)
- Returns elapsed time as f32 seconds
- Used for animation, jump timing, and time-based effects
- Zero-cost after initialization (static lookup)
**Usage:**
```rust
Time::init(); // In main() before game loop
let time = Time::get_time_elapsed(); // Anywhere in code
```
## Current Implementation Status
### Implemented Features
**ECS Architecture:**
- ✅ Full ECS conversion completed
- ✅ Entity system with EntityManager (spawn/despawn/query)
- ✅ Component storages (Transform, Mesh, Physics, Movement, Input, PlayerTag, StateMachine)
- ✅ Systems pipeline (input → state machine → physics → physics sync → render)
- ✅ No `Rc<RefCell<>>` - clean component ownership
- ✅ Event bus integrated as complementary to systems
**Core Rendering:**
- ✅ wgpu renderer with Vulkan backend
- ✅ Low-res framebuffer (160×120) with Bayer dithering
- ✅ Multiple render pipelines (standard mesh + terrain)
- ✅ Directional lighting with diffuse + ambient
- ✅ Terrain rendering (glTF with baked heights, no shader displacement)
- ✅ EXR heightmap loading for physics colliders
- ✅ glTF mesh loading
- ✅ render_system (ECS-based DrawCall generation)
**Input System:**
- ✅ Two-layer input pipeline (InputState → InputComponent)
- ✅ player_input_system converts raw input to gameplay commands
- ✅ SDL event handling in InputState
- ✅ Per-entity InputComponent for controllable entities
**Camera & Debug:**
- ✅ 3D camera with rotation (yaw/pitch)
- ✅ Noclip mode for development (in debug/noclip.rs)
- ✅ Mouse look with relative mouse mode
- ✅ Toggle with 'I' key, 'N' for noclip mode
**Physics:**
- ✅ rapier3d integration with PhysicsManager singleton
- ✅ PhysicsComponent storage (rigidbody/collider handles)
- ✅ physics_sync_system (syncs physics → transforms)
- ✅ Physics step integrated into game loop
- ⚠️ Ground detection not yet implemented
- ⚠️ Movement physics not yet connected
**State Machines:**
- ✅ Generic StateMachine implementation
- ✅ StateMachineStorage (ECS component)
- ✅ state_machine_system updates all state machines
- ✅ Transitions can query ECS components
- ⚠️ Player state transitions not yet configured
**Player:**
- ✅ Player entity spawning function
- ✅ Components: Transform, Mesh, Physics, Movement, Input, PlayerTag
- ⚠️ Movement system not yet implemented
- ⚠️ State machine not yet attached to player
- ⚠️ Currently inactive (noclip camera used instead)
**Movement Configuration:**
- ✅ Horizontal movement config (Bezier acceleration curves)
- ✅ Vertical movement config (jump mechanics)
- ✅ MovementComponent storage
- ⚠️ Movement system not yet implemented
- ⚠️ Not yet integrated with physics
### Not Yet Implemented
- ❌ Movement system (apply InputComponent → physics velocities)
- ❌ Ground detection and collision response
- ❌ Player state machine configuration
- ❌ Camera follow behavior (tracks player entity)
- ❌ Snow deformation compute shaders
- ❌ Debug UI system
### Current Focus
**ECS migration is complete!** The architecture is now fully entity-component-system based with clean separation of data and logic. The next steps are:
1. Implement movement_system to apply InputComponent to physics
2. Configure player state machine transitions
3. Implement ground detection
4. Add camera follow system
5. Integrate snow deformation
The noclip camera mode serves as the primary navigation method for testing. Press 'N' to toggle noclip mode.