Skip to content

Commit

Permalink
Implement grid shader (Astrabit-ST#102)
Browse files Browse the repository at this point in the history
* feat: add a grid shader to the map editor and tilepicker

* feat: add toggles for grid lines

* fix: account for differing provoking vertex in OpenGL/WebGL

OpenGL and WebGL use the last vertex of each triangle as the provoking
vertex, the vertex whose output is used for `@interpolate(flat)` values
in the fragment shader's input. All other backends use the first vertex
instead.

* style: add a second layer of grid lines to increase visibility

* style: move grid toggle into layer dropdown

* fix: clamp pixels per point to be at least 1

If this value is less than 1, the grid lines will not render properly.

* fix: don't allow non-integer pixels per point

* style: don't use double-layered grid lines if map scale < 90%

* style: change map scale threshold from 90% to 50%
  • Loading branch information
white-axe authored Feb 11, 2024
1 parent c15bf30 commit 22d7155
Show file tree
Hide file tree
Showing 11 changed files with 725 additions and 15 deletions.
18 changes: 14 additions & 4 deletions crates/components/src/map_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ impl MapView {

self.previous_scale = self.scale;

let grid_inner_thickness = if self.scale >= 50. { 1. } else { 0. };

let ctrl_drag = ui.input(|i| {
if is_focused {
// Handle pan
Expand Down Expand Up @@ -571,8 +573,12 @@ impl MapView {
self.selected_event_id = selected_event.map(|e| e.id);

// Draw the fog and collision layers
self.map
.paint_overlay(graphics_state.clone(), ui.painter(), canvas_rect);
self.map.paint_overlay(
graphics_state.clone(),
ui.painter(),
grid_inner_thickness,
canvas_rect,
);

// Draw white rectangles on the border of all events
while let Some(rect) = self.event_rects.pop() {
Expand All @@ -596,8 +602,12 @@ impl MapView {
}
} else {
// Draw the fog and collision layers
self.map
.paint_overlay(graphics_state.clone(), ui.painter(), canvas_rect);
self.map.paint_overlay(
graphics_state.clone(),
ui.painter(),
grid_inner_thickness,
canvas_rect,
);
}

// Do we display the visible region?
Expand Down
30 changes: 26 additions & 4 deletions crates/components/src/tilepicker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub struct Tilepicker {
pub selected_tiles_right: i16,
pub selected_tiles_bottom: i16,

pub coll_enabled: bool,
pub grid_enabled: bool,

drag_origin: Option<egui::Pos2>,

resources: Arc<Resources>,
Expand All @@ -36,6 +39,7 @@ pub struct Tilepicker {
struct Resources {
tiles: luminol_graphics::tiles::Tiles,
collision: luminol_graphics::collision::Collision,
grid: luminol_graphics::grid::Grid,
}

// wgpu types are not Send + Sync on webassembly, so we use fragile to make sure we never access any wgpu resources across thread boundaries
Expand All @@ -44,12 +48,13 @@ struct Callback {
graphics_state: Fragile<Arc<luminol_graphics::GraphicsState>>,

coll_enabled: bool,
grid_enabled: bool,
}

impl luminol_egui_wgpu::CallbackTrait for Callback {
fn paint<'a>(
&'a self,
_info: egui::PaintCallbackInfo,
info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'a>,
_callback_resources: &'a luminol_egui_wgpu::CallbackResources,
) {
Expand All @@ -63,6 +68,10 @@ impl luminol_egui_wgpu::CallbackTrait for Callback {
if self.coll_enabled {
resources.collision.draw(graphics_state, render_pass);
}

if self.grid_enabled {
resources.grid.draw(graphics_state, &info, render_pass);
}
}
}

Expand Down Expand Up @@ -136,6 +145,13 @@ impl Tilepicker {
&tilepicker_data,
);

let grid = luminol_graphics::grid::Grid::new(
&update_state.graphics,
viewport.clone(),
tilepicker_data.xsize(),
tilepicker_data.ysize(),
);

let mut passages =
luminol_data::Table2::new(tilepicker_data.xsize(), tilepicker_data.ysize());
for x in 0..8 {
Expand All @@ -159,13 +175,19 @@ impl Tilepicker {
);

Ok(Self {
resources: Arc::new(Resources { tiles, collision }),
resources: Arc::new(Resources {
tiles,
collision,
grid,
}),
viewport,
ani_time: None,
selected_tiles_left: 0,
selected_tiles_top: 0,
selected_tiles_right: 0,
selected_tiles_bottom: 0,
coll_enabled: false,
grid_enabled: true,
drag_origin: None,
})
}
Expand All @@ -186,7 +208,6 @@ impl Tilepicker {
update_state: &luminol_core::UpdateState<'_>,
ui: &mut egui::Ui,
scroll_rect: egui::Rect,
coll_enabled: bool,
) -> egui::Response {
let time = ui.ctx().input(|i| i.time);
let graphics_state = update_state.graphics.clone();
Expand Down Expand Up @@ -235,7 +256,8 @@ impl Tilepicker {
Callback {
resources: Fragile::new(self.resources.clone()),
graphics_state: Fragile::new(graphics_state.clone()),
coll_enabled,
coll_enabled: self.coll_enabled,
grid_enabled: self.grid_enabled,
},
));

Expand Down
129 changes: 129 additions & 0 deletions crates/graphics/src/grid/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (C) 2023 Lily Lyons
//
// This file is part of Luminol.
//
// Luminol is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Luminol is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see <http://www.gnu.org/licenses/>.

use crossbeam::atomic::AtomicCell;
use wgpu::util::DeviceExt;

use crate::{BindGroupLayoutBuilder, GraphicsState};

#[derive(Debug)]
pub struct Display {
data: AtomicCell<Data>,
uniform: Option<wgpu::Buffer>,
}

#[repr(C, align(16))]
#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Data {
viewport_size_in_pixels: [f32; 2],
pixels_per_point: f32,
inner_thickness_in_points: f32,
}

impl Display {
pub fn new(graphics_state: &GraphicsState) -> Self {
let display = Data {
viewport_size_in_pixels: [0., 0.],
pixels_per_point: 1.,
inner_thickness_in_points: 1.,
};

let uniform = (!graphics_state.push_constants_supported()).then(|| {
graphics_state.render_state.device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("grid display buffer"),
contents: bytemuck::bytes_of(&display),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
},
)
});

Display {
data: AtomicCell::new(display),
uniform,
}
}

pub fn as_bytes(&self) -> [u8; std::mem::size_of::<Data>()] {
bytemuck::cast(self.data.load())
}

pub fn as_buffer(&self) -> Option<&wgpu::Buffer> {
self.uniform.as_ref()
}

pub fn set_inner_thickness(
&self,
render_state: &luminol_egui_wgpu::RenderState,
inner_thickness_in_points: f32,
) {
let data = self.data.load();
if data.inner_thickness_in_points != inner_thickness_in_points {
self.data.store(Data {
inner_thickness_in_points,
..data
});
self.regen_buffer(render_state);
}
}

pub(super) fn update_viewport_size(
&self,
render_state: &luminol_egui_wgpu::RenderState,
info: &egui::PaintCallbackInfo,
) {
let viewport_size = info.viewport_in_pixels();
let viewport_size = [
viewport_size.width_px as f32,
viewport_size.height_px as f32,
];
let pixels_per_point = info.pixels_per_point.max(1.).floor();
let data = self.data.load();
if data.viewport_size_in_pixels != viewport_size
|| data.pixels_per_point != pixels_per_point
{
self.data.store(Data {
viewport_size_in_pixels: viewport_size,
pixels_per_point,
..data
});
self.regen_buffer(render_state);
}
}

fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) {
if let Some(uniform) = &self.uniform {
render_state
.queue
.write_buffer(uniform, 0, bytemuck::bytes_of(&self.data.load()));
}
}
}

pub fn add_to_bind_group_layout(
layout_builder: &mut BindGroupLayoutBuilder,
) -> &mut BindGroupLayoutBuilder {
layout_builder.append(
wgpu::ShaderStages::FRAGMENT,
wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
None,
)
}
85 changes: 85 additions & 0 deletions crates/graphics/src/grid/grid.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
struct VertexInput {
@location(0) position: vec2<f32>,
}

struct InstanceInput {
@location(1) tile_position: vec2<f32>,
}

struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) position: vec2<f32>,
// The fragment shader sees this as the position of the provoking vertex,
// which is set to the vertex at the right angle of every triangle
@location(1) @interpolate(flat) vertex_position: vec2<f32>,
}

struct Viewport {
proj: mat4x4<f32>,
}

struct Display {
viewport_size_in_pixels: vec2<f32>,
pixels_per_point: f32,
inner_thickness_in_points: f32,
}

#if USE_PUSH_CONSTANTS == true
struct PushConstants {
viewport: Viewport,
display: Display,
}
var<push_constant> push_constants: PushConstants;
#else
@group(0) @binding(0)
var<uniform> viewport: Viewport;
@group(0) @binding(1)
var<uniform> display: Display;
#endif

@vertex
fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
var out: VertexOutput;

#if USE_PUSH_CONSTANTS == true
let viewport = push_constants.viewport;
#endif

out.position = (viewport.proj * vec4<f32>((vertex.position + instance.tile_position) * 32., 0., 1.)).xy;
out.vertex_position = out.position;
out.clip_position = vec4<f32>(out.position, 0., 1.);
return out;
}

@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
#if USE_PUSH_CONSTANTS == true
let display = push_constants.display;
#endif

if display.viewport_size_in_pixels.x == 0. || display.viewport_size_in_pixels.y == 0. {
discard;
}

var color: f32;
var alpha: f32;

let diff = abs(input.position - input.vertex_position) * (display.viewport_size_in_pixels / 2.);

let adjusted_outer_thickness = 1.001 * display.pixels_per_point;
let adjusted_inner_thickness = display.inner_thickness_in_points * adjusted_outer_thickness;

if diff.x < adjusted_outer_thickness + adjusted_inner_thickness || diff.y < adjusted_outer_thickness + adjusted_inner_thickness {
if diff.x < adjusted_inner_thickness || diff.y < adjusted_inner_thickness {
color = 0.1;
} else {
color = 0.7;
}
alpha = 0.25;
} else {
color = 0.;
alpha = 0.;
}

return vec4<f32>(color, color, color, alpha);
}
Loading

0 comments on commit 22d7155

Please sign in to comment.