rendering, physics, player and camera WIP
This commit is contained in:
656
CLAUDE.md
Normal file
656
CLAUDE.md
Normal file
@@ -0,0 +1,656 @@
|
||||
# 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.
|
||||
|
||||
## 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 Height Deformation:**
|
||||
- EXR heightmap files loaded via `exr` crate (single-channel R32Float format)
|
||||
- Height displacement applied in vertex shader
|
||||
- Separate terrain pipeline with texture sampling in vertex stage
|
||||
- TerrainUniforms includes height_scale parameter for tweaking displacement strength
|
||||
- R32Float textures require non-filterable samplers (FilterMode::Nearest)
|
||||
|
||||
**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 height 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 mesh generation and pipeline creation
|
||||
- `postprocess.rs` - Low-res framebuffer and blit shader for upscaling
|
||||
- `mesh.rs` - Vertex/Mesh structs, plane/cube mesh generation, glTF loading
|
||||
- `heightmap.rs` - EXR heightmap loading using `exr` crate
|
||||
- `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)
|
||||
- **image**: Image loading and processing (includes EXR support)
|
||||
- **half**: Float16 support (dependency of exr)
|
||||
- **kurbo**: Bezier curve evaluation for movement acceleration curves
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### EXR Heightmap Loading
|
||||
When loading EXR files with the `exr` crate:
|
||||
- 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()`
|
||||
- Create R32Float texture for height data
|
||||
- R32Float is non-filterable, requires `FilterMode::Nearest` sampler
|
||||
|
||||
### wgpu Texture Formats
|
||||
- R32Float = single-channel 32-bit float, **non-filterable**
|
||||
- Use `TextureSampleType::Float { filterable: false }` in bind group layout
|
||||
- Use `SamplerBindingType::NonFiltering` for sampler binding
|
||||
- Attempting linear filtering on R32Float causes validation errors
|
||||
|
||||
### Multiple Render Pipelines
|
||||
- `Pipeline` enum determines which pipeline to use per DrawCall
|
||||
- Different pipelines can have different shaders, bind group layouts, uniforms
|
||||
- Terrain pipeline: includes height texture binding in vertex stage
|
||||
- Standard pipeline: basic mesh rendering without height displacement
|
||||
- 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
|
||||
- ✅ EXR heightmap loading and terrain displacement
|
||||
- ✅ 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.
|
||||
2457
Cargo.lock
generated
Executable file
2457
Cargo.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "snow_trail_sdl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sdl3 = { version = "0.16", features = ["raw-window-handle"] }
|
||||
wgpu = "27"
|
||||
pollster = "0.3"
|
||||
glam = "0.30"
|
||||
anyhow = "1.0"
|
||||
rapier3d = "0.31"
|
||||
bytemuck = { version = "1.14", features = ["derive"] }
|
||||
gltf = "1.4"
|
||||
image = { version = "0.25", features = ["exr"] }
|
||||
exr = "1.72"
|
||||
half = "2.4"
|
||||
kurbo = "0.11"
|
||||
nalgebra = { version = "0.34.1", features = ["convert-glam030"] }
|
||||
BIN
blender/player_mesh.blend
Normal file
BIN
blender/player_mesh.blend
Normal file
Binary file not shown.
BIN
blender/player_mesh.blend1
Normal file
BIN
blender/player_mesh.blend1
Normal file
Binary file not shown.
195
meshes/burrs.gltf
Executable file
195
meshes/burrs.gltf
Executable file
File diff suppressed because one or more lines are too long
BIN
meshes/player_mesh.glb
Normal file
BIN
meshes/player_mesh.glb
Normal file
Binary file not shown.
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
brace_style = "AlwaysNextLine"
|
||||
control_brace_style = "AlwaysNextLine"
|
||||
28
shaders/blit.wgsl
Normal file
28
shaders/blit.wgsl
Normal file
@@ -0,0 +1,28 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_texture: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var t_sampler: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
output.clip_position = vec4<f32>(input.position, 0.0, 1.0);
|
||||
output.uv = input.uv;
|
||||
return output;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(t_texture, t_sampler, input.uv);
|
||||
}
|
||||
97
shaders/standard.wgsl
Normal file
97
shaders/standard.wgsl
Normal file
@@ -0,0 +1,97 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) world_position: vec3<f32>,
|
||||
@location(1) world_normal: vec3<f32>,
|
||||
}
|
||||
|
||||
struct Uniforms {
|
||||
model: mat4x4<f32>,
|
||||
view: mat4x4<f32>,
|
||||
projection: mat4x4<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> uniforms: Uniforms;
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
|
||||
let world_pos = uniforms.model * vec4<f32>(input.position, 1.0);
|
||||
output.world_position = world_pos.xyz;
|
||||
output.world_normal = (uniforms.model * vec4<f32>(input.normal, 0.0)).xyz;
|
||||
output.clip_position = uniforms.projection * uniforms.view * world_pos;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
fn bayer_2x2_dither(value: f32, screen_pos: vec2<f32>) -> f32 {
|
||||
let pattern = array<f32, 4>(
|
||||
0.0/4.0, 2.0/4.0,
|
||||
3.0/4.0, 1.0/4.0
|
||||
);
|
||||
let x = i32(screen_pos.x) % 2;
|
||||
let y = i32(screen_pos.y) % 2;
|
||||
let index = y * 2 + x;
|
||||
return select(0.0, 1.0, value > pattern[index]);
|
||||
}
|
||||
|
||||
fn bayer_4x4_dither(value: f32, screen_pos: vec2<f32>) -> f32 {
|
||||
let pattern = array<f32, 16>(
|
||||
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
|
||||
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
|
||||
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
|
||||
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
|
||||
);
|
||||
let x = i32(screen_pos.x) % 4;
|
||||
let y = i32(screen_pos.y) % 4;
|
||||
let index = y * 4 + x;
|
||||
return select(0.0, 1.0, value > pattern[index]);
|
||||
}
|
||||
|
||||
fn bayer_8x8_dither(value: f32, screen_pos: vec2<f32>) -> f32 {
|
||||
let pattern = array<f32, 64>(
|
||||
0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0,
|
||||
48.0/64.0, 16.0/64.0, 56.0/64.0, 24.0/64.0, 50.0/64.0, 18.0/64.0, 58.0/64.0, 26.0/64.0,
|
||||
12.0/64.0, 44.0/64.0, 4.0/64.0, 36.0/64.0, 14.0/64.0, 46.0/64.0, 6.0/64.0, 38.0/64.0,
|
||||
60.0/64.0, 28.0/64.0, 52.0/64.0, 20.0/64.0, 62.0/64.0, 30.0/64.0, 54.0/64.0, 22.0/64.0,
|
||||
3.0/64.0, 35.0/64.0, 11.0/64.0, 43.0/64.0, 1.0/64.0, 33.0/64.0, 9.0/64.0, 41.0/64.0,
|
||||
51.0/64.0, 19.0/64.0, 59.0/64.0, 27.0/64.0, 49.0/64.0, 17.0/64.0, 57.0/64.0, 25.0/64.0,
|
||||
15.0/64.0, 47.0/64.0, 7.0/64.0, 39.0/64.0, 13.0/64.0, 45.0/64.0, 5.0/64.0, 37.0/64.0,
|
||||
63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0
|
||||
);
|
||||
let x = i32(screen_pos.x) % 8;
|
||||
let y = i32(screen_pos.y) % 8;
|
||||
let index = y * 8 + x;
|
||||
return select(0.0, 1.0, value > pattern[index]);
|
||||
}
|
||||
|
||||
|
||||
@fragment
|
||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let light_pos = vec3<f32>(5.0, 5.0, 5.0);
|
||||
let light_color = vec3<f32>(1.0, 1.0, 1.0);
|
||||
let object_color = vec3<f32>(1.0, 1.0, 1.0);
|
||||
|
||||
let ambient_strength = 0.3;
|
||||
let ambient = ambient_strength * light_color;
|
||||
|
||||
let norm = normalize(input.world_normal);
|
||||
let light_dir = normalize(vec3<f32>(1.0, -1.0, 1.0));
|
||||
let diff = max(dot(norm, light_dir), 0.0);
|
||||
let diffuse = diff * light_color;
|
||||
|
||||
let result = (ambient + diffuse) * object_color;
|
||||
|
||||
let dithered_r = bayer_8x8_dither(result.r, input.clip_position.xy);
|
||||
let dithered_g = bayer_8x8_dither(result.g, input.clip_position.xy);
|
||||
let dithered_b = bayer_8x8_dither(result.b, input.clip_position.xy);
|
||||
|
||||
return vec4<f32>(dithered_r, dithered_g, dithered_b, 1.0);
|
||||
}
|
||||
120
shaders/terrain.wgsl
Normal file
120
shaders/terrain.wgsl
Normal file
@@ -0,0 +1,120 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) world_position: vec3<f32>,
|
||||
@location(1) world_normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct Uniforms {
|
||||
model: mat4x4<f32>,
|
||||
view: mat4x4<f32>,
|
||||
projection: mat4x4<f32>,
|
||||
height_scale: f32,
|
||||
time: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> uniforms: Uniforms;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var height_texture: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var height_sampler: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
|
||||
let height = textureSampleLevel(height_texture, height_sampler, input.uv, 0.0).r;
|
||||
|
||||
var displaced_pos = input.position;
|
||||
displaced_pos.y += height * uniforms.height_scale;
|
||||
|
||||
let texel_size = vec2<f32>(1.0 / 512.0, 1.0 / 512.0);
|
||||
let height_left = textureSampleLevel(height_texture, height_sampler, input.uv - vec2<f32>(texel_size.x, 0.0), 0.0).r;
|
||||
let height_right = textureSampleLevel(height_texture, height_sampler, input.uv + vec2<f32>(texel_size.x, 0.0), 0.0).r;
|
||||
let height_down = textureSampleLevel(height_texture, height_sampler, input.uv - vec2<f32>(0.0, texel_size.y), 0.0).r;
|
||||
let height_up = textureSampleLevel(height_texture, height_sampler, input.uv + vec2<f32>(0.0, texel_size.y), 0.0).r;
|
||||
|
||||
let dh_dx = (height_right - height_left) * uniforms.height_scale;
|
||||
let dh_dz = (height_up - height_down) * uniforms.height_scale;
|
||||
|
||||
let normal = normalize(vec3<f32>(-dh_dx, 1.0, -dh_dz));
|
||||
|
||||
let world_pos = uniforms.model * vec4<f32>(displaced_pos, 1.0);
|
||||
output.world_position = world_pos.xyz;
|
||||
output.world_normal = normalize((uniforms.model * vec4<f32>(normal, 0.0)).xyz);
|
||||
output.clip_position = uniforms.projection * uniforms.view * world_pos;
|
||||
output.uv = input.uv;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hash(p: vec2<f32>) -> f32 {
|
||||
var p3 = fract(vec3<f32>(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
fn should_glitter(screen_pos: vec2<f32>, time: f32) -> bool {
|
||||
let pixel_pos = floor(screen_pos);
|
||||
let h = hash(pixel_pos);
|
||||
let time_offset = h * 6283.18;
|
||||
let sparkle_rate = 0.2;
|
||||
let sparkle = sin(time * sparkle_rate + time_offset) * 0.5 + 0.5;
|
||||
let threshold = 0.95;
|
||||
return sparkle > threshold && h > 0.95;
|
||||
}
|
||||
|
||||
fn bayer_8x8_dither(value: f32, screen_pos: vec2<f32>) -> f32 {
|
||||
let pattern = array<f32, 64>(
|
||||
0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0,
|
||||
48.0/64.0, 16.0/64.0, 56.0/64.0, 24.0/64.0, 50.0/64.0, 18.0/64.0, 58.0/64.0, 26.0/64.0,
|
||||
12.0/64.0, 44.0/64.0, 4.0/64.0, 36.0/64.0, 14.0/64.0, 46.0/64.0, 6.0/64.0, 38.0/64.0,
|
||||
60.0/64.0, 28.0/64.0, 52.0/64.0, 20.0/64.0, 62.0/64.0, 30.0/64.0, 54.0/64.0, 22.0/64.0,
|
||||
3.0/64.0, 35.0/64.0, 11.0/64.0, 43.0/64.0, 1.0/64.0, 33.0/64.0, 9.0/64.0, 41.0/64.0,
|
||||
51.0/64.0, 19.0/64.0, 59.0/64.0, 27.0/64.0, 49.0/64.0, 17.0/64.0, 57.0/64.0, 25.0/64.0,
|
||||
15.0/64.0, 47.0/64.0, 7.0/64.0, 39.0/64.0, 13.0/64.0, 45.0/64.0, 5.0/64.0, 37.0/64.0,
|
||||
63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0
|
||||
);
|
||||
let x = i32(screen_pos.x) % 8;
|
||||
let y = i32(screen_pos.y) % 8;
|
||||
let index = y * 8 + x;
|
||||
return select(0.2, 1.0, value > pattern[index]);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let light_dir = normalize(vec3<f32>(-0.5, -1.0, -0.5));
|
||||
let light_color = vec3<f32>(1.0, 1.0, 1.0);
|
||||
let object_color = vec3<f32>(1.0, 1.0, 1.0);
|
||||
|
||||
let ambient_strength = 0.2;
|
||||
let ambient = ambient_strength * light_color;
|
||||
|
||||
let norm = normalize(input.world_normal);
|
||||
let diff = max(dot(norm, -light_dir), 0.0);
|
||||
let diffuse = diff * light_color;
|
||||
|
||||
let result = (ambient + diffuse) * object_color;
|
||||
|
||||
var dithered_r = bayer_8x8_dither(result.r, input.clip_position.xy);
|
||||
var dithered_g = bayer_8x8_dither(result.g, input.clip_position.xy);
|
||||
var dithered_b = bayer_8x8_dither(result.b, input.clip_position.xy);
|
||||
|
||||
let is_grey_or_black = dithered_r == 0.0 || (dithered_r == dithered_g && dithered_g == dithered_b);
|
||||
if (is_grey_or_black && should_glitter(input.clip_position.xy, uniforms.time)) {
|
||||
dithered_r = 1.0;
|
||||
dithered_g = 1.0;
|
||||
dithered_b = 1.0;
|
||||
}
|
||||
|
||||
return vec4<f32>(dithered_r, dithered_g, dithered_b, 1.0);
|
||||
}
|
||||
168
src/camera.rs
Normal file
168
src/camera.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::{Mat4, Vec3};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
pub struct CameraUniforms
|
||||
{
|
||||
pub model: [[f32; 4]; 4],
|
||||
pub view: [[f32; 4]; 4],
|
||||
pub projection: [[f32; 4]; 4],
|
||||
}
|
||||
|
||||
impl CameraUniforms
|
||||
{
|
||||
pub fn new(model: Mat4, view: Mat4, projection: Mat4) -> Self
|
||||
{
|
||||
Self {
|
||||
model: model.to_cols_array_2d(),
|
||||
view: view.to_cols_array_2d(),
|
||||
projection: projection.to_cols_array_2d(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Camera
|
||||
{
|
||||
pub position: Vec3,
|
||||
pub target: Vec3,
|
||||
pub up: Vec3,
|
||||
pub fov: f32,
|
||||
pub aspect: f32,
|
||||
pub near: f32,
|
||||
pub far: f32,
|
||||
pub yaw: f32,
|
||||
pub pitch: f32,
|
||||
pub is_following: bool,
|
||||
pub follow_offset: Vec3,
|
||||
}
|
||||
|
||||
impl Camera
|
||||
{
|
||||
pub fn init(aspect: f32) -> Self
|
||||
{
|
||||
Self {
|
||||
position: Vec3::new(15.0, 15.0, 15.0),
|
||||
target: Vec3::ZERO,
|
||||
up: Vec3::Y,
|
||||
fov: 45.0_f32.to_radians(),
|
||||
aspect,
|
||||
near: 0.1,
|
||||
far: 100.0,
|
||||
yaw: -135.0_f32.to_radians(),
|
||||
pitch: -30.0_f32.to_radians(),
|
||||
is_following: true,
|
||||
follow_offset: Vec3::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::look_at_rh(self.position, self.target, self.up)
|
||||
}
|
||||
|
||||
pub fn projection_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::perspective_rh(self.fov, self.aspect, self.near, self.far)
|
||||
}
|
||||
|
||||
pub fn update_rotation(&mut self, mouse_delta: (f32, f32), sensitivity: f32)
|
||||
{
|
||||
self.yaw += mouse_delta.0 * sensitivity;
|
||||
self.pitch -= mouse_delta.1 * sensitivity;
|
||||
|
||||
self.pitch = self
|
||||
.pitch
|
||||
.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
|
||||
}
|
||||
|
||||
pub fn get_forward(&self) -> Vec3
|
||||
{
|
||||
Vec3::new(
|
||||
self.yaw.cos() * self.pitch.cos(),
|
||||
self.pitch.sin(),
|
||||
self.yaw.sin() * self.pitch.cos(),
|
||||
)
|
||||
.normalize()
|
||||
}
|
||||
|
||||
pub fn get_right(&self) -> Vec3
|
||||
{
|
||||
self.get_forward().cross(Vec3::Y).normalize()
|
||||
}
|
||||
|
||||
pub fn get_forward_horizontal(&self) -> Vec3
|
||||
{
|
||||
Vec3::new(self.yaw.cos(), 0.0, self.yaw.sin()).normalize()
|
||||
}
|
||||
|
||||
pub fn get_right_horizontal(&self) -> Vec3
|
||||
{
|
||||
self.get_forward_horizontal().cross(Vec3::Y).normalize()
|
||||
}
|
||||
|
||||
pub fn update_noclip(&mut self, input: Vec3, speed: f32)
|
||||
{
|
||||
let forward = self.get_forward();
|
||||
let right = self.get_right();
|
||||
|
||||
self.position += forward * input.z * speed;
|
||||
self.position += right * input.x * speed;
|
||||
self.position += Vec3::Y * input.y * speed;
|
||||
|
||||
self.target = self.position + forward;
|
||||
}
|
||||
|
||||
pub fn start_following(&mut self, target_position: Vec3)
|
||||
{
|
||||
self.is_following = true;
|
||||
self.follow_offset = self.position - target_position;
|
||||
|
||||
let distance = self.follow_offset.length();
|
||||
if distance > 0.0
|
||||
{
|
||||
self.pitch = (self.follow_offset.y / distance).asin();
|
||||
self.yaw = self.follow_offset.z.atan2(self.follow_offset.x) + std::f32::consts::PI;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_following(&mut self)
|
||||
{
|
||||
self.is_following = false;
|
||||
|
||||
let look_direction = (self.target - self.position).normalize();
|
||||
|
||||
self.yaw = look_direction.z.atan2(look_direction.x);
|
||||
self.pitch = look_direction.y.asin();
|
||||
}
|
||||
|
||||
pub fn update_follow(&mut self, target_position: Vec3, mouse_delta: (f32, f32), sensitivity: f32)
|
||||
{
|
||||
if !self.is_following
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if mouse_delta.0.abs() > 0.0 || mouse_delta.1.abs() > 0.0
|
||||
{
|
||||
self.yaw += mouse_delta.0 * sensitivity;
|
||||
self.pitch += mouse_delta.1 * sensitivity;
|
||||
|
||||
self.pitch = self
|
||||
.pitch
|
||||
.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
|
||||
}
|
||||
|
||||
let distance = self.follow_offset.length();
|
||||
|
||||
let orbit_yaw = self.yaw + std::f32::consts::PI;
|
||||
|
||||
let offset_x = distance * orbit_yaw.cos() * self.pitch.cos();
|
||||
let offset_y = distance * self.pitch.sin();
|
||||
let offset_z = distance * orbit_yaw.sin() * self.pitch.cos();
|
||||
|
||||
self.follow_offset = Vec3::new(offset_x, offset_y, offset_z);
|
||||
self.position = target_position + self.follow_offset;
|
||||
self.target = target_position;
|
||||
}
|
||||
}
|
||||
61
src/components/camera.rs
Normal file
61
src/components/camera.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use glam::Mat4;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CameraComponent
|
||||
{
|
||||
pub fov: f32,
|
||||
pub aspect: f32,
|
||||
pub near: f32,
|
||||
pub far: f32,
|
||||
pub yaw: f32,
|
||||
pub pitch: f32,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl CameraComponent
|
||||
{
|
||||
pub fn new(aspect: f32) -> Self
|
||||
{
|
||||
Self {
|
||||
fov: 45.0_f32.to_radians(),
|
||||
aspect,
|
||||
near: 0.1,
|
||||
far: 100.0,
|
||||
yaw: -135.0_f32.to_radians(),
|
||||
pitch: -30.0_f32.to_radians(),
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn projection_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::perspective_rh(self.fov, self.aspect, self.near, self.far)
|
||||
}
|
||||
|
||||
pub fn get_forward(&self) -> glam::Vec3
|
||||
{
|
||||
glam::Vec3::new(
|
||||
self.yaw.cos() * self.pitch.cos(),
|
||||
self.pitch.sin(),
|
||||
self.yaw.sin() * self.pitch.cos(),
|
||||
)
|
||||
.normalize()
|
||||
}
|
||||
|
||||
pub fn get_right(&self) -> glam::Vec3
|
||||
{
|
||||
self.get_forward().cross(glam::Vec3::Y).normalize()
|
||||
}
|
||||
|
||||
pub fn get_forward_horizontal(&self) -> glam::Vec3
|
||||
{
|
||||
glam::Vec3::new(self.yaw.cos(), 0.0, self.yaw.sin()).normalize()
|
||||
}
|
||||
|
||||
pub fn get_right_horizontal(&self) -> glam::Vec3
|
||||
{
|
||||
self.get_forward_horizontal()
|
||||
.cross(glam::Vec3::Y)
|
||||
.normalize()
|
||||
}
|
||||
}
|
||||
32
src/components/camera_follow.rs
Normal file
32
src/components/camera_follow.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::entity::EntityHandle;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CameraFollowComponent
|
||||
{
|
||||
pub target_entity: EntityHandle,
|
||||
pub offset: Vec3,
|
||||
pub is_following: bool,
|
||||
}
|
||||
|
||||
impl CameraFollowComponent
|
||||
{
|
||||
pub fn new(target_entity: EntityHandle) -> Self
|
||||
{
|
||||
Self {
|
||||
target_entity,
|
||||
offset: Vec3::ZERO,
|
||||
is_following: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_offset(target_entity: EntityHandle, offset: Vec3) -> Self
|
||||
{
|
||||
Self {
|
||||
target_entity,
|
||||
offset,
|
||||
is_following: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/components/input.rs
Normal file
9
src/components/input.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use glam::Vec3;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct InputComponent
|
||||
{
|
||||
pub move_direction: Vec3,
|
||||
pub jump_pressed: bool,
|
||||
pub jump_just_pressed: bool,
|
||||
}
|
||||
82
src/components/jump.rs
Normal file
82
src/components/jump.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use glam::Vec3;
|
||||
use kurbo::CubicBez;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JumpComponent
|
||||
{
|
||||
pub jump_config: JumpConfig,
|
||||
}
|
||||
|
||||
impl JumpComponent
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
jump_config: JumpConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct JumpConfig
|
||||
{
|
||||
pub jump_height: f32,
|
||||
pub jump_duration: f32,
|
||||
pub air_control_force: f32,
|
||||
pub max_air_momentum: f32,
|
||||
pub air_damping_active: f32,
|
||||
pub air_damping_passive: f32,
|
||||
pub jump_curve: CubicBez,
|
||||
pub jump_context: JumpContext,
|
||||
}
|
||||
|
||||
impl Default for JumpConfig
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self {
|
||||
jump_height: 2.0,
|
||||
jump_duration: 0.15,
|
||||
air_control_force: 10.0,
|
||||
max_air_momentum: 8.0,
|
||||
air_damping_active: 0.4,
|
||||
air_damping_passive: 0.9,
|
||||
jump_curve: CubicBez::new(
|
||||
(0.0, 0.0),
|
||||
(0.4, 0.75),
|
||||
(0.7, 0.9),
|
||||
(1.0, 1.0),
|
||||
),
|
||||
jump_context: JumpContext::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct JumpContext
|
||||
{
|
||||
pub in_progress: bool,
|
||||
pub duration: f32,
|
||||
pub execution_time: f32,
|
||||
pub origin_height: f32,
|
||||
pub normal: Vec3,
|
||||
}
|
||||
|
||||
impl JumpContext
|
||||
{
|
||||
fn start(time: f32, current_height: f32, surface_normal: Vec3) -> Self
|
||||
{
|
||||
Self {
|
||||
in_progress: false,
|
||||
duration: 0.0,
|
||||
execution_time: time,
|
||||
origin_height: current_height,
|
||||
normal: surface_normal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self)
|
||||
{
|
||||
self.in_progress = false;
|
||||
}
|
||||
}
|
||||
11
src/components/mesh.rs
Normal file
11
src/components/mesh.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::mesh::Mesh;
|
||||
use crate::render::Pipeline;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MeshComponent
|
||||
{
|
||||
pub mesh: Rc<Mesh>,
|
||||
pub pipeline: Pipeline,
|
||||
}
|
||||
16
src/components/mod.rs
Normal file
16
src/components/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod camera;
|
||||
pub mod camera_follow;
|
||||
pub mod input;
|
||||
pub mod jump;
|
||||
pub mod mesh;
|
||||
pub mod movement;
|
||||
pub mod physics;
|
||||
pub mod player_tag;
|
||||
pub mod state_machine;
|
||||
|
||||
pub use camera::CameraComponent;
|
||||
pub use camera_follow::CameraFollowComponent;
|
||||
pub use input::InputComponent;
|
||||
pub use mesh::MeshComponent;
|
||||
pub use movement::MovementComponent;
|
||||
pub use physics::PhysicsComponent;
|
||||
73
src/components/movement.rs
Normal file
73
src/components/movement.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use glam::Vec3;
|
||||
use kurbo::CubicBez;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MovementComponent
|
||||
{
|
||||
pub movement_config: MovementConfig,
|
||||
}
|
||||
|
||||
impl MovementComponent
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
movement_config: MovementConfig::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MovementConfig
|
||||
{
|
||||
pub walking_acceleration: f32,
|
||||
pub walking_acceleration_duration: f32,
|
||||
pub walking_acceleration_curve: CubicBez,
|
||||
pub walking_damping: f32,
|
||||
pub max_walking_speed: f32,
|
||||
pub idle_damping: f32,
|
||||
pub movement_context: MovementContext,
|
||||
}
|
||||
|
||||
impl MovementConfig
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
walking_acceleration: 250.0,
|
||||
walking_acceleration_duration: 0.1,
|
||||
walking_acceleration_curve: CubicBez::new(
|
||||
(0.0, 0.0),
|
||||
(0.5, 0.3),
|
||||
(0.75, 0.9),
|
||||
(1.0, 1.0),
|
||||
),
|
||||
walking_damping: 0.8,
|
||||
max_walking_speed: 6.0,
|
||||
idle_damping: 0.1,
|
||||
movement_context: MovementContext::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MovementContext
|
||||
{
|
||||
pub forward_direction: Vec3,
|
||||
pub is_floored: bool,
|
||||
pub last_floored_time: u64,
|
||||
pub surface_normal: Vec3,
|
||||
}
|
||||
|
||||
impl MovementContext
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
forward_direction: Vec3::Z,
|
||||
is_floored: false,
|
||||
last_floored_time: 0,
|
||||
surface_normal: Vec3::Y,
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/components/physics.rs
Normal file
8
src/components/physics.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use rapier3d::prelude::{ColliderHandle, RigidBodyHandle};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct PhysicsComponent
|
||||
{
|
||||
pub rigidbody: RigidBodyHandle,
|
||||
pub collider: Option<ColliderHandle>,
|
||||
}
|
||||
2
src/components/player_tag.rs
Normal file
2
src/components/player_tag.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct PlayerTag;
|
||||
1
src/components/state_machine.rs
Normal file
1
src/components/state_machine.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
149
src/debug/collider_debug.rs
Normal file
149
src/debug/collider_debug.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use std::cell::OnceCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::{Mat4, Vec3};
|
||||
use nalgebra::DMatrix;
|
||||
use rapier3d::parry::shape::HeightField;
|
||||
|
||||
use crate::{
|
||||
mesh::{Mesh, Vertex},
|
||||
physics::PhysicsManager,
|
||||
render::{self, DrawCall, Pipeline},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static WIREFRAME_BOX: OnceCell<Rc<Mesh>> = OnceCell::new();
|
||||
static DEBUG_HEIGHTFIELD: OnceCell<Option<Rc<Mesh>>> = OnceCell::new();
|
||||
}
|
||||
|
||||
pub fn set_debug_heightfield(heightfield: &HeightField, scale: [f32; 3], offset: [f32; 3])
|
||||
{
|
||||
DEBUG_HEIGHTFIELD.with(|cell| {
|
||||
cell.get_or_init(|| {
|
||||
render::with_device(|device| {
|
||||
Some(Rc::new(create_heightfield_wireframe(
|
||||
device,
|
||||
heightfield,
|
||||
scale,
|
||||
offset,
|
||||
)))
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn create_heightfield_wireframe(
|
||||
device: &wgpu::Device,
|
||||
heightfield: &HeightField,
|
||||
scale: [f32; 3],
|
||||
offset: [f32; 3],
|
||||
) -> Mesh
|
||||
{
|
||||
let nrows = heightfield.nrows();
|
||||
let ncols = heightfield.ncols();
|
||||
|
||||
let mut vertices = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
|
||||
for row in 0..nrows
|
||||
{
|
||||
for col in 0..ncols
|
||||
{
|
||||
let x = col as f32 * scale[0];
|
||||
let y = heightfield.heights()[(row, col)] * scale[1];
|
||||
let z = row as f32 * scale[2];
|
||||
|
||||
vertices.push(Vertex {
|
||||
position: [x + offset[0], y + offset[1], z + offset[2]],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for row in 0..nrows
|
||||
{
|
||||
for col in 0..ncols
|
||||
{
|
||||
let idx = (row * ncols + col) as u32;
|
||||
|
||||
if col < ncols - 1
|
||||
{
|
||||
indices.push(idx);
|
||||
indices.push(idx + 1);
|
||||
}
|
||||
|
||||
if row < nrows - 1
|
||||
{
|
||||
indices.push(idx);
|
||||
indices.push(idx + ncols as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !vertices.is_empty()
|
||||
{
|
||||
let first = &vertices[0].position;
|
||||
let last = &vertices[vertices.len() - 1].position;
|
||||
println!(
|
||||
"Heightfield bounds: ({}, {}, {}) to ({}, {}, {})",
|
||||
first[0], first[1], first[2], last[0], last[1], last[2]
|
||||
);
|
||||
println!(
|
||||
"Total vertices: {}, indices: {}",
|
||||
vertices.len(),
|
||||
indices.len()
|
||||
);
|
||||
}
|
||||
|
||||
Mesh::new(device, &vertices, &indices)
|
||||
}
|
||||
|
||||
pub fn render_collider_debug() -> Vec<DrawCall>
|
||||
{
|
||||
let mut draw_calls = Vec::new();
|
||||
|
||||
let aabbs = PhysicsManager::get_all_collider_aabbs();
|
||||
|
||||
WIREFRAME_BOX.with(|cell| {
|
||||
let wireframe_box = cell.get_or_init(|| {
|
||||
render::with_device(|device| Rc::new(Mesh::create_wireframe_box(device)))
|
||||
});
|
||||
|
||||
for (mins, maxs) in aabbs
|
||||
{
|
||||
let min = Vec3::from(mins);
|
||||
let max = Vec3::from(maxs);
|
||||
|
||||
let center = (min + max) * 0.5;
|
||||
let size = max - min;
|
||||
|
||||
let scale = Mat4::from_scale(size);
|
||||
let translation = Mat4::from_translation(center);
|
||||
let model = translation * scale;
|
||||
|
||||
draw_calls.push(DrawCall {
|
||||
vertex_buffer: wireframe_box.vertex_buffer.clone(),
|
||||
index_buffer: wireframe_box.index_buffer.clone(),
|
||||
num_indices: wireframe_box.num_indices,
|
||||
model,
|
||||
pipeline: Pipeline::Wireframe,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
DEBUG_HEIGHTFIELD.with(|cell| {
|
||||
if let Some(Some(heightfield_mesh)) = cell.get()
|
||||
{
|
||||
draw_calls.push(DrawCall {
|
||||
vertex_buffer: heightfield_mesh.vertex_buffer.clone(),
|
||||
index_buffer: heightfield_mesh.index_buffer.clone(),
|
||||
num_indices: heightfield_mesh.num_indices,
|
||||
model: Mat4::IDENTITY,
|
||||
pipeline: Pipeline::Wireframe,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
draw_calls
|
||||
}
|
||||
5
src/debug/mod.rs
Normal file
5
src/debug/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod collider_debug;
|
||||
pub mod noclip;
|
||||
|
||||
pub use collider_debug::{render_collider_debug, set_debug_heightfield};
|
||||
pub use noclip::{update_follow_camera, update_noclip_camera};
|
||||
59
src/debug/noclip.rs
Normal file
59
src/debug/noclip.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::utility::input::InputState;
|
||||
use crate::world::World;
|
||||
|
||||
pub fn update_noclip_camera(camera: &mut Camera, input_state: &InputState, delta: f32)
|
||||
{
|
||||
camera.update_rotation(input_state.mouse_delta, 0.0008);
|
||||
|
||||
let mut input_vec = Vec3::ZERO;
|
||||
|
||||
if input_state.w
|
||||
{
|
||||
input_vec.z += 1.0;
|
||||
}
|
||||
if input_state.s
|
||||
{
|
||||
input_vec.z -= 1.0;
|
||||
}
|
||||
if input_state.d
|
||||
{
|
||||
input_vec.x += 1.0;
|
||||
}
|
||||
if input_state.a
|
||||
{
|
||||
input_vec.x -= 1.0;
|
||||
}
|
||||
if input_state.space
|
||||
{
|
||||
input_vec.y += 1.0;
|
||||
}
|
||||
|
||||
if input_vec.length_squared() > 0.0
|
||||
{
|
||||
input_vec = input_vec.normalize();
|
||||
}
|
||||
|
||||
let mut speed = 10.0 * delta;
|
||||
if input_state.shift
|
||||
{
|
||||
speed *= 2.0;
|
||||
}
|
||||
|
||||
camera.update_noclip(input_vec, speed);
|
||||
}
|
||||
|
||||
pub fn update_follow_camera(camera: &mut Camera, world: &World, input_state: &InputState)
|
||||
{
|
||||
let player_entities = world.player_tags.all();
|
||||
|
||||
if let Some(&player_entity) = player_entities.first()
|
||||
{
|
||||
if let Some(player_transform) = world.transforms.get(player_entity)
|
||||
{
|
||||
camera.update_follow(player_transform.position, input_state.mouse_delta, 0.0008);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/draw.rs
Normal file
71
src/draw.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::mesh::Mesh;
|
||||
use crate::render::{DrawCall, Pipeline};
|
||||
|
||||
pub type DrawHandle = usize;
|
||||
|
||||
struct DrawEntry
|
||||
{
|
||||
mesh: Rc<Mesh>,
|
||||
entity: EntityHandle,
|
||||
pipeline: Pipeline,
|
||||
}
|
||||
|
||||
pub struct DrawManager
|
||||
{
|
||||
entries: HashMap<DrawHandle, DrawEntry>,
|
||||
next_handle: DrawHandle,
|
||||
}
|
||||
|
||||
impl DrawManager
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
entries: HashMap::new(),
|
||||
next_handle: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_mesh_internal(
|
||||
&mut self,
|
||||
mesh: Rc<Mesh>,
|
||||
entity: EntityHandle,
|
||||
pipeline: Pipeline,
|
||||
) -> DrawHandle
|
||||
{
|
||||
let handle = self.next_handle;
|
||||
self.next_handle += 1;
|
||||
|
||||
self.entries.insert(
|
||||
handle,
|
||||
DrawEntry {
|
||||
mesh,
|
||||
entity,
|
||||
pipeline,
|
||||
},
|
||||
);
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn clear_mesh_internal(&mut self, handle: DrawHandle)
|
||||
{
|
||||
self.entries.remove(&handle);
|
||||
}
|
||||
|
||||
pub fn collect_draw_calls(&self) -> Vec<DrawCall>
|
||||
{
|
||||
vec![]
|
||||
}
|
||||
|
||||
pub fn draw_mesh(_mesh: Rc<Mesh>, _entity: EntityHandle, _pipeline: Pipeline) -> DrawHandle
|
||||
{
|
||||
0
|
||||
}
|
||||
|
||||
pub fn clear_mesh(_handle: DrawHandle) {}
|
||||
}
|
||||
43
src/entity.rs
Normal file
43
src/entity.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub type EntityHandle = u64;
|
||||
|
||||
pub struct EntityManager
|
||||
{
|
||||
next_id: EntityHandle,
|
||||
alive: HashSet<EntityHandle>,
|
||||
}
|
||||
|
||||
impl EntityManager
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
next_id: 0,
|
||||
alive: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&mut self) -> EntityHandle
|
||||
{
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.alive.insert(id);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn despawn(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.alive.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn is_alive(&self, entity: EntityHandle) -> bool
|
||||
{
|
||||
self.alive.contains(&entity)
|
||||
}
|
||||
|
||||
pub fn all_entities(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.alive.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
70
src/event.rs
Normal file
70
src/event.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub trait Event: std::fmt::Debug {}
|
||||
|
||||
type EventHandler<T> = Box<dyn FnMut(&T)>;
|
||||
|
||||
pub struct EventBus
|
||||
{
|
||||
handlers: HashMap<TypeId, Box<dyn Any>>,
|
||||
}
|
||||
|
||||
impl EventBus
|
||||
{
|
||||
fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
handlers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe_internal<T: Event + 'static, F: FnMut(&T) + 'static>(&mut self, handler: F)
|
||||
{
|
||||
let type_id = TypeId::of::<T>();
|
||||
let handlers: &mut Vec<EventHandler<T>> = self
|
||||
.handlers
|
||||
.entry(type_id)
|
||||
.or_insert_with(|| Box::new(Vec::<EventHandler<T>>::new()))
|
||||
.downcast_mut()
|
||||
.unwrap();
|
||||
|
||||
handlers.push(Box::new(handler));
|
||||
}
|
||||
|
||||
fn publish_internal<T: Event + 'static>(&mut self, event: &T)
|
||||
{
|
||||
let type_id = TypeId::of::<T>();
|
||||
|
||||
if let Some(handlers) = self.handlers.get_mut(&type_id)
|
||||
{
|
||||
let typed_handlers = handlers.downcast_mut::<Vec<EventHandler<T>>>().unwrap();
|
||||
for handler in typed_handlers
|
||||
{
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe<T: Event + 'static, F: FnMut(&T) + 'static>(handler: F)
|
||||
{
|
||||
EVENT_BUS.with(|bus| bus.borrow_mut().subscribe_internal(handler));
|
||||
}
|
||||
|
||||
pub fn publish<T: Event + 'static>(event: &T)
|
||||
{
|
||||
EVENT_BUS.with(|bus| bus.borrow_mut().publish_internal(event));
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static EVENT_BUS: RefCell<EventBus> = RefCell::new(EventBus::new());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpdateEvent
|
||||
{
|
||||
pub delta: f32,
|
||||
}
|
||||
impl Event for UpdateEvent {}
|
||||
67
src/heightmap.rs
Normal file
67
src/heightmap.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use exr::prelude::{ReadChannels, ReadLayers};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn load_exr_heightmap(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(wgpu::Texture, wgpu::TextureView, wgpu::Sampler), Box<dyn std::error::Error>>
|
||||
{
|
||||
let image = exr::prelude::read()
|
||||
.no_deep_data()
|
||||
.largest_resolution_level()
|
||||
.all_channels()
|
||||
.all_layers()
|
||||
.all_attributes()
|
||||
.from_file(path)?;
|
||||
|
||||
let layer = &image.layer_data[0];
|
||||
let width = layer.size.width() as u32;
|
||||
let height = layer.size.height() as u32;
|
||||
|
||||
let channel = &layer.channel_data.list[0];
|
||||
let float_data: Vec<f32> = channel.sample_data.values_as_f32().collect();
|
||||
|
||||
let texture_size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Height Map Texture"),
|
||||
size: texture_size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R32Float,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
queue.write_texture(
|
||||
texture.as_image_copy(),
|
||||
bytemuck::cast_slice(&float_data),
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
texture_size,
|
||||
);
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Height Map Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok((texture, view, sampler))
|
||||
}
|
||||
233
src/main.rs
Executable file
233
src/main.rs
Executable file
@@ -0,0 +1,233 @@
|
||||
mod camera;
|
||||
mod components;
|
||||
mod debug;
|
||||
mod draw;
|
||||
mod entity;
|
||||
mod event;
|
||||
mod heightmap;
|
||||
mod mesh;
|
||||
mod physics;
|
||||
mod picking;
|
||||
mod player;
|
||||
mod postprocess;
|
||||
mod render;
|
||||
mod shader;
|
||||
mod state;
|
||||
mod systems;
|
||||
mod terrain;
|
||||
mod utility;
|
||||
mod world;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use glam::Vec3;
|
||||
use render::Renderer;
|
||||
use utility::input::InputState;
|
||||
use world::{Transform, World};
|
||||
|
||||
use crate::components::{CameraComponent, CameraFollowComponent};
|
||||
use crate::debug::render_collider_debug;
|
||||
use crate::entity::EntityHandle;
|
||||
use crate::physics::PhysicsManager;
|
||||
use crate::player::Player;
|
||||
use crate::systems::{
|
||||
camera_follow_system, camera_input_system, camera_noclip_system, physics_sync_system,
|
||||
player_input_system, render_system, start_camera_following, state_machine_physics_system,
|
||||
state_machine_system, stop_camera_following,
|
||||
};
|
||||
use crate::terrain::Terrain;
|
||||
use crate::utility::time::Time;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let sdl_context = sdl3::init()?;
|
||||
let video_subsystem = sdl_context.video()?;
|
||||
|
||||
let window = video_subsystem
|
||||
.window("snow_trail", 800, 600)
|
||||
.position_centered()
|
||||
.resizable()
|
||||
.vulkan()
|
||||
.build()?;
|
||||
|
||||
let renderer = pollster::block_on(Renderer::new(&window, 1))?;
|
||||
render::init(renderer);
|
||||
|
||||
let terrain_data = render::with_device(|device| {
|
||||
render::with_queue(|queue| {
|
||||
let height_map =
|
||||
heightmap::load_exr_heightmap(device, queue, "textures/height_map_x0_y0.exr");
|
||||
let (height_texture, height_view, height_sampler) = height_map.unwrap();
|
||||
render::TerrainData {
|
||||
height_texture,
|
||||
height_view,
|
||||
height_sampler,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
render::set_terrain_data(terrain_data);
|
||||
|
||||
let mut world = World::new();
|
||||
let player_entity = Player::spawn(&mut world);
|
||||
let _terrain_entity = Terrain::spawn(&mut world, "textures/height_map_x0_y0.exr", 10.0)?;
|
||||
|
||||
let camera_entity = spawn_camera(&mut world, player_entity);
|
||||
start_camera_following(&mut world, camera_entity);
|
||||
|
||||
let mut noclip_mode = false;
|
||||
|
||||
let mut event_pump = sdl_context.event_pump()?;
|
||||
let mut input_state = InputState::new();
|
||||
|
||||
sdl_context.mouse().set_relative_mouse_mode(&window, true);
|
||||
|
||||
Time::init();
|
||||
let mut last_frame = Instant::now();
|
||||
let target_fps = 60;
|
||||
let frame_duration = Duration::from_millis(1000 / target_fps);
|
||||
|
||||
const FIXED_TIMESTEP: f32 = 1.0 / 60.0;
|
||||
let mut physics_accumulator = 0.0;
|
||||
|
||||
'running: loop
|
||||
{
|
||||
let frame_start = Instant::now();
|
||||
let time = Time::get_time_elapsed();
|
||||
let delta = (frame_start - last_frame).as_secs_f32();
|
||||
last_frame = frame_start;
|
||||
|
||||
for event in event_pump.poll_iter()
|
||||
{
|
||||
let mouse_capture_changed = input_state.handle_event(&event);
|
||||
|
||||
if mouse_capture_changed
|
||||
{
|
||||
sdl_context
|
||||
.mouse()
|
||||
.set_relative_mouse_mode(&window, input_state.mouse_captured);
|
||||
}
|
||||
}
|
||||
|
||||
if input_state.quit_requested
|
||||
{
|
||||
break 'running;
|
||||
}
|
||||
|
||||
input_state.process_post_events();
|
||||
|
||||
if input_state.noclip_just_pressed
|
||||
{
|
||||
noclip_mode = !noclip_mode;
|
||||
|
||||
if noclip_mode
|
||||
{
|
||||
stop_camera_following(&mut world, camera_entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
start_camera_following(&mut world, camera_entity);
|
||||
}
|
||||
}
|
||||
|
||||
camera_input_system(&mut world, &input_state);
|
||||
|
||||
if noclip_mode
|
||||
{
|
||||
camera_noclip_system(&mut world, &input_state, delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
camera_follow_system(&mut world);
|
||||
player_input_system(&mut world, &input_state);
|
||||
}
|
||||
|
||||
|
||||
physics_accumulator += delta;
|
||||
|
||||
while physics_accumulator >= FIXED_TIMESTEP
|
||||
{
|
||||
state_machine_physics_system(&mut world, FIXED_TIMESTEP);
|
||||
|
||||
PhysicsManager::physics_step();
|
||||
|
||||
physics_sync_system(&mut world);
|
||||
|
||||
physics_accumulator -= FIXED_TIMESTEP;
|
||||
}
|
||||
|
||||
state_machine_system(&mut world, delta);
|
||||
|
||||
let mut draw_calls = render_system(&world);
|
||||
draw_calls.extend(render_collider_debug());
|
||||
|
||||
if let Some((camera_entity, camera_component)) = world.cameras.get_active()
|
||||
{
|
||||
if let Some(camera_transform) = world.transforms.get(camera_entity)
|
||||
{
|
||||
let view = get_view_matrix(&world, camera_entity, camera_transform, camera_component);
|
||||
let projection = camera_component.projection_matrix();
|
||||
|
||||
render::render_with_matrices(&view, &projection, &draw_calls, time);
|
||||
}
|
||||
}
|
||||
|
||||
input_state.clear_just_pressed();
|
||||
|
||||
let frame_time = frame_start.elapsed();
|
||||
if frame_time < frame_duration
|
||||
{
|
||||
std::thread::sleep(frame_duration - frame_time);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_camera(world: &mut World, target_entity: EntityHandle) -> EntityHandle
|
||||
{
|
||||
let camera_entity = world.spawn();
|
||||
|
||||
let camera_component = CameraComponent::new(render::aspect_ratio());
|
||||
let camera_follow = CameraFollowComponent::new(target_entity);
|
||||
|
||||
let initial_position = Vec3::new(15.0, 15.0, 15.0);
|
||||
let transform = Transform {
|
||||
position: initial_position,
|
||||
rotation: glam::Quat::IDENTITY,
|
||||
scale: Vec3::ONE,
|
||||
};
|
||||
|
||||
world.cameras.insert(camera_entity, camera_component);
|
||||
world.camera_follows.insert(camera_entity, camera_follow);
|
||||
world.transforms.insert(camera_entity, transform);
|
||||
|
||||
camera_entity
|
||||
}
|
||||
|
||||
fn get_view_matrix(
|
||||
world: &World,
|
||||
camera_entity: EntityHandle,
|
||||
camera_transform: &Transform,
|
||||
camera_component: &CameraComponent,
|
||||
) -> glam::Mat4
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get(camera_entity)
|
||||
{
|
||||
if follow.is_following
|
||||
{
|
||||
if let Some(target_transform) = world.transforms.get(follow.target_entity)
|
||||
{
|
||||
return glam::Mat4::look_at_rh(
|
||||
camera_transform.position,
|
||||
target_transform.position,
|
||||
Vec3::Y,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let forward = camera_component.get_forward();
|
||||
let target = camera_transform.position + forward;
|
||||
glam::Mat4::look_at_rh(camera_transform.position, target, Vec3::Y)
|
||||
}
|
||||
414
src/mesh.rs
Normal file
414
src/mesh.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::{Mat4, Vec3};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::utility::transform::Transform;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
pub struct Vertex
|
||||
{
|
||||
pub position: [f32; 3],
|
||||
pub normal: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
}
|
||||
|
||||
impl Vertex
|
||||
{
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static>
|
||||
{
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x3,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: (std::mem::size_of::<[f32; 3]>() * 2) as wgpu::BufferAddress,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Mesh
|
||||
{
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub index_buffer: wgpu::Buffer,
|
||||
pub num_indices: u32,
|
||||
}
|
||||
|
||||
impl Mesh
|
||||
{
|
||||
pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: &[u32]) -> Self
|
||||
{
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Index Buffer"),
|
||||
contents: bytemuck::cast_slice(indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
Self {
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
num_indices: indices.len() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_cube_mesh(device: &wgpu::Device) -> Mesh
|
||||
{
|
||||
let vertices = vec![
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.5],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.5],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.5],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.5],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.5],
|
||||
normal: [1.0, 0.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, -0.5],
|
||||
normal: [1.0, 0.0, 0.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, -0.5],
|
||||
normal: [1.0, 0.0, 0.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.5],
|
||||
normal: [1.0, 0.0, 0.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, -0.5],
|
||||
normal: [0.0, 0.0, -1.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, -0.5],
|
||||
normal: [0.0, 0.0, -1.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, -0.5],
|
||||
normal: [0.0, 0.0, -1.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, -0.5],
|
||||
normal: [0.0, 0.0, -1.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, -0.5],
|
||||
normal: [-1.0, 0.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.5],
|
||||
normal: [-1.0, 0.0, 0.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.5],
|
||||
normal: [-1.0, 0.0, 0.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, -0.5],
|
||||
normal: [-1.0, 0.0, 0.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, -0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, -0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, -0.5],
|
||||
normal: [0.0, -1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, -0.5],
|
||||
normal: [0.0, -1.0, 0.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.5],
|
||||
normal: [0.0, -1.0, 0.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.5],
|
||||
normal: [0.0, -1.0, 0.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
];
|
||||
|
||||
let indices = vec![
|
||||
0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 8, 9, 10, 10, 11, 8, 12, 13, 14, 14, 15, 12, 16,
|
||||
17, 18, 18, 19, 16, 20, 21, 22, 22, 23, 20,
|
||||
];
|
||||
|
||||
Mesh::new(device, &vertices, &indices)
|
||||
}
|
||||
|
||||
pub fn create_plane_mesh(
|
||||
device: &wgpu::Device,
|
||||
width: f32,
|
||||
height: f32,
|
||||
subdivisions_x: u32,
|
||||
subdivisions_y: u32,
|
||||
) -> Mesh
|
||||
{
|
||||
let mut vertices = Vec::new();
|
||||
let mut indices = Vec::new();
|
||||
|
||||
for y in 0..=subdivisions_y
|
||||
{
|
||||
for x in 0..=subdivisions_x
|
||||
{
|
||||
let fx = x as f32 / subdivisions_x as f32;
|
||||
let fy = y as f32 / subdivisions_y as f32;
|
||||
|
||||
let px = (fx - 0.5) * width;
|
||||
let py = 0.0;
|
||||
let pz = (fy - 0.5) * height;
|
||||
|
||||
vertices.push(Vertex {
|
||||
position: [px, py, pz],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [fx, fy],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..subdivisions_y
|
||||
{
|
||||
for x in 0..subdivisions_x
|
||||
{
|
||||
let row_stride = subdivisions_x + 1;
|
||||
let i0 = y * row_stride + x;
|
||||
let i1 = i0 + 1;
|
||||
let i2 = i0 + row_stride;
|
||||
let i3 = i2 + 1;
|
||||
|
||||
indices.push(i0);
|
||||
indices.push(i2);
|
||||
indices.push(i1);
|
||||
|
||||
indices.push(i1);
|
||||
indices.push(i2);
|
||||
indices.push(i3);
|
||||
}
|
||||
}
|
||||
|
||||
Mesh::new(device, &vertices, &indices)
|
||||
}
|
||||
|
||||
pub fn create_wireframe_box(device: &wgpu::Device) -> Mesh
|
||||
{
|
||||
let vertices = vec![
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, -0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, -0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, -0.5, 0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, -0.5, 0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, -0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, -0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.5, 0.5, 0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
Vertex {
|
||||
position: [-0.5, 0.5, 0.5],
|
||||
normal: [0.0, 1.0, 0.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
];
|
||||
|
||||
let indices = vec![
|
||||
0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7,
|
||||
];
|
||||
|
||||
Mesh::new(device, &vertices, &indices)
|
||||
}
|
||||
|
||||
pub fn load_gltf_mesh(
|
||||
device: &wgpu::Device,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<Mesh, Box<dyn std::error::Error>>
|
||||
{
|
||||
let (gltf, buffers, _images) = gltf::import(path)?;
|
||||
|
||||
let mut all_vertices = Vec::new();
|
||||
let mut all_indices = Vec::new();
|
||||
|
||||
for scene in gltf.scenes()
|
||||
{
|
||||
for node in scene.nodes()
|
||||
{
|
||||
Self::process_node(
|
||||
&node,
|
||||
&buffers,
|
||||
Mat4::IDENTITY,
|
||||
&mut all_vertices,
|
||||
&mut all_indices,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Mesh::new(device, &all_vertices, &all_indices))
|
||||
}
|
||||
|
||||
fn process_node(
|
||||
node: &gltf::Node,
|
||||
buffers: &[gltf::buffer::Data],
|
||||
parent_transform: Mat4,
|
||||
all_vertices: &mut Vec<Vertex>,
|
||||
all_indices: &mut Vec<u32>,
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
|
||||
let global_transform = parent_transform * local_transform;
|
||||
|
||||
if let Some(mesh) = node.mesh()
|
||||
{
|
||||
for primitive in mesh.primitives()
|
||||
{
|
||||
let reader =
|
||||
primitive.reader(|buffer| buffers.get(buffer.index()).map(|data| &data[..]));
|
||||
|
||||
let positions = reader
|
||||
.read_positions()
|
||||
.ok_or("Missing position data")?
|
||||
.collect::<Vec<[f32; 3]>>();
|
||||
|
||||
let normals = reader
|
||||
.read_normals()
|
||||
.ok_or("Missing normal data")?
|
||||
.collect::<Vec<[f32; 3]>>();
|
||||
|
||||
let uvs = reader
|
||||
.read_tex_coords(0)
|
||||
.map(|iter| iter.into_f32().collect::<Vec<[f32; 2]>>())
|
||||
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
|
||||
|
||||
let base_index = all_vertices.len() as u32;
|
||||
|
||||
let normal_matrix = global_transform.inverse().transpose();
|
||||
|
||||
for ((pos, normal), uv) in positions.iter().zip(normals.iter()).zip(uvs.iter())
|
||||
{
|
||||
let pos_vec3 = Vec3::from(*pos);
|
||||
let normal_vec3 = Vec3::from(*normal);
|
||||
|
||||
let transformed_pos = global_transform.transform_point3(pos_vec3);
|
||||
let transformed_normal =
|
||||
normal_matrix.transform_vector3(normal_vec3).normalize();
|
||||
|
||||
all_vertices.push(Vertex {
|
||||
position: transformed_pos.into(),
|
||||
normal: transformed_normal.into(),
|
||||
uv: *uv,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(indices_reader) = reader.read_indices()
|
||||
{
|
||||
all_indices.extend(indices_reader.into_u32().map(|i| i + base_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for child in node.children()
|
||||
{
|
||||
Self::process_node(&child, buffers, global_transform, all_vertices, all_indices)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_mesh(path: impl AsRef<Path>) -> Result<Mesh, Box<dyn std::error::Error>>
|
||||
{
|
||||
crate::render::with_device(|device| Mesh::load_gltf_mesh(device, path))
|
||||
}
|
||||
}
|
||||
288
src/physics.rs
Normal file
288
src/physics.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use nalgebra::DMatrix;
|
||||
use rapier3d::{
|
||||
math::Vector,
|
||||
na::vector,
|
||||
prelude::{
|
||||
CCDSolver, Collider, ColliderHandle, ColliderSet, DefaultBroadPhase, ImpulseJointSet,
|
||||
IntegrationParameters, IslandManager, MultibodyJointSet, NarrowPhase, PhysicsPipeline, Ray,
|
||||
RigidBody, RigidBodyHandle, RigidBodySet,
|
||||
},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static GLOBAL_PHYSICS: RefCell<PhysicsManager> = RefCell::new(PhysicsManager::new());
|
||||
}
|
||||
|
||||
pub struct HeightfieldData
|
||||
{
|
||||
pub heights: DMatrix<f32>,
|
||||
pub scale: Vector<f32>,
|
||||
pub position: Vector<f32>,
|
||||
}
|
||||
|
||||
pub struct PhysicsManager
|
||||
{
|
||||
rigidbody_set: RigidBodySet,
|
||||
collider_set: ColliderSet,
|
||||
gravity: Vector<f32>,
|
||||
integration_parameters: IntegrationParameters,
|
||||
physics_pipeline: PhysicsPipeline,
|
||||
island_manager: IslandManager,
|
||||
broad_phase: DefaultBroadPhase,
|
||||
narrow_phase: NarrowPhase,
|
||||
impulse_joint_set: ImpulseJointSet,
|
||||
multibody_joint_set: MultibodyJointSet,
|
||||
ccd_solver: CCDSolver,
|
||||
physics_hooks: (),
|
||||
event_handler: (),
|
||||
heightfield_data: Option<HeightfieldData>,
|
||||
}
|
||||
|
||||
impl PhysicsManager
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
rigidbody_set: RigidBodySet::new(),
|
||||
collider_set: ColliderSet::new(),
|
||||
gravity: vector![0.0, -9.81, 0.0],
|
||||
integration_parameters: IntegrationParameters::default(),
|
||||
physics_pipeline: PhysicsPipeline::new(),
|
||||
island_manager: IslandManager::new(),
|
||||
broad_phase: DefaultBroadPhase::new(),
|
||||
narrow_phase: NarrowPhase::new(),
|
||||
impulse_joint_set: ImpulseJointSet::new(),
|
||||
multibody_joint_set: MultibodyJointSet::new(),
|
||||
ccd_solver: CCDSolver::new(),
|
||||
physics_hooks: (),
|
||||
event_handler: (),
|
||||
heightfield_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn step(&mut self)
|
||||
{
|
||||
self.physics_pipeline.step(
|
||||
&self.gravity,
|
||||
&self.integration_parameters,
|
||||
&mut self.island_manager,
|
||||
&mut self.broad_phase,
|
||||
&mut self.narrow_phase,
|
||||
&mut self.rigidbody_set,
|
||||
&mut self.collider_set,
|
||||
&mut self.impulse_joint_set,
|
||||
&mut self.multibody_joint_set,
|
||||
&mut self.ccd_solver,
|
||||
&self.physics_hooks,
|
||||
&self.event_handler,
|
||||
);
|
||||
}
|
||||
|
||||
fn add_collider_internal(
|
||||
&mut self,
|
||||
collider: Collider,
|
||||
parent: Option<RigidBodyHandle>,
|
||||
) -> ColliderHandle
|
||||
{
|
||||
if let Some(parent) = parent
|
||||
{
|
||||
self.collider_set
|
||||
.insert_with_parent(collider, parent, &mut self.rigidbody_set)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.collider_set.insert(collider)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn physics_step()
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| manager.borrow_mut().step());
|
||||
}
|
||||
|
||||
pub fn add_rigidbody(rigidbody: RigidBody) -> RigidBodyHandle
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| manager.borrow_mut().rigidbody_set.insert(rigidbody))
|
||||
}
|
||||
|
||||
pub fn add_collider(collider: Collider, parent: Option<RigidBodyHandle>) -> ColliderHandle
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| manager.borrow_mut().add_collider_internal(collider, parent))
|
||||
}
|
||||
|
||||
pub fn with_rigidbody_mut<F, R>(handle: RigidBodyHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut RigidBody) -> R,
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| manager.borrow_mut().rigidbody_set.get_mut(handle).map(f))
|
||||
}
|
||||
|
||||
pub fn with_collider_mut<F, R>(handle: ColliderHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut Collider) -> R,
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| manager.borrow_mut().collider_set.get_mut(handle).map(f))
|
||||
}
|
||||
|
||||
pub fn get_rigidbody_position(handle: RigidBodyHandle) -> Option<rapier3d::na::Isometry3<f32>>
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| {
|
||||
manager
|
||||
.borrow()
|
||||
.rigidbody_set
|
||||
.get(handle)
|
||||
.map(|rb| *rb.position())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_collider_aabbs() -> Vec<([f32; 3], [f32; 3])>
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| {
|
||||
let manager = manager.borrow();
|
||||
manager
|
||||
.collider_set
|
||||
.iter()
|
||||
.map(|(_, collider)| {
|
||||
let aabb = collider.compute_aabb();
|
||||
(
|
||||
[aabb.mins.x, aabb.mins.y, aabb.mins.z],
|
||||
[aabb.maxs.x, aabb.maxs.y, aabb.maxs.z],
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn raycast(
|
||||
origin: Vector<f32>,
|
||||
direction: Vector<f32>,
|
||||
max_distance: f32,
|
||||
exclude_rigidbody: Option<RigidBodyHandle>,
|
||||
) -> Option<(ColliderHandle, f32)>
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| {
|
||||
let manager = manager.borrow();
|
||||
let ray = Ray::new(origin.into(), direction);
|
||||
|
||||
let mut closest_hit: Option<(ColliderHandle, f32)> = None;
|
||||
|
||||
for (handle, collider) in manager.collider_set.iter()
|
||||
{
|
||||
if let Some(exclude_rb) = exclude_rigidbody
|
||||
{
|
||||
if collider.parent() == Some(exclude_rb)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(toi) =
|
||||
collider
|
||||
.shape()
|
||||
.cast_ray(collider.position(), &ray, max_distance, true)
|
||||
{
|
||||
if let Some((_, closest_toi)) = closest_hit
|
||||
{
|
||||
if toi < closest_toi
|
||||
{
|
||||
closest_hit = Some((handle, toi));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
closest_hit = Some((handle, toi));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closest_hit
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_heightfield_data(heights: DMatrix<f32>, scale: Vector<f32>, position: Vector<f32>)
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| {
|
||||
manager.borrow_mut().heightfield_data = Some(HeightfieldData {
|
||||
heights,
|
||||
scale,
|
||||
position,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_terrain_height_at(x: f32, z: f32) -> Option<f32>
|
||||
{
|
||||
GLOBAL_PHYSICS.with(|manager| {
|
||||
let manager = manager.borrow();
|
||||
let data = manager.heightfield_data.as_ref()?;
|
||||
|
||||
let local_x = x - data.position.x;
|
||||
let local_z = z - data.position.z;
|
||||
|
||||
let normalized_x = (local_x / data.scale.x) + 0.5;
|
||||
let normalized_z = (local_z / data.scale.z) + 0.5;
|
||||
|
||||
let nrows = data.heights.nrows();
|
||||
let ncols = data.heights.ncols();
|
||||
|
||||
let row_f = normalized_z * (nrows - 1) as f32;
|
||||
let col_f = normalized_x * (ncols - 1) as f32;
|
||||
|
||||
if row_f < 0.0
|
||||
|| row_f >= (nrows - 1) as f32
|
||||
|| col_f < 0.0
|
||||
|| col_f >= (ncols - 1) as f32
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let row0 = row_f.floor() as usize;
|
||||
let row1 = (row0 + 1).min(nrows - 1);
|
||||
let col0 = col_f.floor() as usize;
|
||||
let col1 = (col0 + 1).min(ncols - 1);
|
||||
|
||||
let frac_row = row_f - row0 as f32;
|
||||
let frac_col = col_f - col0 as f32;
|
||||
|
||||
let h00 = data.heights[(row0, col0)];
|
||||
let h01 = data.heights[(row0, col1)];
|
||||
let h10 = data.heights[(row1, col0)];
|
||||
let h11 = data.heights[(row1, col1)];
|
||||
|
||||
let h0 = h00 * (1.0 - frac_col) + h01 * frac_col;
|
||||
let h1 = h10 * (1.0 - frac_col) + h11 * frac_col;
|
||||
let height = h0 * (1.0 - frac_row) + h1 * frac_row;
|
||||
|
||||
Some(height * data.scale.y + data.position.y)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_terrain_slope_in_direction(x: f32, z: f32, direction_x: f32, direction_z: f32)
|
||||
-> f32
|
||||
{
|
||||
const SAMPLE_DISTANCE: f32 = 0.5;
|
||||
|
||||
let dir_len = (direction_x * direction_x + direction_z * direction_z).sqrt();
|
||||
if dir_len < 0.001
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let norm_dir_x = direction_x / dir_len;
|
||||
let norm_dir_z = direction_z / dir_len;
|
||||
|
||||
let height_current = Self::get_terrain_height_at(x, z).unwrap_or(0.0);
|
||||
let height_forward = Self::get_terrain_height_at(
|
||||
x + norm_dir_x * SAMPLE_DISTANCE,
|
||||
z + norm_dir_z * SAMPLE_DISTANCE,
|
||||
)
|
||||
.unwrap_or(height_current);
|
||||
|
||||
let height_diff = height_forward - height_current;
|
||||
let slope_angle = (height_diff / SAMPLE_DISTANCE).atan();
|
||||
|
||||
slope_angle
|
||||
}
|
||||
}
|
||||
87
src/picking.rs
Normal file
87
src/picking.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::camera::Camera;
|
||||
use crate::mesh::Mesh;
|
||||
use glam::{Mat4, Vec3, Vec4};
|
||||
|
||||
pub struct Ray
|
||||
{
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
}
|
||||
|
||||
impl Ray
|
||||
{
|
||||
pub fn from_screen_position(
|
||||
screen_x: f32,
|
||||
screen_y: f32,
|
||||
screen_width: u32,
|
||||
screen_height: u32,
|
||||
camera: &Camera,
|
||||
) -> Self
|
||||
{
|
||||
let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0;
|
||||
let ndc_y = 1.0 - (2.0 * screen_y) / screen_height as f32;
|
||||
|
||||
let clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
|
||||
|
||||
let view_matrix = camera.view_matrix();
|
||||
let projection_matrix = camera.projection_matrix();
|
||||
let inv_projection = projection_matrix.inverse();
|
||||
let inv_view = view_matrix.inverse();
|
||||
|
||||
let eye_coords = inv_projection * clip_coords;
|
||||
let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0);
|
||||
|
||||
let world_coords = inv_view * eye_coords;
|
||||
let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize();
|
||||
|
||||
Ray {
|
||||
origin: camera.position,
|
||||
direction,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects_mesh(&self, mesh: &Mesh, transform: &Mat4) -> Option<f32>
|
||||
{
|
||||
let inv_transform = transform.inverse();
|
||||
let local_origin = inv_transform.transform_point3(self.origin);
|
||||
let local_direction = inv_transform.transform_vector3(self.direction).normalize();
|
||||
|
||||
let mut closest_distance = f32::MAX;
|
||||
let mut hit = false;
|
||||
|
||||
for triangle_idx in (0..mesh.num_indices).step_by(3)
|
||||
{
|
||||
let distance =
|
||||
self.intersects_triangle_local(local_origin, local_direction, mesh, triangle_idx);
|
||||
|
||||
if let Some(d) = distance
|
||||
{
|
||||
if d < closest_distance
|
||||
{
|
||||
closest_distance = d;
|
||||
hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hit
|
||||
{
|
||||
Some(closest_distance)
|
||||
}
|
||||
else
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn intersects_triangle_local(
|
||||
&self,
|
||||
local_origin: Vec3,
|
||||
local_direction: Vec3,
|
||||
_mesh: &Mesh,
|
||||
_triangle_idx: u32,
|
||||
) -> Option<f32>
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
604
src/player.rs
Normal file
604
src/player.rs
Normal file
@@ -0,0 +1,604 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::Vec3;
|
||||
use kurbo::ParamCurve;
|
||||
use rapier3d::{
|
||||
control::{CharacterAutostep, KinematicCharacterController},
|
||||
math::Vector,
|
||||
prelude::{ColliderBuilder, RigidBodyBuilder},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
jump::JumpComponent, InputComponent, MeshComponent, MovementComponent, PhysicsComponent,
|
||||
},
|
||||
entity::EntityHandle,
|
||||
mesh::Mesh,
|
||||
physics::PhysicsManager,
|
||||
render::Pipeline,
|
||||
state::{State, StateMachine},
|
||||
world::{Transform, World},
|
||||
};
|
||||
|
||||
pub struct Player;
|
||||
|
||||
impl Player
|
||||
{
|
||||
pub fn spawn(world: &mut World) -> EntityHandle
|
||||
{
|
||||
let entity = world.spawn();
|
||||
|
||||
let initial_position = Vec3::new(0.0, 5.0, 0.0);
|
||||
|
||||
let rigidbody = RigidBodyBuilder::kinematic_position_based()
|
||||
.translation(initial_position.into())
|
||||
.build();
|
||||
let collider = ColliderBuilder::capsule_y(0.5, 0.5).build();
|
||||
let _controller = KinematicCharacterController {
|
||||
slide: true,
|
||||
autostep: Some(CharacterAutostep::default()),
|
||||
max_slope_climb_angle: 45.0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody);
|
||||
let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle));
|
||||
|
||||
let mesh = Mesh::load_mesh("meshes/player_mesh.glb").expect("missing player mesh");
|
||||
|
||||
let falling_state = PlayerFallingState { entity };
|
||||
let idle_state = PlayerIdleState { entity };
|
||||
let walking_state = PlayerWalkingState {
|
||||
entity,
|
||||
enter_time_stamp: 0.0,
|
||||
};
|
||||
let jumping_state = PlayerJumpingState {
|
||||
entity,
|
||||
enter_time_stamp: 0.0,
|
||||
};
|
||||
|
||||
let mut state_machine = StateMachine::new(Box::new(falling_state));
|
||||
state_machine.add_state(walking_state);
|
||||
state_machine.add_state(idle_state);
|
||||
state_machine.add_state(jumping_state);
|
||||
|
||||
let entity_id = entity;
|
||||
|
||||
state_machine.add_transition::<PlayerFallingState, PlayerIdleState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
let has_input = world
|
||||
.inputs
|
||||
.with(entity_id, |i| i.move_direction.length() > 0.01)
|
||||
.unwrap_or(false);
|
||||
is_grounded && !has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerFallingState, PlayerWalkingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
let has_input = world
|
||||
.inputs
|
||||
.with(entity_id, |i| i.move_direction.length() > 0.01)
|
||||
.unwrap_or(false);
|
||||
is_grounded && has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerIdleState, PlayerWalkingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
let has_input = world
|
||||
.inputs
|
||||
.with(entity_id, |i| i.move_direction.length() > 0.01)
|
||||
.unwrap_or(false);
|
||||
is_grounded && has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerWalkingState, PlayerIdleState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
let has_input = world
|
||||
.inputs
|
||||
.with(entity_id, |i| i.move_direction.length() > 0.01)
|
||||
.unwrap_or(false);
|
||||
is_grounded && !has_input
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerIdleState, PlayerFallingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
!is_grounded
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerWalkingState, PlayerFallingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
!is_grounded
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerIdleState, PlayerJumpingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
let jump_pressed = world
|
||||
.inputs
|
||||
.with(entity_id, |i| i.jump_just_pressed)
|
||||
.unwrap_or(false);
|
||||
is_grounded && jump_pressed
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerWalkingState, PlayerJumpingState>(move |world| {
|
||||
let is_grounded = world
|
||||
.movements
|
||||
.with(entity_id, |m| m.movement_config.movement_context.is_floored)
|
||||
.unwrap_or(false);
|
||||
let jump_pressed = world
|
||||
.inputs
|
||||
.with(entity_id, |i| i.jump_just_pressed)
|
||||
.unwrap_or(false);
|
||||
is_grounded && jump_pressed
|
||||
});
|
||||
|
||||
state_machine.add_transition::<PlayerJumpingState, PlayerFallingState>(move |world| {
|
||||
world
|
||||
.jumps
|
||||
.with(entity_id, |jump| {
|
||||
jump.jump_config.jump_context.duration >= jump.jump_config.jump_duration
|
||||
})
|
||||
.unwrap_or(true)
|
||||
});
|
||||
|
||||
world
|
||||
.transforms
|
||||
.insert(entity, Transform::from_position(initial_position));
|
||||
world.movements.insert(entity, MovementComponent::new());
|
||||
world.jumps.insert(entity, JumpComponent::new());
|
||||
world.inputs.insert(entity, InputComponent::default());
|
||||
world.physics.insert(
|
||||
entity,
|
||||
PhysicsComponent {
|
||||
rigidbody: rigidbody_handle,
|
||||
collider: Some(collider_handle),
|
||||
},
|
||||
);
|
||||
world.meshes.insert(
|
||||
entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(mesh),
|
||||
pipeline: Pipeline::Render,
|
||||
},
|
||||
);
|
||||
world.player_tags.insert(entity);
|
||||
world.state_machines.insert(entity, state_machine);
|
||||
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn despawn(world: &mut World, entity: EntityHandle)
|
||||
{
|
||||
world.despawn(entity);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerFallingState
|
||||
{
|
||||
entity: EntityHandle,
|
||||
}
|
||||
|
||||
impl State for PlayerFallingState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
{
|
||||
"PlayerFallingState"
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, world: &mut World)
|
||||
{
|
||||
println!("entered falling");
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, world: &mut World) {}
|
||||
|
||||
fn on_state_update(&mut self, world: &mut World, delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
{
|
||||
const GRAVITY: f32 = -9.81 * 5.0;
|
||||
const GROUND_CHECK_DISTANCE: f32 = 0.6;
|
||||
|
||||
let (current_pos, velocity) = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let mut vel = *rigidbody.linvel();
|
||||
vel.y += GRAVITY * delta;
|
||||
(*rigidbody.translation(), vel)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let terrain_height = PhysicsManager::get_terrain_height_at(current_pos.x, current_pos.z);
|
||||
|
||||
let is_grounded = if let Some(height) = terrain_height
|
||||
{
|
||||
let target_y = height + 1.0;
|
||||
let distance_to_ground = current_pos.y - target_y;
|
||||
|
||||
if distance_to_ground < GROUND_CHECK_DISTANCE && velocity.y <= 0.01
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = Vector::new(current_pos.x, target_y, current_pos.z);
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
|
||||
});
|
||||
});
|
||||
true
|
||||
}
|
||||
else
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = current_pos + velocity * delta;
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(velocity, true);
|
||||
});
|
||||
});
|
||||
false
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_pos = current_pos + velocity * delta;
|
||||
rigidbody.set_next_kinematic_translation(next_pos);
|
||||
rigidbody.set_linvel(velocity, true);
|
||||
});
|
||||
});
|
||||
false
|
||||
};
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
movement.movement_config.movement_context.is_floored = is_grounded;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerIdleState
|
||||
{
|
||||
entity: EntityHandle,
|
||||
}
|
||||
|
||||
impl State for PlayerIdleState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
{
|
||||
"PlayerIdleState"
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, world: &mut World)
|
||||
{
|
||||
println!("entered idle");
|
||||
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let current_velocity = *rigidbody.linvel();
|
||||
let idle_damping = world
|
||||
.movements
|
||||
.with(self.entity, |m| m.movement_config.idle_damping)
|
||||
.unwrap_or(0.1);
|
||||
|
||||
let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z);
|
||||
let new_horizontal_velocity = horizontal_velocity * idle_damping;
|
||||
|
||||
rigidbody.set_linvel(
|
||||
Vector::new(
|
||||
new_horizontal_velocity.x,
|
||||
current_velocity.y,
|
||||
new_horizontal_velocity.z,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, _world: &mut World) {}
|
||||
|
||||
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
{
|
||||
const GROUND_CHECK_DISTANCE: f32 = 0.6;
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
*rigidbody.translation()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let terrain_height =
|
||||
PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);
|
||||
|
||||
if let Some(height) = terrain_height
|
||||
{
|
||||
let target_y = height + 1.0;
|
||||
let distance_to_ground = current_translation.y - target_y;
|
||||
|
||||
if distance_to_ground.abs() < GROUND_CHECK_DISTANCE
|
||||
{
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let next_translation =
|
||||
Vector::new(current_translation.x, target_y, current_translation.z);
|
||||
rigidbody.set_next_kinematic_translation(next_translation);
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
movement.movement_config.movement_context.is_floored = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerWalkingState
|
||||
{
|
||||
entity: EntityHandle,
|
||||
enter_time_stamp: f32,
|
||||
}
|
||||
|
||||
impl State for PlayerWalkingState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
{
|
||||
"PlayerWalkingState"
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, _world: &mut World)
|
||||
{
|
||||
use crate::utility::time::Time;
|
||||
self.enter_time_stamp = Time::get_time_elapsed();
|
||||
println!("entered walking");
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, _world: &mut World) {}
|
||||
|
||||
fn on_state_update(&mut self, world: &mut World, delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
{
|
||||
use crate::utility::time::Time;
|
||||
|
||||
let (movement_input, walking_config) = world
|
||||
.movements
|
||||
.with(self.entity, |movement| {
|
||||
let input = world
|
||||
.inputs
|
||||
.with(self.entity, |input| input.move_direction)
|
||||
.unwrap_or(Vec3::ZERO);
|
||||
(input, movement.movement_config.clone())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let current_time = Time::get_time_elapsed();
|
||||
let elapsed_time = current_time - self.enter_time_stamp;
|
||||
|
||||
let t = (elapsed_time / walking_config.walking_acceleration_duration).clamp(0.0, 1.0);
|
||||
let acceleration_amount = walking_config.walking_acceleration_curve.eval(t as f64).y as f32;
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
*rigidbody.translation()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let terrain_height =
|
||||
PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);
|
||||
|
||||
let target_y = if let Some(height) = terrain_height
|
||||
{
|
||||
height + 1.0
|
||||
}
|
||||
else
|
||||
{
|
||||
current_translation.y
|
||||
};
|
||||
|
||||
let slope_angle = if movement_input.length_squared() > 0.01
|
||||
{
|
||||
PhysicsManager::get_terrain_slope_in_direction(
|
||||
current_translation.x,
|
||||
current_translation.z,
|
||||
movement_input.x,
|
||||
movement_input.z,
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
0.0
|
||||
};
|
||||
|
||||
let slope_multiplier = {
|
||||
const MAX_SLOPE_ANGLE: f32 = std::f32::consts::PI / 4.0;
|
||||
|
||||
if slope_angle > 0.0
|
||||
{
|
||||
let uphill_factor = (slope_angle / MAX_SLOPE_ANGLE).min(1.0);
|
||||
1.0 - uphill_factor * 0.9
|
||||
}
|
||||
else
|
||||
{
|
||||
let downhill_factor = (slope_angle.abs() / MAX_SLOPE_ANGLE).min(1.0);
|
||||
1.0 + downhill_factor * 0.5
|
||||
}
|
||||
};
|
||||
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let current_velocity = *rigidbody.linvel();
|
||||
|
||||
let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z);
|
||||
|
||||
let walking_force = movement_input
|
||||
* walking_config.walking_acceleration
|
||||
* delta
|
||||
* acceleration_amount;
|
||||
|
||||
let new_horizontal_velocity = (walking_force
|
||||
+ horizontal_velocity * walking_config.walking_damping)
|
||||
.clamp_length_max(walking_config.max_walking_speed * slope_multiplier);
|
||||
|
||||
let next_translation = Vector::new(
|
||||
current_translation.x + new_horizontal_velocity.x * delta,
|
||||
target_y,
|
||||
current_translation.z + new_horizontal_velocity.z * delta,
|
||||
);
|
||||
|
||||
rigidbody.set_linvel(
|
||||
Vector::new(new_horizontal_velocity.x, 0.0, new_horizontal_velocity.z),
|
||||
true,
|
||||
);
|
||||
rigidbody.set_next_kinematic_translation(next_translation);
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
movement.movement_config.movement_context.is_floored = terrain_height.is_some();
|
||||
});
|
||||
|
||||
if movement_input.length_squared() > 0.1
|
||||
{
|
||||
world.transforms.with_mut(self.entity, |transform| {
|
||||
let target_rotation = f32::atan2(movement_input.x, movement_input.z);
|
||||
transform.rotation.y = target_rotation;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerJumpingState
|
||||
{
|
||||
entity: EntityHandle,
|
||||
enter_time_stamp: f32,
|
||||
}
|
||||
|
||||
impl State for PlayerJumpingState
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str
|
||||
{
|
||||
"PlayerJumpingState"
|
||||
}
|
||||
|
||||
fn on_state_enter(&mut self, world: &mut World)
|
||||
{
|
||||
use crate::utility::time::Time;
|
||||
self.enter_time_stamp = Time::get_time_elapsed();
|
||||
|
||||
let current_position = world.transforms.get(self.entity).unwrap().position;
|
||||
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
jump.jump_config.jump_context.in_progress = true;
|
||||
jump.jump_config.jump_context.execution_time = self.enter_time_stamp;
|
||||
jump.jump_config.jump_context.origin_height = current_position.y;
|
||||
jump.jump_config.jump_context.duration = 0.0;
|
||||
jump.jump_config.jump_context.normal = Vec3::Y;
|
||||
});
|
||||
|
||||
println!("entered jumping");
|
||||
}
|
||||
|
||||
fn on_state_exit(&mut self, world: &mut World)
|
||||
{
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
jump.jump_config.jump_context.in_progress = false;
|
||||
jump.jump_config.jump_context.duration = 0.0;
|
||||
});
|
||||
|
||||
println!("exited jumping");
|
||||
}
|
||||
|
||||
fn on_state_update(&mut self, _world: &mut World, _delta: f32) {}
|
||||
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32)
|
||||
{
|
||||
use crate::utility::time::Time;
|
||||
|
||||
let current_time = Time::get_time_elapsed();
|
||||
|
||||
world.jumps.with_mut(self.entity, |jump| {
|
||||
jump.jump_config.jump_context.duration = current_time - jump.jump_config.jump_context.execution_time;
|
||||
});
|
||||
|
||||
let jump_config = world
|
||||
.jumps
|
||||
.with_mut(self.entity, |jump| jump.jump_config.clone())
|
||||
.unwrap();
|
||||
|
||||
let elapsed_time = jump_config.jump_context.duration;
|
||||
let normalized_time = (elapsed_time / jump_config.jump_duration).min(1.0);
|
||||
let height_progress = jump_config.jump_curve.eval(normalized_time as f64).y as f32;
|
||||
|
||||
let origin_height = jump_config.jump_context.origin_height;
|
||||
let target_height = origin_height + height_progress * jump_config.jump_height;
|
||||
|
||||
let current_translation = world
|
||||
.physics
|
||||
.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
*rigidbody.translation()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap();
|
||||
|
||||
let current_y = current_translation.y;
|
||||
let height_diff = target_height - current_y;
|
||||
let required_velocity = height_diff / delta;
|
||||
|
||||
world.physics.with(self.entity, |physics| {
|
||||
PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
|
||||
let current_velocity = *rigidbody.linvel();
|
||||
let next_translation = Vector::new(
|
||||
current_translation.x + current_velocity.x * delta,
|
||||
current_translation.y + required_velocity * delta,
|
||||
current_translation.z + current_velocity.z * delta,
|
||||
);
|
||||
|
||||
rigidbody.set_linvel(
|
||||
Vector::new(current_velocity.x, required_velocity, current_velocity.z),
|
||||
true,
|
||||
);
|
||||
rigidbody.set_next_kinematic_translation(next_translation);
|
||||
});
|
||||
});
|
||||
|
||||
world.movements.with_mut(self.entity, |movement| {
|
||||
movement.movement_config.movement_context.is_floored = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
200
src/postprocess.rs
Normal file
200
src/postprocess.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
pub struct ScreenVertex
|
||||
{
|
||||
pub position: [f32; 2],
|
||||
pub uv: [f32; 2],
|
||||
}
|
||||
|
||||
impl ScreenVertex
|
||||
{
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static>
|
||||
{
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<ScreenVertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: 0,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x2,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LowResFramebuffer
|
||||
{
|
||||
pub texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
pub depth_view: wgpu::TextureView,
|
||||
pub sampler: wgpu::Sampler,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl LowResFramebuffer
|
||||
{
|
||||
pub fn new(device: &wgpu::Device, width: u32, height: u32, format: wgpu::TextureFormat)
|
||||
-> Self
|
||||
{
|
||||
let size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
};
|
||||
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Low Res Color Texture"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Low Res Depth Texture"),
|
||||
size,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Low Res Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Self {
|
||||
texture,
|
||||
view,
|
||||
depth_view,
|
||||
sampler,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_fullscreen_quad(device: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer, u32)
|
||||
{
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
let vertices = [
|
||||
ScreenVertex {
|
||||
position: [-1.0, -1.0],
|
||||
uv: [0.0, 1.0],
|
||||
},
|
||||
ScreenVertex {
|
||||
position: [1.0, -1.0],
|
||||
uv: [1.0, 1.0],
|
||||
},
|
||||
ScreenVertex {
|
||||
position: [1.0, 1.0],
|
||||
uv: [1.0, 0.0],
|
||||
},
|
||||
ScreenVertex {
|
||||
position: [-1.0, 1.0],
|
||||
uv: [0.0, 0.0],
|
||||
},
|
||||
];
|
||||
|
||||
let indices: [u16; 6] = [0, 1, 2, 2, 3, 0];
|
||||
|
||||
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Fullscreen Quad Vertex Buffer"),
|
||||
contents: bytemuck::cast_slice(&vertices),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Fullscreen Quad Index Buffer"),
|
||||
contents: bytemuck::cast_slice(&indices),
|
||||
usage: wgpu::BufferUsages::INDEX,
|
||||
});
|
||||
|
||||
(vertex_buffer, index_buffer, indices.len() as u32)
|
||||
}
|
||||
|
||||
pub fn create_blit_pipeline(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shader_source =
|
||||
std::fs::read_to_string("shaders/blit.wgsl").expect("Failed to read blit shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Blit Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Blit Pipeline Layout"),
|
||||
bind_group_layouts: &[bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Blit Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[ScreenVertex::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
800
src/render.rs
Normal file
800
src/render.rs
Normal file
@@ -0,0 +1,800 @@
|
||||
use crate::camera::{Camera, CameraUniforms};
|
||||
use crate::mesh::Mesh;
|
||||
use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer};
|
||||
use crate::shader::create_render_pipeline;
|
||||
use crate::terrain::create_terrain_render_pipeline;
|
||||
use crate::utility::transform::Transform;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::Mat4;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
struct TerrainUniforms
|
||||
{
|
||||
model: [[f32; 4]; 4],
|
||||
view: [[f32; 4]; 4],
|
||||
projection: [[f32; 4]; 4],
|
||||
height_scale: f32,
|
||||
time: f32,
|
||||
_padding: [f32; 2],
|
||||
}
|
||||
|
||||
impl TerrainUniforms
|
||||
{
|
||||
fn new(model: Mat4, view: Mat4, projection: Mat4, height_scale: f32, time: f32) -> Self
|
||||
{
|
||||
Self {
|
||||
model: model.to_cols_array_2d(),
|
||||
view: view.to_cols_array_2d(),
|
||||
projection: projection.to_cols_array_2d(),
|
||||
height_scale,
|
||||
time,
|
||||
_padding: [0.0; 2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Pipeline
|
||||
{
|
||||
Render,
|
||||
Terrain,
|
||||
Wireframe,
|
||||
}
|
||||
|
||||
pub struct DrawCall
|
||||
{
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub index_buffer: wgpu::Buffer,
|
||||
pub num_indices: u32,
|
||||
pub model: Mat4,
|
||||
pub pipeline: Pipeline,
|
||||
}
|
||||
|
||||
pub struct TerrainData
|
||||
{
|
||||
pub height_texture: wgpu::Texture,
|
||||
pub height_view: wgpu::TextureView,
|
||||
pub height_sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
pub struct Renderer
|
||||
{
|
||||
pub device: wgpu::Device,
|
||||
pub queue: wgpu::Queue,
|
||||
pub surface: wgpu::Surface<'static>,
|
||||
pub config: wgpu::SurfaceConfiguration,
|
||||
|
||||
framebuffer: LowResFramebuffer,
|
||||
render_pipeline: wgpu::RenderPipeline,
|
||||
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
|
||||
quad_vb: wgpu::Buffer,
|
||||
quad_ib: wgpu::Buffer,
|
||||
quad_num_indices: u32,
|
||||
blit_pipeline: wgpu::RenderPipeline,
|
||||
blit_bind_group: wgpu::BindGroup,
|
||||
|
||||
terrain_pipeline: Option<wgpu::RenderPipeline>,
|
||||
terrain_bind_group_layout: wgpu::BindGroupLayout,
|
||||
terrain_uniform_buffer: wgpu::Buffer,
|
||||
terrain_bind_group: Option<wgpu::BindGroup>,
|
||||
terrain_height_scale: f32,
|
||||
|
||||
wireframe_pipeline: wgpu::RenderPipeline,
|
||||
}
|
||||
|
||||
impl Renderer
|
||||
{
|
||||
pub async fn new(
|
||||
window: &sdl3::video::Window,
|
||||
render_scale: u32,
|
||||
) -> Result<Self, Box<dyn std::error::Error>>
|
||||
{
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::VULKAN,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = unsafe {
|
||||
let target = wgpu::SurfaceTargetUnsafe::from_window(window)?;
|
||||
instance.create_surface_unsafe(target)?
|
||||
};
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "Failed to find adapter")?;
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor::default())
|
||||
.await?;
|
||||
|
||||
let size = window.size();
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface.get_capabilities(&adapter).formats[0],
|
||||
width: size.0,
|
||||
height: size.1,
|
||||
present_mode: wgpu::PresentMode::Fifo,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let low_res_width = config.width / render_scale;
|
||||
let low_res_height = config.height / render_scale;
|
||||
|
||||
let framebuffer =
|
||||
LowResFramebuffer::new(&device, low_res_width, low_res_height, config.format);
|
||||
|
||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Uniform Buffer"),
|
||||
size: std::mem::size_of::<CameraUniforms>() as wgpu::BufferAddress,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Bind Group Layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Bind Group"),
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
let render_pipeline = create_render_pipeline(&device, &config, &bind_group_layout);
|
||||
|
||||
let (quad_vb, quad_ib, quad_num_indices) = create_fullscreen_quad(&device);
|
||||
|
||||
let blit_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Blit Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let blit_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Blit Bind Group"),
|
||||
layout: &blit_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&framebuffer.view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&framebuffer.sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let blit_pipeline = create_blit_pipeline(&device, config.format, &blit_bind_group_layout);
|
||||
|
||||
let terrain_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Terrain Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let terrain_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Terrain Uniform Buffer"),
|
||||
size: std::mem::size_of::<TerrainUniforms>() as wgpu::BufferAddress,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let wireframe_pipeline =
|
||||
create_wireframe_pipeline(&device, config.format, &bind_group_layout);
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
config,
|
||||
framebuffer,
|
||||
render_pipeline,
|
||||
uniform_buffer,
|
||||
bind_group,
|
||||
quad_vb,
|
||||
quad_ib,
|
||||
quad_num_indices,
|
||||
blit_pipeline,
|
||||
blit_bind_group,
|
||||
terrain_pipeline: None,
|
||||
terrain_bind_group_layout,
|
||||
terrain_uniform_buffer,
|
||||
terrain_bind_group: None,
|
||||
terrain_height_scale: 10.0,
|
||||
wireframe_pipeline,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render(&mut self, camera: &Camera, draw_calls: &[DrawCall], time: f32)
|
||||
{
|
||||
let view = camera.view_matrix();
|
||||
let projection = camera.projection_matrix();
|
||||
|
||||
for (i, draw_call) in draw_calls.iter().enumerate()
|
||||
{
|
||||
match draw_call.pipeline
|
||||
{
|
||||
Pipeline::Render | Pipeline::Wireframe =>
|
||||
{
|
||||
let uniforms = CameraUniforms::new(draw_call.model, view, projection);
|
||||
self.queue.write_buffer(
|
||||
&self.uniform_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&[uniforms]),
|
||||
);
|
||||
}
|
||||
Pipeline::Terrain =>
|
||||
{
|
||||
let uniforms = TerrainUniforms::new(
|
||||
draw_call.model,
|
||||
view,
|
||||
projection,
|
||||
self.terrain_height_scale,
|
||||
time,
|
||||
);
|
||||
self.queue.write_buffer(
|
||||
&self.terrain_uniform_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&[uniforms]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("3D Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.framebuffer.view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: if i == 0
|
||||
{
|
||||
wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 0.1,
|
||||
g: 0.2,
|
||||
b: 0.3,
|
||||
a: 1.0,
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
wgpu::LoadOp::Load
|
||||
},
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.framebuffer.depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: if i == 0
|
||||
{
|
||||
wgpu::LoadOp::Clear(1.0)
|
||||
}
|
||||
else
|
||||
{
|
||||
wgpu::LoadOp::Load
|
||||
},
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
let pipeline = match draw_call.pipeline
|
||||
{
|
||||
Pipeline::Render => &self.render_pipeline,
|
||||
Pipeline::Terrain => &self
|
||||
.terrain_pipeline
|
||||
.as_ref()
|
||||
.expect("terrain_data_missing"),
|
||||
Pipeline::Wireframe => &self.wireframe_pipeline,
|
||||
};
|
||||
let bind_group = match draw_call.pipeline
|
||||
{
|
||||
Pipeline::Render => &self.bind_group,
|
||||
Pipeline::Terrain => &self
|
||||
.terrain_bind_group
|
||||
.as_ref()
|
||||
.expect("terrain data missing"),
|
||||
Pipeline::Wireframe => &self.bind_group,
|
||||
};
|
||||
|
||||
render_pass.set_pipeline(pipeline);
|
||||
render_pass.set_bind_group(0, bind_group, &[]);
|
||||
render_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..));
|
||||
render_pass
|
||||
.set_index_buffer(draw_call.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||
render_pass.draw_indexed(0..draw_call.num_indices, 0, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
|
||||
let frame = match self.surface.get_current_texture()
|
||||
{
|
||||
Ok(frame) => frame,
|
||||
Err(_) =>
|
||||
{
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.surface
|
||||
.get_current_texture()
|
||||
.expect("Failed to acquire next surface texture")
|
||||
}
|
||||
};
|
||||
|
||||
let screen_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut blit_encoder =
|
||||
self.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Blit Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut blit_pass = blit_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Blit Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &screen_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
blit_pass.set_pipeline(&self.blit_pipeline);
|
||||
blit_pass.set_bind_group(0, &self.blit_bind_group, &[]);
|
||||
blit_pass.set_vertex_buffer(0, self.quad_vb.slice(..));
|
||||
blit_pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16);
|
||||
blit_pass.draw_indexed(0..self.quad_num_indices, 0, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(blit_encoder.finish()));
|
||||
frame.present();
|
||||
}
|
||||
|
||||
pub fn render_with_matrices(
|
||||
&mut self,
|
||||
view: &glam::Mat4,
|
||||
projection: &glam::Mat4,
|
||||
draw_calls: &[DrawCall],
|
||||
time: f32,
|
||||
)
|
||||
{
|
||||
for (i, draw_call) in draw_calls.iter().enumerate()
|
||||
{
|
||||
match draw_call.pipeline
|
||||
{
|
||||
Pipeline::Render | Pipeline::Wireframe =>
|
||||
{
|
||||
let uniforms = CameraUniforms::new(draw_call.model, *view, *projection);
|
||||
self.queue.write_buffer(
|
||||
&self.uniform_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&[uniforms]),
|
||||
);
|
||||
}
|
||||
Pipeline::Terrain =>
|
||||
{
|
||||
let uniforms = TerrainUniforms::new(
|
||||
draw_call.model,
|
||||
*view,
|
||||
*projection,
|
||||
self.terrain_height_scale,
|
||||
time,
|
||||
);
|
||||
self.queue.write_buffer(
|
||||
&self.terrain_uniform_buffer,
|
||||
0,
|
||||
bytemuck::cast_slice(&[uniforms]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("3D Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.framebuffer.view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: if i == 0
|
||||
{
|
||||
wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 0.1,
|
||||
g: 0.2,
|
||||
b: 0.3,
|
||||
a: 1.0,
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
wgpu::LoadOp::Load
|
||||
},
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.framebuffer.depth_view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: if i == 0
|
||||
{
|
||||
wgpu::LoadOp::Clear(1.0)
|
||||
}
|
||||
else
|
||||
{
|
||||
wgpu::LoadOp::Load
|
||||
},
|
||||
store: wgpu::StoreOp::Store,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
let pipeline = match draw_call.pipeline
|
||||
{
|
||||
Pipeline::Render => &self.render_pipeline,
|
||||
Pipeline::Terrain => &self
|
||||
.terrain_pipeline
|
||||
.as_ref()
|
||||
.expect("terrain_data_missing"),
|
||||
Pipeline::Wireframe => &self.wireframe_pipeline,
|
||||
};
|
||||
let bind_group = match draw_call.pipeline
|
||||
{
|
||||
Pipeline::Render => &self.bind_group,
|
||||
Pipeline::Terrain => &self
|
||||
.terrain_bind_group
|
||||
.as_ref()
|
||||
.expect("terrain_data_missing"),
|
||||
Pipeline::Wireframe => &self.bind_group,
|
||||
};
|
||||
|
||||
render_pass.set_pipeline(pipeline);
|
||||
render_pass.set_bind_group(0, bind_group, &[]);
|
||||
|
||||
render_pass.set_vertex_buffer(0, draw_call.vertex_buffer.slice(..));
|
||||
render_pass.set_index_buffer(
|
||||
draw_call.index_buffer.slice(..),
|
||||
wgpu::IndexFormat::Uint32,
|
||||
);
|
||||
render_pass.draw_indexed(0..draw_call.num_indices, 0, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
|
||||
let frame = match self.surface.get_current_texture()
|
||||
{
|
||||
Ok(frame) => frame,
|
||||
Err(_) =>
|
||||
{
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.surface
|
||||
.get_current_texture()
|
||||
.expect("Failed to acquire next surface texture")
|
||||
}
|
||||
};
|
||||
|
||||
let screen_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut blit_encoder =
|
||||
self.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Blit Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut blit_pass = blit_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Blit Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &screen_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
blit_pass.set_pipeline(&self.blit_pipeline);
|
||||
blit_pass.set_bind_group(0, &self.blit_bind_group, &[]);
|
||||
blit_pass.set_vertex_buffer(0, self.quad_vb.slice(..));
|
||||
blit_pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16);
|
||||
blit_pass.draw_indexed(0..self.quad_num_indices, 0, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(blit_encoder.finish()));
|
||||
frame.present();
|
||||
}
|
||||
|
||||
pub fn render_scale(&self) -> (u32, u32)
|
||||
{
|
||||
(
|
||||
self.config.width / self.framebuffer.width,
|
||||
self.config.height / self.framebuffer.height,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_terrain_data(&mut self, terrain_data: TerrainData)
|
||||
{
|
||||
let terrain_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Terrain Bind Group"),
|
||||
layout: &self.terrain_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self.terrain_uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&terrain_data.height_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&terrain_data.height_sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let terrain_pipeline = create_terrain_render_pipeline(
|
||||
&self.device,
|
||||
&self.config,
|
||||
&self.terrain_bind_group_layout,
|
||||
);
|
||||
|
||||
self.terrain_bind_group = Some(terrain_bind_group);
|
||||
self.terrain_pipeline = Some(terrain_pipeline);
|
||||
}
|
||||
|
||||
pub fn get_device(&self) -> &wgpu::Device
|
||||
{
|
||||
&self.device
|
||||
}
|
||||
|
||||
pub fn aspect_ratio(&self) -> f32
|
||||
{
|
||||
self.config.width as f32 / self.config.height as f32
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static GLOBAL_RENDERER: RefCell<Option<Renderer>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
pub fn init(renderer: Renderer)
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| *r.borrow_mut() = Some(renderer));
|
||||
}
|
||||
|
||||
pub fn with_device<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&wgpu::Device) -> R,
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| {
|
||||
let renderer = r.borrow();
|
||||
let renderer = renderer.as_ref().expect("Renderer not set");
|
||||
f(&renderer.device)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_queue<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&wgpu::Queue) -> R,
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| {
|
||||
let renderer = r.borrow();
|
||||
let renderer = renderer.as_ref().expect("Renderer not set");
|
||||
f(&renderer.queue)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_terrain_data(terrain_data: TerrainData)
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| {
|
||||
let mut renderer = r.borrow_mut();
|
||||
let renderer = renderer.as_mut().expect("Renderer not set");
|
||||
renderer.set_terrain_data(terrain_data);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn aspect_ratio() -> f32
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| {
|
||||
let renderer = r.borrow();
|
||||
let renderer = renderer.as_ref().expect("Renderer not set");
|
||||
renderer.aspect_ratio()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render(camera: &Camera, draw_calls: &[DrawCall], time: f32)
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| {
|
||||
let mut renderer = r.borrow_mut();
|
||||
let renderer = renderer.as_mut().expect("Renderer not set");
|
||||
renderer.render(camera, draw_calls, time);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_with_matrices(
|
||||
view: &glam::Mat4,
|
||||
projection: &glam::Mat4,
|
||||
draw_calls: &[DrawCall],
|
||||
time: f32,
|
||||
)
|
||||
{
|
||||
GLOBAL_RENDERER.with(|r| {
|
||||
let mut renderer = r.borrow_mut();
|
||||
let renderer = renderer.as_mut().expect("Renderer not set");
|
||||
renderer.render_with_matrices(view, projection, draw_calls, time);
|
||||
});
|
||||
}
|
||||
|
||||
fn create_wireframe_pipeline(
|
||||
device: &wgpu::Device,
|
||||
format: wgpu::TextureFormat,
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shader_source =
|
||||
std::fs::read_to_string("shaders/standard.wgsl").expect("Failed to read shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Wireframe Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Wireframe Pipeline Layout"),
|
||||
bind_group_layouts: &[bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Wireframe Pipeline"),
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[crate::mesh::Vertex::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::LineList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
66
src/shader.rs
Normal file
66
src/shader.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::mesh::Vertex;
|
||||
|
||||
pub fn create_render_pipeline(
|
||||
device: &wgpu::Device,
|
||||
config: &wgpu::SurfaceConfiguration,
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shader_source =
|
||||
std::fs::read_to_string("shaders/standard.wgsl").expect("Failed to read standard shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Render Pipeline Layout"),
|
||||
bind_group_layouts: &[bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Render Pipeline"),
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Vertex::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
141
src/state.rs
Normal file
141
src/state.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::world::World;
|
||||
|
||||
pub trait StateAgent {}
|
||||
|
||||
pub trait State: Any
|
||||
{
|
||||
fn get_state_name(&self) -> &'static str;
|
||||
fn on_state_enter(&mut self, world: &mut World) {}
|
||||
fn on_state_exit(&mut self, world: &mut World) {}
|
||||
fn on_state_update(&mut self, world: &mut World, delta: f32) {}
|
||||
fn on_state_physics_update(&mut self, world: &mut World, delta: f32) {}
|
||||
}
|
||||
|
||||
impl dyn State
|
||||
{
|
||||
fn dyn_type_id(&self) -> std::any::TypeId
|
||||
{
|
||||
Any::type_id(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StateTransition
|
||||
{
|
||||
to_state_id: TypeId,
|
||||
condition: Box<dyn Fn(&World) -> bool>,
|
||||
}
|
||||
|
||||
pub struct StateMachine
|
||||
{
|
||||
state_transitions: HashMap<TypeId, Vec<StateTransition>>,
|
||||
current_state_id: TypeId,
|
||||
states: HashMap<TypeId, Box<dyn State>>,
|
||||
pub time_in_state: f32,
|
||||
}
|
||||
|
||||
impl StateMachine
|
||||
{
|
||||
pub fn new(enter_state: Box<dyn State>) -> Self
|
||||
{
|
||||
let state_id = enter_state.dyn_type_id();
|
||||
let mut states = HashMap::new();
|
||||
states.insert(state_id, enter_state);
|
||||
|
||||
Self {
|
||||
state_transitions: HashMap::new(),
|
||||
current_state_id: state_id,
|
||||
states,
|
||||
time_in_state: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, world: &mut World, delta: f32)
|
||||
{
|
||||
if let Some(next_state_id) = self.get_transition_state_id(world)
|
||||
{
|
||||
self.time_in_state = 0.0;
|
||||
self.transition_to(world, next_state_id);
|
||||
}
|
||||
|
||||
if let Some(current_state) = self.states.get_mut(&self.current_state_id)
|
||||
{
|
||||
current_state.on_state_update(world, delta);
|
||||
}
|
||||
|
||||
self.time_in_state += delta;
|
||||
}
|
||||
|
||||
fn get_transition_state_id(&self, world: &World) -> Option<TypeId>
|
||||
{
|
||||
if let Some(transitions) = self.state_transitions.get(&self.current_state_id)
|
||||
{
|
||||
for transition in transitions
|
||||
{
|
||||
if (transition.condition)(world)
|
||||
{
|
||||
return Some(transition.to_state_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn transition_to(&mut self, world: &mut World, new_state_id: TypeId)
|
||||
{
|
||||
if let Some(current_state) = self.states.get_mut(&self.current_state_id)
|
||||
{
|
||||
current_state.on_state_exit(world);
|
||||
}
|
||||
|
||||
self.current_state_id = new_state_id;
|
||||
|
||||
if let Some(new_state) = self.states.get_mut(&self.current_state_id)
|
||||
{
|
||||
new_state.on_state_enter(world);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_current_state(&self) -> Option<&dyn State>
|
||||
{
|
||||
self.states.get(&self.current_state_id).map(|b| b.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_current_state_mut(&mut self) -> Option<&mut dyn State>
|
||||
{
|
||||
self.states
|
||||
.get_mut(&self.current_state_id)
|
||||
.map(|b| b.as_mut())
|
||||
}
|
||||
|
||||
pub fn add_state<T: State + 'static>(&mut self, state: T)
|
||||
{
|
||||
let state_id = TypeId::of::<T>();
|
||||
self.states.insert(state_id, Box::new(state));
|
||||
}
|
||||
|
||||
pub fn add_transition<TFrom: State + 'static, TTo: State + 'static>(
|
||||
&mut self,
|
||||
condition: impl Fn(&World) -> bool + 'static,
|
||||
)
|
||||
{
|
||||
let from_id = TypeId::of::<TFrom>();
|
||||
let to_id = TypeId::of::<TTo>();
|
||||
|
||||
let transitions = self.state_transitions.entry(from_id).or_default();
|
||||
transitions.push(StateTransition {
|
||||
to_state_id: to_id,
|
||||
condition: Box::new(condition),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_available_transitions_count(&self) -> usize
|
||||
{
|
||||
self.state_transitions
|
||||
.get(&self.current_state_id)
|
||||
.map(|transitions| transitions.len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
202
src/systems/camera.rs
Normal file
202
src/systems/camera.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::utility::input::InputState;
|
||||
use crate::world::World;
|
||||
|
||||
pub fn camera_input_system(world: &mut World, input_state: &InputState)
|
||||
{
|
||||
let cameras: Vec<_> = world.cameras.all();
|
||||
|
||||
for camera_entity in cameras
|
||||
{
|
||||
if let Some(camera) = world.cameras.get_mut(camera_entity)
|
||||
{
|
||||
if !camera.is_active
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if input_state.mouse_delta.0.abs() > 0.0 || input_state.mouse_delta.1.abs() > 0.0
|
||||
{
|
||||
let is_following = world
|
||||
.camera_follows
|
||||
.get(camera_entity)
|
||||
.map(|f| f.is_following)
|
||||
.unwrap_or(false);
|
||||
|
||||
camera.yaw += input_state.mouse_delta.0 * 0.0008;
|
||||
|
||||
if is_following
|
||||
{
|
||||
camera.pitch += input_state.mouse_delta.1 * 0.0008;
|
||||
}
|
||||
else
|
||||
{
|
||||
camera.pitch -= input_state.mouse_delta.1 * 0.0008;
|
||||
}
|
||||
|
||||
camera.pitch = camera
|
||||
.pitch
|
||||
.clamp(-89.0_f32.to_radians(), 89.0_f32.to_radians());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn camera_follow_system(world: &mut World)
|
||||
{
|
||||
let camera_entities: Vec<_> = world.camera_follows.all();
|
||||
|
||||
for camera_entity in camera_entities
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get(camera_entity)
|
||||
{
|
||||
if !follow.is_following
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let target_entity = follow.target_entity;
|
||||
let offset = follow.offset;
|
||||
|
||||
if let Some(target_transform) = world.transforms.get(target_entity)
|
||||
{
|
||||
let target_position = target_transform.position;
|
||||
|
||||
if let Some(camera) = world.cameras.get_mut(camera_entity)
|
||||
{
|
||||
let distance = offset.length();
|
||||
|
||||
let orbit_yaw = camera.yaw + std::f32::consts::PI;
|
||||
|
||||
let offset_x = distance * orbit_yaw.cos() * camera.pitch.cos();
|
||||
let offset_y = distance * camera.pitch.sin();
|
||||
let offset_z = distance * orbit_yaw.sin() * camera.pitch.cos();
|
||||
|
||||
let new_offset = Vec3::new(offset_x, offset_y, offset_z);
|
||||
|
||||
if let Some(camera_transform) = world.transforms.get_mut(camera_entity)
|
||||
{
|
||||
camera_transform.position = target_position + new_offset;
|
||||
}
|
||||
|
||||
if let Some(follow_mut) = world.camera_follows.get_mut(camera_entity)
|
||||
{
|
||||
follow_mut.offset = new_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn camera_noclip_system(world: &mut World, input_state: &InputState, delta: f32)
|
||||
{
|
||||
let cameras: Vec<_> = world.cameras.all();
|
||||
|
||||
for camera_entity in cameras
|
||||
{
|
||||
if let Some(camera) = world.cameras.get(camera_entity)
|
||||
{
|
||||
if !camera.is_active
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let forward = camera.get_forward();
|
||||
let right = camera.get_right();
|
||||
|
||||
let mut input_vec = Vec3::ZERO;
|
||||
|
||||
if input_state.w
|
||||
{
|
||||
input_vec.z += 1.0;
|
||||
}
|
||||
if input_state.s
|
||||
{
|
||||
input_vec.z -= 1.0;
|
||||
}
|
||||
if input_state.d
|
||||
{
|
||||
input_vec.x += 1.0;
|
||||
}
|
||||
if input_state.a
|
||||
{
|
||||
input_vec.x -= 1.0;
|
||||
}
|
||||
if input_state.space
|
||||
{
|
||||
input_vec.y += 1.0;
|
||||
}
|
||||
|
||||
if input_vec.length_squared() > 0.0
|
||||
{
|
||||
input_vec = input_vec.normalize();
|
||||
}
|
||||
|
||||
let mut speed = 10.0 * delta;
|
||||
if input_state.shift
|
||||
{
|
||||
speed *= 2.0;
|
||||
}
|
||||
|
||||
if let Some(camera_transform) = world.transforms.get_mut(camera_entity)
|
||||
{
|
||||
camera_transform.position += forward * input_vec.z * speed;
|
||||
camera_transform.position += right * input_vec.x * speed;
|
||||
camera_transform.position += Vec3::Y * input_vec.y * speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle)
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get_mut(camera_entity)
|
||||
{
|
||||
let target_entity = follow.target_entity;
|
||||
|
||||
if let Some(target_transform) = world.transforms.get(target_entity)
|
||||
{
|
||||
if let Some(camera_transform) = world.transforms.get(camera_entity)
|
||||
{
|
||||
let offset = camera_transform.position - target_transform.position;
|
||||
follow.offset = offset;
|
||||
follow.is_following = true;
|
||||
|
||||
let distance = offset.length();
|
||||
if distance > 0.0
|
||||
{
|
||||
if let Some(camera) = world.cameras.get_mut(camera_entity)
|
||||
{
|
||||
camera.pitch = (offset.y / distance).asin();
|
||||
camera.yaw = offset.z.atan2(offset.x) + std::f32::consts::PI;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle)
|
||||
{
|
||||
if let Some(follow) = world.camera_follows.get_mut(camera_entity)
|
||||
{
|
||||
follow.is_following = false;
|
||||
|
||||
if let Some(camera_transform) = world.transforms.get(camera_entity)
|
||||
{
|
||||
if let Some(target_transform) = world.transforms.get(follow.target_entity)
|
||||
{
|
||||
let look_direction =
|
||||
(target_transform.position - camera_transform.position).normalize();
|
||||
|
||||
if let Some(camera) = world.cameras.get_mut(camera_entity)
|
||||
{
|
||||
camera.yaw = look_direction.z.atan2(look_direction.x);
|
||||
camera.pitch = look_direction.y.asin();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/systems/input.rs
Normal file
58
src/systems/input.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::utility::input::InputState;
|
||||
use crate::world::World;
|
||||
|
||||
pub fn player_input_system(world: &mut World, input_state: &InputState)
|
||||
{
|
||||
let active_camera = world.cameras.get_active();
|
||||
|
||||
if active_camera.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let (_, camera) = active_camera.unwrap();
|
||||
|
||||
let forward = camera.get_forward_horizontal();
|
||||
let right = camera.get_right_horizontal();
|
||||
|
||||
let players = world.player_tags.all();
|
||||
|
||||
for player in players
|
||||
{
|
||||
world.inputs.with_mut(player, |input_component| {
|
||||
let mut local_input = Vec3::ZERO;
|
||||
|
||||
if input_state.w
|
||||
{
|
||||
local_input.z += 1.0;
|
||||
}
|
||||
if input_state.s
|
||||
{
|
||||
local_input.z -= 1.0;
|
||||
}
|
||||
if input_state.a
|
||||
{
|
||||
local_input.x -= 1.0;
|
||||
}
|
||||
if input_state.d
|
||||
{
|
||||
local_input.x += 1.0;
|
||||
}
|
||||
|
||||
let move_direction = if local_input.length_squared() > 0.0
|
||||
{
|
||||
(forward * local_input.z + right * local_input.x).normalize()
|
||||
}
|
||||
else
|
||||
{
|
||||
Vec3::ZERO
|
||||
};
|
||||
|
||||
input_component.move_direction = move_direction;
|
||||
input_component.jump_pressed = input_state.space;
|
||||
input_component.jump_just_pressed = input_state.space_just_pressed;
|
||||
});
|
||||
}
|
||||
}
|
||||
14
src/systems/mod.rs
Normal file
14
src/systems/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub mod camera;
|
||||
pub mod input;
|
||||
pub mod physics_sync;
|
||||
pub mod render;
|
||||
pub mod state_machine;
|
||||
|
||||
pub use camera::{
|
||||
camera_follow_system, camera_input_system, camera_noclip_system, start_camera_following,
|
||||
stop_camera_following,
|
||||
};
|
||||
pub use input::player_input_system;
|
||||
pub use physics_sync::physics_sync_system;
|
||||
pub use render::render_system;
|
||||
pub use state_machine::{state_machine_physics_system, state_machine_system};
|
||||
24
src/systems/physics_sync.rs
Normal file
24
src/systems/physics_sync.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::physics::PhysicsManager;
|
||||
use crate::utility::transform::Transform;
|
||||
use crate::world::World;
|
||||
|
||||
pub fn physics_sync_system(world: &mut World)
|
||||
{
|
||||
let all_entities = world.entities.all_entities();
|
||||
|
||||
for entity in all_entities
|
||||
{
|
||||
if let Some(physics) = world.physics.get(entity)
|
||||
{
|
||||
if let Some(rigidbody_position) =
|
||||
PhysicsManager::get_rigidbody_position(physics.rigidbody)
|
||||
{
|
||||
let transform = Transform::from(rigidbody_position);
|
||||
|
||||
world.transforms.with_mut(entity, |t| {
|
||||
*t = transform;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/systems/render.rs
Normal file
23
src/systems/render.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use crate::render::DrawCall;
|
||||
use crate::world::World;
|
||||
|
||||
pub fn render_system(world: &World) -> Vec<DrawCall>
|
||||
{
|
||||
let all_entities = world.entities.all_entities();
|
||||
|
||||
all_entities
|
||||
.iter()
|
||||
.filter_map(|&entity| {
|
||||
let transform = world.transforms.get(entity)?;
|
||||
let mesh_component = world.meshes.get(entity)?;
|
||||
|
||||
Some(DrawCall {
|
||||
vertex_buffer: mesh_component.mesh.vertex_buffer.clone(),
|
||||
index_buffer: mesh_component.mesh.index_buffer.clone(),
|
||||
num_indices: mesh_component.mesh.num_indices,
|
||||
model: transform.to_matrix(),
|
||||
pipeline: mesh_component.pipeline,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
38
src/systems/state_machine.rs
Normal file
38
src/systems/state_machine.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::world::World;
|
||||
|
||||
pub fn state_machine_system(world: &mut World, delta: f32)
|
||||
{
|
||||
let entities: Vec<_> = world.state_machines.all();
|
||||
|
||||
for entity in entities
|
||||
{
|
||||
if let Some(mut state_machine) = world.state_machines.components.remove(&entity)
|
||||
{
|
||||
state_machine.update(world, delta);
|
||||
world
|
||||
.state_machines
|
||||
.components
|
||||
.insert(entity, state_machine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_machine_physics_system(world: &mut World, delta: f32)
|
||||
{
|
||||
let entities: Vec<_> = world.state_machines.all();
|
||||
|
||||
for entity in entities
|
||||
{
|
||||
if let Some(mut state_machine) = world.state_machines.components.remove(&entity)
|
||||
{
|
||||
if let Some(current_state) = state_machine.get_current_state_mut()
|
||||
{
|
||||
current_state.on_state_physics_update(world, delta);
|
||||
}
|
||||
world
|
||||
.state_machines
|
||||
.components
|
||||
.insert(entity, state_machine);
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/terrain.rs
Normal file
167
src/terrain.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use exr::prelude::{ReadChannels, ReadLayers};
|
||||
use glam::{Vec2, Vec3};
|
||||
use nalgebra::{vector, DMatrix};
|
||||
use rapier3d::{
|
||||
math::Isometry,
|
||||
prelude::{ColliderBuilder, RigidBodyBuilder},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
components::{MeshComponent, PhysicsComponent},
|
||||
entity::EntityHandle,
|
||||
mesh::{Mesh, Vertex},
|
||||
physics::PhysicsManager,
|
||||
render,
|
||||
world::{Transform, World},
|
||||
};
|
||||
|
||||
pub struct Terrain;
|
||||
|
||||
impl Terrain
|
||||
{
|
||||
pub fn spawn(
|
||||
world: &mut World,
|
||||
heightmap_path: &str,
|
||||
height_scale: f32,
|
||||
) -> anyhow::Result<EntityHandle>
|
||||
{
|
||||
let entity = world.spawn();
|
||||
|
||||
let plane_size = Vec2::new(100.0, 100.0);
|
||||
|
||||
let plane_mesh = render::with_device(|device| {
|
||||
Mesh::create_plane_mesh(device, plane_size.x, plane_size.y, 100, 100)
|
||||
});
|
||||
|
||||
let transform = Transform::IDENTITY;
|
||||
|
||||
world.transforms.insert(entity, transform);
|
||||
world.meshes.insert(
|
||||
entity,
|
||||
MeshComponent {
|
||||
mesh: Rc::new(plane_mesh),
|
||||
pipeline: render::Pipeline::Terrain,
|
||||
},
|
||||
);
|
||||
|
||||
let heights = Self::load_heightfield_data(heightmap_path)?;
|
||||
|
||||
println!(
|
||||
"Heightmap dimensions: {} rows × {} cols",
|
||||
heights.nrows(),
|
||||
heights.ncols()
|
||||
);
|
||||
|
||||
let scale = vector![plane_size.x, height_scale, plane_size.y,];
|
||||
|
||||
let body = RigidBodyBuilder::fixed()
|
||||
.translation(transform.get_position().into())
|
||||
.build();
|
||||
|
||||
let rigidbody_handle = PhysicsManager::add_rigidbody(body);
|
||||
|
||||
let collider = ColliderBuilder::heightfield(heights.clone(), scale).build();
|
||||
|
||||
let collider_handle = PhysicsManager::add_collider(collider, Some(rigidbody_handle));
|
||||
|
||||
PhysicsManager::set_heightfield_data(heights, scale, transform.get_position().into());
|
||||
|
||||
world.physics.insert(
|
||||
entity,
|
||||
PhysicsComponent {
|
||||
rigidbody: rigidbody_handle,
|
||||
collider: Some(collider_handle),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(entity)
|
||||
}
|
||||
|
||||
fn load_heightfield_data(path: &str) -> anyhow::Result<DMatrix<f32>>
|
||||
{
|
||||
let image = exr::prelude::read()
|
||||
.no_deep_data()
|
||||
.largest_resolution_level()
|
||||
.all_channels()
|
||||
.all_layers()
|
||||
.all_attributes()
|
||||
.from_file(path)?;
|
||||
|
||||
let layer = &image.layer_data[0];
|
||||
let channel = &layer.channel_data.list[0];
|
||||
|
||||
let width = layer.size.width();
|
||||
let height = layer.size.height();
|
||||
|
||||
let heights: Vec<f32> = channel.sample_data.values_as_f32().collect();
|
||||
|
||||
Ok(DMatrix::from_row_slice(height, width, &heights))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_terrain_render_pipeline(
|
||||
device: &wgpu::Device,
|
||||
config: &wgpu::SurfaceConfiguration,
|
||||
bind_group_layout: &wgpu::BindGroupLayout,
|
||||
) -> wgpu::RenderPipeline
|
||||
{
|
||||
let shader_source =
|
||||
std::fs::read_to_string("shaders/terrain.wgsl").expect("Failed to read terrain shader");
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Terrain Shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
||||
});
|
||||
|
||||
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Terrain Render Pipeline Layout"),
|
||||
bind_group_layouts: &[bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Terrain Render Pipeline"),
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Vertex::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(wgpu::BlendState::REPLACE),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: true,
|
||||
depth_compare: wgpu::CompareFunction::Less,
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
188
src/utility/input.rs
Executable file
188
src/utility/input.rs
Executable file
@@ -0,0 +1,188 @@
|
||||
use glam::Vec2;
|
||||
use sdl3::{event::Event, keyboard::Keycode};
|
||||
|
||||
pub struct InputState
|
||||
{
|
||||
pub w: bool,
|
||||
pub a: bool,
|
||||
pub s: bool,
|
||||
pub d: bool,
|
||||
pub space: bool,
|
||||
pub shift: bool,
|
||||
|
||||
pub space_just_pressed: bool,
|
||||
pub noclip_just_pressed: bool,
|
||||
|
||||
pub mouse_delta: (f32, f32),
|
||||
pub mouse_captured: bool,
|
||||
pub noclip_mode: bool,
|
||||
pub quit_requested: bool,
|
||||
}
|
||||
|
||||
impl InputState
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
w: false,
|
||||
a: false,
|
||||
s: false,
|
||||
d: false,
|
||||
space: false,
|
||||
shift: false,
|
||||
space_just_pressed: false,
|
||||
noclip_just_pressed: false,
|
||||
mouse_delta: (0.0, 0.0),
|
||||
mouse_captured: true,
|
||||
noclip_mode: false,
|
||||
quit_requested: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: &Event) -> bool
|
||||
{
|
||||
match event
|
||||
{
|
||||
Event::Quit { .. } =>
|
||||
{
|
||||
self.quit_requested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Event::KeyDown {
|
||||
keycode: Some(key),
|
||||
repeat,
|
||||
..
|
||||
} =>
|
||||
{
|
||||
if !repeat
|
||||
{
|
||||
self.handle_keydown(*key);
|
||||
|
||||
if *key == Keycode::Escape
|
||||
{
|
||||
self.quit_requested = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if *key == Keycode::I
|
||||
{
|
||||
self.mouse_captured = !self.mouse_captured;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Event::KeyUp {
|
||||
keycode: Some(key), ..
|
||||
} =>
|
||||
{
|
||||
self.handle_keyup(*key);
|
||||
}
|
||||
|
||||
Event::MouseMotion { xrel, yrel, .. } =>
|
||||
{
|
||||
self.handle_mouse_motion(*xrel as f32, *yrel as f32);
|
||||
}
|
||||
|
||||
_ =>
|
||||
{}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn handle_keydown(&mut self, key: Keycode)
|
||||
{
|
||||
match key
|
||||
{
|
||||
Keycode::W => self.w = true,
|
||||
Keycode::A => self.a = true,
|
||||
Keycode::S => self.s = true,
|
||||
Keycode::D => self.d = true,
|
||||
Keycode::Space =>
|
||||
{
|
||||
if !self.space
|
||||
{
|
||||
self.space_just_pressed = true;
|
||||
}
|
||||
self.space = true;
|
||||
}
|
||||
Keycode::LShift | Keycode::RShift => self.shift = true,
|
||||
Keycode::N => self.noclip_just_pressed = true,
|
||||
_ =>
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_keyup(&mut self, key: Keycode)
|
||||
{
|
||||
match key
|
||||
{
|
||||
Keycode::W => self.w = false,
|
||||
Keycode::A => self.a = false,
|
||||
Keycode::S => self.s = false,
|
||||
Keycode::D => self.d = false,
|
||||
Keycode::Space => self.space = false,
|
||||
Keycode::LShift | Keycode::RShift => self.shift = false,
|
||||
_ =>
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mouse_motion(&mut self, xrel: f32, yrel: f32)
|
||||
{
|
||||
if self.mouse_captured
|
||||
{
|
||||
self.mouse_delta = (xrel, yrel);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_post_events(&mut self)
|
||||
{
|
||||
if self.noclip_just_pressed
|
||||
{
|
||||
self.noclip_mode = !self.noclip_mode;
|
||||
println!(
|
||||
"Noclip mode: {}",
|
||||
if self.noclip_mode { "ON" } else { "OFF" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_just_pressed(&mut self)
|
||||
{
|
||||
self.space_just_pressed = false;
|
||||
self.noclip_just_pressed = false;
|
||||
self.mouse_delta = (0.0, 0.0);
|
||||
}
|
||||
|
||||
pub fn get_movement_input(&self) -> Vec2
|
||||
{
|
||||
let mut input = Vec2::ZERO;
|
||||
|
||||
if self.w
|
||||
{
|
||||
input.y += 1.0;
|
||||
}
|
||||
if self.s
|
||||
{
|
||||
input.y -= 1.0;
|
||||
}
|
||||
if self.a
|
||||
{
|
||||
input.x -= 1.0;
|
||||
}
|
||||
if self.d
|
||||
{
|
||||
input.x += 1.0;
|
||||
}
|
||||
|
||||
if input.length_squared() > 1.0
|
||||
{
|
||||
input = input.normalize();
|
||||
}
|
||||
|
||||
input
|
||||
}
|
||||
}
|
||||
3
src/utility/mod.rs
Normal file
3
src/utility/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod input;
|
||||
pub(crate) mod time;
|
||||
pub(crate) mod transform;
|
||||
22
src/utility/time.rs
Normal file
22
src/utility/time.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Instant;
|
||||
|
||||
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
|
||||
{
|
||||
GAME_START
|
||||
.get()
|
||||
.map(|start| start.elapsed().as_secs_f32())
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
}
|
||||
143
src/utility/transform.rs
Normal file
143
src/utility/transform.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use glam::{Mat4, Quat, Vec3};
|
||||
use nalgebra::{self as na, Isometry3};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Transform
|
||||
{
|
||||
pub position: Vec3,
|
||||
pub rotation: Quat,
|
||||
pub scale: Vec3,
|
||||
}
|
||||
|
||||
impl Transform
|
||||
{
|
||||
pub const IDENTITY: Self = Self {
|
||||
position: Vec3::ZERO,
|
||||
rotation: Quat::IDENTITY,
|
||||
scale: Vec3::ONE,
|
||||
};
|
||||
|
||||
pub fn from_position(position: Vec3) -> Self
|
||||
{
|
||||
Self::IDENTITY.translated(position)
|
||||
}
|
||||
|
||||
pub fn to_matrix(&self) -> Mat4
|
||||
{
|
||||
Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
|
||||
}
|
||||
|
||||
pub fn get_matrix(&self) -> Mat4
|
||||
{
|
||||
self.to_matrix()
|
||||
}
|
||||
|
||||
pub fn translated(mut self, new_position: Vec3) -> Self
|
||||
{
|
||||
self.set_position(new_position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> Vec3
|
||||
{
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, position: Vec3)
|
||||
{
|
||||
self.position = position;
|
||||
}
|
||||
|
||||
pub fn set_rotation(&mut self, rotation: Quat)
|
||||
{
|
||||
self.rotation = rotation;
|
||||
}
|
||||
|
||||
pub fn set_scale(&mut self, scale: Vec3)
|
||||
{
|
||||
self.scale = scale;
|
||||
}
|
||||
|
||||
pub fn set_uniform_scale(&mut self, scale: f32)
|
||||
{
|
||||
self.scale = Vec3::splat(scale);
|
||||
}
|
||||
|
||||
pub fn set_matrix(&mut self, matrix: Mat4)
|
||||
{
|
||||
let (scale, rotation, translation) = matrix.to_scale_rotation_translation();
|
||||
self.position = translation;
|
||||
self.rotation = rotation;
|
||||
self.scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::IDENTITY
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Transform> for Mat4
|
||||
{
|
||||
fn from(t: Transform) -> Self
|
||||
{
|
||||
t.to_matrix()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Transform> for Mat4
|
||||
{
|
||||
fn from(t: &Transform) -> Self
|
||||
{
|
||||
t.to_matrix()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Transform> for Isometry3<f32>
|
||||
{
|
||||
fn from(t: Transform) -> Self
|
||||
{
|
||||
let translation = na::Vector3::new(t.position.x, t.position.y, t.position.z);
|
||||
let rotation = na::UnitQuaternion::from_quaternion(na::Quaternion::new(
|
||||
t.rotation.w,
|
||||
t.rotation.x,
|
||||
t.rotation.y,
|
||||
t.rotation.z,
|
||||
));
|
||||
Isometry3::from_parts(translation.into(), rotation)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Transform> for Isometry3<f32>
|
||||
{
|
||||
fn from(t: &Transform) -> Self
|
||||
{
|
||||
(*t).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Isometry3<f32>> for Transform
|
||||
{
|
||||
fn from(iso: Isometry3<f32>) -> Self
|
||||
{
|
||||
let pos = iso.translation.vector;
|
||||
let rot = iso.rotation;
|
||||
|
||||
Self {
|
||||
position: Vec3::new(pos.x, pos.y, pos.z),
|
||||
rotation: Quat::from_xyzw(rot.i, rot.j, rot.k, rot.w),
|
||||
scale: Vec3::ONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Isometry3<f32>> for Transform
|
||||
{
|
||||
fn from(iso: &Isometry3<f32>) -> Self
|
||||
{
|
||||
(*iso).into()
|
||||
}
|
||||
}
|
||||
518
src/world.rs
Normal file
518
src/world.rs
Normal file
@@ -0,0 +1,518 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::components::jump::JumpComponent;
|
||||
use crate::components::{
|
||||
CameraComponent, CameraFollowComponent, InputComponent, MeshComponent, MovementComponent,
|
||||
PhysicsComponent,
|
||||
};
|
||||
use crate::entity::{EntityHandle, EntityManager};
|
||||
use crate::state::StateMachine;
|
||||
|
||||
pub use crate::utility::transform::Transform;
|
||||
|
||||
pub struct TransformStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, Transform>,
|
||||
}
|
||||
|
||||
impl TransformStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: Transform)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&Transform>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut Transform>
|
||||
{
|
||||
self.components.get_mut(&entity)
|
||||
}
|
||||
|
||||
pub fn with<F, R>(&self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&Transform) -> R,
|
||||
{
|
||||
self.components.get(&entity).map(f)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MeshStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, MeshComponent>,
|
||||
}
|
||||
|
||||
impl MeshStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: MeshComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&MeshComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhysicsStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, PhysicsComponent>,
|
||||
}
|
||||
|
||||
impl PhysicsStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: PhysicsComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<PhysicsComponent>
|
||||
{
|
||||
self.components.get(&entity).copied()
|
||||
}
|
||||
|
||||
pub fn with<F, R>(&self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&PhysicsComponent) -> R,
|
||||
{
|
||||
self.components.get(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MovementStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, MovementComponent>,
|
||||
}
|
||||
|
||||
impl MovementStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: MovementComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&MovementComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn with<F, R>(&self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&MovementComponent) -> R,
|
||||
{
|
||||
self.components.get(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut MovementComponent) -> R,
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JumpStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, JumpComponent>,
|
||||
}
|
||||
|
||||
impl JumpStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: JumpComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&JumpComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut JumpComponent>
|
||||
{
|
||||
self.components.get_mut(&entity)
|
||||
}
|
||||
|
||||
pub fn with<F, R>(&self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&JumpComponent) -> R,
|
||||
{
|
||||
self.components.get(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut JumpComponent) -> R,
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, InputComponent>,
|
||||
}
|
||||
|
||||
impl InputStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: InputComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&InputComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn with<F, R>(&self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&InputComponent) -> R,
|
||||
{
|
||||
self.components.get(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut InputComponent) -> R,
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerTagStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, ()>,
|
||||
}
|
||||
|
||||
impl PlayerTagStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.insert(entity, ());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StateMachineStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, StateMachine>,
|
||||
}
|
||||
|
||||
impl StateMachineStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: StateMachine)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut StateMachine) -> R,
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CameraStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, CameraComponent>,
|
||||
}
|
||||
|
||||
impl CameraStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: CameraComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&CameraComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut CameraComponent>
|
||||
{
|
||||
self.components.get_mut(&entity)
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut CameraComponent) -> R,
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
|
||||
pub fn get_active(&self) -> Option<(EntityHandle, &CameraComponent)>
|
||||
{
|
||||
self.components
|
||||
.iter()
|
||||
.find(|(_, cam)| cam.is_active)
|
||||
.map(|(e, c)| (*e, c))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CameraFollowStorage
|
||||
{
|
||||
pub components: HashMap<EntityHandle, CameraFollowComponent>,
|
||||
}
|
||||
|
||||
impl CameraFollowStorage
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
components: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, entity: EntityHandle, component: CameraFollowComponent)
|
||||
{
|
||||
self.components.insert(entity, component);
|
||||
}
|
||||
|
||||
pub fn get(&self, entity: EntityHandle) -> Option<&CameraFollowComponent>
|
||||
{
|
||||
self.components.get(&entity)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, entity: EntityHandle) -> Option<&mut CameraFollowComponent>
|
||||
{
|
||||
self.components.get_mut(&entity)
|
||||
}
|
||||
|
||||
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut CameraFollowComponent) -> R,
|
||||
{
|
||||
self.components.get_mut(&entity).map(f)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.components.remove(&entity);
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<EntityHandle>
|
||||
{
|
||||
self.components.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct World
|
||||
{
|
||||
pub entities: EntityManager,
|
||||
pub transforms: TransformStorage,
|
||||
pub meshes: MeshStorage,
|
||||
pub physics: PhysicsStorage,
|
||||
pub movements: MovementStorage,
|
||||
pub jumps: JumpStorage,
|
||||
pub inputs: InputStorage,
|
||||
pub player_tags: PlayerTagStorage,
|
||||
pub state_machines: StateMachineStorage,
|
||||
pub cameras: CameraStorage,
|
||||
pub camera_follows: CameraFollowStorage,
|
||||
}
|
||||
|
||||
impl World
|
||||
{
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
entities: EntityManager::new(),
|
||||
transforms: TransformStorage::new(),
|
||||
meshes: MeshStorage::new(),
|
||||
physics: PhysicsStorage::new(),
|
||||
movements: MovementStorage::new(),
|
||||
jumps: JumpStorage::new(),
|
||||
inputs: InputStorage::new(),
|
||||
player_tags: PlayerTagStorage::new(),
|
||||
state_machines: StateMachineStorage::new(),
|
||||
cameras: CameraStorage::new(),
|
||||
camera_follows: CameraFollowStorage::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&mut self) -> EntityHandle
|
||||
{
|
||||
self.entities.spawn()
|
||||
}
|
||||
|
||||
pub fn despawn(&mut self, entity: EntityHandle)
|
||||
{
|
||||
self.transforms.remove(entity);
|
||||
self.meshes.remove(entity);
|
||||
self.physics.remove(entity);
|
||||
self.movements.remove(entity);
|
||||
self.jumps.remove(entity);
|
||||
self.inputs.remove(entity);
|
||||
self.player_tags.remove(entity);
|
||||
self.state_machines.remove(entity);
|
||||
self.cameras.remove(entity);
|
||||
self.camera_follows.remove(entity);
|
||||
self.entities.despawn(entity);
|
||||
}
|
||||
}
|
||||
BIN
textures/height_map_x0_y0.exr
Executable file
BIN
textures/height_map_x0_y0.exr
Executable file
Binary file not shown.
Reference in New Issue
Block a user