|
| 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 | +} |
0 commit comments