From 13ad1b235d3a7b6b119deaf5bf59bb5afe17e220 Mon Sep 17 00:00:00 2001 From: Hecatron Date: Mon, 27 May 2024 20:55:27 +0100 Subject: [PATCH] projects: example using the bevy 3d game engine and leptos (#2577) * feat: Added example using the bevy 3d game engine and leptos * fix: moved example to projects * workspace fix --- projects/README.md | 3 + projects/bevy3d_ui/Cargo.toml | 26 ++++ projects/bevy3d_ui/README.md | 15 +++ projects/bevy3d_ui/index.html | 8 ++ projects/bevy3d_ui/public/favicon.ico | Bin 0 -> 15406 bytes projects/bevy3d_ui/rust-toolchain.toml | 2 + .../src/demos/bevydemo1/eventqueue/events.rs | 38 ++++++ .../src/demos/bevydemo1/eventqueue/mod.rs | 2 + .../src/demos/bevydemo1/eventqueue/plugin.rs | 63 +++++++++ projects/bevy3d_ui/src/demos/bevydemo1/mod.rs | 3 + .../bevy3d_ui/src/demos/bevydemo1/scene.rs | 124 ++++++++++++++++++ .../bevy3d_ui/src/demos/bevydemo1/state.rs | 24 ++++ projects/bevy3d_ui/src/demos/mod.rs | 1 + projects/bevy3d_ui/src/main.rs | 11 ++ projects/bevy3d_ui/src/routes/demo1.rs | 52 ++++++++ projects/bevy3d_ui/src/routes/mod.rs | 24 ++++ 16 files changed, 396 insertions(+) create mode 100644 projects/bevy3d_ui/Cargo.toml create mode 100644 projects/bevy3d_ui/README.md create mode 100644 projects/bevy3d_ui/index.html create mode 100644 projects/bevy3d_ui/public/favicon.ico create mode 100644 projects/bevy3d_ui/rust-toolchain.toml create mode 100644 projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs create mode 100644 projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs create mode 100644 projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs create mode 100644 projects/bevy3d_ui/src/demos/bevydemo1/mod.rs create mode 100644 projects/bevy3d_ui/src/demos/bevydemo1/scene.rs create mode 100644 projects/bevy3d_ui/src/demos/bevydemo1/state.rs create mode 100644 projects/bevy3d_ui/src/demos/mod.rs create mode 100644 projects/bevy3d_ui/src/main.rs create mode 100644 projects/bevy3d_ui/src/routes/demo1.rs create mode 100644 projects/bevy3d_ui/src/routes/mod.rs 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 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 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 k<4FtN!5 { + 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> + } +}