diff --git a/projects/README.md b/projects/README.md index ea9f176ce..8032ecdbe 100644 --- a/projects/README.md +++ b/projects/README.md @@ -23,3 +23,6 @@ This example walks you through in explicit detail how to use [Tauri](https://tau ### counter_dwarf_debug This example shows how to add breakpoints within the browser or visual studio code for debugging. + +### bevy3d_ui +This example uses the bevy 3d game engine with leptos within webassembly. diff --git a/projects/bevy3d_ui/Cargo.toml b/projects/bevy3d_ui/Cargo.toml new file mode 100644 index 000000000..7a3343774 --- /dev/null +++ b/projects/bevy3d_ui/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bevy3d_ui" +version = "0.1.0" +edition = "2021" + +[profile.release] +codegen-units = 1 +lto = true + +[dependencies] +leptos = { version = "0.6.11", features = ["csr"] } +leptos_meta = { version = "0.6.11", features = ["csr"] } +leptos_router = { version = "0.6.11", features = ["csr"] } +console_log = "1" +log = "0.4" +console_error_panic_hook = "0.1.7" +bevy = "0.13.2" +crossbeam-channel = "0.5.12" + +[dev-dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3.0" +web-sys = "0.3" + +[workspace] +# The empty workspace here is to keep rust-analyzer satisfied diff --git a/projects/bevy3d_ui/README.md b/projects/bevy3d_ui/README.md new file mode 100644 index 000000000..c71465df3 --- /dev/null +++ b/projects/bevy3d_ui/README.md @@ -0,0 +1,15 @@ +# Bevy 3D UI Example + +This example combines a leptos UI with a bevy 3D view. +Bevy is a 3D game engine written in rust that can be compiled to web assembly by using the wgpu library. +The wgpu library in turn can target the newer webgpu standard or the older webgl for web browsers. + +In the case of a desktop application, if you wanted to use a styled ui via leptos and a 3d view via bevy +you could also combine this with tauri. + +## Quick Start + + * Run `trunk serve to run the example. + * Browse to http://127.0.0.1:8080/ + +It's best to use a web browser with webgpu capability for best results such as Chrome or Opera. diff --git a/projects/bevy3d_ui/index.html b/projects/bevy3d_ui/index.html new file mode 100644 index 000000000..75fa1f12a --- /dev/null +++ b/projects/bevy3d_ui/index.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/projects/bevy3d_ui/public/favicon.ico b/projects/bevy3d_ui/public/favicon.ico new file mode 100644 index 000000000..2ba8527cb Binary files /dev/null and b/projects/bevy3d_ui/public/favicon.ico differ diff --git a/projects/bevy3d_ui/rust-toolchain.toml b/projects/bevy3d_ui/rust-toolchain.toml new file mode 100644 index 000000000..ff2a4ff10 --- /dev/null +++ b/projects/bevy3d_ui/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" # test change diff --git a/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs b/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs new file mode 100644 index 000000000..03513e010 --- /dev/null +++ b/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs @@ -0,0 +1,38 @@ +use bevy::prelude::*; + +/// Event Processor +#[derive(Resource)] +pub struct EventProcessor { + pub sender: crossbeam_channel::Sender, + pub receiver: crossbeam_channel::Receiver, +} + +impl Clone for EventProcessor { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: self.receiver.clone(), + } + } +} + +/// Events sent from the client to bevy +#[derive(Debug)] +pub enum ClientInEvents { + /// Update the 3d model position from the client + CounterEvt(CounterEvtData), +} + +/// Events sent out from bevy to the client +#[derive(Debug)] +pub enum PluginOutEvents { + /// TODO Feed back to the client an event from bevy + Click, +} + +/// Input event to update the bevy view from the client +#[derive(Clone, Debug, Event)] +pub struct CounterEvtData { + /// Amount to move on the Y Axis + pub value: f32, +} diff --git a/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs b/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs new file mode 100644 index 000000000..d6e89b83c --- /dev/null +++ b/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs @@ -0,0 +1,2 @@ +pub mod events; +pub mod plugin; diff --git a/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs b/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs new file mode 100644 index 000000000..bf061f2fb --- /dev/null +++ b/projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs @@ -0,0 +1,63 @@ +use super::events::*; +use bevy::prelude::*; + +/// Events plugin for bevy +#[derive(Clone)] +pub struct DuplexEventsPlugin { + /// Client processor for sending ClientInEvents, receiving PluginOutEvents + client_processor: EventProcessor, + /// Internal processor for sending PluginOutEvents, receiving ClientInEvents + plugin_processor: EventProcessor, +} + +impl DuplexEventsPlugin { + /// Create a new instance + pub fn new() -> DuplexEventsPlugin { + // For sending messages from bevy to the client + let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50); + // For sending message from the client to bevy + let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50); + let instance = DuplexEventsPlugin { + client_processor: EventProcessor { + sender: client_sender, + receiver: client_receiver, + }, + plugin_processor: EventProcessor { + sender: bevy_sender, + receiver: bevy_receiver, + }, + }; + instance + } + + /// Get the client event processor + pub fn get_processor( + &self, + ) -> EventProcessor { + self.client_processor.clone() + } +} + +/// Build the bevy plugin and attach +impl Plugin for DuplexEventsPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(self.plugin_processor.clone()) + .init_resource::>() + .add_systems(PreUpdate, input_events_system); + } +} + +/// Send the event to bevy using EventWriter +fn input_events_system( + int_processor: Res>, + mut counter_event_writer: EventWriter, +) { + for input_event in int_processor.receiver.try_iter() { + match input_event { + ClientInEvents::CounterEvt(event) => { + // Send event through Bevy's event system + counter_event_writer.send(event); + } + } + } +} diff --git a/projects/bevy3d_ui/src/demos/bevydemo1/mod.rs b/projects/bevy3d_ui/src/demos/bevydemo1/mod.rs new file mode 100644 index 000000000..933b4f2d8 --- /dev/null +++ b/projects/bevy3d_ui/src/demos/bevydemo1/mod.rs @@ -0,0 +1,3 @@ +pub mod eventqueue; +pub mod scene; +pub mod state; diff --git a/projects/bevy3d_ui/src/demos/bevydemo1/scene.rs b/projects/bevy3d_ui/src/demos/bevydemo1/scene.rs new file mode 100644 index 000000000..58401d041 --- /dev/null +++ b/projects/bevy3d_ui/src/demos/bevydemo1/scene.rs @@ -0,0 +1,124 @@ +use super::eventqueue::events::{ + ClientInEvents, CounterEvtData, EventProcessor, PluginOutEvents, +}; +use super::eventqueue::plugin::DuplexEventsPlugin; +use super::state::{Shared, SharedResource, SharedState}; +use bevy::prelude::*; + +/// Represents the Cube in the scene +#[derive(Component, Copy, Clone)] +pub struct Cube; + +/// Represents the 3D Scene +#[derive(Clone)] +pub struct Scene { + is_setup: bool, + canvas_id: String, + evt_plugin: DuplexEventsPlugin, + shared_state: Shared, + processor: EventProcessor, +} + +impl Scene { + /// Create a new instance + pub fn new(canvas_id: String) -> Scene { + let plugin = DuplexEventsPlugin::new(); + let instance = Scene { + is_setup: false, + canvas_id: canvas_id, + evt_plugin: plugin.clone(), + shared_state: SharedState::new(), + processor: plugin.get_processor(), + }; + instance + } + + /// Get the shared state + pub fn get_state(&self) -> Shared { + self.shared_state.clone() + } + + /// Get the event processor + pub fn get_processor( + &self, + ) -> EventProcessor { + self.processor.clone() + } + + /// Setup and attach the bevy instance to the html canvas element + pub fn setup(&mut self) { + if self.is_setup == true { + return; + }; + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + canvas: Some(self.canvas_id.clone()), + ..default() + }), + ..default() + })) + .add_plugins(self.evt_plugin.clone()) + .insert_resource(SharedResource(self.shared_state.clone())) + .add_systems(Startup, setup_scene) + .add_systems(Update, handle_bevy_event) + .run(); + self.is_setup = true; + } +} + +/// Setup the scene +fn setup_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + resource: Res, +) { + let name = resource.0.lock().unwrap().name.clone(); + // circular base + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x( + -std::f32::consts::FRAC_PI_2, + )), + ..default() + }); + // cube + commands.spawn(( + PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + material: materials.add(Color::rgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }, + Cube, + )); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + // camera + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.5, 4.5, 9.0) + .looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + commands.spawn(TextBundle::from_section(name, TextStyle::default())); +} + +/// Move the Cube on event +fn handle_bevy_event( + mut counter_event_reader: EventReader, + mut cube_query: Query<&mut Transform, With>, +) { + let mut cube_transform = cube_query.get_single_mut().expect("no cube :("); + for _ev in counter_event_reader.read() { + cube_transform.translation += Vec3::new(0.0, _ev.value, 0.0); + } +} diff --git a/projects/bevy3d_ui/src/demos/bevydemo1/state.rs b/projects/bevy3d_ui/src/demos/bevydemo1/state.rs new file mode 100644 index 000000000..b4aed2ab5 --- /dev/null +++ b/projects/bevy3d_ui/src/demos/bevydemo1/state.rs @@ -0,0 +1,24 @@ +use bevy::ecs::system::Resource; +use std::sync::{Arc, Mutex}; + +pub type Shared = Arc>; + +/// Shared Resource used for Bevy +#[derive(Resource)] +pub struct SharedResource(pub Shared); + +/// Shared State +pub struct SharedState { + pub name: String, +} + +impl SharedState { + /// Get a new shared state + pub fn new() -> Arc> { + let state = SharedState { + name: "This can be used for shared state".to_string(), + }; + let shared = Arc::new(Mutex::new(state)); + shared + } +} diff --git a/projects/bevy3d_ui/src/demos/mod.rs b/projects/bevy3d_ui/src/demos/mod.rs new file mode 100644 index 000000000..d9d3a23a2 --- /dev/null +++ b/projects/bevy3d_ui/src/demos/mod.rs @@ -0,0 +1 @@ +pub mod bevydemo1; diff --git a/projects/bevy3d_ui/src/main.rs b/projects/bevy3d_ui/src/main.rs new file mode 100644 index 000000000..d35c07344 --- /dev/null +++ b/projects/bevy3d_ui/src/main.rs @@ -0,0 +1,11 @@ +mod demos; +mod routes; +use leptos::*; +use routes::RootPage; + +pub fn main() { + // Bevy will output a lot of debug info to the console when this is enabled. + //_ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + mount_to_body(|| view! { }) +} diff --git a/projects/bevy3d_ui/src/routes/demo1.rs b/projects/bevy3d_ui/src/routes/demo1.rs new file mode 100644 index 000000000..6c49894e0 --- /dev/null +++ b/projects/bevy3d_ui/src/routes/demo1.rs @@ -0,0 +1,52 @@ +use crate::demos::bevydemo1::eventqueue::events::{ + ClientInEvents, CounterEvtData, +}; +use crate::demos::bevydemo1::scene::Scene; +use leptos::*; + +/// 3d view component +#[component] +pub fn Demo1() -> impl IntoView { + // Setup a Counter + let initial_value: i32 = 0; + let step: i32 = 1; + let (value, set_value) = create_signal(initial_value); + + // Setup a bevy 3d scene + let scene = Scene::new("#bevy".to_string()); + let sender = scene.get_processor().sender; + let (sender_sig, _set_sender_sig) = create_signal(sender); + let (scene_sig, _set_scene_sig) = create_signal(scene); + + // We need to add the 3D view onto the canvas post render. + create_effect(move |_| { + request_animation_frame(move || { + scene_sig.get().setup(); + }); + }); + + view! { +
+ + + "Value: " {value} "!" + +
+ + + } +} diff --git a/projects/bevy3d_ui/src/routes/mod.rs b/projects/bevy3d_ui/src/routes/mod.rs new file mode 100644 index 000000000..77cc30b61 --- /dev/null +++ b/projects/bevy3d_ui/src/routes/mod.rs @@ -0,0 +1,24 @@ +pub mod demo1; +use demo1::Demo1; +use leptos::*; +use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title}; +use leptos_router::*; + +#[component] +pub fn RootPage() -> impl IntoView { + provide_meta_context(); + + view! { + + + + + + + <Router> + <Routes> + <Route path="" view=|| view! { <Demo1/> }/> + </Routes> + </Router> + } +}