rendering, physics, player and camera WIP

This commit is contained in:
Jonas H
2026-01-01 19:54:00 +01:00
commit 5d2eca0393
51 changed files with 8734 additions and 0 deletions

656
CLAUDE.md Normal file
View 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

File diff suppressed because it is too large Load Diff

19
Cargo.toml Normal file
View 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

Binary file not shown.

BIN
blender/player_mesh.blend1 Normal file

Binary file not shown.

195
meshes/burrs.gltf Executable file

File diff suppressed because one or more lines are too long

BIN
meshes/player_mesh.glb Normal file

Binary file not shown.

2
rustfmt.toml Normal file
View File

@@ -0,0 +1,2 @@
brace_style = "AlwaysNextLine"
control_brace_style = "AlwaysNextLine"

28
shaders/blit.wgsl Normal file
View 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
View 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
View 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
View 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
View 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()
}
}

View 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
View 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
View 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
View 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
View 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;

View 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,
}
}
}

View File

@@ -0,0 +1,8 @@
use rapier3d::prelude::{ColliderHandle, RigidBodyHandle};
#[derive(Copy, Clone, Debug)]
pub struct PhysicsComponent
{
pub rigidbody: RigidBodyHandle,
pub collider: Option<ColliderHandle>,
}

View File

@@ -0,0 +1,2 @@
#[derive(Copy, Clone, Debug)]
pub struct PlayerTag;

View File

@@ -0,0 +1 @@

149
src/debug/collider_debug.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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};

View 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
View 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()
}

View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
pub(crate) mod input;
pub(crate) mod time;
pub(crate) mod transform;

22
src/utility/time.rs Normal file
View 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
View 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
View 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

Binary file not shown.