mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-21 07:55:55 +00:00
Rename projects
This commit is contained in:
@@ -0,0 +1 @@
|
||||
RUSTFLAGS = "--cfg=web_sys_unstable_apis"
|
||||
@@ -0,0 +1,91 @@
|
||||
[package]
|
||||
name = "studio"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "studio"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "studio-standalone"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = { version = "0.2.84" }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
|
||||
[features]
|
||||
wasm = ["dep:console_error_panic_hook"]
|
||||
|
||||
[dependencies.bevy]
|
||||
workspace = true
|
||||
default-features = false
|
||||
# TODO: Narrow this down to the features we actually need
|
||||
# FIXME: WebGPU is sparsely supported, but using the default WebGL backend
|
||||
# causes a panic upstream: "Tex storage 2D multisample is not supported"
|
||||
features = [
|
||||
"animation",
|
||||
"bevy_asset",
|
||||
"bevy_audio",
|
||||
"bevy_gilrs",
|
||||
"bevy_scene",
|
||||
"bevy_winit",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_pbr",
|
||||
"bevy_gltf",
|
||||
"bevy_render",
|
||||
"bevy_sprite",
|
||||
"bevy_text",
|
||||
"bevy_ui",
|
||||
"png",
|
||||
"hdr",
|
||||
"ktx2",
|
||||
"zstd",
|
||||
"vorbis",
|
||||
"x11",
|
||||
"filesystem_watcher",
|
||||
"bevy_gizmos",
|
||||
"android_shared_stdcxx",
|
||||
"tonemapping_luts",
|
||||
"default_font",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies.bevy]
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = [
|
||||
"animation",
|
||||
"bevy_asset",
|
||||
"bevy_audio",
|
||||
"bevy_gilrs",
|
||||
"bevy_scene",
|
||||
"bevy_winit",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_pbr",
|
||||
"bevy_gltf",
|
||||
"bevy_render",
|
||||
"bevy_sprite",
|
||||
"bevy_text",
|
||||
"bevy_ui",
|
||||
"png",
|
||||
"hdr",
|
||||
"ktx2",
|
||||
"zstd",
|
||||
"vorbis",
|
||||
"x11",
|
||||
"filesystem_watcher",
|
||||
"bevy_gizmos",
|
||||
"android_shared_stdcxx",
|
||||
"tonemapping_luts",
|
||||
"default_font",
|
||||
# TODO: This line is the only difference from default. Is there a way to
|
||||
# deduplicate the feature list?
|
||||
"dynamic_linking",
|
||||
]
|
||||
|
||||
# FIXME: Profiles for the non-root package are ignored, but [target] keys are
|
||||
# not allowed in the root package >_<
|
||||
# [target.'cfg(target_family = "wasm")'.profile.release]
|
||||
# opt-level = "z"
|
||||
@@ -0,0 +1,44 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
/// Draw grid-lines at a given scale and camera depth.
|
||||
///
|
||||
/// @param pos The reference coordinates for the grid lines.
|
||||
/// @param depth Distance of the camera from the fragment being shaded.
|
||||
/// @param scale
|
||||
/// `pos` will be multiplied by this value before computing the color value.
|
||||
/// E.g., a scale of 10 will draw 10 lines for each whole-number increment
|
||||
/// on the X and Z axes of `pos`.
|
||||
/// @param depth_min, depth_max
|
||||
/// Controls the opacity of the grid lines relative to the camera depth.
|
||||
/// These will be mapped to 0-1 and multiplied by the color value to produce
|
||||
/// the final output.
|
||||
fn grid_lines(pos: vec4<f32>, depth: f32, scale: f32, depth_min: f32, depth_max: f32) -> f32 {
|
||||
let ref_coords = abs(pos) * scale;
|
||||
let fracs = fract(ref_coords);
|
||||
let thickness = (0.0333 * scale) / (depth * 100.0);
|
||||
|
||||
let x = smoothstep(0.0, thickness, fracs.x);
|
||||
let 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, 10.0, 0.001, 0.1);
|
||||
let base = 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>(0.5, 0.5, 0.5, 0.5) * min(0.5, saturate(dense + base + sparse));
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "studio",
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "./studio/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nxrs/cargo:build",
|
||||
"options": {
|
||||
"release": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"release": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"build:web": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
"wasm-pack build --out-dir ../dist/studio/ -- --features wasm",
|
||||
"node scripts/copy-assets.mjs"
|
||||
],
|
||||
"cwd": "studio",
|
||||
"envFile": "studio/.env",
|
||||
"parallel": false
|
||||
}
|
||||
},
|
||||
"run": {
|
||||
"executor": "@nxrs/cargo:build",
|
||||
"options": {
|
||||
"release": false,
|
||||
"run": true,
|
||||
"bin": "studio-standalone"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nxrs/cargo:test",
|
||||
"options": {}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nxrs/cargo:clippy",
|
||||
"options": {
|
||||
"fix": false,
|
||||
"failOnWarnings": true,
|
||||
"noDeps": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
async function main() {
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||
const assetsSrc = path.resolve(__dirname, "../assets");
|
||||
const assetsDest = path.resolve(__dirname, "../../dist/studio/public/assets");
|
||||
|
||||
await fs.cp(assetsSrc, assetsDest, { recursive: true });
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,41 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod orbit_cam;
|
||||
mod test_scene;
|
||||
mod ui;
|
||||
|
||||
use orbit_cam::{OrbitCam, OrbitCamPlugin};
|
||||
use test_scene::TestScenePlugin;
|
||||
use ui::UiLayerPlugin;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn init() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
App::new()
|
||||
.add_plugins((
|
||||
DefaultPlugins,
|
||||
OrbitCamPlugin,
|
||||
TestScenePlugin,
|
||||
UiLayerPlugin,
|
||||
))
|
||||
.add_systems(Update, draw_world_axes.after(orbit_cam::pan_orbit_camera))
|
||||
.run();
|
||||
}
|
||||
|
||||
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,3 @@
|
||||
fn main() {
|
||||
studio::init();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#[rustfmt::skip]
|
||||
use bevy::{
|
||||
pbr::NotShadowCaster,
|
||||
prelude::*,
|
||||
reflect::{TypePath, TypeUuid},
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
};
|
||||
|
||||
use crate::orbit_cam::{self, OrbitCam};
|
||||
|
||||
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)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
move_plane_with_camera.after(orbit_cam::pan_orbit_camera),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct WorldGridPlane;
|
||||
|
||||
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
|
||||
let plane = meshes.add(Mesh::from(shape::Plane {
|
||||
size: 200.0,
|
||||
subdivisions: 0,
|
||||
}));
|
||||
let proc_grid = mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
|
||||
cmd.spawn((
|
||||
SpatialBundle::from_transform(Transform::IDENTITY),
|
||||
WorldGridPlane,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
MaterialMeshBundle {
|
||||
mesh: plane.clone(),
|
||||
material: proc_grid.clone(),
|
||||
transform: Transform::IDENTITY,
|
||||
..default()
|
||||
},
|
||||
NotShadowCaster,
|
||||
));
|
||||
parent.spawn((
|
||||
MaterialMeshBundle {
|
||||
mesh: plane,
|
||||
material: proc_grid,
|
||||
transform: Transform::IDENTITY.looking_to(Vec3::Z, Vec3::NEG_Y),
|
||||
..default()
|
||||
},
|
||||
NotShadowCaster,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn move_plane_with_camera(
|
||||
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
|
||||
camera: Query<&OrbitCam, Changed<OrbitCam>>,
|
||||
) {
|
||||
if let Ok(&OrbitCam { focus, .. }) = camera.get_single() {
|
||||
let (_, mut xform) = plane.single_mut();
|
||||
|
||||
xform.translation.x = focus.x;
|
||||
xform.translation.z = focus.z;
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use bevy::{
|
||||
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub struct UiLayerPlugin;
|
||||
|
||||
impl Plugin for UiLayerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(FrameTimeDiagnosticsPlugin)
|
||||
.add_systems(Startup, layout)
|
||||
.add_systems(Update, update);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct FramerateCounter;
|
||||
|
||||
fn layout(mut cmd: Commands) {
|
||||
cmd.spawn(NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.),
|
||||
height: Val::Percent(100.),
|
||||
padding: UiRect::all(Val::Px(16.)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
FramerateCounter,
|
||||
TextBundle::from_section(
|
||||
"0 fps",
|
||||
TextStyle {
|
||||
color: Color::GREEN,
|
||||
font_size: 14.,
|
||||
..default()
|
||||
},
|
||||
)
|
||||
.with_text_alignment(TextAlignment::Right)
|
||||
.with_style(Style {
|
||||
// TODO: This doesn't seem to have any effect
|
||||
width: Val::Percent(100.),
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn update(
|
||||
time: Res<Time>,
|
||||
diag: Res<DiagnosticsStore>,
|
||||
mut counter: Query<&mut Text, With<FramerateCounter>>,
|
||||
) {
|
||||
if let Some(fps_diag) = diag.get(FrameTimeDiagnosticsPlugin::FPS) {
|
||||
let fps = if let Some(fps_smoothed) = fps_diag.smoothed() {
|
||||
fps_smoothed
|
||||
} else {
|
||||
let ms = time.delta().as_micros() as f64 / 1000.;
|
||||
1000. / ms
|
||||
};
|
||||
|
||||
counter.single_mut().sections[0].value = format!("{fps:.2} fps");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user