mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
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
This commit is contained in:
parent
a2c7e23d54
commit
13ad1b235d
16 changed files with 396 additions and 0 deletions
|
@ -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.
|
||||
|
|
26
projects/bevy3d_ui/Cargo.toml
Normal file
26
projects/bevy3d_ui/Cargo.toml
Normal file
|
@ -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
|
15
projects/bevy3d_ui/README.md
Normal file
15
projects/bevy3d_ui/README.md
Normal file
|
@ -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.
|
8
projects/bevy3d_ui/index.html
Normal file
8
projects/bevy3d_ui/index.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
BIN
projects/bevy3d_ui/public/favicon.ico
Normal file
BIN
projects/bevy3d_ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
2
projects/bevy3d_ui/rust-toolchain.toml
Normal file
2
projects/bevy3d_ui/rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable" # test change
|
38
projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs
Normal file
38
projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/events.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
/// Event Processor
|
||||
#[derive(Resource)]
|
||||
pub struct EventProcessor<TSender, TReceiver> {
|
||||
pub sender: crossbeam_channel::Sender<TSender>,
|
||||
pub receiver: crossbeam_channel::Receiver<TReceiver>,
|
||||
}
|
||||
|
||||
impl<TSender, TReceiver> Clone for EventProcessor<TSender, TReceiver> {
|
||||
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,
|
||||
}
|
2
projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs
Normal file
2
projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod events;
|
||||
pub mod plugin;
|
63
projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs
Normal file
63
projects/bevy3d_ui/src/demos/bevydemo1/eventqueue/plugin.rs
Normal file
|
@ -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<ClientInEvents, PluginOutEvents>,
|
||||
/// Internal processor for sending PluginOutEvents, receiving ClientInEvents
|
||||
plugin_processor: EventProcessor<PluginOutEvents, ClientInEvents>,
|
||||
}
|
||||
|
||||
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<ClientInEvents, PluginOutEvents> {
|
||||
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::<Events<CounterEvtData>>()
|
||||
.add_systems(PreUpdate, input_events_system);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the event to bevy using EventWriter
|
||||
fn input_events_system(
|
||||
int_processor: Res<EventProcessor<PluginOutEvents, ClientInEvents>>,
|
||||
mut counter_event_writer: EventWriter<CounterEvtData>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
projects/bevy3d_ui/src/demos/bevydemo1/mod.rs
Normal file
3
projects/bevy3d_ui/src/demos/bevydemo1/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod eventqueue;
|
||||
pub mod scene;
|
||||
pub mod state;
|
124
projects/bevy3d_ui/src/demos/bevydemo1/scene.rs
Normal file
124
projects/bevy3d_ui/src/demos/bevydemo1/scene.rs
Normal file
|
@ -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<SharedState>,
|
||||
processor: EventProcessor<ClientInEvents, PluginOutEvents>,
|
||||
}
|
||||
|
||||
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<SharedState> {
|
||||
self.shared_state.clone()
|
||||
}
|
||||
|
||||
/// Get the event processor
|
||||
pub fn get_processor(
|
||||
&self,
|
||||
) -> EventProcessor<ClientInEvents, PluginOutEvents> {
|
||||
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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
resource: Res<SharedResource>,
|
||||
) {
|
||||
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<CounterEvtData>,
|
||||
mut cube_query: Query<&mut Transform, With<Cube>>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
24
projects/bevy3d_ui/src/demos/bevydemo1/state.rs
Normal file
24
projects/bevy3d_ui/src/demos/bevydemo1/state.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use bevy::ecs::system::Resource;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub type Shared<T> = Arc<Mutex<T>>;
|
||||
|
||||
/// Shared Resource used for Bevy
|
||||
#[derive(Resource)]
|
||||
pub struct SharedResource(pub Shared<SharedState>);
|
||||
|
||||
/// Shared State
|
||||
pub struct SharedState {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
/// Get a new shared state
|
||||
pub fn new() -> Arc<Mutex<SharedState>> {
|
||||
let state = SharedState {
|
||||
name: "This can be used for shared state".to_string(),
|
||||
};
|
||||
let shared = Arc::new(Mutex::new(state));
|
||||
shared
|
||||
}
|
||||
}
|
1
projects/bevy3d_ui/src/demos/mod.rs
Normal file
1
projects/bevy3d_ui/src/demos/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod bevydemo1;
|
11
projects/bevy3d_ui/src/main.rs
Normal file
11
projects/bevy3d_ui/src/main.rs
Normal file
|
@ -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! { <RootPage/> })
|
||||
}
|
52
projects/bevy3d_ui/src/routes/demo1.rs
Normal file
52
projects/bevy3d_ui/src/routes/demo1.rs
Normal file
|
@ -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! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.set(0)>"Clear"</button>
|
||||
<button on:click=move |_| {
|
||||
set_value.update(|value| *value -= step);
|
||||
let newpos = (step as f32) / 10.0;
|
||||
sender_sig
|
||||
.get()
|
||||
.send(ClientInEvents::CounterEvt(CounterEvtData { value: -newpos }))
|
||||
.expect("could not send event");
|
||||
}>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| {
|
||||
set_value.update(|value| *value += step);
|
||||
let newpos = step as f32 / 10.0;
|
||||
sender_sig
|
||||
.get()
|
||||
.send(ClientInEvents::CounterEvt(CounterEvtData { value: newpos }))
|
||||
.expect("could not send event");
|
||||
}>"+1"</button>
|
||||
</div>
|
||||
|
||||
<canvas id="bevy" width="800" height="600"></canvas>
|
||||
}
|
||||
}
|
24
projects/bevy3d_ui/src/routes/mod.rs
Normal file
24
projects/bevy3d_ui/src/routes/mod.rs
Normal file
|
@ -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! {
|
||||
<Meta name="charset" content="UTF-8"/>
|
||||
<Meta name="description" content="Leptonic CSR template"/>
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<Meta name="theme-color" content="#e66956"/>
|
||||
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
|
||||
<Title text="Leptos Bevy3D Example"/>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=|| view! { <Demo1/> }/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue