├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── shaders │ └── cell.wgsl ├── branding ├── gui.png └── preview.png ├── deps └── bevy_egui-0.12.1 │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE │ └── src │ ├── egui.wgsl │ ├── egui_node.rs │ ├── lib.rs │ ├── render_systems.rs │ └── systems.rs └── src ├── cell_event.rs ├── cell_renderer.rs ├── cells ├── leddoo │ ├── atomic.rs │ ├── mod.rs │ └── single_threaded.rs ├── mod.rs ├── sims.rs └── tantan │ ├── mod.rs │ ├── multi_threaded.rs │ └── single_threaded.rs ├── main.rs ├── neighbours.rs ├── rotating_camera.rs ├── rule.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "celluar_automata" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | bevy = {git = "https://github.com/bevyengine/bevy.git", rev = "b697e73", features = ["dynamic"]} 10 | bevy_egui = { path = "deps/bevy_egui-0.12.1" } 11 | bevy_fly_camera = "0.8.0" 12 | bytemuck = "*" 13 | rand = "*" 14 | futures-lite = "1.12.0" 15 | 16 | [profile.release] 17 | debug = 1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 3d_celluar_automata is dual-licensed under either 2 | 3 | * MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT) 4 | * Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 5 | 6 | at your option. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3d_celluar_automata 2 | A 3D cellutomata implementation with Rust and Bevy 3 | ![preview image](branding/preview.png) 4 | 5 | Project creation was documented on my youtube page! 6 | https://youtu.be/63qlEpO73C4 7 | 8 | You can configure the simulation using a graphical interface: 9 | ![gui](branding/gui.png) 10 | 11 | There are various implementations by 12 | * [TanTanDev](https://github.com/TanTanDev) 13 | * [leddoo](https://github.com/leddoo) 14 | 15 | 16 | ## License 17 | 3d_celluar_automata is free and open source! All code in this repository is dual-licensed under either: 18 | 19 | * MIT License ([LICENSE-MIT](docs/LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 20 | * Apache License, Version 2.0 ([LICENSE-APACHE](docs/LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 21 | 22 | at your option. 23 | 24 | Unless you explicitly state otherwise, any contribution intentionally submitted 25 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 26 | additional terms or conditions. 27 | -------------------------------------------------------------------------------- /assets/shaders/cell.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_pbr::mesh_view_bind_group 2 | #import bevy_pbr::mesh_struct 3 | 4 | [[group(1), binding(0)]] 5 | var mesh: Mesh; 6 | 7 | struct Vertex { 8 | [[location(0)]] position: vec3; 9 | [[location(1)]] normal: vec3; 10 | [[location(2)]] uv: vec2; 11 | 12 | [[location(3)]] i_pos_scale: vec4; 13 | [[location(4)]] i_color: vec4; 14 | }; 15 | 16 | struct VertexOutput { 17 | [[builtin(position)]] clip_position: vec4; 18 | [[location(0)]] color: vec4; 19 | }; 20 | 21 | [[stage(vertex)]] 22 | fn vertex(vertex: Vertex) -> VertexOutput { 23 | let position = vertex.position * vertex.i_pos_scale.w + vertex.i_pos_scale.xyz; 24 | let world_position = mesh.model * vec4(position, 1.0); 25 | 26 | var out: VertexOutput; 27 | out.clip_position = view.view_proj * world_position; 28 | out.color = vertex.i_color; 29 | return out; 30 | } 31 | 32 | [[stage(fragment)]] 33 | fn fragment(in: VertexOutput) -> [[location(0)]] vec4 { 34 | return in.color; 35 | } -------------------------------------------------------------------------------- /branding/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanTanDev/3d_celluar_automata/cccbf587f6c77bda9378e27dd124c7f1b8ac3b63/branding/gui.png -------------------------------------------------------------------------------- /branding/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanTanDev/3d_celluar_automata/cccbf587f6c77bda9378e27dd124c7f1b8ac3b63/branding/preview.png -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target 3 | Cargo.lock 4 | 5 | # JetBrains 6 | .idea 7 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_egui" 3 | version = "0.12.1" 4 | authors = ["mvlabat "] 5 | description = "A plugin for Egui integration into Bevy" 6 | license = "MIT" 7 | edition = "2021" 8 | repository = "https://github.com/mvlabat/bevy_egui" 9 | exclude = ["assets/**/*", ".github/**/*"] 10 | 11 | [package.metadata.docs.rs] 12 | all-features = true 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | [features] 16 | default = ["manage_clipboard", "open_url"] 17 | manage_clipboard = ["arboard", "thread_local"] 18 | multi_threaded = ["egui/multi_threaded"] 19 | open_url = ["webbrowser"] 20 | 21 | [dependencies] 22 | bevy = {git = "https://github.com/bevyengine/bevy.git", rev = "b697e73", features = ["dynamic"]} 23 | egui = { version = "0.17", features = ["convert_bytemuck"] } 24 | webbrowser = { version = "0.5.5", optional = true } 25 | winit = { version = "0.26.0", features = ["x11"], default-features = false } 26 | bytemuck = { version = "1.7.0", features = ["derive"] } 27 | wgpu = "0.12.0" 28 | 29 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 30 | arboard = { version = "2.0.1", optional = true } 31 | thread_local = { version = "1.1.0", optional = true } 32 | 33 | [dev-dependencies] 34 | once_cell = "1.9.0" 35 | version-sync = "0.9.2" 36 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vladyslav Batyrenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/src/egui.wgsl: -------------------------------------------------------------------------------- 1 | struct EguiTransform { 2 | scale: vec2; 3 | translation: vec2; 4 | }; 5 | 6 | [[group(0), binding(0)]] 7 | var egui_transform: EguiTransform; 8 | 9 | struct VertexOutput { 10 | [[location(0)]] uv: vec2; 11 | [[location(1)]] color: vec4; 12 | [[builtin(position)]] pos: vec4; 13 | }; 14 | 15 | // 0-1 linear from 0-255 sRGB 16 | fn linear_from_srgb(srgb: vec3) -> vec3 { 17 | let cutoff = vec3(srgb < vec3(10.31475)); 18 | let lower = srgb / vec3(3294.6); 19 | let higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); 20 | return mix(higher, lower, cutoff); 21 | } 22 | 23 | // 0-1 linear from 0-255 sRGBA 24 | fn linear_from_srgba(srgba: vec4) -> vec4 { 25 | return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); 26 | } 27 | 28 | [[stage(vertex)]] 29 | fn vs_main( 30 | [[location(0)]] position: vec2, 31 | [[location(1)]] uv: vec2, 32 | [[location(2)]] color: vec4, // 0-1 range 33 | ) -> VertexOutput { 34 | var out: VertexOutput; 35 | out.uv = uv; 36 | out.color = linear_from_srgba(color * 255.0); 37 | out.pos = vec4(position * egui_transform.scale + egui_transform.translation, 0.0, 1.0); 38 | return out; 39 | } 40 | 41 | [[group(1), binding(0)]] 42 | var t_egui: texture_2d; 43 | [[group(1), binding(1)]] 44 | var s_egui: sampler; 45 | 46 | [[stage(fragment)]] 47 | fn fs_main(in: VertexOutput) -> [[location(0)]] vec4 { 48 | let color = in.color * textureSample(t_egui, s_egui, in.uv); 49 | 50 | return color; 51 | } 52 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/src/egui_node.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | core::cast_slice, 3 | ecs::world::{FromWorld, World}, 4 | render::{ 5 | render_graph::{Node, NodeRunError, RenderGraphContext}, 6 | render_resource::{ 7 | std140::AsStd140, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, 8 | BindingType, BlendComponent, BlendFactor, BlendOperation, BlendState, Buffer, 9 | BufferSize, BufferUsages, ColorTargetState, ColorWrites, Extent3d, FrontFace, 10 | IndexFormat, LoadOp, MultisampleState, Operations, PipelineLayoutDescriptor, 11 | PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, 12 | ShaderStages, TextureDimension, TextureFormat, TextureSampleType, TextureViewDimension, 13 | VertexAttribute, VertexFormat, VertexStepMode, 14 | }, 15 | renderer::{RenderContext, RenderDevice, RenderQueue}, 16 | texture::{BevyDefault, Image}, 17 | view::ExtractedWindows, 18 | }, 19 | window::WindowId, 20 | }; 21 | use wgpu::{BufferDescriptor, SamplerBindingType, ShaderModuleDescriptor, ShaderSource}; 22 | 23 | use crate::render_systems::{ 24 | EguiTexture, EguiTextureBindGroups, EguiTransform, EguiTransforms, ExtractedEguiContext, 25 | ExtractedEguiSettings, ExtractedRenderOutput, ExtractedWindowSizes, 26 | }; 27 | 28 | pub struct EguiPipeline { 29 | pipeline: RenderPipeline, 30 | 31 | pub transform_bind_group_layout: BindGroupLayout, 32 | pub texture_bind_group_layout: BindGroupLayout, 33 | } 34 | 35 | impl FromWorld for EguiPipeline { 36 | fn from_world(world: &mut World) -> Self { 37 | let render_device = world.get_resource::().unwrap(); 38 | 39 | let shader_source = ShaderSource::Wgsl(include_str!("egui.wgsl").into()); 40 | let shader_module = render_device.create_shader_module(&ShaderModuleDescriptor { 41 | label: Some("egui shader"), 42 | source: shader_source, 43 | }); 44 | 45 | let transform_bind_group_layout = 46 | render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { 47 | label: Some("egui transform bind group layout"), 48 | entries: &[BindGroupLayoutEntry { 49 | binding: 0, 50 | visibility: ShaderStages::VERTEX, 51 | ty: BindingType::Buffer { 52 | ty: wgpu::BufferBindingType::Uniform, 53 | has_dynamic_offset: true, 54 | min_binding_size: Some( 55 | BufferSize::new(EguiTransform::std140_size_static() as u64).unwrap(), 56 | ), 57 | }, 58 | count: None, 59 | }], 60 | }); 61 | 62 | let texture_bind_group_layout = 63 | render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { 64 | label: Some("egui texture bind group layout"), 65 | entries: &[ 66 | BindGroupLayoutEntry { 67 | binding: 0, 68 | visibility: ShaderStages::FRAGMENT, 69 | ty: BindingType::Texture { 70 | sample_type: TextureSampleType::Float { filterable: true }, 71 | view_dimension: TextureViewDimension::D2, 72 | multisampled: false, 73 | }, 74 | count: None, 75 | }, 76 | BindGroupLayoutEntry { 77 | binding: 1, 78 | visibility: ShaderStages::FRAGMENT, 79 | ty: BindingType::Sampler(SamplerBindingType::Filtering), 80 | count: None, 81 | }, 82 | ], 83 | }); 84 | let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor { 85 | label: Some("egui pipeline layout"), 86 | bind_group_layouts: &[&transform_bind_group_layout, &texture_bind_group_layout], 87 | push_constant_ranges: &[], 88 | }); 89 | 90 | let render_pipeline = 91 | render_device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 92 | label: Some("egui render pipeline"), 93 | layout: Some(&pipeline_layout), 94 | vertex: wgpu::VertexState { 95 | module: &shader_module, 96 | entry_point: "vs_main", 97 | buffers: &[wgpu::VertexBufferLayout { 98 | array_stride: 20, 99 | step_mode: VertexStepMode::Vertex, 100 | attributes: &[ 101 | VertexAttribute { 102 | format: VertexFormat::Float32x2, 103 | offset: 0, 104 | shader_location: 0, 105 | }, 106 | VertexAttribute { 107 | format: VertexFormat::Float32x2, 108 | offset: 8, 109 | shader_location: 1, 110 | }, 111 | VertexAttribute { 112 | format: VertexFormat::Unorm8x4, 113 | offset: 16, 114 | shader_location: 2, 115 | }, 116 | ], 117 | }], 118 | }, 119 | fragment: Some(wgpu::FragmentState { 120 | module: &shader_module, 121 | entry_point: "fs_main", 122 | targets: &[ColorTargetState { 123 | format: TextureFormat::bevy_default(), 124 | blend: Some(BlendState { 125 | color: BlendComponent { 126 | src_factor: BlendFactor::One, 127 | dst_factor: BlendFactor::OneMinusSrcAlpha, 128 | operation: BlendOperation::Add, 129 | }, 130 | alpha: BlendComponent { 131 | src_factor: BlendFactor::One, 132 | dst_factor: BlendFactor::OneMinusSrcAlpha, 133 | operation: BlendOperation::Add, 134 | }, 135 | }), 136 | write_mask: ColorWrites::ALL, 137 | }], 138 | }), 139 | primitive: PrimitiveState { 140 | front_face: FrontFace::Cw, 141 | cull_mode: None, 142 | ..Default::default() 143 | }, 144 | depth_stencil: None, 145 | multisample: MultisampleState::default(), 146 | multiview: None, 147 | }); 148 | 149 | EguiPipeline { 150 | pipeline: render_pipeline, 151 | transform_bind_group_layout, 152 | texture_bind_group_layout, 153 | } 154 | } 155 | } 156 | 157 | #[derive(Debug)] 158 | struct DrawCommand { 159 | vertices_count: usize, 160 | egui_texture: EguiTexture, 161 | clipping_zone: (u32, u32, u32, u32), // x, y, w, h 162 | } 163 | 164 | pub struct EguiNode { 165 | window_id: WindowId, 166 | vertex_data: Vec, 167 | vertex_buffer_capacity: usize, 168 | vertex_buffer: Option, 169 | index_data: Vec, 170 | index_buffer_capacity: usize, 171 | index_buffer: Option, 172 | draw_commands: Vec, 173 | } 174 | 175 | impl EguiNode { 176 | pub fn new(window_id: WindowId) -> Self { 177 | EguiNode { 178 | window_id, 179 | draw_commands: Vec::new(), 180 | vertex_data: Vec::new(), 181 | vertex_buffer_capacity: 0, 182 | vertex_buffer: None, 183 | index_data: Vec::new(), 184 | index_buffer_capacity: 0, 185 | index_buffer: None, 186 | } 187 | } 188 | } 189 | 190 | impl Node for EguiNode { 191 | fn update(&mut self, world: &mut World) { 192 | let mut shapes = world.get_resource_mut::().unwrap(); 193 | let shapes = match shapes.0.get_mut(&self.window_id) { 194 | Some(shapes) => shapes, 195 | None => return, 196 | }; 197 | let shapes = std::mem::take(&mut shapes.shapes); 198 | 199 | let window_size = &world.get_resource::().unwrap().0[&self.window_id]; 200 | let egui_settings = &world.get_resource::().unwrap().0; 201 | let egui_context = &world.get_resource::().unwrap().0; 202 | 203 | let render_device = world.get_resource::().unwrap(); 204 | 205 | let scale_factor = window_size.scale_factor * egui_settings.scale_factor as f32; 206 | if window_size.physical_width == 0.0 || window_size.physical_height == 0.0 { 207 | return; 208 | } 209 | 210 | let egui_paint_jobs = egui_context[&self.window_id].tessellate(shapes); 211 | 212 | let mut index_offset = 0; 213 | 214 | self.draw_commands.clear(); 215 | self.vertex_data.clear(); 216 | self.index_data.clear(); 217 | 218 | for egui::ClippedMesh(rect, triangles) in &egui_paint_jobs { 219 | let (x, y, w, h) = ( 220 | (rect.min.x * scale_factor).round() as u32, 221 | (rect.min.y * scale_factor).round() as u32, 222 | (rect.width() * scale_factor).round() as u32, 223 | (rect.height() * scale_factor).round() as u32, 224 | ); 225 | 226 | if w < 1 227 | || h < 1 228 | || x >= window_size.physical_width as u32 229 | || y >= window_size.physical_height as u32 230 | { 231 | continue; 232 | } 233 | 234 | self.vertex_data 235 | .extend_from_slice(cast_slice(triangles.vertices.as_slice())); 236 | let indices_with_offset = triangles 237 | .indices 238 | .iter() 239 | .map(|i| i + index_offset) 240 | .collect::>(); 241 | self.index_data 242 | .extend_from_slice(cast_slice(indices_with_offset.as_slice())); 243 | index_offset += triangles.vertices.len() as u32; 244 | 245 | let texture_handle = match triangles.texture_id { 246 | egui::TextureId::Managed(id) => EguiTexture::Managed(self.window_id, id), 247 | egui::TextureId::User(id) => EguiTexture::User(id), 248 | }; 249 | 250 | let x_viewport_clamp = (x + w).saturating_sub(window_size.physical_width as u32); 251 | let y_viewport_clamp = (y + h).saturating_sub(window_size.physical_height as u32); 252 | self.draw_commands.push(DrawCommand { 253 | vertices_count: triangles.indices.len(), 254 | egui_texture: texture_handle, 255 | clipping_zone: ( 256 | x, 257 | y, 258 | w.saturating_sub(x_viewport_clamp).max(1), 259 | h.saturating_sub(y_viewport_clamp).max(1), 260 | ), 261 | }); 262 | } 263 | 264 | if self.vertex_data.len() > self.vertex_buffer_capacity { 265 | self.vertex_buffer_capacity = if self.vertex_data.len().is_power_of_two() { 266 | self.vertex_data.len() 267 | } else { 268 | self.vertex_data.len().next_power_of_two() 269 | }; 270 | self.vertex_buffer = Some(render_device.create_buffer(&BufferDescriptor { 271 | label: Some("egui vertex buffer"), 272 | size: self.vertex_buffer_capacity as wgpu::BufferAddress, 273 | usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, 274 | mapped_at_creation: false, 275 | })); 276 | } 277 | if self.index_data.len() > self.index_buffer_capacity { 278 | self.index_buffer_capacity = if self.index_data.len().is_power_of_two() { 279 | self.index_data.len() 280 | } else { 281 | self.index_data.len().next_power_of_two() 282 | }; 283 | self.index_buffer = Some(render_device.create_buffer(&BufferDescriptor { 284 | label: Some("egui index buffer"), 285 | size: self.index_buffer_capacity as wgpu::BufferAddress, 286 | usage: BufferUsages::COPY_DST | BufferUsages::INDEX, 287 | mapped_at_creation: false, 288 | })); 289 | } 290 | } 291 | 292 | fn run( 293 | &self, 294 | _graph: &mut RenderGraphContext, 295 | render_context: &mut RenderContext, 296 | world: &World, 297 | ) -> Result<(), NodeRunError> { 298 | let egui_shaders = world.get_resource::().unwrap(); 299 | let render_queue = world.get_resource::().unwrap(); 300 | 301 | let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) { 302 | (Some(vertex), Some(index)) => (vertex, index), 303 | _ => return Ok(()), 304 | }; 305 | 306 | render_queue.write_buffer(vertex_buffer, 0, &self.vertex_data); 307 | render_queue.write_buffer(index_buffer, 0, &self.index_data); 308 | 309 | let bind_groups = &world 310 | .get_resource::() 311 | .unwrap() 312 | .bind_groups; 313 | 314 | let egui_transforms = world.get_resource::().unwrap(); 315 | 316 | let extracted_window = 317 | &world.get_resource::().unwrap().windows[&self.window_id]; 318 | let swap_chain_texture = extracted_window 319 | .swap_chain_texture 320 | .as_ref() 321 | .unwrap() 322 | .clone(); 323 | 324 | let mut render_pass = 325 | render_context 326 | .command_encoder 327 | .begin_render_pass(&RenderPassDescriptor { 328 | label: Some("egui render pass"), 329 | color_attachments: &[RenderPassColorAttachment { 330 | view: &swap_chain_texture, 331 | resolve_target: None, 332 | ops: Operations { 333 | load: LoadOp::Load, 334 | store: true, 335 | }, 336 | }], 337 | depth_stencil_attachment: None, 338 | }); 339 | 340 | render_pass.set_pipeline(&egui_shaders.pipeline); 341 | render_pass.set_vertex_buffer(0, *self.vertex_buffer.as_ref().unwrap().slice(..)); 342 | render_pass.set_index_buffer( 343 | *self.index_buffer.as_ref().unwrap().slice(..), 344 | IndexFormat::Uint32, 345 | ); 346 | 347 | let transform_buffer_offset = egui_transforms.offsets[&self.window_id]; 348 | let transform_buffer_bind_group = &egui_transforms.bind_group.as_ref().unwrap().1; 349 | render_pass.set_bind_group(0, transform_buffer_bind_group, &[transform_buffer_offset]); 350 | 351 | let mut vertex_offset: u32 = 0; 352 | for draw_command in &self.draw_commands { 353 | if draw_command.clipping_zone.0 < extracted_window.physical_width 354 | && draw_command.clipping_zone.1 < extracted_window.physical_height 355 | { 356 | let texture_bind_group = match bind_groups.get(&draw_command.egui_texture) { 357 | Some(texture_resource) => texture_resource, 358 | None => { 359 | vertex_offset += draw_command.vertices_count as u32; 360 | continue; 361 | } 362 | }; 363 | 364 | render_pass.set_bind_group(1, texture_bind_group, &[]); 365 | 366 | render_pass.set_scissor_rect( 367 | draw_command.clipping_zone.0, 368 | draw_command.clipping_zone.1, 369 | draw_command 370 | .clipping_zone 371 | .2 372 | .min(extracted_window.physical_width), 373 | draw_command 374 | .clipping_zone 375 | .3 376 | .min(extracted_window.physical_height), 377 | ); 378 | 379 | render_pass.draw_indexed( 380 | vertex_offset..(vertex_offset + draw_command.vertices_count as u32), 381 | 0, 382 | 0..1, 383 | ); 384 | vertex_offset += draw_command.vertices_count as u32; 385 | } 386 | } 387 | 388 | Ok(()) 389 | } 390 | } 391 | 392 | pub fn as_color_image(image: &egui::ImageData) -> egui::ColorImage { 393 | match image { 394 | egui::ImageData::Color(image) => image.clone(), 395 | egui::ImageData::Alpha(image) => alpha_image_as_color_image(image), 396 | } 397 | } 398 | 399 | pub fn alpha_image_as_color_image(image: &egui::AlphaImage) -> egui::ColorImage { 400 | let gamma = 1.0; 401 | egui::ColorImage { 402 | size: image.size, 403 | pixels: image.srgba_pixels(gamma).collect(), 404 | } 405 | } 406 | 407 | pub fn color_image_as_bevy_image(egui_image: &egui::ColorImage) -> Image { 408 | let pixels = egui_image 409 | .pixels 410 | .iter() 411 | .flat_map(|color| color.to_array()) 412 | .collect(); 413 | 414 | Image::new( 415 | Extent3d { 416 | width: egui_image.width() as u32, 417 | height: egui_image.height() as u32, 418 | depth_or_array_layers: 1, 419 | }, 420 | TextureDimension::D2, 421 | pixels, 422 | TextureFormat::Rgba8UnormSrgb, 423 | ) 424 | } 425 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! This crate provides a [Egui](https://github.com/emilk/egui) integration for the [Bevy](https://github.com/bevyengine/bevy) game engine. 4 | //! 5 | //! **Trying out:** 6 | //! 7 | //! An example WASM project is live at [mvlabat.github.io/bevy_egui_web_showcase](https://mvlabat.github.io/bevy_egui_web_showcase/index.html) [[source](https://github.com/mvlabat/bevy_egui_web_showcase)]. 8 | //! 9 | //! **Features:** 10 | //! - Desktop and web ([bevy_webgl2](https://github.com/mrk-its/bevy_webgl2)) platforms support 11 | //! - Clipboard (web support is limited to the same window, see [rust-windowing/winit#1829](https://github.com/rust-windowing/winit/issues/1829)) 12 | //! - Opening URLs 13 | //! - Multiple windows support (see [./examples/two_windows.rs](./examples/two_windows.rs)) 14 | //! 15 | //! `bevy_egui` can be compiled with using only `bevy` and `egui` as dependencies: `manage_clipboard` and `open_url` features, 16 | //! that require additional crates, can be disabled. 17 | //! 18 | //! ## Usage 19 | //! 20 | //! Here's a minimal usage example: 21 | //! 22 | //! ```no_run,rust 23 | //! use bevy::prelude::*; 24 | //! use bevy_egui::{egui, EguiContext, EguiPlugin}; 25 | //! 26 | //! fn main() { 27 | //! App::new() 28 | //! .add_plugins(DefaultPlugins) 29 | //! .add_plugin(EguiPlugin) 30 | //! // Systems that create Egui widgets should be run during the `CoreStage::Update` stage, 31 | //! // or after the `EguiSystem::BeginFrame` system (which belongs to the `CoreStage::PreUpdate` stage). 32 | //! .add_system(ui_example) 33 | //! .run(); 34 | //! } 35 | //! 36 | //! fn ui_example(mut egui_context: ResMut) { 37 | //! egui::Window::new("Hello").show(egui_context.ctx_mut(), |ui| { 38 | //! ui.label("world"); 39 | //! }); 40 | //! } 41 | //! ``` 42 | //! 43 | //! For a more advanced example, see [examples/ui.rs](examples/ui.rs). 44 | //! 45 | //! ```bash 46 | //! cargo run --example ui 47 | //! ``` 48 | //! 49 | //! ## See also 50 | //! 51 | //! - [`bevy-inspector-egui`](https://github.com/jakobhellermann/bevy-inspector-egui) 52 | 53 | pub use egui; 54 | 55 | mod egui_node; 56 | mod render_systems; 57 | mod systems; 58 | 59 | use egui_node::EguiNode; 60 | use render_systems::EguiTransforms; 61 | 62 | use crate::systems::*; 63 | #[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))] 64 | use arboard::Clipboard; 65 | use bevy::{ 66 | app::{App, CoreStage, Plugin, StartupStage}, 67 | asset::{AssetEvent, Assets, Handle, HandleId}, 68 | ecs::{ 69 | event::EventReader, 70 | schedule::{ParallelSystemDescriptorCoercion, SystemLabel}, 71 | system::ResMut, 72 | }, 73 | input::InputSystem, 74 | log, 75 | render::{render_graph::RenderGraph, texture::Image, RenderApp, RenderStage}, 76 | utils::HashMap, 77 | window::WindowId, 78 | }; 79 | #[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))] 80 | use std::cell::{RefCell, RefMut}; 81 | #[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))] 82 | use thread_local::ThreadLocal; 83 | 84 | /// Adds all Egui resources and render graph nodes. 85 | pub struct EguiPlugin; 86 | 87 | /// A resource for storing global UI settings. 88 | #[derive(Clone, Debug, PartialEq)] 89 | pub struct EguiSettings { 90 | /// Global scale factor for egui widgets (`1.0` by default). 91 | /// 92 | /// This setting can be used to force the UI to render in physical pixels regardless of DPI as follows: 93 | /// ```rust 94 | /// use bevy::prelude::*; 95 | /// use bevy_egui::EguiSettings; 96 | /// 97 | /// fn update_ui_scale_factor(mut egui_settings: ResMut, windows: Res) { 98 | /// if let Some(window) = windows.get_primary() { 99 | /// egui_settings.scale_factor = 1.0 / window.scale_factor(); 100 | /// } 101 | /// } 102 | /// ``` 103 | pub scale_factor: f64, 104 | } 105 | 106 | impl Default for EguiSettings { 107 | fn default() -> Self { 108 | Self { scale_factor: 1.0 } 109 | } 110 | } 111 | 112 | /// Is used for storing the input passed to Egui. The actual resource is `HashMap`. 113 | /// 114 | /// It gets reset during the [`EguiSystem::ProcessInput`] system. 115 | #[derive(Clone, Debug, Default)] 116 | pub struct EguiInput { 117 | /// Egui's raw input. 118 | pub raw_input: egui::RawInput, 119 | } 120 | 121 | /// A resource for accessing clipboard. 122 | /// 123 | /// The resource is available only if `manage_clipboard` feature is enabled. 124 | #[cfg(feature = "manage_clipboard")] 125 | #[derive(Default)] 126 | pub struct EguiClipboard { 127 | #[cfg(not(target_arch = "wasm32"))] 128 | clipboard: ThreadLocal>>, 129 | #[cfg(target_arch = "wasm32")] 130 | clipboard: String, 131 | } 132 | 133 | #[cfg(feature = "manage_clipboard")] 134 | impl EguiClipboard { 135 | /// Sets clipboard contents. 136 | pub fn set_contents(&mut self, contents: &str) { 137 | self.set_contents_impl(contents); 138 | } 139 | 140 | /// Gets clipboard contents. Returns [`None`] if clipboard provider is unavailable or returns an error. 141 | pub fn get_contents(&self) -> Option { 142 | self.get_contents_impl() 143 | } 144 | 145 | #[cfg(not(target_arch = "wasm32"))] 146 | fn set_contents_impl(&self, contents: &str) { 147 | if let Some(mut clipboard) = self.get() { 148 | if let Err(err) = clipboard.set_text(contents.to_owned()) { 149 | log::error!("Failed to set clipboard contents: {:?}", err); 150 | } 151 | } 152 | } 153 | 154 | #[cfg(target_arch = "wasm32")] 155 | fn set_contents_impl(&mut self, contents: &str) { 156 | self.clipboard = contents.to_owned(); 157 | } 158 | 159 | #[cfg(not(target_arch = "wasm32"))] 160 | fn get_contents_impl(&self) -> Option { 161 | if let Some(mut clipboard) = self.get() { 162 | match clipboard.get_text() { 163 | Ok(contents) => return Some(contents), 164 | Err(err) => log::info!("Failed to get clipboard contents: {:?}", err), 165 | } 166 | }; 167 | None 168 | } 169 | 170 | #[cfg(target_arch = "wasm32")] 171 | #[allow(clippy::unnecessary_wraps)] 172 | fn get_contents_impl(&self) -> Option { 173 | Some(self.clipboard.clone()) 174 | } 175 | 176 | #[cfg(not(target_arch = "wasm32"))] 177 | fn get(&self) -> Option> { 178 | self.clipboard 179 | .get_or(|| { 180 | Clipboard::new() 181 | .map(RefCell::new) 182 | .map_err(|err| { 183 | log::info!("Failed to initialize clipboard: {:?}", err); 184 | }) 185 | .ok() 186 | }) 187 | .as_ref() 188 | .map(|cell| cell.borrow_mut()) 189 | } 190 | } 191 | 192 | /// Is used for storing Egui shapes. The actual resource is `HashMap`. 193 | #[derive(Clone, Default, Debug)] 194 | pub struct EguiRenderOutput { 195 | /// Pairs of rectangles and paint commands. 196 | /// 197 | /// The field gets populated during the [`EguiSystem::ProcessOutput`] system in the [`CoreStage::PostUpdate`] and reset during `EguiNode::update`. 198 | pub shapes: Vec, 199 | 200 | /// The change in egui textures since last frame. 201 | pub textures_delta: egui::TexturesDelta, 202 | } 203 | 204 | /// Is used for storing Egui output. The actual resource is `HashMap`. 205 | #[derive(Clone, Default)] 206 | pub struct EguiOutput { 207 | /// The field gets updated during the [`EguiSystem::ProcessOutput`] system in the [`CoreStage::PostUpdate`]. 208 | pub platform_output: egui::PlatformOutput, 209 | } 210 | 211 | /// A resource for storing `bevy_egui` context. 212 | #[derive(Clone)] 213 | pub struct EguiContext { 214 | ctx: HashMap, 215 | user_textures: HashMap, 216 | last_texture_id: u64, 217 | mouse_position: Option<(WindowId, egui::Vec2)>, 218 | } 219 | 220 | impl EguiContext { 221 | fn new() -> Self { 222 | Self { 223 | ctx: HashMap::default(), 224 | user_textures: Default::default(), 225 | last_texture_id: 0, 226 | mouse_position: None, 227 | } 228 | } 229 | 230 | /// Egui context of the primary window. 231 | /// 232 | /// This function is only available when the `multi_threaded` feature is enabled. 233 | /// The preferable way is to use `ctx_mut` to avoid unpredictable blocking inside UI systems. 234 | #[cfg(feature = "multi_threaded")] 235 | #[track_caller] 236 | pub fn ctx(&self) -> &egui::Context { 237 | self.ctx.get(&WindowId::primary()).expect("`EguiContext::ctx` was called for an uninitialized context (primary window), consider moving your startup system to the `StartupStage::Startup` stage or run it after the `EguiStartupSystem::InitContexts` system") 238 | } 239 | 240 | /// Egui context for a specific window. 241 | /// If you want to display UI on a non-primary window, make sure to set up the render graph by 242 | /// calling [`setup_pipeline`]. 243 | /// 244 | /// This function is only available when the `multi_threaded` feature is enabled. 245 | /// The preferable way is to use `ctx_for_window_mut` to avoid unpredictable blocking inside UI 246 | /// systems. 247 | #[cfg(feature = "multi_threaded")] 248 | #[track_caller] 249 | pub fn ctx_for_window(&self, window: WindowId) -> &egui::Context { 250 | self.ctx 251 | .get(&window) 252 | .unwrap_or_else(|| panic!("`EguiContext::ctx_for_window` was called for an uninitialized context (window {}), consider moving your UI system to the `CoreStage::Update` stage or run it after the `EguiSystem::BeginFrame` system (`StartupStage::Startup` or `EguiStartupSystem::InitContexts` for startup systems respectively)", window)) 253 | } 254 | 255 | /// Fallible variant of [`EguiContext::ctx_for_window`]. Make sure to set up the render graph by 256 | /// calling [`setup_pipeline`]. 257 | /// 258 | /// This function is only available when the `multi_threaded` feature is enabled. 259 | /// The preferable way is to use `try_ctx_for_window_mut` to avoid unpredictable blocking inside 260 | /// UI systems. 261 | #[cfg(feature = "multi_threaded")] 262 | pub fn try_ctx_for_window(&self, window: WindowId) -> Option<&egui::Context> { 263 | self.ctx.get(&window) 264 | } 265 | 266 | /// Egui context of the primary window. 267 | #[track_caller] 268 | pub fn ctx_mut(&mut self) -> &egui::Context { 269 | self.ctx.get(&WindowId::primary()).expect("`EguiContext::ctx_mut` was called for an uninitialized context (primary window), consider moving your startup system to the `StartupStage::Startup` stage or run it after the `EguiStartupSystem::InitContexts` system") 270 | } 271 | 272 | /// Egui context for a specific window. 273 | /// If you want to display UI on a non-primary window, make sure to set up the render graph by 274 | /// calling [`setup_pipeline`]. 275 | #[track_caller] 276 | pub fn ctx_for_window_mut(&mut self, window: WindowId) -> &egui::Context { 277 | self.ctx 278 | .get(&window) 279 | .unwrap_or_else(|| panic!("`EguiContext::ctx_for_window_mut` was called for an uninitialized context (window {}), consider moving your UI system to the `CoreStage::Update` stage or run it after the `EguiSystem::BeginFrame` system (`StartupStage::Startup` or `EguiStartupSystem::InitContexts` for startup systems respectively)", window)) 280 | } 281 | 282 | /// Fallible variant of [`EguiContext::ctx_for_window_mut`]. Make sure to set up the render 283 | /// graph by calling [`setup_pipeline`]. 284 | pub fn try_ctx_for_window_mut(&mut self, window: WindowId) -> Option<&egui::Context> { 285 | self.ctx.get(&window) 286 | } 287 | 288 | /// Allows to get multiple contexts at the same time. This function is useful when you want 289 | /// to get multiple window contexts without using the `multi_threaded` feature. 290 | /// 291 | /// # Panics 292 | /// 293 | /// Panics if the passed window ids aren't unique. 294 | pub fn ctx_for_windows_mut( 295 | &mut self, 296 | ids: [WindowId; N], 297 | ) -> [&egui::Context; N] { 298 | let mut unique_ids = bevy::utils::HashSet::default(); 299 | assert!( 300 | ids.iter().all(move |id| unique_ids.insert(id)), 301 | "Window ids passed to `EguiContext::ctx_for_windows_mut` must be unique: {:?}", 302 | ids 303 | ); 304 | ids.map(|id| self.ctx.get(&id).unwrap_or_else(|| panic!("`EguiContext::ctx_for_windows_mut` was called for an uninitialized context (window {}), consider moving your UI system to the `CoreStage::Update` stage or run it after the `EguiSystem::BeginFrame` system (`StartupStage::Startup` or `EguiStartupSystem::InitContexts` for startup systems respectively)", id))) 305 | } 306 | 307 | /// Fallible variant of [`EguiContext::ctx_for_windows_mut`]. Make sure to set up the render 308 | /// graph by calling [`setup_pipeline`]. 309 | /// 310 | /// # Panics 311 | /// 312 | /// Panics if the passed window ids aren't unique. 313 | pub fn try_ctx_for_windows_mut( 314 | &mut self, 315 | ids: [WindowId; N], 316 | ) -> [Option<&egui::Context>; N] { 317 | let mut unique_ids = bevy::utils::HashSet::default(); 318 | assert!( 319 | ids.iter().all(move |id| unique_ids.insert(id)), 320 | "Window ids passed to `EguiContext::ctx_for_windows_mut` must be unique: {:?}", 321 | ids 322 | ); 323 | ids.map(|id| self.ctx.get(&id)) 324 | } 325 | 326 | /// Can accept either a strong or a weak handle. 327 | /// 328 | /// You may want to pass a weak handle if you control removing texture assets in your 329 | /// application manually and you don't want to bother with cleaning up textures in Egui. 330 | /// 331 | /// You'll want to pass a strong handle if a texture is used only in Egui and there's no 332 | /// handle copies stored anywhere else. 333 | pub fn add_image(&mut self, image: Handle) -> egui::TextureId { 334 | let id = *self.user_textures.entry(image.id).or_insert_with(|| { 335 | let id = self.last_texture_id; 336 | log::debug!("Add a new image (id: {}, handle: {:?})", id, image); 337 | self.last_texture_id += 1; 338 | id 339 | }); 340 | egui::TextureId::User(id) 341 | } 342 | 343 | /// Removes the image handle and an Egui texture id associated with it. 344 | pub fn remove_image(&mut self, image: &Handle) -> Option { 345 | let id = self.user_textures.remove(&image.id); 346 | log::debug!("Remove image (id: {:?}, handle: {:?})", id, image); 347 | id 348 | } 349 | 350 | /// Returns associated Egui texture id. 351 | #[must_use] 352 | pub fn image_id(&self, image: &Handle) -> Option { 353 | self.user_textures 354 | .get(&image.id) 355 | .map(|&id| egui::TextureId::User(id)) 356 | } 357 | } 358 | 359 | #[doc(hidden)] 360 | #[derive(Debug, Default, Clone, Copy, PartialEq)] 361 | pub struct WindowSize { 362 | physical_width: f32, 363 | physical_height: f32, 364 | scale_factor: f32, 365 | } 366 | 367 | impl WindowSize { 368 | fn new(physical_width: f32, physical_height: f32, scale_factor: f32) -> Self { 369 | Self { 370 | physical_width, 371 | physical_height, 372 | scale_factor, 373 | } 374 | } 375 | 376 | #[inline] 377 | fn width(&self) -> f32 { 378 | self.physical_width / self.scale_factor 379 | } 380 | 381 | #[inline] 382 | fn height(&self) -> f32 { 383 | self.physical_height / self.scale_factor 384 | } 385 | } 386 | 387 | /// The names of `bevy_egui` nodes. 388 | pub mod node { 389 | /// The main egui pass. 390 | pub const EGUI_PASS: &str = "egui_pass"; 391 | } 392 | 393 | #[derive(SystemLabel, Clone, Hash, Debug, Eq, PartialEq)] 394 | /// The names of `bevy_egui` startup systems. 395 | pub enum EguiStartupSystem { 396 | /// Initializes Egui contexts for available windows. 397 | InitContexts, 398 | } 399 | 400 | /// The names of egui systems. 401 | #[derive(SystemLabel, Clone, Hash, Debug, Eq, PartialEq)] 402 | pub enum EguiSystem { 403 | /// Reads Egui inputs (keyboard, mouse, etc) and writes them into the [`EguiInput`] resource. 404 | /// 405 | /// To modify the input, you can hook your system like this: 406 | /// 407 | /// `system.after(EguiSystem::ProcessInput).before(EguiSystem::BeginFrame)`. 408 | ProcessInput, 409 | /// Begins the `egui` frame 410 | BeginFrame, 411 | /// Processes the [`EguiOutput`] resource 412 | ProcessOutput, 413 | } 414 | 415 | impl Plugin for EguiPlugin { 416 | fn build(&self, app: &mut App) { 417 | let world = &mut app.world; 418 | world.insert_resource(EguiSettings::default()); 419 | world.insert_resource(HashMap::::default()); 420 | world.insert_resource(HashMap::::default()); 421 | world.insert_resource(HashMap::::default()); 422 | world.insert_resource(HashMap::::default()); 423 | world.insert_resource(EguiManagedTextures::default()); 424 | #[cfg(feature = "manage_clipboard")] 425 | world.insert_resource(EguiClipboard::default()); 426 | world.insert_resource(EguiContext::new()); 427 | 428 | app.add_startup_system_to_stage( 429 | StartupStage::PreStartup, 430 | init_contexts_on_startup.label(EguiStartupSystem::InitContexts), 431 | ); 432 | 433 | app.add_system_to_stage( 434 | CoreStage::PreUpdate, 435 | process_input 436 | .label(EguiSystem::ProcessInput) 437 | .after(InputSystem), 438 | ); 439 | app.add_system_to_stage( 440 | CoreStage::PreUpdate, 441 | begin_frame 442 | .label(EguiSystem::BeginFrame) 443 | .after(EguiSystem::ProcessInput), 444 | ); 445 | app.add_system_to_stage( 446 | CoreStage::PostUpdate, 447 | process_output.label(EguiSystem::ProcessOutput), 448 | ); 449 | app.add_system_to_stage( 450 | CoreStage::PostUpdate, 451 | update_egui_textures.after(EguiSystem::ProcessOutput), 452 | ); 453 | app.add_system_to_stage(CoreStage::Last, free_egui_textures); 454 | 455 | if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { 456 | render_app.init_resource::(); 457 | render_app 458 | .init_resource::() 459 | .init_resource::() 460 | .add_system_to_stage( 461 | RenderStage::Extract, 462 | render_systems::extract_egui_render_data, 463 | ) 464 | .add_system_to_stage(RenderStage::Extract, render_systems::extract_egui_textures) 465 | .add_system_to_stage( 466 | RenderStage::Prepare, 467 | render_systems::prepare_egui_transforms, 468 | ) 469 | .add_system_to_stage(RenderStage::Queue, render_systems::queue_bind_groups); 470 | 471 | let mut render_graph = render_app.world.get_resource_mut::().unwrap(); 472 | setup_pipeline(&mut *render_graph, RenderGraphConfig::default()); 473 | } 474 | } 475 | } 476 | 477 | #[derive(Default)] 478 | pub(crate) struct EguiManagedTextures(HashMap<(WindowId, u64), EguiManagedTexture>); 479 | 480 | pub(crate) struct EguiManagedTexture { 481 | handle: Handle, 482 | /// Stored in full so we can do partial updates (which bevy doesn't support). 483 | color_image: egui::ColorImage, 484 | } 485 | 486 | fn update_egui_textures( 487 | mut egui_render_output: ResMut>, 488 | mut egui_managed_textures: ResMut, 489 | mut image_assets: ResMut>, 490 | ) { 491 | for (&window_id, egui_render_output) in egui_render_output.iter_mut() { 492 | let set_textures = std::mem::take(&mut egui_render_output.textures_delta.set); 493 | 494 | for (texture_id, image_delta) in set_textures { 495 | let color_image = egui_node::as_color_image(&image_delta.image); 496 | 497 | let texture_id = match texture_id { 498 | egui::TextureId::Managed(texture_id) => texture_id, 499 | egui::TextureId::User(_) => continue, 500 | }; 501 | 502 | if let Some(pos) = image_delta.pos { 503 | // Partial update. 504 | if let Some(managed_texture) = 505 | egui_managed_textures.0.get_mut(&(window_id, texture_id)) 506 | { 507 | // TODO: when bevy supports it, only update the part of the texture that changes. 508 | update_image_rect(&mut managed_texture.color_image, pos, &color_image); 509 | let image = egui_node::color_image_as_bevy_image(&managed_texture.color_image); 510 | managed_texture.handle = image_assets.add(image); 511 | } else { 512 | log::warn!("Partial update of a missing texture (id: {:?})", texture_id); 513 | } 514 | } else { 515 | // Full update. 516 | let image = egui_node::color_image_as_bevy_image(&color_image); 517 | let handle = image_assets.add(image); 518 | egui_managed_textures.0.insert( 519 | (window_id, texture_id), 520 | EguiManagedTexture { 521 | handle, 522 | color_image, 523 | }, 524 | ); 525 | } 526 | } 527 | } 528 | 529 | fn update_image_rect(dest: &mut egui::ColorImage, [x, y]: [usize; 2], src: &egui::ColorImage) { 530 | for sy in 0..src.height() { 531 | for sx in 0..src.width() { 532 | dest[(x + sx, y + sy)] = src[(sx, sy)]; 533 | } 534 | } 535 | } 536 | } 537 | 538 | fn free_egui_textures( 539 | mut egui_context: ResMut, 540 | mut egui_render_output: ResMut>, 541 | mut egui_managed_textures: ResMut, 542 | mut image_assets: ResMut>, 543 | mut image_events: EventReader>, 544 | ) { 545 | for (&window_id, egui_render_output) in egui_render_output.iter_mut() { 546 | let free_textures = std::mem::take(&mut egui_render_output.textures_delta.free); 547 | for texture_id in free_textures { 548 | if let egui::TextureId::Managed(texture_id) = texture_id { 549 | let managed_texture = egui_managed_textures.0.remove(&(window_id, texture_id)); 550 | if let Some(managed_texture) = managed_texture { 551 | image_assets.remove(managed_texture.handle); 552 | } 553 | } 554 | } 555 | } 556 | 557 | for image_event in image_events.iter() { 558 | if let AssetEvent::Removed { handle } = image_event { 559 | egui_context.remove_image(handle); 560 | } 561 | } 562 | } 563 | 564 | /// Egui's render graph config. 565 | pub struct RenderGraphConfig { 566 | /// Target window. 567 | pub window_id: WindowId, 568 | /// Render pass name. 569 | pub egui_pass: &'static str, 570 | } 571 | 572 | impl Default for RenderGraphConfig { 573 | fn default() -> Self { 574 | RenderGraphConfig { 575 | window_id: WindowId::primary(), 576 | egui_pass: node::EGUI_PASS, 577 | } 578 | } 579 | } 580 | 581 | /// Set up egui render pipeline. 582 | /// 583 | /// The pipeline for the primary window will already be set up by the [`EguiPlugin`], 584 | /// so you'll only need to manually call this if you want to use multiple windows. 585 | pub fn setup_pipeline(render_graph: &mut RenderGraph, config: RenderGraphConfig) { 586 | render_graph.add_node(config.egui_pass, EguiNode::new(config.window_id)); 587 | 588 | render_graph 589 | .add_node_edge( 590 | bevy::core_pipeline::node::MAIN_PASS_DRIVER, 591 | config.egui_pass, 592 | ) 593 | .unwrap(); 594 | 595 | let _ = render_graph.add_node_edge("ui_pass_driver", config.egui_pass); 596 | } 597 | 598 | #[cfg(test)] 599 | mod tests { 600 | use super::*; 601 | use bevy::{render::options::WgpuOptions, winit::WinitPlugin, DefaultPlugins}; 602 | 603 | #[test] 604 | fn test_readme_deps() { 605 | version_sync::assert_markdown_deps_updated!("README.md"); 606 | } 607 | 608 | #[test] 609 | fn test_headless_mode() { 610 | App::new() 611 | .insert_resource(WgpuOptions { 612 | backends: None, 613 | ..Default::default() 614 | }) 615 | .add_plugins_with(DefaultPlugins, |group| group.disable::()) 616 | .add_plugin(EguiPlugin) 617 | .update(); 618 | } 619 | 620 | #[test] 621 | fn test_ctx_for_windows_mut_unique_check_passes() { 622 | let mut egui_context = EguiContext::new(); 623 | let primary_window = WindowId::primary(); 624 | let second_window = WindowId::new(); 625 | egui_context.ctx.insert(primary_window, Default::default()); 626 | egui_context.ctx.insert(second_window, Default::default()); 627 | let [primary_ctx, second_ctx] = 628 | egui_context.ctx_for_windows_mut([primary_window, second_window]); 629 | assert!(primary_ctx != second_ctx); 630 | } 631 | 632 | #[test] 633 | #[should_panic( 634 | expected = "Window ids passed to `EguiContext::ctx_for_windows_mut` must be unique" 635 | )] 636 | fn test_ctx_for_windows_mut_unique_check_panics() { 637 | let mut egui_context = EguiContext::new(); 638 | let primary_window = WindowId::primary(); 639 | egui_context.ctx.insert(primary_window, Default::default()); 640 | egui_context.ctx_for_windows_mut([primary_window, primary_window]); 641 | } 642 | 643 | #[test] 644 | fn test_try_ctx_for_windows_mut_unique_check_passes() { 645 | let mut egui_context = EguiContext::new(); 646 | let primary_window = WindowId::primary(); 647 | let second_window = WindowId::new(); 648 | egui_context.ctx.insert(primary_window, Default::default()); 649 | egui_context.ctx.insert(second_window, Default::default()); 650 | let [primary_ctx, second_ctx] = 651 | egui_context.try_ctx_for_windows_mut([primary_window, second_window]); 652 | assert!(primary_ctx.is_some()); 653 | assert!(second_ctx.is_some()); 654 | assert!(primary_ctx != second_ctx); 655 | } 656 | 657 | #[test] 658 | #[should_panic( 659 | expected = "Window ids passed to `EguiContext::ctx_for_windows_mut` must be unique" 660 | )] 661 | fn test_try_ctx_for_windows_mut_unique_check_panics() { 662 | let mut egui_context = EguiContext::new(); 663 | let primary_window = WindowId::primary(); 664 | egui_context.ctx.insert(primary_window, Default::default()); 665 | egui_context.try_ctx_for_windows_mut([primary_window, primary_window]); 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/src/render_systems.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::HandleId, 3 | prelude::*, 4 | render::{ 5 | render_asset::RenderAssets, 6 | render_resource::{std140::AsStd140, BindGroup, BufferId, DynamicUniformVec}, 7 | renderer::{RenderDevice, RenderQueue}, 8 | texture::Image, 9 | }, 10 | utils::HashMap, 11 | window::WindowId, 12 | }; 13 | use wgpu::{BindGroupDescriptor, BindGroupEntry, BindingResource}; 14 | 15 | use crate::{ 16 | egui_node::EguiPipeline, EguiContext, EguiManagedTextures, EguiRenderOutput, EguiSettings, 17 | WindowSize, 18 | }; 19 | 20 | pub(crate) struct ExtractedRenderOutput(pub HashMap); 21 | pub(crate) struct ExtractedWindowSizes(pub HashMap); 22 | pub(crate) struct ExtractedEguiSettings(pub EguiSettings); 23 | pub(crate) struct ExtractedEguiContext(pub HashMap); 24 | 25 | #[derive(Debug, PartialEq, Eq, Hash)] 26 | pub(crate) enum EguiTexture { 27 | /// Textures allocated via egui. 28 | Managed(WindowId, u64), 29 | /// Textures allocated via bevy. 30 | User(u64), 31 | } 32 | 33 | pub(crate) struct ExtractedEguiTextures { 34 | pub(crate) egui_textures: HashMap<(WindowId, u64), Handle>, 35 | pub(crate) user_textures: HashMap, 36 | } 37 | 38 | impl ExtractedEguiTextures { 39 | pub(crate) fn handles(&self) -> impl Iterator + '_ { 40 | self.egui_textures 41 | .iter() 42 | .map(|(&(window, texture_id), handle)| { 43 | (EguiTexture::Managed(window, texture_id), handle.id) 44 | }) 45 | .chain( 46 | self.user_textures 47 | .iter() 48 | .map(|(handle, id)| (EguiTexture::User(*id), *handle)), 49 | ) 50 | } 51 | } 52 | 53 | pub(crate) fn extract_egui_render_data( 54 | mut commands: Commands, 55 | mut egui_render_output: ResMut>, 56 | window_sizes: ResMut>, 57 | egui_settings: Res, 58 | egui_context: Res, 59 | ) { 60 | let render_output = std::mem::take(&mut *egui_render_output); 61 | commands.insert_resource(ExtractedRenderOutput(render_output)); 62 | commands.insert_resource(ExtractedEguiSettings(egui_settings.clone())); 63 | commands.insert_resource(ExtractedEguiContext(egui_context.ctx.clone())); 64 | commands.insert_resource(ExtractedWindowSizes(window_sizes.clone())); 65 | } 66 | 67 | pub(crate) fn extract_egui_textures( 68 | mut commands: Commands, 69 | egui_context: Res, 70 | egui_managed_textures: ResMut, 71 | ) { 72 | commands.insert_resource(ExtractedEguiTextures { 73 | egui_textures: egui_managed_textures 74 | .0 75 | .iter() 76 | .map(|(&(window_id, texture_id), managed_texture)| { 77 | ((window_id, texture_id), managed_texture.handle.clone()) 78 | }) 79 | .collect(), 80 | user_textures: egui_context.user_textures.clone(), 81 | }); 82 | } 83 | 84 | #[derive(Default)] 85 | pub(crate) struct EguiTransforms { 86 | pub buffer: DynamicUniformVec, 87 | pub offsets: HashMap, 88 | pub bind_group: Option<(BufferId, BindGroup)>, 89 | } 90 | 91 | #[derive(AsStd140)] 92 | pub(crate) struct EguiTransform { 93 | scale: Vec2, 94 | translation: Vec2, 95 | } 96 | impl EguiTransform { 97 | fn new(window_size: WindowSize, egui_settings: &EguiSettings) -> Self { 98 | EguiTransform { 99 | scale: Vec2::new( 100 | 2.0 / (window_size.width() / egui_settings.scale_factor as f32), 101 | -2.0 / (window_size.height() / egui_settings.scale_factor as f32), 102 | ), 103 | translation: Vec2::new(-1.0, 1.0), 104 | } 105 | } 106 | } 107 | 108 | pub(crate) fn prepare_egui_transforms( 109 | mut egui_transforms: ResMut, 110 | window_sizes: Res, 111 | egui_settings: Res, 112 | 113 | render_device: Res, 114 | render_queue: Res, 115 | 116 | egui_pipeline: Res, 117 | ) { 118 | egui_transforms.buffer.clear(); 119 | egui_transforms.offsets.clear(); 120 | 121 | for (window, size) in &window_sizes.0 { 122 | let offset = egui_transforms 123 | .buffer 124 | .push(EguiTransform::new(*size, &egui_settings.0)); 125 | egui_transforms.offsets.insert(*window, offset); 126 | } 127 | 128 | egui_transforms 129 | .buffer 130 | .write_buffer(&render_device, &render_queue); 131 | 132 | if let Some(buffer) = egui_transforms.buffer.uniform_buffer() { 133 | match egui_transforms.bind_group { 134 | Some((id, _)) if buffer.id() == id => {} 135 | _ => { 136 | let transform_bind_group = render_device.create_bind_group(&BindGroupDescriptor { 137 | label: Some("egui transform bind group"), 138 | layout: &egui_pipeline.transform_bind_group_layout, 139 | entries: &[BindGroupEntry { 140 | binding: 0, 141 | resource: egui_transforms.buffer.binding().unwrap(), 142 | }], 143 | }); 144 | egui_transforms.bind_group = Some((buffer.id(), transform_bind_group)); 145 | } 146 | }; 147 | } 148 | } 149 | 150 | pub(crate) struct EguiTextureBindGroups { 151 | pub(crate) bind_groups: HashMap, 152 | } 153 | 154 | pub(crate) fn queue_bind_groups( 155 | mut commands: Commands, 156 | egui_textures: Res, 157 | render_device: Res, 158 | gpu_images: Res>, 159 | egui_pipeline: Res, 160 | ) { 161 | let bind_groups = egui_textures 162 | .handles() 163 | .filter_map(|(texture, handle_id)| { 164 | let gpu_image = gpu_images.get(&Handle::weak(handle_id))?; 165 | let bind_group = render_device.create_bind_group(&BindGroupDescriptor { 166 | label: None, 167 | layout: &egui_pipeline.texture_bind_group_layout, 168 | entries: &[ 169 | BindGroupEntry { 170 | binding: 0, 171 | resource: BindingResource::TextureView(&gpu_image.texture_view), 172 | }, 173 | BindGroupEntry { 174 | binding: 1, 175 | resource: BindingResource::Sampler(&gpu_image.sampler), 176 | }, 177 | ], 178 | }); 179 | Some((texture, bind_group)) 180 | }) 181 | .collect(); 182 | 183 | commands.insert_resource(EguiTextureBindGroups { bind_groups }) 184 | } 185 | -------------------------------------------------------------------------------- /deps/bevy_egui-0.12.1/src/systems.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{EguiContext, EguiInput, EguiOutput, EguiRenderOutput, EguiSettings, WindowSize}; 4 | #[cfg(feature = "open_url")] 5 | use bevy::log; 6 | use bevy::{ 7 | app::EventReader, 8 | core::Time, 9 | ecs::system::{Local, Res, ResMut, SystemParam}, 10 | input::{ 11 | keyboard::{KeyCode, KeyboardInput}, 12 | mouse::{MouseButton, MouseButtonInput, MouseScrollUnit, MouseWheel}, 13 | ElementState, Input, 14 | }, 15 | utils::HashMap, 16 | window::{ 17 | CursorEntered, CursorLeft, CursorMoved, ReceivedCharacter, WindowCreated, WindowFocused, 18 | WindowId, Windows, 19 | }, 20 | //winit::WinitWindows, 21 | }; 22 | 23 | #[derive(SystemParam)] 24 | pub struct InputEvents<'w, 's> { 25 | ev_cursor_entered: EventReader<'w, 's, CursorEntered>, 26 | ev_cursor_left: EventReader<'w, 's, CursorLeft>, 27 | ev_cursor: EventReader<'w, 's, CursorMoved>, 28 | ev_mouse_button_input: EventReader<'w, 's, MouseButtonInput>, 29 | ev_mouse_wheel: EventReader<'w, 's, MouseWheel>, 30 | ev_received_character: EventReader<'w, 's, ReceivedCharacter>, 31 | ev_keyboard_input: EventReader<'w, 's, KeyboardInput>, 32 | ev_window_focused: EventReader<'w, 's, WindowFocused>, 33 | ev_window_created: EventReader<'w, 's, WindowCreated>, 34 | } 35 | 36 | #[derive(SystemParam)] 37 | pub struct InputResources<'w, 's> { 38 | #[cfg(feature = "manage_clipboard")] 39 | egui_clipboard: Res<'w, crate::EguiClipboard>, 40 | keyboard_input: Res<'w, Input>, 41 | egui_input: ResMut<'w, HashMap>, 42 | #[system_param(ignore)] 43 | marker: PhantomData<&'s usize>, 44 | } 45 | 46 | #[derive(SystemParam)] 47 | pub struct WindowResources<'w, 's> { 48 | focused_window: Local<'s, Option>, 49 | windows: ResMut<'w, Windows>, 50 | window_sizes: ResMut<'w, HashMap>, 51 | #[system_param(ignore)] 52 | _marker: PhantomData<&'s usize>, 53 | } 54 | 55 | pub fn init_contexts_on_startup( 56 | mut egui_context: ResMut, 57 | mut egui_input: ResMut>, 58 | mut window_resources: WindowResources, 59 | egui_settings: Res, 60 | ) { 61 | update_window_contexts( 62 | &mut egui_context, 63 | &mut egui_input, 64 | &mut window_resources, 65 | &egui_settings, 66 | ); 67 | } 68 | 69 | pub fn process_input( 70 | mut egui_context: ResMut, 71 | mut input_events: InputEvents, 72 | mut input_resources: InputResources, 73 | mut window_resources: WindowResources, 74 | egui_settings: Res, 75 | time: Res