commit 5d2eca0393c9f9a875641f565093f7793342af29 Author: Jonas H Date: Thu Jan 1 19:54:00 2026 +0100 rendering, physics, player and camera WIP diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..74b0704 --- /dev/null +++ b/CLAUDE.md @@ -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` +- No `Rc>` - 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>` 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` 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 +- 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>`** - 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, +} + +impl TransformStorage { + pub fn insert(&mut self, entity: EntityHandle, component: Transform) { } + pub fn get(&self, entity: EntityHandle) -> Option<&Transform> { } + pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option { } + pub fn remove(&mut self, entity: EntityHandle) { } + pub fn all(&self) -> Vec { } +} +``` + +**Key Features:** +- No `Rc>` 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-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::(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 = 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>` - 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. diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..e353c72 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2457 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags", + "core-foundation", + "libc", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" + +[[package]] +name = "glam" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b" + +[[package]] +name = "glam" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16" + +[[package]] +name = "glam" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776" + +[[package]] +name = "glam" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34" + +[[package]] +name = "glam" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca" + +[[package]] +name = "glam" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f" + +[[package]] +name = "glam" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" + +[[package]] +name = "glam" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774" + +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + +[[package]] +name = "glam" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "glam" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gltf" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" +dependencies = [ + "base64", + "byteorder", + "gltf-json", + "image", + "lazy_static", + "serde_json", + "urlencoding", +] + +[[package]] +name = "gltf-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" +dependencies = [ + "inflections", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gltf-json" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" +dependencies = [ + "gltf-derive", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.0", + "zune-jpeg 0.5.7", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.17", + "unicode-ident", +] + +[[package]] +name = "nalgebra" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d5b3eff5cd580f93da45e64715e8c20a3996342f1e466599cf7a267a0c2f5f" +dependencies = [ + "approx", + "glam 0.14.0", + "glam 0.15.2", + "glam 0.16.0", + "glam 0.17.3", + "glam 0.18.0", + "glam 0.19.0", + "glam 0.20.5", + "glam 0.21.3", + "glam 0.22.0", + "glam 0.23.0", + "glam 0.24.2", + "glam 0.25.0", + "glam 0.27.0", + "glam 0.28.0", + "glam 0.29.3", + "glam 0.30.9", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parry3d" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99471b7b6870f7fe406d5611dd4b4c9b07aa3e5436b1d27e1515f9832bb0c6b" +dependencies = [ + "approx", + "arrayvec", + "bitflags", + "downcast-rs", + "either", + "ena", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "log", + "nalgebra", + "num-derive", + "num-traits", + "ordered-float", + "rstar", + "simba", + "slab", + "smallvec", + "spade", + "static_assertions", + "thiserror 2.0.17", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "portable-atomic" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + +[[package]] +name = "rapier3d" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68073fdc88f6b709002767ce8deffffb05ac06824bf9f98a23e270bcea64ba9f" +dependencies = [ + "approx", + "arrayvec", + "bit-vec", + "bitflags", + "downcast-rs", + "log", + "nalgebra", + "num-derive", + "num-traits", + "ordered-float", + "parry3d", + "profiling", + "rustc-hash 2.1.1", + "simba", + "static_assertions", + "thiserror 2.0.17", + "wide", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdl3" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ad6a0142275b5a39c20051c071ab6719068d3ecb345cc96f78b9818e73e44a" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "objc2", + "raw-window-handle", + "sdl3-sys", +] + +[[package]] +name = "sdl3-sys" +version = "0.5.11+SDL3-3.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73979b5f78819ede7fb6b7534161fe70f3d7a56cc09e7e29c7b58c2b525abfa6" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snow_trail_sdl" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytemuck", + "exr", + "glam 0.30.9", + "gltf", + "half", + "image", + "kurbo", + "nalgebra", + "pollster", + "rapier3d", + "sdl3", + "wgpu", +] + +[[package]] +name = "spade" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990" +dependencies = [ + "hashbrown 0.15.5", + "num-traits", + "robust", + "smallvec", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.17", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.17", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.17", + "web-sys", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d915729b0e7d5fe35c2f294c5dc10b30207cc637920e5b59077bfa3da63f28" +dependencies = [ + "zune-core 0.5.0", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0536837 --- /dev/null +++ b/Cargo.toml @@ -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"] } diff --git a/blender/player_mesh.blend b/blender/player_mesh.blend new file mode 100644 index 0000000..df1d9b2 Binary files /dev/null and b/blender/player_mesh.blend differ diff --git a/blender/player_mesh.blend1 b/blender/player_mesh.blend1 new file mode 100644 index 0000000..44076c2 Binary files /dev/null and b/blender/player_mesh.blend1 differ diff --git a/meshes/burrs.gltf b/meshes/burrs.gltf new file mode 100755 index 0000000..f54f690 --- /dev/null +++ b/meshes/burrs.gltf @@ -0,0 +1,195 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.3.47", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0, + 1, + 2 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"hook" + }, + { + "name":"hook_thickness", + "rotation":[ + 0.7071068286895752, + 0, + 0, + 0.7071068286895752 + ], + "translation":[ + 0, + 1.12388014793396, + 0 + ] + }, + { + "mesh":1, + "name":"loop" + } + ], + "meshes":[ + { + "name":"hook", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3 + } + ] + }, + { + "name":"loop", + "primitives":[ + { + "attributes":{ + "POSITION":4, + "NORMAL":5, + "TEXCOORD_0":6 + }, + "indices":7 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":82, + "max":[ + 0.25674867630004883, + 1.1536540985107422, + 0.11764351278543472 + ], + "min":[ + -0.18208253383636475, + -0.0005484152352437377, + -0.10493266582489014 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":82, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":82, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":348, + "type":"SCALAR" + }, + { + "bufferView":4, + "componentType":5126, + "count":318, + "max":[ + 0.4516136944293976, + 0.8688175678253174, + 0.49221301078796387 + ], + "min":[ + -0.5062326788902283, + -0.06346084177494049, + -0.40157178044319153 + ], + "type":"VEC3" + }, + { + "bufferView":5, + "componentType":5126, + "count":318, + "type":"VEC3" + }, + { + "bufferView":6, + "componentType":5126, + "count":318, + "type":"VEC2" + }, + { + "bufferView":7, + "componentType":5123, + "count":1440, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":984, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":984, + "byteOffset":984, + "target":34962 + }, + { + "buffer":0, + "byteLength":656, + "byteOffset":1968, + "target":34962 + }, + { + "buffer":0, + "byteLength":696, + "byteOffset":2624, + "target":34963 + }, + { + "buffer":0, + "byteLength":3816, + "byteOffset":3320, + "target":34962 + }, + { + "buffer":0, + "byteLength":3816, + "byteOffset":7136, + "target":34962 + }, + { + "buffer":0, + "byteLength":2544, + "byteOffset":10952, + "target":34962 + }, + { + "buffer":0, + "byteLength":2880, + "byteOffset":13496, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":16376, + "uri":"data:application/octet-stream;base64,8FS0PYfDDzqc8Ue9D76wPVvqDDq9YFQ9bLflutRKNrcuKc4971S0vYbDD7qg8Uc9D76wvVvqDLq+YFS9zbflOvRKNjcvKc69E61HPX5tYz8p/SS9zRhCPX5tYz+Ao1c9hTbNPeAIVD8Ilcw9ZBUePkKkRD/4D149dnofPkKkRD+1kB69y8rSPeAIVD+iC7C9E61HPX5tYz8p/SS9E61HPX5tYz8p/SS9zRhCPX5tYz+Ao1c9hTbNPeAIVD8Ilcw9ZBUePkKkRD/4D149dnofPkKkRD+1kB69y8rSPeAIVD+iC7C930qRPaPTcD/Lw0i930qRPaPTcD/Lw0i92oGPPfQkcT9un3s9g3AmPh95az8V7/A9TgKDPvl7ZT/X3IE9kHSDPqgqZT+PqUC9iDkoPn3Waj8ldNO90yiQPWRUcz/vrUe90yiQPWRUcz/vrUe9JymOPeDlcz8EsXc9JwMdPuAffz8vdOs9kvFzPjLkhD/EwHY9aPF0PnSbhD8ynki91AIfPuj8fT/G6tO9WLBdPT7adj9BkkW9WLBdPT7adj9BkkW9OyRbPYxRdz+EzG89ESHLPboLhj9A/eI9CvsUPgczkD+0yWY9EZ4VPmD3jz8WlU69L63NPWyUhT+IYdK9FOxJPQxLeD/8cUK9FOxJPQxLeD/8cUK9siZLPX2yeD/7/Ww9mdM3PfMbiD+dlt893kUjPfCqkz+G7GE9PgsiPTh3kz95g029WV41PYO0hz9c2c+9rEhAPUsjdz9THUG9rEhAPUsjdz9THUG9og4+Pb13dz9MBG89L/7TuwRhhT8WHuE9JtRwvfDbjj9hUmU9HZpuvbexjj9Ez0q9qV2wu5IMhT+QA8+9YD+TPI+zbz84IUS9YD+TPI+zbz84IUS9WHyFPFqubz9jP289kZyMvSs/eT+MzeE9vJMbvphqgT8slmQ9WtsZvjNtgT93yk69DbuFvZVJeT8Uk9G9cOaPO67iQD+zaEu9cOaPO67iQD+zaEu94A8pO1O0QD/1Xmk9i6q5vUUfQD9BpN492HM6vpK4Pz9calw95Jg4vu3mPz9TXVi9vj6yvft7QD9vI9a9zOBcPeE9zD4bGk+9zOBcPeE9zD4bGk+92npVPfj6yz59JmE92fEhvW9Txj6qWdk9KH4Evs/uwD6U/1M9rKQCvrgxwT4IQVy9+SUTvUHZxj7w5ta98FS0PYfDDzqc8Ue98FS0PYfDDzqc8Ue9D76wPVvqDDq9YFQ9bLflutRKNrcuKc4971S0vYbDD7qg8Uc9D76wvVvqDLq+YFS9zbflOvRKNjcvKc69XyjLO77+f78AAAAAXyjLO77+f78AAAAAXyjLO77+f78AAAAAXyjLO77+f78AAAAAXyjLO77+f78AAAAAXyjLO77+f78AAAAAhjw/v8kqKr/5ljK8hjw/v8kqKr/5ljK8hjw/v8kqKr/5ljK8hjw/v8kqKr/5ljK8hjw/v8kqKr/5ljK8hjw/v8kqKr/5ljK8KexNv8EcmD6itQO/KexNv8EcmD6itQO/hjJQv93fmj6Qgf4+JoXAvN7NLr6uK3w/wFYsP856FL+d2uo+oAAvP2MRFr9hnt6+3cW6u/HZOb57vnu/N7VTv9242j0qTw2/N7VTv9242j0qTw2/qPBUv1gx9z1Jswo/hsKGPWs0kb2uzH4/k1VYP6nReL5U2vM+ooRaP6X+gb6x5+i+mYuoPdqNwr1M+H2/cXE9v8hEur5U0hC/cXE9v8hEur5U0hC/kGdAv+NvrL4KMxE/W3akPTLirT3vPn4/lVFJP0Yz0z79aus+mHdLP7QVyj61B+y+5IfJPfuBNz3Nf36/9BEVvzHWJr+P1fi+9BEVvzHWJr+P1fi+1u8Yv1XyIr/ywfk+L+5rOkA/xj00zH4/NqHYPrPiSD9Q8Oc+yfvaPiN7RD8ze/S+sxFQPO1veT0SgX+/Bw0+PJpXOL8JnDG/Bw0+PJpXOL8JnDG/2KnxPDSvNL9GMjU/uY5tvBKGBT6GyX0/MORQvV5fZD8G4uU+r3FXvZW8YD+ttPO+Gr2fvA9cyz2Xr36/OGQpP6McGL/pIeq+OGQpP6McGL/pIeq+MWgjPyTXFL9bJwE/8zgJvR4glj3NKn8/bxMBv59kOz+Hmeo+mwwAv1wcOT+/6fO+SYfJvJxxTj3ZmH+/BOFDP9iorb6cGQy/BOFDP9iorb6cGQy/u9o+P3HUrb4o0hI/7D/XvYTzMD2AV34/Hm9QvyoJuT7Kpug+kwBNvwKZuT5ZHvS++ceYvSvpNz0cB3+/pcVdP8bt6zxjVf++pcVdP8bt6zxjVf++ZFZZP4qUsjz4KQc/fwxgvU1kqrsAnX8/Y1Viv0iYf7yxG+8+AyZev/sAGrw9Z/6+8nilvDoRAzyJ8H+/RPVeP0Xkxz1zlva+RPVeP0Xkxz1zlva+WbhaP/VAvj2Z4wI/jY8gvC8NPrxy+H8/sdNdv3WV5b1DCvk+MlFZv+1W2709gQS/HbTIPF0SA7sz7H+/i9dgP2bplT296vG+i9dgP2bplT296vG+w8JcPwXHjz2JXQA/euRHPPFGWLxq9X8/8+Vcv6/nyL1G2P0+6ylYv3fxwb21/Aa/FxlAPWOK27tptn+/AAAAAAAAgD/NzEw+AACAP83MzD4AAIA/mpkZPwAAgD/NzEw/AACAPwAAgD8AAIA/AAAAAAAAgD/NzEw+AACAP83MzD4AAIA/mpkZPwAAgD/NzEw/AACAPwAAgD8AAIA/AAAAAAAAAAAAAAAAAACAPwAAAABVVVU/AAAAAKqqKj8AAAAAAAAAPwAAAACqqqo+AAAAAKyqKj45juM9AAAAADmO4z0AAIA/OY7jPVVVVT85juM9qqoqPzmO4z0AAAA/OY7jPaqqqj45juM9rKoqPjmOYz4AAAAAOY5jPgAAgD85jmM+VVVVPzmOYz6qqio/OY5jPgAAAD85jmM+qqqqPjmOYz6sqio+q6qqPgAAAACrqqo+AACAP6uqqj5VVVU/q6qqPqqqKj+rqqo+AAAAP6uqqj6qqqo+q6qqPqyqKj45juM+AAAAADmO4z4AAIA/OY7jPlVVVT85juM+qqoqPzmO4z4AAAA/OY7jPqqqqj45juM+rKoqPuQ4Dj8AAAAA5DgOPwAAgD/kOA4/VVVVP+Q4Dj+qqio/5DgOPwAAAD/kOA4/qqqqPuQ4Dj+sqio+q6oqPwAAAACrqio/AACAP6uqKj9VVVU/q6oqP6qqKj+rqio/AAAAP6uqKj+qqqo+q6oqP6yqKj5yHEc/AAAAAHIcRz8AAIA/chxHP1VVVT9yHEc/qqoqP3IcRz8AAAA/chxHP6qqqj5yHEc/rKoqPjmOYz8AAAAAOY5jPwAAgD85jmM/VVVVPzmOYz+qqio/OY5jPwAAAD85jmM/qqqqPjmOYz+sqio+AACAPwAAAAAAAIA/AACAPwAAgD9VVVU/AACAP6qqKj8AAIA/AAAAPwAAgD+qqqo+AACAP6yqKj4EAAIAAwAFAAIABAAFAAEAAgAAAAEABQAJAAsACgAIAAsACQAIAAYACwAHAAYACAAMABMAGQAMABkAEgAOABUAFAAOABQADQAPABYAFQAPABUADgAQABcAFgAQABYADwARABgAFwARABcAEAASABkAGAASABgAEQATABoAIAATACAAGQAVABwAGwAVABsAFAAWAB0AHAAWABwAFQAXAB4AHQAXAB0AFgAYAB8AHgAYAB4AFwAZACAAHwAZAB8AGAAaACEAJwAaACcAIAAcACMAIgAcACIAGwAdACQAIwAdACMAHAAeACUAJAAeACQAHQAfACYAJQAfACUAHgAgACcAJgAgACYAHwAhACgALgAhAC4AJwAjACoAKQAjACkAIgAkACsAKgAkACoAIwAlACwAKwAlACsAJAAmAC0ALAAmACwAJQAnAC4ALQAnAC0AJgAoAC8ANQAoADUALgAqADEAMAAqADAAKQArADIAMQArADEAKgAsADMAMgAsADIAKwAtADQAMwAtADMALAAuADUANAAuADQALQAvADYAPAAvADwANQAxADgANwAxADcAMAAyADkAOAAyADgAMQAzADoAOQAzADkAMgA0ADsAOgA0ADoAMwA1ADwAOwA1ADsANAA2AD0AQwA2AEMAPAA4AD8APgA4AD4ANwA5AEAAPwA5AD8AOAA6AEEAQAA6AEAAOQA7AEIAQQA7AEEAOgA8AEMAQgA8AEIAOwA9AEQASgA9AEoAQwA/AEYARQA/AEUAPgBAAEcARgBAAEYAPwBBAEgARwBBAEcAQABCAEkASABCAEgAQQBDAEoASQBDAEkAQgBEAEsAUQBEAFEASgBGAE0ATABGAEwARQBHAE4ATQBHAE0ARgBIAE8ATgBIAE4ARwBJAFAATwBJAE8ASABKAFEAUABKAFAASQDN5C8724hLPwYBqL7N5C8724hLPwYBqL5trgE61GpeP9Gazb5trgE61GpeP9Gazb4F3nk+dGM4PxkohL4F3nk+dGM4PxkohL4F3nk+dGM4PxkohL6aL5g+bt5JPwQeor6aL5g+bt5JPwQeor6aL5g+bt5JPwQeor79p7g+Bd4gPnlNgr79p7g+Bd4gPnlNgr6CJ+E+/lXjPSFkmL6CJ+E+/lXjPSFkmL4mDIM+Wv8xP1p+PD4mDIM+Wv8xP1p+PD4mDIM+Wv8xP1p+PD5JQqQ+lptCP03Qcj5JQqQ+lptCP03Qcj5JQqQ+lptCP03Qcj6apxw9chCQPSXDxz6apxw9chCQPSXDxz4VHaY97A46PKEP6D4VHaY97A46PKEP6D5dO5C+nrcxP4n4Mz5dO5C+nrcxP4n4Mz7M1rK+/YZDP4C4YT7M1rK+/YZDP4C4YT52kLK+5a+QPZBNoT52kLK+5a+QPZBNoT46XNy+DyeaPO3Krz46XNy+DyeaPO3Krz61o9a+NJIjPjEDWr61o9a+NJIjPjEDWr53mAG/xFD7PbdJgL53mAG/xFD7PbdJgL5eVlC+nDsxP/Rajb5eVlC+nDsxP/Rajb5eVlC+nDsxP/Rajb5eVlC+nDsxP/Rajb75jYe+IUtAP8/5r775jYe+IUtAP8/5r775jYe+IUtAP8/5r775jYe+IUtAP8/5r745+ti+KFa2Psznbb6Hy7q+8K4tP9EVCDytB7C+Y9ArP6ZxYb6tB7C+Y9ArP6ZxYb5m3Dg8EkBRP0R4tb5m3Dg8EkBRP0R4tb6LYGc+RUcwP2eot76LYGc+RUcwP2eot77VdY0+ExW+PhtSu77VdY0+ExW+PhtSu7786UW969wuP/w+ub786UW969wuP/w+ub6LZoE+fajoPZ6rR765DVU810cmvRIFrD4yOLC+rcARvbGIiz58IOC+MgP8PQtNW757BDy+xY71PhCCr74kqQY+7OEnP7olqT4kqQY+7OEnP7olqT5UtGC++oYoP6VwnD75PZ6+1sALPooW0j75PZ6+1sALPooW0j7Y0p28vtIGPpT07z7Y0p28vtIGPpT07z6KbNc+75i1PpqgbL5ahaw+VRYtP3y0X75ahaw+VRYtP3y0X75e77k+Dj8tPznmQDzHxkI+aBkCPi1IpD7HxkI+aBkCPi1IpD6EocM+9NsHPrKXhL2EocM+9NsHPrKXhL2bhqI+9hv8Pv8UUz6bhqI+9hv8Pv8UUz47V+6+bmIHPr5ssb07V+6+bmIHPr5ssb2L3+G+Xb8CPt99Yz7Y1s2+Ahf1PtoZMD7Y1s2+Ahf1PtoZMD6tnDy+yLJWP6CBBj6tnDy+yLJWP6CBBj62Wjs+AZVWP2YtCT62Wjs+AZVWP2YtCT4IcE4+bUNWPwQBYb7ENde6YupVP4LDqr4FFFW+UNdUP4auYr4S5M++EEijPoJQa76tWdC+VCj1PjoHrr2tWdC+VCj1PjoHrr2OXK2+620rP7mB+jxgPcS+a9PQPvqIPL1gPcS+a9PQPvqIPL1u5cS+Igo1PwHrND1MZOu+MB6dPk4Rgr4qZeq+GbPWPpnwNL0qZeq+GbPWPpnwNL1yhay+ziRBP3Y4mL1yhay+ziRBP3Y4mL27TJ++QxgqPxVHX77Z64q+RFdFP5avML3Z64q+RFdFP5avML2ckra+g2oyP4tcgL7Yf6u+38VWP5zYU73Yf6u+38VWP5zYU71N7sa+3OLxPh/ckb6TorG+eo7PPgAgmb6TorG+eo7PPgAgmb6RSte+gTLSPvfTtb6RSte+gTLSPvfTtb7woQk8UC9MPyw3rL7ItfQ9qZpLP8vdtb7ItfQ9qZpLP8vdtb4XrGQ+61YtP4iYqb47a+89sRtJP1KBob47a+89sRtJP1KBob5pCIM+4hI3P8D0v7418uE7qipZP+cQxr6tIRE+BjpcPxphxb6tIRE+BjpcPxphxb66dZo+tSv6PpDjub66dZo+tSv6PpDjub48DJE+j1CoPvqwr77ko6o+BrvdPuMinr7ko6o+BrvdPuMinr50waQ+bZygPhYawr50Gsw+NCHiPrNdur50Gsw+NCHiPrNdur61qec9ejn4PhX7ur6a3Yi92zUsPxxtq76BIMU9dZ7PPsUdo76BIMU9dZ7PPsUdo77oY7i9/Xo0P4Kcwr6sNNc9jLrQPjNKwL6sNNc9jLrQPjNKwL5ld429jKFKP1L1tr4jMda99IJIP0l6or4jMda99IJIP0l6or5FKgu+XStaP2wvx75FKgu+XStaP2wvx76pJYQ+H7r+PZQIS75EBk8+TigGvOqdoz1EBk8+TigGvOqdoz1S0yc8TnVnvKrRrD6yQ2Q+NlbZPLzYpj2yQ2Q+NlbZPLzYpj2DegM9QoxuvR/3vT6Rtpc+uG3DPb72Yb5GdpI+GUMOvQ7rwD1GdpI+GUMOvQ7rwD3DOj++5is6vXc9zD5mJqq+NK07vJPKjT6egTq+kNdAPLfW2D6egTq+kNdAPLfW2D6BR8K+/ixUvVXUlT4IBVC+ud5LvesJ8j4IBVC+ud5LvesJ8j4sYty+ggYTOuuqujwsYty+ggYTOuuqujzL9dW+CMEGPtbjV74tJ9W+fEPzPMax9jwtJ9W+fEPzPMax9jwyyvK+4gPaPX4oc773oQC/RbDHvFujqTz3oQC/RbDHvFujqTwexLi+N/myPpVtpb4exLi+N/myPpVtpb59wjO+AZQBP5v+pL6k5FO+YokGPxiiur5Y0ps9peyvPq3SpL5Y0ps9peyvPq3SpL5aXBE+qIUnP09BmT5qjK+8005BPz0glz5qjK+8005BPz0glz41/16+ZMcnP/O5jj5j/rq78yNGP4/GaD5j/rq78yNGP4/GaD5N/IC+cngxP3WZnz43NTA+FXUwP8HjrT7ZSry7bIFYP5Cxjz7ZSry7bIFYP5Cxjz6GDaa+S9HLPs1Fsz7IIpu+jJgIPgdxyD78866+zhPEPk26mD78866+zhPEPk26mD70P7K+coXYPXdr1T6W6tW+wkbIPihirD6W6tW+wkbIPihirD4xLz++JoETPYKw7D4xLz++JoETPYKw7D5ypoy8WxsGPnKg5T7fm0g7+6/LPVgD/D6c9O09fzzJPvGK0D6c9O09fzzJPvGK0D5NEx0+EezEPvA1tj5NEx0+EezEPvA1tj6oeVU+BZ3FPr8b1z6oeVU+BZ3FPr8b1z5JKM0+fIWiPs5+br5X+8M++DvzPmNckb5X+8M++DvzPmNckb6PqJ4+RjErPxIZXb4Qj7I+W7k0P2IyfL7pOec+ajObPq11g77RLao+QMJBP1E/kr1mCaw+6j4rPw6uBj2Wwoo+NNpFP4CKJb2Wwoo+NNpFP4CKJb3yzsI+9GA0P4jdTD3nF6g+qhRYPy9XNL3nF6g+qhRYPy9XNL0buM8+PYPzPofcqL0buM8+PYPzPofcqL3Xcr0+GgrRPsAgQb3Xcr0+GgrRPsAgQb2ks+I++wfUPh1jLL2ks+I++wfUPh1jLL2TXB4+PkkDPugtpD5qgY4+e60gPSNL3j2j8Lo+5N4MPiusur303dQ+pfnfPR+9z73DJUQ+fvvFPZzftj46gMc+CsO3PhiaBTzrO5c+D/QDP9W2Rj4o3a0+m4YJP8UmZj68E2M+7T21PiftsD68E2M+7T21PiftsD5m7+C+/yQNPgqj2L2GrO6+diYvPejYKj1z/dC+9YYCPp8fbj6xo+6+WQHPPUjjez4dx/2+SmrpPdqs970vPNK+4B2xPjU2iT4vPNK+4B2xPjU2iT4+1ry+WUgBP2Y3Kz6XttW+iN0HPx9vQj7qJN2+OCWzPtBSELsla0G+toJOP8gjCj4e8Qg4NeNWP2rvST5WAT8+43lOPy8HDD7QIl4+y/haPwRoJz6WtWG+zTpbP5uVIj5Yl3g+sFhWP+h7IL0yX04+gihOP0pOXb7AGW4+kR5bP9WLf74e7+M96jtWP0nqn74e7+M96jtWP0nqn77Khv65ZWRPP+fapL4zdAK7wF9cP6S0vr44AO693yhVP/tCoL44AO693yhVP/tCoL7X906+EiBNPy+aX76bJHW+E8JYPwESgr67K32+NqxVP7/1K727K32+NqxVP7/1K70x88W+VzvpPrsQm72iot++VIrtPsF/m73K/52+o1o/P8lCgL0fZbS+WrFKP/0ejb3frrq+APDmPoWGkL5hRtS+GC7pPtdOo76UMO49Q1ZHP27bqr4lLgg+nC9UP+eYw74D3po+QITtPpQIrr49tbA+IubwPhe7wb4G6tw9/VTrPo5Pr76v6ek9KbTsPvViw7711529AcRGP9/Bq755Cca9ApxSPz7gxL5LXUo+BoIePPDInj2C/XM++O0CvZ2srz1M0Dq+gnSpvHNKzD7LU0m+wveBvTu83D7q49K+EvZ5PDanzzyTWvC+3mWzvH84mDxtEbC+2aO5Prp1nr6NCsm+5ne6Pnzjsb4A+qQ9uJu3PpAVn743J7A9h4i3PiJEsr4aDYe8Cp4/P5JZiT6eUYm8vY1LPyMynD5GDqK+d+7JPvM2qT6gcLu+0r/MPpQCtz4eyzq+dg4hPWIb5D6/REm+jR9Qun2f9T4QxvA9qsPIPvpnxD6KZRw+10fJPgT52j5sc7g+0cPoPnrUkL5iTM8+GCXsPmwoo75d4pw+NcM/PxNAeL2tGbE+HKhLPzDygr3yssQ+81ToPqjYmb1kz90+cPnqPmK+lb0KfIE+23A1PYaFyD0hx5c+0waFOy6s3D37TL4++T69Pl7OD7zHYdc+r2O+PnxjgLsesUY+mQO6PvnfrD7/A24+HNK5Pg2Jwj5v3d++Z149PVMsHz0x6f2+jP0uPDBuBz1L48G+oYi2Pglnij7evNy+GcS4PooAlz4mF8++MMa5Ps8UebznDum+2868Pjw4XbyW6C26Q3lPP7DPST5PhCa6fhpcP9YPbT5Am3c+me1OP3QQF7391Y4+iolbP5TjIb3X1+E9PShPPzwEm77r1AE+jlhcP4E0s76vCeS9IoNOPwdlm776oQe+wrVaP+z+s76ryHm+gn1OP7YZH73BEZK+PoFaP6ewNb15xw89JDZEvwsvJD95xw89JDZEvwsvJD+t/Ae820geP60wSb+t/Ae820geP60wSb8b3y+/+BMPv6vF7T4b3y+/+BMPv6vF7T4b3y+/+BMPv6vF7T4Pwio/Q7QLPwnZAb8Pwio/Q7QLPwnZAb8Pwio/Q7QLPwnZAb/J2xi/DOgKP+c+Fz/J2xi/DOgKP+c+Fz8C1Ro/ZMUQv8+KD78C1Ro/ZMUQv8+KD7+5wzO/Tbbkvm7tDb+5wzO/Tbbkvm7tDb+5wzO/Tbbkvm7tDb9ZBi4/zE78PlYOCz9ZBi4/zE78PlYOCz9ZBi4/zE78PlYOCz+gngC/d87uPiZgOr+gngC/d87uPiZgOr/Us90+y/gDv7xKPT/Us90+y/gDv7xKPT/6Kzg/KjANvzgs2L76Kzg/KjANvzgs2L7rti6/CvcSPx2f5z7rti6/CvcSPx2f5z67H0c/7FSUPr3GDr+7H0c/7FSUPr3GDr8LNEC/dyTGviUJCT8LNEC/dyTGviUJCT8fWW8/0MMovM2QtT4fWW8/0MMovM2QtT6TKDi/DYuovqeWHL+TKDi/DYuovqeWHL8eIAE/9XiYvot8Tz8eIAE/9XiYvot8Tz8eIAE/9XiYvot8Tz8eIAE/9XiYvot8Tz97UAm/TRS7PlTCQr97UAm/TRS7PlTCQr97UAm/TRS7PlTCQr97UAm/TRS7PlTCQr9Dw1O+JCV1P/lpTT4FIIe+ADOqvWYBdr/zHjK/82bCvm0UHD/zHjK/82bCvm0UHD/Rw/48ETIzv8mmNr/Rw/48ETIzv8mmNr+sjxW/YqjKviBiNb+sjxW/YqjKviBiNb8ij4++/YRWP3m1774ij4++/YRWP3m1777bn2I/VJfnPrTP3b3bn2I/VJfnPrTP3b2tH1S/cA0Av9W3gL5u5+i+ABAXv56/Kr++NQk/91U5vw5a3r6Udwo/mxAlvyZDCr+53MY9ugN4v0J5ab7740S/3ZI6vozTHD/740S/3ZI6vozTHD9Pqf4+g1e3vglJSj8rx9Q+ZA3pPl6XST8rx9Q+ZA3pPl6XST8cVRS/GzYgP76pBT8cVRS/GzYgP76pBT8KbYk+qXZ0P+K+AT5iPjo/JPC4vudTFT9iPjo/JPC4vudTFT+cG6c+3RgevW/Hcb/XcEw/KojjPunRz77XcEw/KojjPunRz75mpXk+ASKbPhTZaz9mpXk+ASKbPhTZaz/KYpA+MzN1v97rYj3KYpA+MzN1v97rYj0u4K++JOeuPhv0Xz8u4K++JOeuPhv0Xz9ajui+MT0dP2gzJb/Yhda+jjBmv8c5Ab7Yhda+jjBmv8c5Ab4JQw0/r2YWPwyHF78JQw0/r2YWPwyHF7+VDQ+/HSQYPxcSFL+VDQ+/HSQYPxcSFL9DnjG/v/UbPy6dxD4uK4e8AJkrP2ftPT9OgiM/V6QwP4JWrj629W8//nvHPZhFqz6RANY9kEYOP5wiU7+RANY9kEYOP5wiU7+frEs/ymezvgEI/b7JTno/n0pVvvQOx7zJTno/n0pVvvQOx7y8yXW/VMl2PmM0Eb5SmXC/MGimPmp1170fCnq/b3JbPlQ0IjwfCnq/b3JbPlQ0IjycLj2/dWorv6FemL2cLj2/dWorv6FemL0fIA4+GDEkv3krQT9Y7EQ/WC4jv6veNT1Y7EQ/WC4jv6veNT0gL3S/sQOLPqFiA74YWT2/HxgrP3cBor0YWT2/HxgrP3cBor32pA6/UMG5PdxNUz8QNgk/QREDvt+fVT8QNgk/QREDvt+fVT+MBxy/kZbuPYHASL+MBxy/kZbuPYHASL+o44M84aB1vwUGkL67Npa+t7Acv2X/O7+7Npa+t7Acv2X/O79k1Fm/9s8EvwoqqT3909i+Cuk1vwPYDz/909i+Cuk1vwPYDz+RUwU+W3/7PPmyfb+pCAo9eFD6vionX78rhqc+iVkkPwuCMb8rhqc+iVkkPwuCMb8Frjm/ztQavq7uK78Frjm/ztQavq7uK7/790++EaLBPnw2Zz/PmTW/a8N+vWm7Mz/PmTW/a8N+vWm7Mz9ii1s9wLI7vs9Je78Oczg/xuCcPcNvML8Oczg/xuCcPcNvML9ynjg/pUUxP92mpLxF+RE/ooOAPiI/SD/LhUU+WmSIPgTCcT/LhUU+WmSIPgTCcT/ozgQ+NcjDvJnDfb9dXy2+NeGPvqHUcb9dXy2+NeGPvqHUcb9pUQ0/5sDfvhfLNb+kHdQ+ycijvrIhWj+kHdQ+ycijvrIhWj954am+/tMKP/yaRb954am+/tMKP/yaRb8g5+a+7dEMP23vMz9wyzC/DHAPv5gj6r5wyzC/DHAPv5gj6r6ntw6/tiyhPtanRL/9OQK/NPtaPz3ix739OQK/NPtaPz3ix72gPKw9WH1+v7tJjD3ZIZm+yTVHv+FhDb+JZAc//opXvy2D2j2JZAc//opXvy2D2j3zexQ+zLwmv5asPr/iCFA/fhAcPdneFL+4oTg+qbIOP613T7+4oTg+qbIOP613T78e4za+84t7v8B8UD33cUq+VM8Wv42TSD/3cUq+VM8Wv42TSD967gw/X+xCvxRDr7567gw/X+xCvxRDr76O9V4/ay/Bvhgvob4Jfms/tMHGPislZL0Jfms/tMHGPislZL0anUA+rD05v0wCKr92H1K/nB8Sv1rctbx2H1K/nB8Sv1rctbwaIAE/UGUIv17zLb8aIAE/UGUIv17zLb+WcYo9uZS0vkPsbj+iZHe8jFcovsV8fL8DTTG/pBw3v8ESvz0DTTG/pBw3v8ESvz3AaTC/hPEEv4xiAb8Sym69HGMbvzbmSj8Sym69HGMbvzbmSj/OVy8/uWMyv/rqWb5ScAG8Ljs0v/fKNb9ScAG8Ljs0v/fKNb9S/TK+dYiQPih6cT9/YsM9GEGkPlQ8cT8JLpC6gKY6Pwg1Lz8JLpC6gKY6Pwg1Lz/FDR4/Kn0uPVUWST8lrWU/mju7PiOjfb5muEw/oVdovvJODr9muEw/oVdovvJODr+u8MO+fz7GPco1az9Oukq/15VDPiN7FD9Oukq/15VDPiN7FD/0pPm9hjE0P9YkMz/0pPm9hjE0P9YkMz8Y9im/auYPP9CF/L7DrK27xcTTPceffj83M06/yf+7Plks7j43M06/yf+7Plks7j6thR6/P0dYvSmPSL+thR6/P0dYvSmPSL9q0xc/mLFIPZa8TT9q0xc/mLFIPZa8TT/gIG2/JxCHPtPGiT51RhQ/xPWoPbOdTz91RhQ/xPWoPbOdTz/mu4W+0R8iv9N+Oj92wHs/Zf02Pp00AL0EJHs/IIsOPho+Cr7gFkc/Azsfv5/Gur2Q9UC/chCevs2DFL+U3EK/r60lv+4GLL2U3EK/r60lv+4GLL0O2Xg/iddsPmbVI73gCjo/49MvP9UPPjzgCjo/49MvP9UPPjwJMJC8SGMTP0NEUb8JMJC8SGMTP0NEUb+LNnm/iZKpPLNDab6LNnm/iZKpPLNDab5KN3s/D/MwOyQFRT5KN3s/D/MwOyQFRT5eyqO+YYEFP4R/Sr94jhM/8KhCP1k9mT4pmEy/cPYGP1bQkz5qxmU/9ICRvliWrD79QGM/J4owvkqT2j6wG9G9Sqcbv0+OST/jiUW/3/rivquI6b4TlmI/uaIjvrrL3z5KVis/vZTSvstoHr9KVis/vZTSvstoHr+/rFY/MdVjPgqc/j539P2++0ldP2eNqD3AFxQ/0czZPhEsMr9MqHu/Aic1PvpARj1tOHS/QLcvPkTMez6aXNq+VZZdvcEiZ7+aXNq+VZZdvcEiZ782b1M/+I0HvwpARr7TWX2/6rbIPUOk1j0OMaq9z1UQv9JbUj+eESI/vKbVvjfoJr+wtVE7KS4NP7WMVb9tMyW/34HGvt2AKL+SQBM+sOh5P0UyJj7xaQC+XVl7P4jSET71y1K/QQcRP0x8A71CIFi/EY2MvnWv6z7/JP09ufZ9P4j6wrwy8Km+ecgqP127Kj8y8Km+ecgqP127Kj+ngTc7Hd35vEHhfz8iHuu8nGJ0P/PClz71mJU+V8Y1P2kCJD/1mJU+V8Y1P2kCJD9rwV4/bqBvvWSI+j4P4I2+0vdzP0vh+r0iJkg/LQ8fP1CXVL0iJkg/LQ8fP1CXVL1/ymc/rlL8PWz7z74TUFm/dQzePu+4mr4jjrU+K1xvv+4Nvju9gnO/ouSDPrHgLb4o3Ls+ADKqvW4xbT/wAW+/QJa0PhyegL1Nw+y+we9fv7dIFL45lG09mISAvtFbd78lMmi/savPvQVC0T5pIh8+E8mYPesqfL8HBNg+d0jhPhjvSj8kLsw9KRr8PY3EfL+7IB8/MtQJv5OpET+eMjs9RTySvpYOdb/Fbly/4qXGPqBJqL5y0cS9yJR5v7qFTb7HuDg+KkOYPvQDcL/S5IO91Lp7v5UvLj679mg/xtuZvsU6kr5cAJE8gbx3vy+4gL4DIEQ/VsnnvhmU6T6i9wY+GhKnvpKfb7801kG+7ay2vItOez91m5W+8soBv3KXT7/wysy8h6Z0vyg3lr63LV29YxOuPj1acD+HqXc/nixHvqvkJb5elIO+S0ZQPlPccT/evPw9fHhiP1415r4r702+Q3ABvPXCej/2TFu/6OHPPcd9Ab+v+1Q8mZ90Ps+QeD82a+2+etiBvT07Yj9neXc/FomBPuAaHr0/Q9a+Ce1nv2ZFg71V2ng/BZ1ePr/VtL2E6lS/ntN8Pl+e/r4N1Ww/obSqPq/2Ob6VhJy+7q9yP8Vztb00uGQ//Y81vpNP0z7xaHS/6zKGvq0UED472Fo/OrpjvtED8D6ULbS+4I05vnUXa7/CV2g/rKVKvmCXvT7EDAQ/EzJbP/QF5zwnPXW/GUtzPjCnJD6lMhU/s0dyvgoER7+/Un+/3BmNPRc6vbwFFVM/NxXjvszfsz67o2+/s4CVvX4tsD5n1SO7GAqzvknWb7/u3DU8and9PxJCDz7i/nC/H82qvjPMTL1toN494nJ+P/iIhTyaPuq+IxEYvoZwYD91r1g9q7R8P86GGj6d9vU+W6utvJ91YD8RXjq+gFB7P/DIZT2pxnc/sUCAvgfDsbwQPTC+nQR8P3GYEL2ENX0+LgMWP91Efz9AnUw9hDV9Pi4DFj/dRH8/QJ1MPbdMrz58nh8/xCBHPwAAAACo3lw/AO4HPbdMrz58nh8/xCBHPwAAAACo3lw/AO4HPU6GXj8apf4+fDtfP6g+Gz9Ohl4/GqX+Pnw7Xz+oPhs/QjzZPiVWTz9zzhU/IMyFPSjuPz8AGCg9QjzZPiVWTz9zzhU/IMyFPSjuPz8AGCg9AAAAPzDP/j4AAEA/pHB9PwAAAD8wz/4+AABAP6RwfT9SKbo9urZIP3r2qj4wC6A9Uim6Pbq2SD969qo+MAugPXBIwT6Glv8+TFsYP7f3aD9wSME+hpb/PkxbGD+392g/FekgPoCU8T46bBE/R8UiPxXpID6AlPE+OmwRP0fFIj9SOx8+TLEfP/F8Yz4AAAAAAABAP1yPAj8AAIA/Ms/+PlI7Hz5MsR8/8XxjPgAAAAAAAEA/XI8CPwAAgD8yz/4+Cy1KPsq4pz59V5I+4N8FPvl2az4wurs9+nZrPjC6uz1bqHk/qGf5PVyoeT+oZ/k99+BiP8Af6D344GI/wB/oPfzTYz84ENc+/NNjPzoQ1z6ITHo/lJPYPohMej+Wk9g+VdJUPxp/Jz8jmT4/falpP+fOHT8hrmA/XlcaPwgaKT9LcT0/5AoMP4JzBz9c3w4+g3MHP1zfDj7hH8Q+kLEVPgsEyD5Wh9s+DATIPlaH2z7k0Pw+tAvaPuTQ/D62C9o+sYZVP+ZDrT7t00k/WMW3Pe3TST9gxbc9njpGP2DL4T12YxM/euPZPndjEz9649k+naZCP2zO2T6dpkI/bs7ZPq9KHj/IalY+sEoeP8RqVj5dsGQ+4FHSPl2wZD7iUdI+IsKqPuZS2T4nmZ8+DBJbPieZnz4QEls+FQEhPg77Qj8VASE+D/tCPwTMtT6iR0Y/Bcy1PqJHRj9RLK4+i/glPyTHfT4UOhk/lN4jPoPqJD+1vEY+buCtPsVrej7S7YY+xWt6PtTthj68ZJQ+BGMBPr0wfD5snYw+vzB8PmqdjD69ZJQ+CGMBPra8Rj5w4K0+vTB8PmydjD6+MHw+bJ2MPjrGiz7Q5mk9O8aLPuDmaT3OzGo+WBWsPf1P/D0CNDQ/elqOPhALID3NzGo+WBWsPf1P/D0CNDQ/elqOPhALID3DWkg+NNlqPgUzQj54lHE+HrYoP1CqEj8EM0I+eJRxPh+2KD9RqhI/DyB6P6Al7T18Hm4/ABZ4PX0ebj8AFng9x2BiP8Bx2j2985Y+1NAaP8QRbj+ARSo9x2BiP8hx2j0QIHo/mCXtPb3zlj7U0Bo/xBFuP4BFKj18HF8/FgCIPnwcXz8YAIg+2GJjP6Bc2j6K01I/EKV+Pn2yXT9o0Yc+2GJjP6Bc2j6K01I/FKV+Pn2yXT9q0Yc+bzZvPxYA9T4oxno/OMPbPsCdTz8B5w4/KUNvPyS6/j4oxno/OMPbPr+dTz8C5w4/KUNvPyS6/j5xOH4/wAKMPm04Tj482ho/cKJ/P2gxjD5tOE4+PNoaP3Cifz9qMYw+bLBVP7x5Jj++I04/0GZLP78jTj/QZks/C7c+P2tPaz8oQy8/Irr+Pr+dTz+mV0w/C7c+P2tPaz9ssFU/vHkmPyhDLz8iuv4+wJ1PP6ZXTD8+ryw/FrJvP5xaHT/uXmE/OqTgPtoy/z6oLSw/LTRzP5taHT/uXmE/OqTgPtoy/z6nLSw/LTRzP5uvFj/kn0U/nK8WP+SfRT8cmRk/95IoP37eiD6Clfg+xOMUP37eRT8bmRk/95IoP37eiD6Clfg+xOMUP37eRT+tgSk/WqQUP62BKT9bpBQ/2qc9P5pACz/Zpz0/mkALP8T+TT+BnhE/xP5NP4KeET/ApQg/lIoIPiHY6j7wVLc9ItjqPuhUtz0YB8I+POMPPkzjgz5vBkw/s0nrPpjrkj0YB8I+OOMPPsGlCD+Uigg+TOODPm8GTD+xSes+mOuSPRUcuj6ApZM+aHTHPpyI3j7RFLY+2GyUPtEUtj7abJQ+aHTHPpqI3j7SFLY+2GyUPtMUtj7abJQ+yBXhPoQY9j7JFeE+hhj2PtIU/T4AHN0+0xT9PgAc3T7q6Ag/QEiQPuvoCD9ASJA+1SYLPxbskD7WJgs/FuyQPtYmCz8W7JA+1iYLPxjskD6oRlY//Au0PvsJUj8gyXU+/QlSPyDJdT5Umkk/4HSoPVSaST/gdKg9qEZWP/wLtD5qp0Q/EG0lPT+0RT94+9U9fkTEPlB6Nz93h0M/wBeoPD+0RT9w+9U9fUTEPlB6Nz93h0M/wBeoPNbjTj/eFYQ+2ONOP94VhD5hKjo/DgyQPj06Tz8K1Ik+YSo6PwwMkD49Ok8/CNSJPtfFET8g99w+oTMuP9iB9T5B+UQ/UuDcPkH5RD9S4Nw+18URPx733D7zvTc/hKmUPqqVHT+8H0o+qpUdP7gfSj4xYw4/8LuUPjFjDj/yu5Q+bQpfPsTs1D4oSoo+uuTvPqiirD4yg9w+qKKsPjKD3D5tCl8+xOzUPgDjsT76tJc+AeOxPvq0lz6Yi6A+sHtPPpiLoD6we08+TIaAPhiUkT4NWBs+XXVDPwq0gz4qLUo/CMC4PtcIRz8JwLg+1whHPwxYGz5ddUM/abK/PsIjNz9aRK4+CnElP1pErj4KcSU/ydiWPvQCHD/K2JY+9AIcPwK7fT6C9Rg/Art9PoL1GD8I304+QOgbPwnfTj5A6Bs/o3sjPhR7JD+jeyM+FHskP/M5Bz60IzQ/9TkHPrQjND9Rlno+IOiHPlCWej4e6Ic+RTSMPqCXXT1ENIw+gJddPSNURz5s+Gs+IlRHPmz4az5eHG4/AB5rPV4cbj8AHms9JuBeP074hz4m4F4/TviHPo44bz8Yn/Y+jThvPxif9j7EdH4/iAqMPsV0fj+GCow+vmJOP/OOSz++Yk4/845LP6aZLD/ER3A/ppksP8RHcD/4YhY/U6pFP/hiFj9UqkU/v18pPwNQFD+/Xyk/AlAUP+1DTj+XKhE/7UNOP5cqET8P6+o+aEOxPQ7r6j5gQ7E9+3G5Pgaskz76cbk+BqyTPtwC4T7onPc+3ALhPuic9z74PQk/uEGQPvg9CT+2QZA+kitSPxxDdz6SK1I/IEN3Pmt3RD/g3Bc9bXdEP+DcFz088k4/5gqFPj3yTj/mCoU+4WAuPzoL9z7hYC4/Ogv3PlolOD+a5JM+WiU4P5zkkz6Izg0/dveTPofODT9295M+jA2KPoZX8T6MDYo+hFfxPr+Xsj5ADpc+v5eyPkAOlz4rDoA+BsiQPisOgD4EyJA+67uDPgp8Sj/su4M+CnxKP2x1wD4uMjc/bHXAPi4yNz9I3ZY+7s8bP0jdlj7uzxs/Q8NOPkC7Gz9Ew04+P7sbP/a2BT5sJjQ/9bYFPmwmND8sAFsAAgEsAAIBWgBbAC0AXQBbAF0AAgECAV0AGQACARkAXgBaAAIBXgBaAF4AIAAtAFwAAwEtAAMBYABcACwAYQBcAGEAAwEDAWEAIgADASIAYgBgAAMBYgBgAGIAGwAtAGUABAEtAAQBXQBlAC8AZgBlAGYABAEEAWYAJQAEASUAaABdAAQBaABdAGgAGQAuAGQABQEuAAUBaQBkAC0AYABkAGAABQEFAWAAGwAFARsAawBpAAUBawBpAGsAKQAvAGwABgEvAAYBZgBsACwAWgBsAFoABgEGAVoAIAAGASAAbQBmAAYBbQBmAG0AJQAsAGwABwEsAAcBYQBsAC4AaQBsAGkABwEHAWkAKQAHASkAbwBhAAcBbwBhAG8AIgAwAHMACAEwAAgBcQBzADMAdABzAHQACAEIAXQABgAIAQYAdgBxAAgBdgBxAHYAAQAyAHIACQEyAAkBdwByADEAeAByAHgACQEJAXgAAwAJAQMAegB3AAkBegB3AHoACQAzAHwACgEzAAoBdAB8ADUAfQB8AH0ACgEKAX0ACgAKAQoAfwB0AAoBfwB0AH8ABgA0AHsACwE0AAsBgAB7ADIAdwB7AHcACwELAXcACQALAQkAggCAAAsBggCAAIIADAA1AIMADAE1AAwBfQCDADYAhACDAIQADAEMAYQAJwAMAScAhgB9AAwBhgB9AIYACgA3AIMADQE3AA0BhwCDADQAgACDAIAADQENAYAADAANAQwAiQCHAA0BiQCHAIkAKwA2AIoADgE2AA4BhACKADAAcQCKAHEADgEOAXEAAQAOAQEAjACEAA4BjACEAIwAJwAxAIoADwExAA8BeACKADcAhwCKAIcADwEPAYcAKwAPASsAjgB4AA8BjgB4AI4AAwA4AJEAEAE4ABABjwCRADkAkgCRAJIAEAEQAZIAFQAQARUAlACPABABlACPAJQACwA5AJAAEQE5ABEBlQCQADgAlgCQAJYAEQERAZYADQARAQ0AmACVABEBmACVAJgAFwA5AJkAEgE5ABIBkgCZADoAmgCZAJoAEgESAZoAHQASAR0AnACSABIBnACSAJwAFQA6AJkAEwE6ABMBnQCZADkAlQCZAJUAEwETAZUAFwATARcAnwCdABMBnwCdAJ8AHwA6AKEAFAE6ABQBmgChADsAogChAKIAFAEUAaIAIQAUASEApACaABQBpACaAKQAHQA7AKAAFQE7ABUBpQCgADoAnQCgAJ0AFQEVAZ0AHwAVAR8ApwClABUBpwClAKcAIwA7AKgAFgE7ABYBogCoADwAqgCoAKoAFgEWAaoAJgAWASYAbgCiABYBbgCiAG4AIQA8AKkAFwE8ABcBqwCpADsApQCpAKUAFwEXAaUAIwAXASMAcACrABcBcACrAHAAKgA8AK0AGAE8ABgBqgCtADgAjwCtAI8AGAEYAY8ACwAYAQsAhQCqABgBhQCqAIUAJgA4AKwAGQE4ABkBlgCsADwAqwCsAKsAGQEZAasAKgAZASoAiACWABkBiACWAIgADQA9ALAAGgE9ABoBrgCwAD8AsQCwALEAGgEaAbEAGQAaARkAswCuABoBswCuALMADwA/AK8AGwE/ABsBtACvAD4AtQCvALUAGwEbAbUAEgAbARIAtwC0ABsBtwC0ALcAGwA/ALgAHAE/ABwBsQC4AEAAuQC4ALkAHAEcAbkAHAAcARwAugCxABwBugCxALoAGQBBALgAHQFBAB0BvAC4AD8AtAC4ALQAHQEdAbQAGwAdARsAvQC8AB0BvQC8AL0AHgBAAL8AHgFAAB4BuQC/AEIAwQC/AMEAHgEeAcEAFAAeARQAmwC5AB4BmwC5AJsAHABDAMAAHwFDAB8BwgDAAEEAvADAALwAHwEfAbwAHgAfAR4AngDCAB8BngDCAJ4AFgBCAMQAIAFCACABwQDEAD0ArgDEAK4AIAEgAa4ADwAgAQ8AxQDBACABxQDBAMUAFAA+AMMAIQE+ACEBtQDDAEMAwgDDAMIAIQEhAcIAFgAhARYAyAC1ACEByAC1AMgAEgBEAMsAIgFEACIByQDLAEUAzADLAMwAIgEiAcwABQAiAQUAfgDJACIBfgDJAH4ACgBGAMoAIwFGACMBzQDKAEQAzgDKAM4AIwEjAc4ADAAjAQwAgQDNACMBgQDNAIEACABFAM8AJAFFACQBzADPAEcA0ADPANAAJAEkAdAAEAAkARAA0gDMACQB0gDMANIABQBHAM8AJQFHACUB0wDPAEYAzQDPAM0AJQElAc0ACAAlAQgA1QDTACUB1QDTANUAEwBHANYAJgFHACYB0ADWAEQAyQDWAMkAJgEmAckACgAmAQoA2QDQACYB2QDQANkAEABEANcAJwFEACcBzgDXAEcA0wDXANMAJwEnAdMAEwAnARMA2wDOACcB2wDOANsADABIAN0AKAFIACgB3ADdAEsA3gDdAN4AKAEoAd4ACgAoAQoAkwDcACgBkwDcAJMAFABKAN0AKQFKACkB3wDdAEkA4ADdAOAAKQEpAeAAFgApARYAlwDfACkBlwDfAJcADABLAOEAKgFLACoB3gDhAEwA4gDhAOIAKgEqAeIADwAqAQ8A2ADeACoB2ADeANgACgBNAOEAKwFNACsB4wDhAEoA3wDhAN8AKwErAd8ADAArAQwA2gDjACsB2gDjANoAEgBMAOUALAFMACwB4gDlAEgA3ADlANwALAEsAdwAFAAsARQAxgDiACwBxgDiAMYADwBJAOQALQFJAC0B4ADkAE0A4wDkAOMALQEtAeMAEgAtARIAxwDgAC0BxwDgAMcAFgBPAOcALgFPAC4B5gDnAFAA6ADnAOgALgEuAegAHAAuARwAowDmAC4BowDmAKMAIABQAOcALwFQAC8B6QDnAE4A6gDnAOoALwEvAeoAIgAvASIApgDpAC8BpgDpAKYAHgBQAOsAMAFQADAB6ADrAFEA7QDrAO0AMAEwAe0AGQAwARkAuwDoADABuwDoALsAHABSAOwAMQFSADEB7gDsAFAA6QDsAOkAMQExAekAHgAxAR4AvgDuADEBvgDuAL4AGwBRAO8AMgFRADIB7QDvAE8A5gDvAOYAMgEyAeYAIAAyASAAXwDtADIBXwDtAF8AGQBOAO8AMwFOADMB6gDvAFIA7gDvAO4AMwEzAe4AGwAzARsAYwDqADMBYwDqAGMAIgBTAPEANAFTADQB8ADxAFYA8gDxAPIANAE0AfIADgA0AQ4AsgDwADQBsgDwALIAGABVAPEANQFVADUB8wDxAFQA9ADxAPQANQE1AfQAGgA1ARoAtgDzADUBtgDzALYAEQBWAPUANgFWADYB8gD1AFcA9gD1APYANgE2AfYABAA2AQQA0QDyADYB0QDyANEADgBXAPUANwFXADcB9wD1AFUA8wD1APMANwE3AfMAEQA3AREA1AD3ADcB1AD3ANQABwBXAPkAOAFXADgB9gD5AFgA+gD5APoAOAE4AfoAAAA4AQAAdQD2ADgBdQD2AHUABABYAPgAOQFYADkB+wD4AFcA9wD4APcAOQE5AfcABwA5AQcAeQD7ADkBeQD7AHkAAgBYAPwAOgFYADoB+gD8AFkA/gD8AP4AOgE6Af4AJAA6ASQAiwD6ADoBiwD6AIsAAABZAP0AOwFZADsB/wD9AFgA+wD9APsAOwE7AfsAAgA7AQIAjQD/ADsBjQD/AI0AKABZAAABPAFZADwB/gAAAVMA8AAAAfAAPAE8AfAAGAA8ARgAZwD+ADwBZwD+AGcAJABUAAEBPQFUAD0B9AABAVkA/wABAf8APQE9Af8AKAA9ASgAagD0AD0BagD0AGoAGgA=" + } + ] +} diff --git a/meshes/player_mesh.glb b/meshes/player_mesh.glb new file mode 100644 index 0000000..05e5a4b Binary files /dev/null and b/meshes/player_mesh.glb differ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3763c44 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +brace_style = "AlwaysNextLine" +control_brace_style = "AlwaysNextLine" diff --git a/shaders/blit.wgsl b/shaders/blit.wgsl new file mode 100644 index 0000000..4b728df --- /dev/null +++ b/shaders/blit.wgsl @@ -0,0 +1,28 @@ +struct VertexInput { + @location(0) position: vec2, + @location(1) uv: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) uv: vec2, +} + +@group(0) @binding(0) +var t_texture: texture_2d; + +@group(0) @binding(1) +var t_sampler: sampler; + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.clip_position = vec4(input.position, 0.0, 1.0); + output.uv = input.uv; + return output; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + return textureSample(t_texture, t_sampler, input.uv); +} diff --git a/shaders/standard.wgsl b/shaders/standard.wgsl new file mode 100644 index 0000000..f80b05e --- /dev/null +++ b/shaders/standard.wgsl @@ -0,0 +1,97 @@ +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) uv: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_position: vec3, + @location(1) world_normal: vec3, +} + +struct Uniforms { + model: mat4x4, + view: mat4x4, + projection: mat4x4, +} + +@group(0) @binding(0) +var uniforms: Uniforms; + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + let world_pos = uniforms.model * vec4(input.position, 1.0); + output.world_position = world_pos.xyz; + output.world_normal = (uniforms.model * vec4(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 { + let pattern = array( + 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 { + let pattern = array( + 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 { + let pattern = array( + 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 { + let light_pos = vec3(5.0, 5.0, 5.0); + let light_color = vec3(1.0, 1.0, 1.0); + let object_color = vec3(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(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(dithered_r, dithered_g, dithered_b, 1.0); +} diff --git a/shaders/terrain.wgsl b/shaders/terrain.wgsl new file mode 100644 index 0000000..b53f38d --- /dev/null +++ b/shaders/terrain.wgsl @@ -0,0 +1,120 @@ +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) uv: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_position: vec3, + @location(1) world_normal: vec3, + @location(2) uv: vec2, +} + +struct Uniforms { + model: mat4x4, + view: mat4x4, + projection: mat4x4, + height_scale: f32, + time: f32, +} + +@group(0) @binding(0) +var uniforms: Uniforms; + +@group(0) @binding(1) +var height_texture: texture_2d; + +@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(1.0 / 512.0, 1.0 / 512.0); + let height_left = textureSampleLevel(height_texture, height_sampler, input.uv - vec2(texel_size.x, 0.0), 0.0).r; + let height_right = textureSampleLevel(height_texture, height_sampler, input.uv + vec2(texel_size.x, 0.0), 0.0).r; + let height_down = textureSampleLevel(height_texture, height_sampler, input.uv - vec2(0.0, texel_size.y), 0.0).r; + let height_up = textureSampleLevel(height_texture, height_sampler, input.uv + vec2(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(-dh_dx, 1.0, -dh_dz)); + + let world_pos = uniforms.model * vec4(displaced_pos, 1.0); + output.world_position = world_pos.xyz; + output.world_normal = normalize((uniforms.model * vec4(normal, 0.0)).xyz); + output.clip_position = uniforms.projection * uniforms.view * world_pos; + output.uv = input.uv; + + return output; +} + +fn hash(p: vec2) -> f32 { + var p3 = fract(vec3(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, 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 { + let pattern = array( + 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 { + let light_dir = normalize(vec3(-0.5, -1.0, -0.5)); + let light_color = vec3(1.0, 1.0, 1.0); + let object_color = vec3(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(dithered_r, dithered_g, dithered_b, 1.0); +} diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..b14dca1 --- /dev/null +++ b/src/camera.rs @@ -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; + } +} diff --git a/src/components/camera.rs b/src/components/camera.rs new file mode 100644 index 0000000..81df1e7 --- /dev/null +++ b/src/components/camera.rs @@ -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() + } +} diff --git a/src/components/camera_follow.rs b/src/components/camera_follow.rs new file mode 100644 index 0000000..f081d26 --- /dev/null +++ b/src/components/camera_follow.rs @@ -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, + } + } +} diff --git a/src/components/input.rs b/src/components/input.rs new file mode 100644 index 0000000..e481aee --- /dev/null +++ b/src/components/input.rs @@ -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, +} diff --git a/src/components/jump.rs b/src/components/jump.rs new file mode 100644 index 0000000..f07788f --- /dev/null +++ b/src/components/jump.rs @@ -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; + } +} diff --git a/src/components/mesh.rs b/src/components/mesh.rs new file mode 100644 index 0000000..cc0a4c4 --- /dev/null +++ b/src/components/mesh.rs @@ -0,0 +1,11 @@ +use std::rc::Rc; + +use crate::mesh::Mesh; +use crate::render::Pipeline; + +#[derive(Clone)] +pub struct MeshComponent +{ + pub mesh: Rc, + pub pipeline: Pipeline, +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..c61dbb1 --- /dev/null +++ b/src/components/mod.rs @@ -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; diff --git a/src/components/movement.rs b/src/components/movement.rs new file mode 100644 index 0000000..ab17df3 --- /dev/null +++ b/src/components/movement.rs @@ -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, + } + } +} diff --git a/src/components/physics.rs b/src/components/physics.rs new file mode 100644 index 0000000..3f59c07 --- /dev/null +++ b/src/components/physics.rs @@ -0,0 +1,8 @@ +use rapier3d::prelude::{ColliderHandle, RigidBodyHandle}; + +#[derive(Copy, Clone, Debug)] +pub struct PhysicsComponent +{ + pub rigidbody: RigidBodyHandle, + pub collider: Option, +} diff --git a/src/components/player_tag.rs b/src/components/player_tag.rs new file mode 100644 index 0000000..ae1e27b --- /dev/null +++ b/src/components/player_tag.rs @@ -0,0 +1,2 @@ +#[derive(Copy, Clone, Debug)] +pub struct PlayerTag; diff --git a/src/components/state_machine.rs b/src/components/state_machine.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/components/state_machine.rs @@ -0,0 +1 @@ + diff --git a/src/debug/collider_debug.rs b/src/debug/collider_debug.rs new file mode 100644 index 0000000..33dd0e4 --- /dev/null +++ b/src/debug/collider_debug.rs @@ -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> = OnceCell::new(); + static DEBUG_HEIGHTFIELD: OnceCell>> = 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 +{ + 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 +} diff --git a/src/debug/mod.rs b/src/debug/mod.rs new file mode 100644 index 0000000..c7e4bce --- /dev/null +++ b/src/debug/mod.rs @@ -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}; diff --git a/src/debug/noclip.rs b/src/debug/noclip.rs new file mode 100644 index 0000000..d2f6073 --- /dev/null +++ b/src/debug/noclip.rs @@ -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); + } + } +} diff --git a/src/draw.rs b/src/draw.rs new file mode 100644 index 0000000..0761904 --- /dev/null +++ b/src/draw.rs @@ -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, + entity: EntityHandle, + pipeline: Pipeline, +} + +pub struct DrawManager +{ + entries: HashMap, + 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, + 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 + { + vec![] + } + + pub fn draw_mesh(_mesh: Rc, _entity: EntityHandle, _pipeline: Pipeline) -> DrawHandle + { + 0 + } + + pub fn clear_mesh(_handle: DrawHandle) {} +} diff --git a/src/entity.rs b/src/entity.rs new file mode 100644 index 0000000..acd062e --- /dev/null +++ b/src/entity.rs @@ -0,0 +1,43 @@ +use std::collections::HashSet; + +pub type EntityHandle = u64; + +pub struct EntityManager +{ + next_id: EntityHandle, + alive: HashSet, +} + +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 + { + self.alive.iter().copied().collect() + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..285c91f --- /dev/null +++ b/src/event.rs @@ -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 = Box; + +pub struct EventBus +{ + handlers: HashMap>, +} + +impl EventBus +{ + fn new() -> Self + { + Self { + handlers: HashMap::new(), + } + } + + fn subscribe_internal(&mut self, handler: F) + { + let type_id = TypeId::of::(); + let handlers: &mut Vec> = self + .handlers + .entry(type_id) + .or_insert_with(|| Box::new(Vec::>::new())) + .downcast_mut() + .unwrap(); + + handlers.push(Box::new(handler)); + } + + fn publish_internal(&mut self, event: &T) + { + let type_id = TypeId::of::(); + + if let Some(handlers) = self.handlers.get_mut(&type_id) + { + let typed_handlers = handlers.downcast_mut::>>().unwrap(); + for handler in typed_handlers + { + handler(event); + } + } + } + + pub fn subscribe(handler: F) + { + EVENT_BUS.with(|bus| bus.borrow_mut().subscribe_internal(handler)); + } + + pub fn publish(event: &T) + { + EVENT_BUS.with(|bus| bus.borrow_mut().publish_internal(event)); + } +} + +thread_local! { + static EVENT_BUS: RefCell = RefCell::new(EventBus::new()); +} + +#[derive(Debug, Clone)] +pub struct UpdateEvent +{ + pub delta: f32, +} +impl Event for UpdateEvent {} diff --git a/src/heightmap.rs b/src/heightmap.rs new file mode 100644 index 0000000..baff890 --- /dev/null +++ b/src/heightmap.rs @@ -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, +) -> Result<(wgpu::Texture, wgpu::TextureView, wgpu::Sampler), Box> +{ + 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 = 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)) +} diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..52a3634 --- /dev/null +++ b/src/main.rs @@ -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> +{ + 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) +} diff --git a/src/mesh.rs b/src/mesh.rs new file mode 100644 index 0000000..4849025 --- /dev/null +++ b/src/mesh.rs @@ -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::() 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, + ) -> Result> + { + 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, + all_indices: &mut Vec, + ) -> Result<(), Box> + { + 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::>(); + + let normals = reader + .read_normals() + .ok_or("Missing normal data")? + .collect::>(); + + let uvs = reader + .read_tex_coords(0) + .map(|iter| iter.into_f32().collect::>()) + .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) -> Result> + { + crate::render::with_device(|device| Mesh::load_gltf_mesh(device, path)) + } +} diff --git a/src/physics.rs b/src/physics.rs new file mode 100644 index 0000000..1640692 --- /dev/null +++ b/src/physics.rs @@ -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 = RefCell::new(PhysicsManager::new()); +} + +pub struct HeightfieldData +{ + pub heights: DMatrix, + pub scale: Vector, + pub position: Vector, +} + +pub struct PhysicsManager +{ + rigidbody_set: RigidBodySet, + collider_set: ColliderSet, + gravity: Vector, + 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, +} + +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, + ) -> 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) -> ColliderHandle + { + GLOBAL_PHYSICS.with(|manager| manager.borrow_mut().add_collider_internal(collider, parent)) + } + + pub fn with_rigidbody_mut(handle: RigidBodyHandle, f: F) -> Option + 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(handle: ColliderHandle, f: F) -> Option + 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> + { + 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, + direction: Vector, + max_distance: f32, + exclude_rigidbody: Option, + ) -> 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, scale: Vector, position: Vector) + { + 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 + { + 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 + } +} diff --git a/src/picking.rs b/src/picking.rs new file mode 100644 index 0000000..e331a5b --- /dev/null +++ b/src/picking.rs @@ -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 + { + 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 + { + None + } +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..9ed81a4 --- /dev/null +++ b/src/player.rs @@ -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::(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::(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::(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::(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::(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::(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::(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::(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::(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; + }); + } +} diff --git a/src/postprocess.rs b/src/postprocess.rs new file mode 100644 index 0000000..4d614df --- /dev/null +++ b/src/postprocess.rs @@ -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::() 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, + }) +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..9eacb47 --- /dev/null +++ b/src/render.rs @@ -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, + terrain_bind_group_layout: wgpu::BindGroupLayout, + terrain_uniform_buffer: wgpu::Buffer, + terrain_bind_group: Option, + terrain_height_scale: f32, + + wireframe_pipeline: wgpu::RenderPipeline, +} + +impl Renderer +{ + pub async fn new( + window: &sdl3::video::Window, + render_scale: u32, + ) -> Result> + { + 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::() 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::() 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> = RefCell::new(None); +} + +pub fn init(renderer: Renderer) +{ + GLOBAL_RENDERER.with(|r| *r.borrow_mut() = Some(renderer)); +} + +pub fn with_device(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: 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, + }) +} diff --git a/src/shader.rs b/src/shader.rs new file mode 100644 index 0000000..a89a009 --- /dev/null +++ b/src/shader.rs @@ -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, + }) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..bfb947c --- /dev/null +++ b/src/state.rs @@ -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 bool>, +} + +pub struct StateMachine +{ + state_transitions: HashMap>, + current_state_id: TypeId, + states: HashMap>, + pub time_in_state: f32, +} + +impl StateMachine +{ + pub fn new(enter_state: Box) -> 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 + { + 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(&mut self, state: T) + { + let state_id = TypeId::of::(); + self.states.insert(state_id, Box::new(state)); + } + + pub fn add_transition( + &mut self, + condition: impl Fn(&World) -> bool + 'static, + ) + { + let from_id = TypeId::of::(); + let to_id = TypeId::of::(); + + 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) + } +} diff --git a/src/systems/camera.rs b/src/systems/camera.rs new file mode 100644 index 0000000..19a94ed --- /dev/null +++ b/src/systems/camera.rs @@ -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(); + } + } + } + } +} diff --git a/src/systems/input.rs b/src/systems/input.rs new file mode 100644 index 0000000..f21e1eb --- /dev/null +++ b/src/systems/input.rs @@ -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; + }); + } +} diff --git a/src/systems/mod.rs b/src/systems/mod.rs new file mode 100644 index 0000000..fc54fc6 --- /dev/null +++ b/src/systems/mod.rs @@ -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}; diff --git a/src/systems/physics_sync.rs b/src/systems/physics_sync.rs new file mode 100644 index 0000000..0addf0b --- /dev/null +++ b/src/systems/physics_sync.rs @@ -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; + }); + } + } + } +} diff --git a/src/systems/render.rs b/src/systems/render.rs new file mode 100644 index 0000000..619963d --- /dev/null +++ b/src/systems/render.rs @@ -0,0 +1,23 @@ +use crate::render::DrawCall; +use crate::world::World; + +pub fn render_system(world: &World) -> Vec +{ + 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() +} diff --git a/src/systems/state_machine.rs b/src/systems/state_machine.rs new file mode 100644 index 0000000..d52c250 --- /dev/null +++ b/src/systems/state_machine.rs @@ -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); + } + } +} diff --git a/src/terrain.rs b/src/terrain.rs new file mode 100644 index 0000000..1388f4b --- /dev/null +++ b/src/terrain.rs @@ -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 + { + 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> + { + 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 = 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, + }) +} diff --git a/src/utility/input.rs b/src/utility/input.rs new file mode 100755 index 0000000..b0ecc0e --- /dev/null +++ b/src/utility/input.rs @@ -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 + } +} diff --git a/src/utility/mod.rs b/src/utility/mod.rs new file mode 100644 index 0000000..5f8564a --- /dev/null +++ b/src/utility/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod input; +pub(crate) mod time; +pub(crate) mod transform; diff --git a/src/utility/time.rs b/src/utility/time.rs new file mode 100644 index 0000000..57682de --- /dev/null +++ b/src/utility/time.rs @@ -0,0 +1,22 @@ +use std::sync::OnceLock; +use std::time::Instant; + +static GAME_START: OnceLock = 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) + } +} diff --git a/src/utility/transform.rs b/src/utility/transform.rs new file mode 100644 index 0000000..fdfa30b --- /dev/null +++ b/src/utility/transform.rs @@ -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 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 for Isometry3 +{ + 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 +{ + fn from(t: &Transform) -> Self + { + (*t).into() + } +} + +impl From> for Transform +{ + fn from(iso: Isometry3) -> 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> for Transform +{ + fn from(iso: &Isometry3) -> Self + { + (*iso).into() + } +} diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..04696b7 --- /dev/null +++ b/src/world.rs @@ -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, +} + +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(&self, entity: EntityHandle, f: F) -> Option + where + F: FnOnce(&Transform) -> R, + { + self.components.get(&entity).map(f) + } + + pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + self.components.keys().copied().collect() + } +} + +pub struct MeshStorage +{ + pub components: HashMap, +} + +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 + { + self.components.keys().copied().collect() + } +} + +pub struct PhysicsStorage +{ + pub components: HashMap, +} + +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 + { + self.components.get(&entity).copied() + } + + pub fn with(&self, entity: EntityHandle, f: F) -> Option + 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 + { + self.components.keys().copied().collect() + } +} + +pub struct MovementStorage +{ + pub components: HashMap, +} + +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(&self, entity: EntityHandle, f: F) -> Option + where + F: FnOnce(&MovementComponent) -> R, + { + self.components.get(&entity).map(f) + } + + pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + self.components.keys().copied().collect() + } +} + +pub struct JumpStorage +{ + pub components: HashMap, +} + +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(&self, entity: EntityHandle, f: F) -> Option + where + F: FnOnce(&JumpComponent) -> R, + { + self.components.get(&entity).map(f) + } + + pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + self.components.keys().copied().collect() + } +} + +pub struct InputStorage +{ + pub components: HashMap, +} + +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(&self, entity: EntityHandle, f: F) -> Option + where + F: FnOnce(&InputComponent) -> R, + { + self.components.get(&entity).map(f) + } + + pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + self.components.keys().copied().collect() + } +} + +pub struct PlayerTagStorage +{ + pub components: HashMap, +} + +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 + { + self.components.keys().copied().collect() + } +} + +pub struct StateMachineStorage +{ + pub components: HashMap, +} + +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(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + self.components.keys().copied().collect() + } +} + +pub struct CameraStorage +{ + pub components: HashMap, +} + +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(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + 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, +} + +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(&mut self, entity: EntityHandle, f: F) -> Option + 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 + { + 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); + } +} diff --git a/textures/height_map_x0_y0.exr b/textures/height_map_x0_y0.exr new file mode 100755 index 0000000..801b1d9 Binary files /dev/null and b/textures/height_map_x0_y0.exr differ