Add a bevy-texture example based on wgpu-texture (#4360)
* Add a bevy-texture example based on wgpu-texture * Use git branch of Blitz * Update existing code to new blitz * Use Device from Dioxus for bevy * Use ManualTextureView Signed-off-by: Nico Burns <nico@nicoburns.com> * Fix texture size on window resize * Clean code * Rename example to 'bevy-example' in 'bevy' directory * Upgrade to blitz 0.1.0-alpha.5 and anyrender 0.4 * Install alsa in CI Signed-off-by: Nico Burns <nico@nicoburns.com> * Add libudev to CI Signed-off-by: Nico Burns <nico@nicoburns.com> --------- Signed-off-by: Nico Burns <nico@nicoburns.com> Co-authored-by: Jerome Caucat <jerome.caucat@smartshape.com> Co-authored-by: Nico Burns <nico@nicoburns.com>
This commit is contained in:
14 files changed
+2426
-79
No files matched your search
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "bevy-example"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
native = ["dioxus/native"]
|
||||
tracing = ["dep:tracing-subscriber", "dioxus-native/tracing"]
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.16" }
|
||||
dioxus-native = { path = "../../packages/native" }
|
||||
dioxus = { workspace = true }
|
||||
wgpu = "24"
|
||||
color = "0.3"
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,163 @@
|
||||
use crate::bevy_scene_plugin::BevyScenePlugin;
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::{
|
||||
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget},
|
||||
render_resource::TextureFormat,
|
||||
renderer::{
|
||||
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue,
|
||||
WgpuWrapper,
|
||||
},
|
||||
settings::{RenderCreation, RenderResources},
|
||||
RenderPlugin,
|
||||
},
|
||||
};
|
||||
use dioxus_native::{CustomPaintCtx, DeviceHandle, TextureHandle};
|
||||
use std::sync::Arc;
|
||||
use wgpu::Instance;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct UIData {
|
||||
pub color: [f32; 3],
|
||||
}
|
||||
|
||||
pub struct BevyRenderer {
|
||||
app: App,
|
||||
wgpu_device: wgpu::Device,
|
||||
last_texture_size: (u32, u32),
|
||||
texture_handle: Option<TextureHandle>,
|
||||
manual_texture_view_handle: Option<ManualTextureViewHandle>,
|
||||
}
|
||||
|
||||
impl BevyRenderer {
|
||||
pub fn new(instance: &Instance, device_handle: &DeviceHandle) -> Self {
|
||||
// Create a headless Bevy App.
|
||||
let mut app = App::new();
|
||||
app.add_plugins(
|
||||
DefaultPlugins
|
||||
.set(RenderPlugin {
|
||||
// Reuse the render resources from the Dioxus native renderer.
|
||||
render_creation: RenderCreation::Manual(RenderResources(
|
||||
RenderDevice::new(WgpuWrapper::new(device_handle.device.clone())),
|
||||
RenderQueue(Arc::new(WgpuWrapper::new(device_handle.queue.clone()))),
|
||||
RenderAdapterInfo(WgpuWrapper::new(device_handle.adapter.get_info())),
|
||||
RenderAdapter(Arc::new(WgpuWrapper::new(device_handle.adapter.clone()))),
|
||||
RenderInstance(Arc::new(WgpuWrapper::new(instance.clone()))),
|
||||
)),
|
||||
synchronous_pipeline_compilation: true,
|
||||
..default()
|
||||
})
|
||||
.set(WindowPlugin {
|
||||
primary_window: None,
|
||||
exit_condition: bevy::window::ExitCondition::DontExit,
|
||||
close_when_requested: false,
|
||||
})
|
||||
.disable::<bevy::winit::WinitPlugin>(),
|
||||
);
|
||||
|
||||
// Setup the rendering to texture.
|
||||
app.insert_resource(ManualTextureViews::default());
|
||||
|
||||
// Add data from the UI.
|
||||
app.insert_resource(UIData::default());
|
||||
|
||||
// Add the Bevy scene.
|
||||
app.add_plugins(BevyScenePlugin {});
|
||||
|
||||
// Initialize the app to set up the render world properly.
|
||||
app.finish();
|
||||
app.cleanup();
|
||||
|
||||
Self {
|
||||
app,
|
||||
wgpu_device: device_handle.device.clone(),
|
||||
last_texture_size: (0, 0),
|
||||
texture_handle: None,
|
||||
manual_texture_view_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
ctx: CustomPaintCtx<'_>,
|
||||
color: [f32; 3],
|
||||
width: u32,
|
||||
height: u32,
|
||||
_start_time: &std::time::Instant,
|
||||
) -> Option<TextureHandle> {
|
||||
// Update the UI data.
|
||||
if let Some(mut ui) = self.app.world_mut().get_resource_mut::<UIData>() {
|
||||
ui.color = color;
|
||||
}
|
||||
|
||||
// Init self.texture_handle if None or if width/height changed.
|
||||
self.init_texture(ctx, width, height);
|
||||
// Run one frame of the Bevy app to render the 3D scene.
|
||||
self.app.update();
|
||||
|
||||
self.texture_handle
|
||||
}
|
||||
|
||||
fn init_texture(&mut self, mut ctx: CustomPaintCtx<'_>, width: u32, height: u32) {
|
||||
// Reuse self.texture_handle if already initialized to the correct size.
|
||||
let current_size = (width, height);
|
||||
if self.texture_handle.is_some() && self.last_texture_size == current_size {
|
||||
return;
|
||||
}
|
||||
|
||||
let world = self.app.world_mut();
|
||||
|
||||
// Skip if no camera.
|
||||
if world.query::<&Camera>().single(world).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut manual_texture_views) = world.get_resource_mut::<ManualTextureViews>() {
|
||||
// Clean previous texture if any.
|
||||
if let Some(texture_handle) = self.texture_handle {
|
||||
ctx.unregister_texture(texture_handle);
|
||||
self.texture_handle = None;
|
||||
}
|
||||
if let Some(old_handle) = self.manual_texture_view_handle {
|
||||
manual_texture_views.remove(&old_handle);
|
||||
self.manual_texture_view_handle = None;
|
||||
}
|
||||
|
||||
// Create the texture for the camera target and the CustomPaintCtx.
|
||||
let format = TextureFormat::Rgba8UnormSrgb;
|
||||
let wgpu_texture = self.wgpu_device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let wgpu_texture_view =
|
||||
wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let manual_texture_view = ManualTextureView {
|
||||
texture_view: wgpu_texture_view.into(),
|
||||
size: bevy::math::UVec2::new(width, height),
|
||||
format,
|
||||
};
|
||||
let manual_texture_view_handle = ManualTextureViewHandle(0);
|
||||
manual_texture_views.insert(manual_texture_view_handle, manual_texture_view);
|
||||
|
||||
if let Ok(mut camera) = world.query::<&mut Camera>().single_mut(world) {
|
||||
camera.target = RenderTarget::TextureView(manual_texture_view_handle);
|
||||
|
||||
self.last_texture_size = current_size;
|
||||
self.manual_texture_view_handle = Some(manual_texture_view_handle);
|
||||
self.texture_handle = Some(ctx.register_texture(wgpu_texture));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use crate::bevy_renderer::UIData;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct DynamicColoredCube;
|
||||
|
||||
pub struct BevyScenePlugin {}
|
||||
|
||||
impl Plugin for BevyScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(ClearColor(bevy::color::Color::srgba(0.0, 0.0, 0.0, 0.0)));
|
||||
app.add_systems(Startup, setup);
|
||||
app.add_systems(Update, (animate, update_cube_color));
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: bevy::color::Color::srgb(1.0, 0.0, 0.0),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 0.5,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_xyz(0.0, 0.0, 0.0),
|
||||
DynamicColoredCube,
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
DirectionalLight {
|
||||
color: bevy::color::Color::WHITE,
|
||||
illuminance: 10000.0,
|
||||
shadows_enabled: false,
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
));
|
||||
|
||||
commands.insert_resource(AmbientLight {
|
||||
color: bevy::color::Color::WHITE,
|
||||
brightness: 100.0,
|
||||
affects_lightmapped_meshes: true,
|
||||
});
|
||||
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
|
||||
Name::new("MainCamera"),
|
||||
));
|
||||
}
|
||||
|
||||
fn animate(time: Res<Time>, mut cube_query: Query<&mut Transform, With<DynamicColoredCube>>) {
|
||||
for mut transform in cube_query.iter_mut() {
|
||||
transform.rotation = Quat::from_rotation_y(time.elapsed_secs());
|
||||
transform.translation.x = (time.elapsed_secs() * 2.0).sin() * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_cube_color(
|
||||
ui: Res<UIData>,
|
||||
cube_query: Query<&MeshMaterial3d<StandardMaterial>, With<DynamicColoredCube>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
if ui.is_changed() {
|
||||
for mesh_material in cube_query.iter() {
|
||||
if let Some(material) = materials.get_mut(&mesh_material.0) {
|
||||
let [r, g, b] = ui.color;
|
||||
material.base_color = bevy::color::Color::srgb(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::bevy_renderer::BevyRenderer;
|
||||
use crate::Color;
|
||||
use dioxus_native::{CustomPaintCtx, CustomPaintSource, DeviceHandle, TextureHandle};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use wgpu::Instance;
|
||||
|
||||
pub enum DemoMessage {
|
||||
// Color in RGB format
|
||||
SetColor(Color),
|
||||
}
|
||||
|
||||
enum DemoRendererState {
|
||||
Active(Box<BevyRenderer>),
|
||||
Suspended,
|
||||
}
|
||||
|
||||
pub struct DemoPaintSource {
|
||||
state: DemoRendererState,
|
||||
start_time: std::time::Instant,
|
||||
tx: Sender<DemoMessage>,
|
||||
rx: Receiver<DemoMessage>,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl DemoPaintSource {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = channel();
|
||||
Self::with_channel(tx, rx)
|
||||
}
|
||||
|
||||
pub fn with_channel(tx: Sender<DemoMessage>, rx: Receiver<DemoMessage>) -> Self {
|
||||
Self {
|
||||
state: DemoRendererState::Suspended,
|
||||
start_time: std::time::Instant::now(),
|
||||
tx,
|
||||
rx,
|
||||
color: Color::WHITE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> Sender<DemoMessage> {
|
||||
self.tx.clone()
|
||||
}
|
||||
|
||||
fn process_messages(&mut self) {
|
||||
loop {
|
||||
match self.rx.try_recv() {
|
||||
Err(_) => return,
|
||||
Ok(msg) => match msg {
|
||||
DemoMessage::SetColor(color) => self.color = color,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
ctx: CustomPaintCtx<'_>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Option<TextureHandle> {
|
||||
if width == 0 || height == 0 {
|
||||
return None;
|
||||
}
|
||||
let DemoRendererState::Active(state) = &mut self.state else {
|
||||
return None;
|
||||
};
|
||||
|
||||
state.render(ctx, self.color.components, width, height, &self.start_time)
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomPaintSource for DemoPaintSource {
|
||||
fn resume(&mut self, instance: &Instance, device_handle: &DeviceHandle) {
|
||||
let active_state = BevyRenderer::new(instance, device_handle);
|
||||
self.state = DemoRendererState::Active(Box::new(active_state));
|
||||
}
|
||||
|
||||
fn suspend(&mut self) {
|
||||
self.state = DemoRendererState::Suspended;
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
ctx: CustomPaintCtx<'_>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_scale: f64,
|
||||
) -> Option<TextureHandle> {
|
||||
self.process_messages();
|
||||
self.render(ctx, width, height)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::any::Any;
|
||||
|
||||
use color::{palette::css::WHITE, parse_color};
|
||||
use color::{OpaqueColor, Srgb};
|
||||
use demo_renderer::{DemoMessage, DemoPaintSource};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_native::use_wgpu;
|
||||
use wgpu::Limits;
|
||||
|
||||
mod bevy_renderer;
|
||||
mod bevy_scene_plugin;
|
||||
mod demo_renderer;
|
||||
|
||||
// CSS Styles
|
||||
static STYLES: Asset = asset!("./src/styles.css");
|
||||
|
||||
type Color = OpaqueColor<Srgb>;
|
||||
|
||||
fn limits() -> Limits {
|
||||
Limits {
|
||||
max_storage_buffers_per_shader_stage: 12,
|
||||
..Limits::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let config: Vec<Box<dyn Any>> = vec![Box::new(limits())];
|
||||
dioxus_native::launch_cfg(app, Vec::new(), config);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut show_cube = use_signal(|| true);
|
||||
|
||||
let color_str = use_signal(|| String::from("red"));
|
||||
let color = use_memo(move || {
|
||||
parse_color(&color_str())
|
||||
.map(|c| c.to_alpha_color())
|
||||
.unwrap_or(WHITE)
|
||||
.split()
|
||||
.0
|
||||
});
|
||||
|
||||
use_effect(move || println!("{:?}", color().components));
|
||||
|
||||
rsx!(
|
||||
document::Link { rel: "stylesheet", href: STYLES }
|
||||
div { id:"overlay",
|
||||
h2 { "Control Panel" },
|
||||
button {
|
||||
onclick: move |_| show_cube.toggle(),
|
||||
if show_cube() {
|
||||
"Hide cube"
|
||||
} else {
|
||||
"Show cube"
|
||||
}
|
||||
}
|
||||
br {}
|
||||
ColorControl { label: "Color:", color_str },
|
||||
p { "This overlay demonstrates that the custom Bevy content can be rendered beneath layers of HTML content" }
|
||||
}
|
||||
div { id:"underlay",
|
||||
h2 { "Underlay" },
|
||||
p { "This underlay demonstrates that the custom Bevy content can be rendered above layers and blended with the content underneath" }
|
||||
}
|
||||
header {
|
||||
h1 { "Blitz Bevy Demo" }
|
||||
}
|
||||
if show_cube() {
|
||||
SpinningCube { color }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ColorControl(label: &'static str, color_str: Signal<String>) -> Element {
|
||||
rsx!(div {
|
||||
class: "color-control",
|
||||
{ label },
|
||||
input {
|
||||
value: color_str(),
|
||||
oninput: move |evt| {
|
||||
*color_str.write() = evt.value()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SpinningCube(color: Memo<Color>) -> Element {
|
||||
// Create custom paint source and register it with the renderer
|
||||
let paint_source = DemoPaintSource::new();
|
||||
let sender = paint_source.sender();
|
||||
let paint_source_id = use_wgpu(move || paint_source);
|
||||
|
||||
use_effect(move || {
|
||||
sender.send(DemoMessage::SetColor(color())).unwrap();
|
||||
});
|
||||
|
||||
rsx!(
|
||||
div { id:"canvas-container",
|
||||
canvas {
|
||||
id: "demo-canvas",
|
||||
"src": paint_source_id
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, main {
|
||||
height: 100%;
|
||||
font-family: system-ui, sans;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-rows: 100px 1fr;
|
||||
grid-template-columns: 100%;
|
||||
background: #f4e8d2;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
display: grid;
|
||||
opacity: 0.8;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 10px 40px;
|
||||
background-color: white;
|
||||
z-index: 100;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
position: absolute;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding-top: 40%;
|
||||
padding-inline: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#underlay {
|
||||
position: absolute;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
z-index: -10;
|
||||
background-color: black;
|
||||
padding-top: 40%;
|
||||
padding-inline: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.color-control {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
> input {
|
||||
width: 150px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
use crate::Color;
|
||||
use dioxus_native::{CustomPaintCtx, CustomPaintSource, TextureHandle};
|
||||
use dioxus_native::{CustomPaintCtx, CustomPaintSource, DeviceHandle, TextureHandle};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::time::Instant;
|
||||
use wgpu::{Device, Queue};
|
||||
use wgpu::Instance;
|
||||
|
||||
pub struct DemoPaintSource {
|
||||
state: DemoRendererState,
|
||||
@@ -15,9 +15,9 @@ pub struct DemoPaintSource {
|
||||
}
|
||||
|
||||
impl CustomPaintSource for DemoPaintSource {
|
||||
fn resume(&mut self, device: &Device, queue: &Queue) {
|
||||
fn resume(&mut self, _instance: &Instance, device_handle: &DeviceHandle) {
|
||||
// TODO: work out what to do about width/height
|
||||
let active_state = ActiveDemoRenderer::new(device, queue);
|
||||
let active_state = ActiveDemoRenderer::new(device_handle);
|
||||
self.state = DemoRendererState::Active(Box::new(active_state));
|
||||
}
|
||||
|
||||
@@ -110,7 +110,10 @@ impl DemoPaintSource {
|
||||
}
|
||||
|
||||
impl ActiveDemoRenderer {
|
||||
pub(crate) fn new(device: &Device, queue: &Queue) -> Self {
|
||||
pub(crate) fn new(device_handle: &DeviceHandle) -> Self {
|
||||
let device = &device_handle.device;
|
||||
let queue = &device_handle.queue;
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
|
||||
|
||||
Reference in new issue
Block a user