From a60d86f2d2a32ffc1c4eec21f5c6dbdf1b5480f2 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Wed, 27 Sep 2023 23:15:19 -0400 Subject: [PATCH] Camera controller, basic test scene --- Cargo.lock | 1 + hello-world/Cargo.toml | 3 + .../assets/shaders/procedural_grid.wgsl | 49 +++++ hello-world/src/main.rs | 31 +++- hello-world/src/orbit_cam.rs | 169 ++++++++++++++++++ hello-world/src/people.rs | 6 +- hello-world/src/test_scene.rs | 78 ++++++++ 7 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 hello-world/assets/shaders/procedural_grid.wgsl create mode 100644 hello-world/src/orbit_cam.rs create mode 100644 hello-world/src/test_scene.rs diff --git a/Cargo.lock b/Cargo.lock index adf75f7..527447b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1803,6 +1803,7 @@ name = "hello-world" version = "0.1.0" dependencies = [ "bevy", + "console_error_panic_hook", ] [[package]] diff --git a/hello-world/Cargo.toml b/hello-world/Cargo.toml index ba775ee..78a6dd1 100644 --- a/hello-world/Cargo.toml +++ b/hello-world/Cargo.toml @@ -8,6 +8,9 @@ edition = "2021" [dependencies] bevy = { workspace = true } +[target.'cfg(target_family = "wasm")'.dependencies] +console_error_panic_hook = "0.1.7" + [target.'cfg(not(target_family = "wasm"))'.dependencies.bevy] workspace = true features = ["dynamic_linking"] diff --git a/hello-world/assets/shaders/procedural_grid.wgsl b/hello-world/assets/shaders/procedural_grid.wgsl new file mode 100644 index 0000000..3d4b7a6 --- /dev/null +++ b/hello-world/assets/shaders/procedural_grid.wgsl @@ -0,0 +1,49 @@ +// TODO: Figure out how many of these imports we actually need +#import bevy_pbr::mesh_view_bindings +#import bevy_pbr::mesh_bindings + +#import bevy_pbr::mesh_functions +#import bevy_pbr::prepass_utils + +#import bevy_pbr::mesh_vertex_output MeshVertexOutput + +fn inv_lerp(a: f32, b: f32, v: f32) -> f32 { + if (a == b) { return 1.0; } + return (v - a) / (b - a); +} + +fn grid_lines(pos: vec4, depth: f32, scale: f32, depth_min: f32, depth_max: f32) -> f32 { + let ref_coords = abs(pos) * scale; + + // TODO: `modf` no longer takes a pointer as its second argument, but that + // change is not yet reflected in the Naga version used at time of writing. + // When Bevy updates its Naga dependency, this will need to be updated: + // ``` + // let fracs = modf(ref_coords).fract; + // ``` + // Reference: https://www.w3.org/TR/2023/WD-WGSL-20230927/#modf-builtin + var wholes = vec4(0.0, 0.0, 0.0, 0.0); + modf(ref_coords, &wholes); + let fracs = ref_coords - wholes; + + let thickness = (0.0333 * scale) / (depth * 100.0); + + var x = smoothstep(0.0, thickness, fracs.x); + var z = smoothstep(0.0, thickness, fracs.z); + let value = 1.0 - saturate(x * z); + + let mask = inv_lerp(depth_min, depth_max, depth); + + return saturate(value * mask); +} + +@fragment +fn fragment(in: MeshVertexOutput) -> @location(0) vec4 { + let depth = in.position.z; + let pos = in.world_position; + + let dense = grid_lines(pos, depth, 1.0, 0.0001, 0.01); + let sparse = grid_lines(pos, depth, 0.1, 0.00005, 0.005); + + return vec4(1.0, 1.0, 1.0, 1.0) * 0.5 * saturate(dense + sparse); +} diff --git a/hello-world/src/main.rs b/hello-world/src/main.rs index 8879c12..0bbc21e 100644 --- a/hello-world/src/main.rs +++ b/hello-world/src/main.rs @@ -1,14 +1,41 @@ use bevy::prelude::*; +mod orbit_cam; mod people; +mod test_scene; + +use orbit_cam::{OrbitCam, OrbitCamPlugin}; +use test_scene::TestScenePlugin; fn main() { + #[cfg(target_arch = "wasm32")] + console_error_panic_hook::set_once(); + App::new() - .add_plugins((DefaultPlugins, people::Plugin)) + .add_plugins(( + DefaultPlugins, + people::Plugin, + OrbitCamPlugin, + TestScenePlugin, + )) .add_systems(Startup, hello_world) + .add_systems(Update, draw_world_axes.after(orbit_cam::pan_orbit_camera)) .run(); } fn hello_world() { - println!("Hello, world!"); + info!("Hello, world!"); +} + +fn draw_world_axes(mut gizmos: Gizmos, q: Query<&OrbitCam>) { + let &OrbitCam { focus, radius, .. } = q.single(); + + let mut draw_axis = |direction: Vec3, color: Color| { + let end = focus + direction * radius / 15.; + gizmos.line(focus, end, color); + }; + + draw_axis(Vec3::X, Color::RED); + draw_axis(Vec3::Y, Color::GREEN); + draw_axis(Vec3::Z, Color::BLUE); } diff --git a/hello-world/src/orbit_cam.rs b/hello-world/src/orbit_cam.rs new file mode 100644 index 0000000..b184b1a --- /dev/null +++ b/hello-world/src/orbit_cam.rs @@ -0,0 +1,169 @@ +use std::f32::consts; + +use bevy::{ + core_pipeline::prepass::{DepthPrepass, NormalPrepass}, + input::mouse::{MouseButton, MouseMotion}, + prelude::*, + window::PrimaryWindow, +}; + +pub struct OrbitCamPlugin; + +impl Plugin for OrbitCamPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, spawn_camera) + .add_systems(Update, pan_orbit_camera); + } +} + +#[derive(Component)] +pub struct OrbitCam { + pub focus: Vec3, + pub radius: f32, + pub upside_down: bool, +} + +impl OrbitCam { + // TODO: Make these configurable + const ORBIT_BTN: MouseButton = MouseButton::Left; + const PAN_BTN: MouseButton = MouseButton::Middle; + const ZOOM_BTN: MouseButton = MouseButton::Right; +} + +impl Default for OrbitCam { + fn default() -> Self { + Self { + focus: Vec3::ZERO, + radius: 10.0, + upside_down: false, + } + } +} + +enum ControlMode { + Orbit, + Pan, + Zoom, +} + +fn spawn_camera(mut cmd: Commands) { + let camera_xform = Transform::from_xyz(-2.5, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y); + + cmd.spawn(( + Camera3dBundle { + transform: camera_xform, + ..default() + }, + DepthPrepass, + NormalPrepass, + OrbitCam { + radius: camera_xform.translation.length(), + ..default() + }, + )); +} + +pub fn pan_orbit_camera( + window: Query<&Window, With>, + mut mouse_motion: EventReader, + mouse_buttons: Res>, + keyboard: Res>, + mut camera: Query<(&mut OrbitCam, &mut Transform, &Projection)>, +) { + if !(keyboard.pressed(KeyCode::AltLeft) || keyboard.pressed(KeyCode::AltRight)) { + return; + } + + let (mut orbit, mut xform, proj) = camera.single_mut(); + + if mouse_buttons.just_pressed(OrbitCam::ORBIT_BTN) + || mouse_buttons.just_released(OrbitCam::ORBIT_BTN) + { + orbit.upside_down = xform.up().y <= 0.0; + } + + let control_mode = if mouse_buttons.pressed(OrbitCam::ORBIT_BTN) { + ControlMode::Orbit + } else if mouse_buttons.pressed(OrbitCam::ZOOM_BTN) { + ControlMode::Zoom + } else if mouse_buttons.pressed(OrbitCam::PAN_BTN) { + ControlMode::Pan + } else { + return; + }; + + let window = window.single(); + let mouse_delta = mouse_motion + .into_iter() + .fold(Vec2::ZERO, |accum, current| accum + current.delta); + + match control_mode { + ControlMode::Orbit => { + do_orbit(window, mouse_delta, &mut xform, &orbit); + } + ControlMode::Pan => { + do_pan(proj, window, mouse_delta, &mut xform, &mut orbit); + } + ControlMode::Zoom => { + let delta = (mouse_delta.y * 1.5) / window.height(); + orbit.radius -= orbit.radius * delta; + xform.translation = orbit.focus + xform.back() * orbit.radius; + } + } +} + +#[inline] +fn do_orbit(win: &Window, mouse_delta: Vec2, xform: &mut Transform, orbit: &OrbitCam) { + // Translate to origin + xform.translation += xform.forward() * orbit.radius; + + // Calculate rotation + let delta_x = { + let delta = mouse_delta.x / win.width() * consts::PI * 2.0; + match orbit.upside_down { + true => -delta, + false => delta, + } + }; + let delta_y = mouse_delta.y / win.height() * consts::PI; + + let yaw = Quat::from_rotation_y(-delta_x); + let pitch = Quat::from_rotation_x(-delta_y); + + // Rotate around global Y and local X + xform.rotation = yaw * xform.rotation; + xform.rotation *= pitch; + + // Translate back to the original position + xform.translation += xform.back() * orbit.radius; +} + +#[inline] +fn do_pan( + proj: &Projection, + win: &Window, + mouse_delta: Vec2, + xform: &mut Transform, + orbit: &mut OrbitCam, +) { + // Normalize the pan distance with respect to the window resolution + let pan = match proj { + Projection::Perspective(p) => { + mouse_delta + * Vec2 { + x: (p.fov * p.aspect_ratio) / win.width(), + y: p.fov / win.height(), + } + } + _ => mouse_delta, + }; + + // Pan along local XY plane, with the distance scaled by the radius + let up = xform.up(); + let left = xform.left(); + let delta = (pan.y * up + pan.x * left) * orbit.radius * 0.8; + + // Update camera translation and focus point + xform.translation += delta; + orbit.focus += delta; +} diff --git a/hello-world/src/people.rs b/hello-world/src/people.rs index e2c066e..7af7036 100644 --- a/hello-world/src/people.rs +++ b/hello-world/src/people.rs @@ -1,7 +1,9 @@ use std::time::Duration; use bevy::{ - prelude::{App, Commands, Component, Query, Res, ResMut, Resource, Startup, Update, With}, + prelude::{ + info, App, Commands, Component, Query, Res, ResMut, Resource, Startup, Update, With, + }, time::{Time, TimerMode}, }; @@ -48,7 +50,7 @@ fn startup(mut cmds: Commands) { fn greet(time: Res