mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-19 07:53:25 +00:00
Camera controller, basic test scene
This commit is contained in:
Generated
+1
@@ -1803,6 +1803,7 @@ name = "hello-world"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy",
|
"bevy",
|
||||||
|
"console_error_panic_hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { workspace = true }
|
bevy = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
|
||||||
[target.'cfg(not(target_family = "wasm"))'.dependencies.bevy]
|
[target.'cfg(not(target_family = "wasm"))'.dependencies.bevy]
|
||||||
workspace = true
|
workspace = true
|
||||||
features = ["dynamic_linking"]
|
features = ["dynamic_linking"]
|
||||||
|
|||||||
@@ -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<f32>, 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<f32>(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<f32> {
|
||||||
|
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<f32>(1.0, 1.0, 1.0, 1.0) * 0.5 * saturate(dense + sparse);
|
||||||
|
}
|
||||||
+29
-2
@@ -1,14 +1,41 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
mod orbit_cam;
|
||||||
mod people;
|
mod people;
|
||||||
|
mod test_scene;
|
||||||
|
|
||||||
|
use orbit_cam::{OrbitCam, OrbitCamPlugin};
|
||||||
|
use test_scene::TestScenePlugin;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins((DefaultPlugins, people::Plugin))
|
.add_plugins((
|
||||||
|
DefaultPlugins,
|
||||||
|
people::Plugin,
|
||||||
|
OrbitCamPlugin,
|
||||||
|
TestScenePlugin,
|
||||||
|
))
|
||||||
.add_systems(Startup, hello_world)
|
.add_systems(Startup, hello_world)
|
||||||
|
.add_systems(Update, draw_world_axes.after(orbit_cam::pan_orbit_camera))
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hello_world() {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<PrimaryWindow>>,
|
||||||
|
mut mouse_motion: EventReader<MouseMotion>,
|
||||||
|
mouse_buttons: Res<Input<MouseButton>>,
|
||||||
|
keyboard: Res<Input<KeyCode>>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bevy::{
|
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},
|
time::{Time, TimerMode},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ fn startup(mut cmds: Commands) {
|
|||||||
fn greet(time: Res<Time>, mut timer: ResMut<Timer>, q: Query<&Name, With<Person>>) {
|
fn greet(time: Res<Time>, mut timer: ResMut<Timer>, q: Query<&Name, With<Person>>) {
|
||||||
if timer.tick(time.delta()).just_finished() {
|
if timer.tick(time.delta()).just_finished() {
|
||||||
for name in &q {
|
for name in &q {
|
||||||
println!("Hello, {}!", name.0);
|
info!("Hello, {}!", name.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use bevy::{
|
||||||
|
pbr::NotShadowCaster,
|
||||||
|
prelude::*,
|
||||||
|
reflect::{TypePath, TypeUuid},
|
||||||
|
render::render_resource::{AsBindGroup, ShaderRef},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct TestScenePlugin;
|
||||||
|
|
||||||
|
impl Plugin for TestScenePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
|
||||||
|
.add_plugins(MaterialPlugin::<ProceduralGridMaterial> {
|
||||||
|
prepass_enabled: true,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.add_systems(Startup, spawn_scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_scene(
|
||||||
|
mut cmd: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut std_mats: ResMut<Assets<StandardMaterial>>,
|
||||||
|
mut mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
|
||||||
|
) {
|
||||||
|
let mut light_xform = Transform::IDENTITY;
|
||||||
|
light_xform.rotation = Quat::from_rotation_y(f32::to_radians(-70.0)) * light_xform.rotation;
|
||||||
|
light_xform.rotation *= Quat::from_rotation_x(f32::to_radians(-60.0));
|
||||||
|
|
||||||
|
cmd.spawn(DirectionalLightBundle {
|
||||||
|
directional_light: DirectionalLight {
|
||||||
|
illuminance: 25_000.0,
|
||||||
|
shadows_enabled: true,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
transform: light_xform,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cube
|
||||||
|
cmd.spawn(PbrBundle {
|
||||||
|
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
|
||||||
|
material: std_mats.add(StandardMaterial {
|
||||||
|
base_color: Color::SILVER,
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
transform: Transform::IDENTITY,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plane
|
||||||
|
cmd.spawn((
|
||||||
|
MaterialMeshBundle {
|
||||||
|
mesh: meshes.add(Mesh::from(shape::Plane {
|
||||||
|
size: 10_000.0,
|
||||||
|
subdivisions: 0,
|
||||||
|
})),
|
||||||
|
material: mat_proc_grid.add(ProceduralGridMaterial {}),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
NotShadowCaster,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AsBindGroup, Clone, TypePath, TypeUuid)]
|
||||||
|
#[uuid = "CE331A13-F247-4F5B-A75E-670B2BD5776A"]
|
||||||
|
struct ProceduralGridMaterial {}
|
||||||
|
|
||||||
|
impl Material for ProceduralGridMaterial {
|
||||||
|
fn fragment_shader() -> ShaderRef {
|
||||||
|
"shaders/procedural_grid.wgsl".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha_mode(&self) -> AlphaMode {
|
||||||
|
AlphaMode::Blend
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user