tirbofish/dropbear · commit
fd6bf0d98f2e9bb9d6de38e8cc157d6e865eeaf7
feature: created a new grid for the UI editor with pan and zoom.
Signature present but could not be verified.
Unverified
@@ -251,7 +251,7 @@ fn collect_available_animations( ), c )] -fn animation_component_exists_for_entity( +fn exists_for_entity( #[dropbear_macro::define(WorldPtr)] world: &World, #[dropbear_macro::entity] entity: Entity, ) -> DropbearNativeResult<bool> { @@ -1,15 +1,419 @@ +use std::sync::Arc; +use bytemuck::{Pod, Zeroable}; +use dropbear_engine::graphics::SharedGraphicsContext; +use egui::TextureId; +use glam::{Mat4, Vec2, Vec4}; +use kino_ui::camera::Camera2D; +use wgpu::TextureFormat; + +pub mod viewport; + pub struct UiEditor { + pub grid_pipeline: Option<UIGridPipeline>, + pub active_entity: Option<hecs::Entity>, + pub camera: Camera2D, + zoom: f32, + camera_position: Vec2, } impl UiEditor { pub fn new() -> Self { + let mut camera = Camera2D::default(); + camera.set_zoom(1.0); + camera.target(Vec2::ZERO); + Self { + grid_pipeline: None, active_entity: None, + camera, + zoom: 1.0, + camera_position: Vec2::ZERO, } } pub fn update(&mut self) { } + + pub fn render(&mut self, graphics: Arc<SharedGraphicsContext>, width: u32, height: u32) { + if self.grid_pipeline.is_none() { + self.setup(graphics.clone()); + } + + let screen_size = Vec2::new(width.max(1) as f32, height.max(1) as f32); + let view_proj = self.view_proj(screen_size); + let inv_view_proj = self.inv_view_proj(screen_size); + + if let Some(grid) = self.grid_pipeline.as_mut() { + grid.render_to_texture(graphics, width, height, view_proj, inv_view_proj); + } + } + + pub fn zoom_by(&mut self, delta: f32) { + self.zoom = (self.zoom + delta).clamp(0.1, 10.0); + self.camera.set_zoom(self.zoom); + self.camera.target(self.camera_position); + } + + pub fn zoom(&self) -> f32 { + self.zoom + } + + pub fn pan_by_pixels(&mut self, delta_pixels: Vec2) { + self.camera_position -= delta_pixels / self.zoom.max(0.1); + self.camera.target(self.camera_position); + } + + pub fn view_proj(&self, screen_size: Vec2) -> Mat4 { + let width = screen_size.x.max(1.0); + let height = screen_size.y.max(1.0); + let half_w = width / (2.0 * self.zoom.max(0.1)); + let half_h = height / (2.0 * self.zoom.max(0.1)); + + let view = Mat4::from_translation((-self.camera_position).extend(0.0)); + let proj = Mat4::orthographic_rh( + -half_w, + half_w, + half_h, + -half_h, + -1.0, + 1.0, + ); + + proj * view + } + + pub fn inv_view_proj(&self, screen_size: Vec2) -> Mat4 { + self.view_proj(screen_size).inverse() + } + + pub fn world_from_screen_pixels(&self, pixel: Vec2, viewport_size: Vec2) -> Vec2 { + let viewport_size = Vec2::new(viewport_size.x.max(1.0), viewport_size.y.max(1.0)); + let ndc = Vec2::new( + (pixel.x / viewport_size.x) * 2.0 - 1.0, + 1.0 - (pixel.y / viewport_size.y) * 2.0, + ); + + let world = self.inv_view_proj(viewport_size) * Vec4::new(ndc.x, ndc.y, 0.0, 1.0); + Vec2::new(world.x, world.y) / world.w + } + + pub fn texture_id(&self) -> Option<TextureId> { + self.grid_pipeline.as_ref().and_then(|pipeline| pipeline.texture_id) + } + + fn setup(&mut self, graphics: Arc<SharedGraphicsContext>) { + self.grid_pipeline = Some(UIGridPipeline::new(graphics.clone())); + } +} + +pub struct UIGridPipeline { + size: wgpu::Extent3d, + sample_count: u32, + format: TextureFormat, + + texture_id: Option<TextureId>, + resolve_texture: Option<wgpu::Texture>, + resolve_view: Option<wgpu::TextureView>, + msaa_texture: Option<wgpu::Texture>, + msaa_view: Option<wgpu::TextureView>, + + camera_buffer: wgpu::Buffer, + camera_bind_group: wgpu::BindGroup, + + pipeline: wgpu::RenderPipeline, +} + +impl UIGridPipeline { + pub fn new(graphics: Arc<SharedGraphicsContext>) -> Self { + let shader = graphics + .device + .create_shader_module(wgpu::include_wgsl!("shader/grid.wgsl")); + + let camera_bind_group_layout = graphics + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("ui grid camera bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let camera_buffer = graphics.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("ui grid camera uniform buffer"), + size: std::mem::size_of::<UIGridCameraUniform>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let camera_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ui grid camera bind group"), + layout: &camera_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + + let layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("ui grid pipeline layout"), + bind_group_layouts: &[&camera_bind_group_layout], + push_constant_ranges: &[], + }); + + let sample_count: u32 = (*graphics.antialiasing.read()).into(); + let format = graphics.surface_format.add_srgb_suffix(); + + let pipeline = graphics.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("ui grid pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + cache: None, + }); + + Self { + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + sample_count, + format, + texture_id: None, + resolve_texture: None, + resolve_view: None, + msaa_texture: None, + msaa_view: None, + camera_buffer, + camera_bind_group, + pipeline, + } + } + + fn update_camera_uniform( + &self, + graphics: &SharedGraphicsContext, + view_proj: Mat4, + inv_view_proj: Mat4, + width: u32, + height: u32, + ) { + let screen_size = Vec2::new(width.max(1) as f32, height.max(1) as f32); + + let uniform = UIGridCameraUniform { + view_proj: view_proj.to_cols_array_2d(), + inv_view_proj: inv_view_proj.to_cols_array_2d(), + viewport_size: [screen_size.x, screen_size.y], + _padding: [0.0, 0.0], + }; + + graphics + .queue + .write_buffer(&self.camera_buffer, 0, bytemuck::bytes_of(&uniform)); + } + + fn create_texture( + device: &wgpu::Device, + size: wgpu::Extent3d, + format: TextureFormat, + sample_count: u32, + label: &'static str, + ) -> (wgpu::Texture, wgpu::TextureView) { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size, + mip_level_count: 1, + sample_count, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + (texture, view) + } + + fn recreate_targets_if_needed( + &mut self, + graphics: &SharedGraphicsContext, + width: u32, + height: u32, + ) { + let width = width.max(1); + let height = height.max(1); + let new_sample_count: u32 = (*graphics.antialiasing.read()).into(); + + let needs_recreate = self.size.width != width + || self.size.height != height + || self.sample_count != new_sample_count + || self.resolve_view.is_none(); + + if !needs_recreate { + return; + } + + self.size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + self.sample_count = new_sample_count; + + let (resolve_texture, resolve_view) = Self::create_texture( + &graphics.device, + self.size, + self.format, + 1, + "ui viewport resolve texture", + ); + + if let Some(texture_id) = self.texture_id { + graphics + .egui_renderer + .lock() + .renderer() + .update_egui_texture_from_wgpu_texture( + &graphics.device, + &resolve_view, + wgpu::FilterMode::Linear, + texture_id, + ); + } else { + let texture_id = graphics + .egui_renderer + .lock() + .renderer() + .register_native_texture( + &graphics.device, + &resolve_view, + wgpu::FilterMode::Linear, + ); + self.texture_id = Some(texture_id); + } + + self.resolve_texture = Some(resolve_texture); + self.resolve_view = Some(resolve_view); + + if self.sample_count > 1 { + let (msaa_texture, msaa_view) = Self::create_texture( + &graphics.device, + self.size, + self.format, + self.sample_count, + "ui viewport msaa texture", + ); + self.msaa_texture = Some(msaa_texture); + self.msaa_view = Some(msaa_view); + } else { + self.msaa_texture = None; + self.msaa_view = None; + } + } + + pub fn render_to_texture( + &mut self, + graphics: Arc<SharedGraphicsContext>, + width: u32, + height: u32, + view_proj: Mat4, + inv_view_proj: Mat4, + ) { + self.recreate_targets_if_needed(&graphics, width, height); + self.update_camera_uniform(&graphics, view_proj, inv_view_proj, width, height); + + let Some(resolve_view) = self.resolve_view.as_ref() else { + return; + }; + + let mut encoder = graphics + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("ui viewport grid encoder"), + }); + + let (color_view, resolve_target) = if self.sample_count > 1 { + let Some(msaa_view) = self.msaa_view.as_ref() else { + return; + }; + (msaa_view, Some(resolve_view)) + } else { + (resolve_view, None) + }; + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("ui viewport grid render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: color_view, + depth_slice: None, + resolve_target, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + self.render(&mut render_pass); + } + + graphics.queue.submit(std::iter::once(encoder.finish())); + } + + pub fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.camera_bind_group, &[]); + render_pass.draw(0..3, 0..1); + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct UIGridCameraUniform { + view_proj: [[f32; 4]; 4], + inv_view_proj: [[f32; 4]; 4], + viewport_size: [f32; 2], + _padding: [f32; 2], } @@ -0,0 +1,68 @@ +struct CameraUniform { + view_proj: mat4x4<f32>, + inv_view_proj: mat4x4<f32>, + viewport_size: vec2<f32>, + _padding: vec2<f32>, +}; + +@group(0) @binding(0) +var<uniform> camera: CameraUniform; + +fn grid_alpha(pixel_uv: vec2<f32>, cell_size: f32, line_width: f32) -> f32 { + let uv = pixel_uv / cell_size; + let derivative = fwidth(uv) * line_width; + let grid = abs(fract(uv - 0.5) - 0.5) / derivative; + let l = min(grid.x, grid.y); + return 1.0 - clamp(l, 0.0, 1.0); +} + +fn axis_alpha(v: f32, line_width: f32) -> f32 { + let d = abs(v); + let fw = max(fwidth(v), 1e-5) * line_width; + return 1.0 - smoothstep(0.0, fw, d); +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> { + var positions = array<vec2<f32>, 3>( + vec2<f32>(-1.0, -1.0), + vec2<f32>(3.0, -1.0), + vec2<f32>(-1.0, 3.0), + ); + + let position = positions[vertex_index]; + return vec4<f32>(position, 0.0, 1.0); +} + +@fragment +fn fs_main(@builtin(position) frag_pos: vec4<f32>) -> @location(0) vec4<f32> { + let ndc = vec2<f32>( + (frag_pos.x / camera.viewport_size.x) * 2.0 - 1.0, + 1.0 - (frag_pos.y / camera.viewport_size.y) * 2.0, + ); + + let world = camera.inv_view_proj * vec4<f32>(ndc, 0.0, 1.0); + let world_pos = world.xy / world.w; + + let fine = grid_alpha(world_pos, 10.0, 0.8); + let coarse = grid_alpha(world_pos, 100.0, 1.0); + let axis_x = axis_alpha(world_pos.y, 1.8); + let axis_y = axis_alpha(world_pos.x, 1.8); + let axis = max(axis_x, axis_y); + + let fine_color = vec3<f32>(0.25, 0.25, 0.25); + let coarse_color = vec3<f32>(0.38, 0.38, 0.38); + + var color = vec3<f32>(0.13, 0.13, 0.13); + + var alpha = 0.0; + alpha = max(alpha, fine * 0.6); + color = mix(color, fine_color, fine * 0.6); + alpha = max(alpha, coarse); + color = mix(color, coarse_color, coarse); + + color = mix(color, vec3<f32>(1.0, 1.0, 1.0), axis); + alpha = max(alpha, axis); + + return vec4<f32>(color, alpha); +} @@ -0,0 +1,96 @@ +use egui::Ui; +use glam::Vec2; +use crate::editor::{EditorTabDock, EditorTabDockDescriptor, EditorTabViewer}; +use crate::editor::page::EditorTabVisibility; + +pub struct UIViewport; + +impl EditorTabDock for UIViewport { + fn desc() -> EditorTabDockDescriptor { + EditorTabDockDescriptor { + id: "UI Viewport", + title: "UI Viewport".to_string(), + visibility: EditorTabVisibility::UIEditor, + } + } + + fn display(viewer: &mut EditorTabViewer<'_>, ui: &mut Ui) { + let available_rect = ui.available_rect_before_wrap(); + let available_size = available_rect.size(); + let pixels_per_point = ui.ctx().pixels_per_point(); + + let desired_width = (available_size.x * pixels_per_point).max(1.0).round() as u32; + let desired_height = (available_size.y * pixels_per_point).max(1.0).round() as u32; + + viewer + .ui_editor + .render(viewer.graphics.clone(), desired_width, desired_height); + + if let Some(texture_id) = viewer.ui_editor.texture_id() { + let response = ui.add_sized( + [available_size.x, available_size.y], + egui::Image::new((texture_id, available_size)).sense(egui::Sense::drag()), + ); + + let ppp = ui.ctx().pixels_per_point(); + let viewport_pixels = Vec2::new(available_size.x * ppp, available_size.y * ppp); + + if response.hovered() { + let scroll_y = ui.ctx().input(|i| i.raw_scroll_delta.y); + if scroll_y.abs() > 0.0 { + let zoom_delta = scroll_y * 0.0025; + viewer.ui_editor.zoom_by(zoom_delta); + } + } + + let is_middle_down = ui + .ctx() + .input(|i| i.pointer.button_down(egui::PointerButton::Middle)); + + if response.hovered() && is_middle_down { + let pointer_delta_points = ui.ctx().input(|i| i.pointer.delta()); + if pointer_delta_points != egui::Vec2::ZERO { + let delta_pixels = Vec2::new( + pointer_delta_points.x * ppp, + pointer_delta_points.y * ppp, + ); + viewer.ui_editor.pan_by_pixels(delta_pixels); + } + } + + let cursor_info = response.hover_pos().map(|hover_pos| { + let local = hover_pos - response.rect.min; + let pixel = Vec2::new(local.x * ppp, local.y * ppp); + viewer.ui_editor.world_from_screen_pixels(pixel, viewport_pixels) + }); + + let zoom_percent = viewer.ui_editor.zoom() * 100.0; + let hud = if let Some(world) = cursor_info { + format!( + "Coords: ({:.1}, {:.1}) u | zoom: {:.0}%", + world.x, + world.y, + zoom_percent + ) + } else { + format!( + "Coords: (-, -) u | zoom: {:.0}%", + zoom_percent + ) + }; + + let text_pos = response.rect.left_top() + egui::vec2(8.0, 8.0); + ui.painter().text( + text_pos, + egui::Align2::LEFT_TOP, + hud, + egui::TextStyle::Monospace.resolve(ui.style()), + egui::Color32::WHITE, + ); + } else { + ui.centered_and_justified(|ui| { + ui.label("UI viewport texture is initialising..."); + }); + } + } +} @@ -13,9 +13,7 @@ pub mod utils; pub use redback_runtime as runtime; use crate::editor::{ - EditorTabRegistry, asset_viewer::AssetViewerDock, build_console::BuildConsoleDock, - dock::ConsoleDock, entity_list::EntityListDock, resource::ResourceInspectorDock, - viewport::ViewportDock, + EditorTabRegistry, asset_viewer::AssetViewerDock, build_console::BuildConsoleDock, dock::ConsoleDock, entity_list::EntityListDock, resource::ResourceInspectorDock, ui::viewport::UIViewport, viewport::ViewportDock }; dropbear_engine::features! { @@ -31,4 +29,5 @@ pub fn register_docks(registry: &mut EditorTabRegistry) { registry.register::<ResourceInspectorDock>(); registry.register::<BuildConsoleDock>(); registry.register::<ConsoleDock>(); + registry.register::<UIViewport>(); } @@ -511,8 +511,35 @@ impl SignalController for Editor { Ok(()) } Signal::AddComponent(entity, component) => { - let component = component.clone(); let registry = self.component_registry.clone(); + + let Some(component_id) = registry.id_for_component(component.as_ref()) else { + warn!( + "Failed to resolve component type for add request on entity {:?}", + entity + ); + self.signal = Signal::None; + return Ok(()); + }; + + if registry + .find_entities_by_numeric_id(&self.world, component_id) + .contains(&entity) + { + let component_name = registry + .get_descriptor_by_numeric_id(component_id) + .map(|desc| desc.type_name.as_str()) + .unwrap_or("Unknown"); + + warn!( + "Entity {:?} already has component '{}'", + entity, + component_name + ); + self.signal = Signal::None; + return Ok(()); + } + let graphics_clone = graphics.clone(); let init_future = async move { let Some(loader_future) = @@ -448,7 +448,7 @@ typedef struct StringArray { typedef void* WorldPtr; -int32_t dropbear_animation_animation_component_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0); +int32_t dropbear_animation_exists_for_entity(WorldPtr world, uint64_t entity, bool* out0); int32_t dropbear_animation_get_active_animation_index(WorldPtr world, uint64_t entity, int32_t* out0, bool* out0_present); int32_t dropbear_animation_get_available_animations(WorldPtr world, uint64_t entity, StringArray* out0); int32_t dropbear_animation_get_index_from_string(WorldPtr world, uint64_t entity, const char* name, int32_t* out0, bool* out0_present);