Skip to content

Commit c1b1c40

Browse files
author
Jerome Caucat
committed
Add a bevy-texture example based on wgpu-texture
1 parent a66bffb commit c1b1c40

File tree

8 files changed

+2479
-36
lines changed

8 files changed

+2479
-36
lines changed

Cargo.lock

Lines changed: 1850 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ members = [
114114
"examples/fullstack-auth",
115115
"examples/fullstack-websockets",
116116
"examples/wgpu-texture",
117+
"examples/bevy-texture",
117118

118119
# Playwright tests
119120
"packages/playwright-tests/liveview",

examples/bevy-texture/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "bevy-texture"
3+
version = "0.0.0"
4+
edition = "2021"
5+
license = "MIT"
6+
publish = false
7+
8+
[features]
9+
tracing = ["dep:tracing-subscriber", "dioxus-native/tracing"]
10+
11+
[dependencies]
12+
bevy = { version = "0.16" }
13+
dioxus-native = { path = "../../packages/native" }
14+
dioxus = { workspace = true }
15+
wgpu = "24"
16+
color = "0.3"
17+
tracing-subscriber = { workspace = true, optional = true }
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
use crate::bevy_scene_plugin::BevyScenePlugin;
2+
use bevy::{
3+
prelude::*,
4+
render::{
5+
camera::RenderTarget,
6+
render_asset::RenderAssetUsages,
7+
render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
8+
settings::{RenderCreation, WgpuSettings},
9+
view::screenshot::{Screenshot, ScreenshotCaptured},
10+
RenderPlugin,
11+
},
12+
};
13+
use dioxus_native::{CustomPaintCtx, TextureHandle};
14+
15+
#[derive(Resource, Default)]
16+
pub struct UIData {
17+
pub width: u32,
18+
pub height: u32,
19+
pub color: [f32; 3],
20+
}
21+
22+
pub struct BevyRenderer {
23+
app: App,
24+
texture_handle: Option<TextureHandle>,
25+
wgpu_texture: Option<wgpu::Texture>,
26+
wgpu_device: wgpu::Device,
27+
wgpu_queue: wgpu::Queue,
28+
last_texture_size: (u32, u32),
29+
}
30+
31+
impl BevyRenderer {
32+
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
33+
// Create and headless app
34+
let mut app = App::new();
35+
app.add_plugins(
36+
DefaultPlugins
37+
.set(RenderPlugin {
38+
render_creation: RenderCreation::Automatic(WgpuSettings {
39+
backends: Some(wgpu::Backends::PRIMARY),
40+
..default()
41+
}),
42+
synchronous_pipeline_compilation: true,
43+
..default()
44+
})
45+
.set(WindowPlugin {
46+
primary_window: None,
47+
exit_condition: bevy::window::ExitCondition::DontExit,
48+
close_when_requested: false,
49+
})
50+
.disable::<bevy::winit::WinitPlugin>(),
51+
);
52+
53+
// Setup the rendering to texture
54+
let render_target_image = Handle::<Image>::default();
55+
app.insert_resource(RenderTargetImage(render_target_image))
56+
.insert_resource(RenderedTextureData::default())
57+
.insert_resource(SceneReady::default())
58+
.insert_resource(UIData::default())
59+
.add_systems(
60+
Update,
61+
(
62+
mark_scene_ready,
63+
update_render_target_size,
64+
request_screenshot,
65+
),
66+
)
67+
.add_systems(Last, update_camera_render_target)
68+
.add_observer(handle_screenshot_captured);
69+
70+
// Add the scene
71+
app.add_plugins(BevyScenePlugin {});
72+
73+
// Initialize the app to set up render world properly
74+
app.finish();
75+
app.cleanup();
76+
77+
Self {
78+
app,
79+
texture_handle: None,
80+
wgpu_texture: None,
81+
wgpu_device: device.clone(),
82+
wgpu_queue: queue.clone(),
83+
last_texture_size: (0, 0),
84+
}
85+
}
86+
87+
pub fn render(
88+
&mut self,
89+
ctx: CustomPaintCtx<'_>,
90+
color: [f32; 3],
91+
width: u32,
92+
height: u32,
93+
_start_time: &std::time::Instant,
94+
) -> Option<TextureHandle> {
95+
// Update the UI data
96+
if let Some(mut ui) = self.app.world_mut().get_resource_mut::<UIData>() {
97+
ui.width = width;
98+
ui.height = height;
99+
ui.color = color;
100+
}
101+
102+
// Run one frame of the Bevy app to render the 3D scene, and update the texture.
103+
self.app.update();
104+
self.update_texture(ctx);
105+
106+
self.texture_handle
107+
}
108+
109+
fn update_texture(&mut self, mut ctx: CustomPaintCtx<'_>) {
110+
// Copy the rendered content from Bevy's render target to our WGPU texture
111+
if let Some(rendered_data) = self.app.world().get_resource::<RenderedTextureData>() {
112+
if let Some(image_data) = &rendered_data.data {
113+
let width = rendered_data.width;
114+
let height = rendered_data.height;
115+
116+
// Create/recreate texture if it doesn't exist or size changed
117+
let current_size = (width, height);
118+
if self.texture_handle.is_none() || self.last_texture_size != current_size {
119+
println!("Creating WGPU texture {width}x{height}");
120+
self.last_texture_size = current_size;
121+
122+
// Create a WGPU texture for dioxus
123+
let wgpu_texture = self.wgpu_device.create_texture(&wgpu::TextureDescriptor {
124+
label: Some("Bevy 3D Render Target"),
125+
size: wgpu::Extent3d {
126+
width,
127+
height,
128+
depth_or_array_layers: 1,
129+
},
130+
mip_level_count: 1,
131+
sample_count: 1,
132+
dimension: wgpu::TextureDimension::D2,
133+
format: wgpu::TextureFormat::Rgba8UnormSrgb,
134+
usage: wgpu::TextureUsages::TEXTURE_BINDING
135+
| wgpu::TextureUsages::COPY_DST
136+
| wgpu::TextureUsages::COPY_SRC,
137+
view_formats: &[],
138+
});
139+
self.texture_handle = Some(ctx.register_texture(wgpu_texture.clone()));
140+
self.wgpu_texture = Some(wgpu_texture);
141+
}
142+
143+
// Copy texture data to WGPU
144+
let bytes_per_row = width * 4; // 4 bytes per pixel (RGBA8)
145+
self.wgpu_queue.write_texture(
146+
wgpu::TexelCopyTextureInfo {
147+
texture: self.wgpu_texture.as_ref().unwrap(),
148+
mip_level: 0,
149+
origin: wgpu::Origin3d::ZERO,
150+
aspect: wgpu::TextureAspect::All,
151+
},
152+
image_data,
153+
wgpu::TexelCopyBufferLayout {
154+
offset: 0,
155+
bytes_per_row: Some(bytes_per_row),
156+
rows_per_image: None, // This can be None for single 2D texture
157+
},
158+
wgpu::Extent3d {
159+
width,
160+
height,
161+
depth_or_array_layers: 1,
162+
},
163+
);
164+
165+
return;
166+
}
167+
}
168+
169+
self.texture_handle = None;
170+
}
171+
}
172+
173+
#[derive(Resource)]
174+
struct RenderTargetImage(pub Handle<Image>);
175+
176+
#[derive(Resource, Default)]
177+
struct RenderedTextureData {
178+
pub data: Option<Vec<u8>>,
179+
pub width: u32,
180+
pub height: u32,
181+
pub pending_screenshot: bool,
182+
}
183+
184+
#[derive(Resource, Default)]
185+
struct SceneReady(pub bool);
186+
187+
fn mark_scene_ready(mut scene_ready: ResMut<SceneReady>, camera_query: Query<&Camera>) {
188+
if !scene_ready.0 {
189+
scene_ready.0 = camera_query.iter().count() > 0;
190+
}
191+
}
192+
193+
fn update_render_target_size(
194+
mut images: ResMut<Assets<Image>>,
195+
mut render_target_res: ResMut<RenderTargetImage>,
196+
ui: Res<UIData>,
197+
mut last_size: Local<(u32, u32)>,
198+
) {
199+
// Only recreate the render target if the size changed and we have valid dimensions
200+
if ui.width == 0 || ui.height == 0 {
201+
return;
202+
}
203+
204+
let current_size = (ui.width, ui.height);
205+
if *last_size == current_size {
206+
return;
207+
}
208+
209+
println!("Updating render target size to {}x{}", ui.width, ui.height);
210+
*last_size = current_size;
211+
212+
// Create the render target image with the new size
213+
let mut image = Image::new_fill(
214+
Extent3d {
215+
width: ui.width,
216+
height: ui.height,
217+
depth_or_array_layers: 1,
218+
},
219+
TextureDimension::D2,
220+
&[0; 4], // Black fill
221+
TextureFormat::bevy_default(),
222+
RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
223+
);
224+
image.texture_descriptor.usage =
225+
TextureUsages::COPY_SRC | TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING;
226+
227+
// Update the handle with the new image
228+
let handle = images.add(image);
229+
render_target_res.0 = handle;
230+
}
231+
232+
fn request_screenshot(
233+
mut commands: Commands,
234+
render_target: Res<RenderTargetImage>,
235+
mut texture_data: ResMut<RenderedTextureData>,
236+
scene_ready: Res<SceneReady>,
237+
) {
238+
if scene_ready.0 && !texture_data.pending_screenshot {
239+
commands.spawn(Screenshot::image(render_target.0.clone()));
240+
texture_data.pending_screenshot = true;
241+
}
242+
}
243+
244+
fn update_camera_render_target(
245+
mut cameras: Query<&mut Camera>,
246+
render_target_res: Res<RenderTargetImage>,
247+
scene_ready: Res<SceneReady>,
248+
images: Res<Assets<Image>>,
249+
mut last_handle: Local<Option<Handle<Image>>>,
250+
) {
251+
// Only set camera target after scene is ready and image exists
252+
if !scene_ready.0 || images.get(&render_target_res.0).is_none() {
253+
return;
254+
}
255+
256+
// Update camera target if the render target handle changed
257+
if last_handle.as_ref() != Some(&render_target_res.0) {
258+
for mut camera in cameras.iter_mut() {
259+
camera.target = RenderTarget::Image(render_target_res.0.clone().into());
260+
println!("Updated camera target to render target image");
261+
}
262+
*last_handle = Some(render_target_res.0.clone());
263+
}
264+
}
265+
266+
fn handle_screenshot_captured(
267+
trigger: Trigger<ScreenshotCaptured>,
268+
mut texture_data: ResMut<RenderedTextureData>,
269+
) {
270+
// Get raw data from Bevy Image
271+
let captured_image = &trigger.event().0;
272+
if let Some(data) = &captured_image.data {
273+
texture_data.data = Some(data.clone());
274+
texture_data.width = captured_image.width();
275+
texture_data.height = captured_image.height();
276+
texture_data.pending_screenshot = false;
277+
} else {
278+
texture_data.pending_screenshot = false;
279+
}
280+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::bevy_renderer::UIData;
2+
use bevy::prelude::*;
3+
4+
#[derive(Component)]
5+
pub struct DynamicColoredCube;
6+
7+
pub struct BevyScenePlugin {}
8+
9+
impl Plugin for BevyScenePlugin {
10+
fn build(&self, app: &mut App) {
11+
app.insert_resource(ClearColor(bevy::color::Color::srgba(0.0, 0.0, 0.0, 0.0)));
12+
app.add_systems(Startup, setup);
13+
app.add_systems(Update, (animate, update_cube_color));
14+
}
15+
}
16+
17+
fn setup(
18+
mut commands: Commands,
19+
mut meshes: ResMut<Assets<Mesh>>,
20+
mut materials: ResMut<Assets<StandardMaterial>>,
21+
) {
22+
commands.spawn((
23+
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
24+
MeshMaterial3d(materials.add(StandardMaterial {
25+
base_color: bevy::color::Color::srgb(1.0, 0.0, 0.0),
26+
metallic: 0.0,
27+
perceptual_roughness: 0.5,
28+
..default()
29+
})),
30+
Transform::from_xyz(0.0, 0.0, 0.0),
31+
DynamicColoredCube,
32+
));
33+
34+
commands.spawn((
35+
DirectionalLight {
36+
color: bevy::color::Color::WHITE,
37+
illuminance: 10000.0,
38+
shadows_enabled: false,
39+
..default()
40+
},
41+
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
42+
));
43+
44+
commands.insert_resource(AmbientLight {
45+
color: bevy::color::Color::WHITE,
46+
brightness: 100.0,
47+
affects_lightmapped_meshes: true,
48+
});
49+
50+
commands.spawn((
51+
Camera3d::default(),
52+
Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
53+
Name::new("MainCamera"),
54+
));
55+
}
56+
57+
fn animate(time: Res<Time>, mut cube_query: Query<&mut Transform, With<DynamicColoredCube>>) {
58+
for mut transform in cube_query.iter_mut() {
59+
transform.rotation = Quat::from_rotation_y(time.elapsed_secs());
60+
transform.translation.x = (time.elapsed_secs() * 2.0).sin() * 0.5;
61+
}
62+
}
63+
64+
fn update_cube_color(
65+
ui: Res<UIData>,
66+
cube_query: Query<&MeshMaterial3d<StandardMaterial>, With<DynamicColoredCube>>,
67+
mut materials: ResMut<Assets<StandardMaterial>>,
68+
) {
69+
if ui.is_changed() {
70+
for mesh_material in cube_query.iter() {
71+
if let Some(material) = materials.get_mut(&mesh_material.0) {
72+
let [r, g, b] = ui.color;
73+
material.base_color = bevy::color::Color::srgb(r, g, b);
74+
}
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)