From 16626cc277fe86e4d9e13b49ef88fc87f0c7f7a5 Mon Sep 17 00:00:00 2001 From: Jonas H Date: Thu, 2 Apr 2026 19:48:47 +0200 Subject: [PATCH] gizmo --- src/debug/gizmo.rs | 178 +++++++++++++++++++++++++++++++++++++++++ src/debug/mod.rs | 1 + src/main.rs | 30 ++++++- src/render/mod.rs | 20 ++++- src/render/pipeline.rs | 151 +++++++++++++++++++--------------- src/render/types.rs | 1 + src/world.rs | 5 ++ 7 files changed, 317 insertions(+), 69 deletions(-) create mode 100644 src/debug/gizmo.rs diff --git a/src/debug/gizmo.rs b/src/debug/gizmo.rs new file mode 100644 index 0000000..4b0bcfe --- /dev/null +++ b/src/debug/gizmo.rs @@ -0,0 +1,178 @@ +use bytemuck::cast_slice; +use glam::{Mat4, Vec3}; +use wgpu::util::DeviceExt; + +use crate::entity::EntityHandle; +use crate::loaders::mesh::{InstanceRaw, Mesh, Vertex}; +use crate::render::{self, DrawCall, Pipeline}; +use crate::utility::transform::Transform; +use crate::world::Storage; + +const GIZMO_DISTANCE_SCALE: f32 = 0.1; +const ARROW_BASE: f32 = 0.85; +const ARROW_SPREAD: f32 = 0.08; + +pub fn create_gizmo_mesh(device: &wgpu::Device) -> Mesh +{ + let vertices = vec![ + Vertex { + position: [0.0, 0.0, 0.0], + normal: [1.0, 0.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [1.0, 0.0, 0.0], + normal: [1.0, 0.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, 0.0, 0.0], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, 1.0, 0.0], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, 0.0, 0.0], + normal: [0.0, 0.0, 1.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, 0.0, 1.0], + normal: [0.0, 0.0, 1.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [ARROW_BASE, ARROW_SPREAD, 0.0], + normal: [1.0, 0.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [ARROW_BASE, -ARROW_SPREAD, 0.0], + normal: [1.0, 0.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [ARROW_BASE, 0.0, ARROW_SPREAD], + normal: [1.0, 0.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [ARROW_BASE, 0.0, -ARROW_SPREAD], + normal: [1.0, 0.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [ARROW_SPREAD, ARROW_BASE, 0.0], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [-ARROW_SPREAD, ARROW_BASE, 0.0], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, ARROW_BASE, ARROW_SPREAD], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, ARROW_BASE, -ARROW_SPREAD], + normal: [0.0, 1.0, 0.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [ARROW_SPREAD, 0.0, ARROW_BASE], + normal: [0.0, 0.0, 1.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [-ARROW_SPREAD, 0.0, ARROW_BASE], + normal: [0.0, 0.0, 1.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, ARROW_SPREAD, ARROW_BASE], + normal: [0.0, 0.0, 1.0], + uv: [0.0, 0.0], + }, + Vertex { + position: [0.0, -ARROW_SPREAD, ARROW_BASE], + normal: [0.0, 0.0, 1.0], + uv: [0.0, 0.0], + }, + ]; + + let indices = vec![ + 0, 1, 2, 3, 4, 5, 6, 1, 7, 1, 8, 1, 9, 1, 10, 3, 11, 3, 12, 3, 13, 3, 14, 5, 15, 5, 16, 5, + 17, 5, + ]; + + Mesh::new(device, &vertices, &indices) +} + +pub fn create_gizmo_instance_buffer(device: &wgpu::Device) -> wgpu::Buffer +{ + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Gizmo Instance Buffer"), + contents: cast_slice(&[InstanceRaw { + model: Mat4::IDENTITY.to_cols_array_2d(), + dissolve_amount: 0.0, + _padding: [0.0; 3], + }]), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }) +} + +pub fn render_transform_gizmo( + entity: EntityHandle, + transforms: &Storage, + camera_position: Vec3, + gizmo_mesh: &Mesh, + instance_buffer: &wgpu::Buffer, +) -> Vec +{ + let transform = match transforms.get(entity) + { + Some(t) => t, + None => return Vec::new(), + }; + + let distance = camera_position.distance(transform.position); + let scale = (distance * GIZMO_DISTANCE_SCALE).max(0.1); + + let model = Mat4::from_scale_rotation_translation( + Vec3::splat(scale), + transform.rotation, + transform.position, + ); + + let instance_data = InstanceRaw { + model: model.to_cols_array_2d(), + dissolve_amount: 0.0, + _padding: [0.0; 3], + }; + + render::with_queue(|queue| { + queue.write_buffer(instance_buffer, 0, cast_slice(&[instance_data])); + }); + + vec![DrawCall { + vertex_buffer: gizmo_mesh.vertex_buffer.clone(), + index_buffer: gizmo_mesh.index_buffer.clone(), + num_indices: gizmo_mesh.num_indices, + model, + pipeline: Pipeline::GizmoLines, + instance_buffer: Some(instance_buffer.clone()), + num_instances: 1, + tile_scale: 4.0, + enable_dissolve: false, + enable_snow_light: false, + displacement_bind_group: None, + entity: None, + }] +} diff --git a/src/debug/mod.rs b/src/debug/mod.rs index 374edf2..b068f55 100644 --- a/src/debug/mod.rs +++ b/src/debug/mod.rs @@ -1,4 +1,5 @@ pub mod collider_debug; +pub mod gizmo; pub mod mode; pub use mode::DebugMode; diff --git a/src/main.rs b/src/main.rs index 79a6773..16fffd7 100755 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ use crate::bundles::terrain::{TerrainBundle, TerrainConfig}; use crate::bundles::test_char::TestCharBundle; use crate::bundles::Bundle; use crate::components::intent::{FollowPlayerIntent, StopFollowingIntent}; -use crate::debug::{collider_debug, DebugMode}; +use crate::debug::{collider_debug, gizmo, DebugMode}; use crate::editor::{editor_loop, EditorState, FrameStats}; use crate::entity::EntityHandle; use crate::loaders::scene::Space; @@ -281,6 +281,15 @@ fn toggle_editor(game: &mut Game) .set_relative_mouse_mode(&game.window, false); game.editor.right_mouse_held = false; game.input_state.mouse_captured = false; + if game.world.gizmo_mesh.is_none() + { + game.world.gizmo_mesh = Some(render::with_device(|device| { + gizmo::create_gizmo_mesh(device) + })); + game.world.gizmo_instance_buffer = Some(render::with_device(|device| { + gizmo::create_gizmo_instance_buffer(device) + })); + } } else { @@ -477,6 +486,25 @@ fn main() -> Result<(), Box> draw_calls.extend(collider_debug::render_collider_debug()); } + if game.editor.active + { + if let Some(entity) = game.editor.selected_entity + { + let cam_pos = game.world.active_camera_position(); + if let (Some(ref mesh), Some(ref buf)) = + (&game.world.gizmo_mesh, &game.world.gizmo_instance_buffer) + { + draw_calls.extend(gizmo::render_transform_gizmo( + entity, + &game.world.transforms, + cam_pos, + mesh, + buf, + )); + } + } + } + game.stats.draw_call_count = draw_calls.len(); game.stats.fps = 1.0 / delta; game.stats.frame_ms = delta * 1000.0; diff --git a/src/render/mod.rs b/src/render/mod.rs index 9005ae5..3e77a19 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -32,8 +32,8 @@ use crate::paths; use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer}; use crate::texture::{DitherTextures, FlowmapTexture}; use pipeline::{ - create_debug_lines_pipeline, create_main_pipeline, create_snow_clipmap_pipeline, - create_wireframe_pipeline, + create_debug_lines_pipeline, create_gizmo_lines_pipeline, create_main_pipeline, + create_snow_clipmap_pipeline, create_wireframe_pipeline, }; use std::num::NonZeroU64; @@ -53,6 +53,7 @@ pub struct Renderer snow_clipmap_pipeline: wgpu::RenderPipeline, wireframe_pipeline: Option, debug_lines_pipeline: Option, + gizmo_lines_pipeline: Option, debug_overlay: Option, billboard_pipeline: BillboardPipeline, particle_pipeline: ParticlePipeline, @@ -531,6 +532,12 @@ impl Renderer &bind_group_layout, )); + let gizmo_lines_pipeline = Some(create_gizmo_lines_pipeline( + &device, + config.format, + &bind_group_layout, + )); + let billboard_pipeline = BillboardPipeline::new(&device, config.format); let particle_pipeline = ParticlePipeline::new(&device, config.format); let font_atlas = font_atlas::FontAtlas::load(&device, &queue); @@ -564,6 +571,7 @@ impl Renderer snow_clipmap_pipeline, wireframe_pipeline, debug_lines_pipeline, + gizmo_lines_pipeline, debug_overlay, billboard_pipeline, particle_pipeline, @@ -740,6 +748,10 @@ impl Renderer .debug_lines_pipeline .as_ref() .unwrap_or(&self.standard_pipeline), + Pipeline::GizmoLines => self + .gizmo_lines_pipeline + .as_ref() + .unwrap_or(&self.standard_pipeline), }; render_pass.set_pipeline(pipeline); render_pass.set_bind_group(0, &self.bind_group, &[offset as u32]); @@ -770,7 +782,7 @@ impl Renderer { if matches!( draw_call.pipeline, - Pipeline::SnowClipmap | Pipeline::DebugLines + Pipeline::SnowClipmap | Pipeline::DebugLines | Pipeline::GizmoLines ) { continue; @@ -864,7 +876,7 @@ impl Renderer if matches!( draw_call.pipeline, - Pipeline::SnowClipmap | Pipeline::DebugLines + Pipeline::SnowClipmap | Pipeline::DebugLines | Pipeline::GizmoLines ) { continue; diff --git a/src/render/pipeline.rs b/src/render/pipeline.rs index 19e9c55..b4bfff6 100644 --- a/src/render/pipeline.rs +++ b/src/render/pipeline.rs @@ -1,6 +1,92 @@ use crate::paths; use wesl::Wesl; +fn create_lines_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, + bind_group_layout: &wgpu::BindGroupLayout, + fragment_entry_point: &str, + label_prefix: &str, +) -> wgpu::RenderPipeline +{ + let compiler = Wesl::new(&paths::SHADERS_DIR); + let shader_source = compiler + .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap()) + .inspect_err(|e| eprintln!("WESL error: {e}")) + .unwrap() + .to_string(); + + let shader_label = format!("{label_prefix} Shader"); + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(&shader_label), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let layout_label = format!("{label_prefix} Pipeline Layout"); + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some(&layout_label), + bind_group_layouts: &[bind_group_layout], + immediate_size: 0, + }); + + let pipeline_label = format!("{label_prefix} Pipeline"); + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(&pipeline_label), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[ + crate::loaders::mesh::Vertex::desc(), + crate::loaders::mesh::InstanceRaw::desc(), + ], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some(fragment_entry_point), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::LineList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Always, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }) +} + +pub fn create_gizmo_lines_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, + bind_group_layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline +{ + create_lines_pipeline(device, format, bind_group_layout, "fs_gizmo", "Gizmo Lines") +} + pub fn create_shadow_pipeline( device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout, @@ -215,70 +301,7 @@ pub fn create_debug_lines_pipeline( bind_group_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { - let compiler = Wesl::new(&paths::SHADERS_DIR); - let shader_source = compiler - .compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap()) - .inspect_err(|e| eprintln!("WESL error: {e}")) - .unwrap() - .to_string(); - - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Debug Lines Shader"), - source: wgpu::ShaderSource::Wgsl(shader_source.into()), - }); - - let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Debug Lines Pipeline Layout"), - bind_group_layouts: &[bind_group_layout], - immediate_size: 0, - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Debug Lines Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[ - crate::loaders::mesh::Vertex::desc(), - crate::loaders::mesh::InstanceRaw::desc(), - ], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::LineList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: None, - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: false, - depth_compare: wgpu::CompareFunction::Always, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview_mask: None, - cache: None, - }) + create_lines_pipeline(device, format, bind_group_layout, "fs_main", "Debug Lines") } pub fn create_snow_clipmap_pipeline( diff --git a/src/render/types.rs b/src/render/types.rs index d42a2cd..5f5afc1 100644 --- a/src/render/types.rs +++ b/src/render/types.rs @@ -143,6 +143,7 @@ pub enum Pipeline Standard, SnowClipmap, DebugLines, + GizmoLines, } pub struct DrawCall diff --git a/src/world.rs b/src/world.rs index 2afb9f4..f84c854 100644 --- a/src/world.rs +++ b/src/world.rs @@ -19,6 +19,7 @@ use crate::components::{ }; use crate::debug::DebugMode; use crate::entity::{EntityHandle, EntityManager}; +use crate::loaders::mesh::Mesh; use crate::render::snow::SnowLayer; use crate::states::state::StateMachine; use crate::systems::particle::ParticleBuffers; @@ -125,6 +126,8 @@ pub struct World pub snow_layer: Option, pub debug_mode: DebugMode, pub was_dialog_active: bool, + pub gizmo_mesh: Option, + pub gizmo_instance_buffer: Option, } impl World @@ -172,6 +175,8 @@ impl World snow_layer: None, debug_mode: DebugMode::default(), was_dialog_active: false, + gizmo_mesh: None, + gizmo_instance_buffer: None, } }