Skip to content

Commit

Permalink
Use grid size based arithmetics for offsets in hex tile layout
Browse files Browse the repository at this point in the history
By refraining from using sqrt(3) based arithmetics we get the tiles
aligned to the provided textures and grid size. This is not
mathematically exact, but arguably what the user wants when providing
tiles of specific size.

The central realization here is that neighboring tiles on the "diagonal"
axis are always offset by a half tile size in one direction and a 3/4
tile size in the other. So, by providing tiles with sizes divisible by
4, you can get pixel perfect alignment of tiles.

The calculations are also quite a bit simpler this way.

See #456 for some further comments on this.
  • Loading branch information
ulfhermann committed Jan 2, 2024
1 parent b08a5d9 commit 759524f
Showing 9 changed files with 23 additions and 106 deletions.
67 changes: 17 additions & 50 deletions src/helpers/hex_grid/axial.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Code for the axial coordinate system.
use crate::helpers::hex_grid::consts::{DOUBLE_INV_SQRT_3, HALF_SQRT_3, INV_SQRT_3};
use crate::helpers::hex_grid::cube::{CubePos, FractionalCubePos};
use crate::helpers::hex_grid::neighbors::{
HexColDirection, HexDirection, HexRowDirection, HEX_OFFSETS,
@@ -9,7 +8,7 @@ use crate::helpers::hex_grid::offset::{ColEvenPos, ColOddPos, RowEvenPos, RowOdd
use crate::map::HexCoordSystem;
use crate::tiles::TilePos;
use crate::{TilemapGridSize, TilemapSize};
use bevy::math::{Mat2, Vec2};
use bevy::math::Vec2;
use std::ops::{Add, Mul, Sub};

/// A position in a hex grid labelled according to [`HexCoordSystem::Row`] or
@@ -184,34 +183,6 @@ impl From<ColEvenPos> for AxialPos {
}
}

/// The matrix for mapping from [`AxialPos`], to world position when hexes are arranged
/// in row format ("pointy top" per Red Blob Games). See
/// [Size and Spacing](https://www.redblobgames.com/grids/hexagons/#size-and-spacing)
/// at Red Blob Games for an interactive visual explanation, but note that:
/// 1) we consider increasing-y to be the same as "going up", while RBG considers increasing-y to be "going down",
/// 2) our vectors have magnitude 1 (in order to allow for easy scaling based on grid-size)
pub const ROW_BASIS: Mat2 = Mat2::from_cols(Vec2::new(1.0, 0.0), Vec2::new(0.5, HALF_SQRT_3));

/// The inverse of [`ROW_BASIS`].
pub const INV_ROW_BASIS: Mat2 = Mat2::from_cols(
Vec2::new(1.0, 0.0),
Vec2::new(-1.0 * INV_SQRT_3, DOUBLE_INV_SQRT_3),
);

/// The matrix for mapping from [`AxialPos`], to world position when hexes are arranged
/// in column format ("flat top" per Red Blob Games). See
/// [Size and Spacing](https://www.redblobgames.com/grids/hexagons/#size-and-spacing)
/// at Red Blob Games for an interactive visual explanation, but note that:
/// 1) we consider increasing-y to be the same as "going up", while RBG considers increasing-y to be "going down",
/// 2) our vectors have magnitude 1 (in order to allow for easy scaling based on grid-size)
pub const COL_BASIS: Mat2 = Mat2::from_cols(Vec2::new(HALF_SQRT_3, 0.5), Vec2::new(0.0, 1.0));

/// The inverse of [`COL_BASIS`].
pub const INV_COL_BASIS: Mat2 = Mat2::from_cols(
Vec2::new(DOUBLE_INV_SQRT_3, -1.0 * INV_SQRT_3),
Vec2::new(0.0, 1.0),
);

pub const UNIT_Q: AxialPos = AxialPos { q: 1, r: 0 };

pub const UNIT_R: AxialPos = AxialPos { q: 0, r: -1 };
@@ -238,14 +209,17 @@ impl AxialPos {
(*self - *other).magnitude()
}

/// Project a vector representing a fractional axial position (i.e. the components can be `f32`)
/// into world space.
/// Project a vector, representing a fractional axial position (i.e. the components can be `f32`)
/// on a row-oriented grid ("pointy top"), into world space.
///
/// This is a helper function for [`center_in_world_row`](`Self::center_in_world_row`),
/// [`corner_offset_in_world_row`](`Self::corner_offset_in_world_row`) and
/// [`corner_in_world_row`](`Self::corner_in_world_row`).
#[inline]
pub fn project_row(axial_pos: Vec2, grid_size: &TilemapGridSize) -> Vec2 {
let unscaled_pos = ROW_BASIS * axial_pos;
Vec2::new(
grid_size.x * unscaled_pos.x,
ROW_BASIS.y_axis.y * grid_size.y * unscaled_pos.y,
grid_size.x * (axial_pos.x + axial_pos.y / 2.0),
grid_size.y * axial_pos.y * 0.75,
)
}

@@ -297,10 +271,9 @@ impl AxialPos {
/// [`corner_in_world_col`](`Self::corner_in_world_col`).
#[inline]
pub fn project_col(axial_pos: Vec2, grid_size: &TilemapGridSize) -> Vec2 {
let unscaled_pos = COL_BASIS * axial_pos;
Vec2::new(
COL_BASIS.x_axis.x * grid_size.x * unscaled_pos.x,
grid_size.y * unscaled_pos.y,
grid_size.x * axial_pos.x * 0.75,
grid_size.y * (axial_pos.y + axial_pos.x / 2.0),
)
}

@@ -350,12 +323,9 @@ impl AxialPos {
/// * The world position corresponding to `[0.0, 0.0]` lies on the hex grid at index `(0, 0)`.
#[inline]
pub fn from_world_pos_row(world_pos: &Vec2, grid_size: &TilemapGridSize) -> AxialPos {
let normalized_world_pos = Vec2::new(
world_pos.x / grid_size.x,
world_pos.y / (ROW_BASIS.y_axis.y * grid_size.y),
);
let frac_pos = FractionalAxialPos::from(INV_ROW_BASIS * normalized_world_pos);
frac_pos.round()
let r = world_pos.y * 4.0 / grid_size.y / 3.0;
let q = world_pos.x / grid_size.x - r / 2.0;
FractionalAxialPos { q, r }.round()
}

/// Returns the axial position of the hex grid containing the given world position, assuming that:
@@ -364,12 +334,9 @@ impl AxialPos {
/// * The world position corresponding to `[0.0, 0.0]` lies on the hex grid at index `(0, 0)`.
#[inline]
pub fn from_world_pos_col(world_pos: &Vec2, grid_size: &TilemapGridSize) -> AxialPos {
let normalized_world_pos = Vec2::new(
world_pos.x / (COL_BASIS.x_axis.x * grid_size.x),
world_pos.y / grid_size.y,
);
let frac_pos = FractionalAxialPos::from(INV_COL_BASIS * normalized_world_pos);
frac_pos.round()
let q = world_pos.x * 4.0 / grid_size.x / 3.0;
let r = world_pos.y / grid_size.y - q / 2.0;
FractionalAxialPos { q, r }.round()
}

/// Try converting into a [`TilePos`].
13 changes: 0 additions & 13 deletions src/helpers/hex_grid/consts.rs

This file was deleted.

1 change: 0 additions & 1 deletion src/helpers/hex_grid/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod axial;
pub mod consts;
pub mod cube;
pub mod neighbors;
pub mod offset;
8 changes: 1 addition & 7 deletions src/render/shaders/column_even_hex.wgsl
Original file line number Diff line number Diff line change
@@ -5,13 +5,7 @@

// Gets the screen space coordinates of the bottom left of an isometric tile position.
fn hex_col_tile_pos_to_world_pos(pos: vec2<f32>, grid_width: f32, grid_height: f32) -> vec2<f32> {
let SQRT_3: f32 = 1.7320508;
let HALF_SQRT_3: f32 = 0.8660254;
let COL_BASIS_X: vec2<f32> = vec2<f32>(HALF_SQRT_3, 0.5);
let COL_BASIS_Y: vec2<f32> = vec2<f32>(0.0, 1.0);

let unscaled_pos = pos.x * COL_BASIS_X + pos.y * COL_BASIS_Y;
return vec2<f32>(COL_BASIS_X.x * grid_width * unscaled_pos.x, grid_height * unscaled_pos.y);
return vec2<f32>(grid_width * pos.x * 0.75, grid_height * (pos.y + pos.x / 2.0));
}

fn col_even_to_axial(offset_pos: vec2<f32>) -> vec2<f32> {
8 changes: 1 addition & 7 deletions src/render/shaders/column_hex.wgsl
Original file line number Diff line number Diff line change
@@ -5,13 +5,7 @@

// Gets the screen space coordinates of the bottom left of an isometric tile position.
fn hex_col_tile_pos_to_world_pos(pos: vec2<f32>, grid_width: f32, grid_height: f32) -> vec2<f32> {
let SQRT_3: f32 = 1.7320508;
let HALF_SQRT_3: f32 = 0.8660254;
let COL_BASIS_X: vec2<f32> = vec2<f32>(HALF_SQRT_3, 0.5);
let COL_BASIS_Y: vec2<f32> = vec2<f32>(0.0, 1.0);

let unscaled_pos = pos.x * COL_BASIS_X + pos.y * COL_BASIS_Y;
return vec2<f32>(COL_BASIS_X.x * grid_width * unscaled_pos.x, grid_height * unscaled_pos.y);
return vec2<f32>(grid_width * pos.x * 0.75, grid_height * (pos.y + pos.x / 2.0));
}

fn get_mesh(v_index: u32, vertex_position: vec3<f32>) -> MeshOutput {
8 changes: 1 addition & 7 deletions src/render/shaders/column_odd_hex.wgsl
Original file line number Diff line number Diff line change
@@ -6,13 +6,7 @@

// Gets the screen space coordinates of the bottom left of an isometric tile position.
fn hex_col_tile_pos_to_world_pos(pos: vec2<f32>, grid_width: f32, grid_height: f32) -> vec2<f32> {
let SQRT_3: f32 = 1.7320508;
let HALF_SQRT_3: f32 = 0.8660254;
let COL_BASIS_X: vec2<f32> = vec2<f32>(HALF_SQRT_3, 0.5);
let COL_BASIS_Y: vec2<f32> = vec2<f32>(0.0, 1.0);

let unscaled_pos = pos.x * COL_BASIS_X + pos.y * COL_BASIS_Y;
return vec2<f32>(COL_BASIS_X.x * grid_width * unscaled_pos.x, grid_height * unscaled_pos.y);
return vec2<f32>(grid_width * pos.x * 0.75, grid_height * (pos.y + pos.x / 2.0));
}

fn col_even_to_axial(offset_pos: vec2<f32>) -> vec2<f32> {
8 changes: 1 addition & 7 deletions src/render/shaders/row_even_hex.wgsl
Original file line number Diff line number Diff line change
@@ -5,13 +5,7 @@

// Gets the screen space coordinates of the bottom left of an isometric tile position.
fn hex_row_tile_pos_to_world_pos(pos: vec2<f32>, grid_width: f32, grid_height: f32) -> vec2<f32> {
let SQRT_3: f32 = 1.7320508;
let HALF_SQRT_3: f32 = 0.8660254;
let ROW_BASIS_X: vec2<f32> = vec2<f32>(1.0, 0.0);
let ROW_BASIS_Y: vec2<f32> = vec2<f32>(0.5, HALF_SQRT_3);

let unscaled_pos = pos.x * ROW_BASIS_X + pos.y * ROW_BASIS_Y;
return vec2<f32>(grid_width * unscaled_pos.x, ROW_BASIS_Y.y * grid_height * unscaled_pos.y);
return vec2<f32>(grid_width * (pos.x + pos.y / 2.0), grid_height * pos.y * 0.75);
}

fn row_even_to_axial(offset_pos: vec2<f32>) -> vec2<f32> {
8 changes: 1 addition & 7 deletions src/render/shaders/row_hex.wgsl
Original file line number Diff line number Diff line change
@@ -5,13 +5,7 @@

// Gets the screen space coordinates of the bottom left of an isometric tile position.
fn hex_row_tile_pos_to_world_pos(pos: vec2<f32>, grid_width: f32, grid_height: f32) -> vec2<f32> {
let SQRT_3: f32 = 1.7320508;
let HALF_SQRT_3: f32 = 0.8660254;
let ROW_BASIS_X: vec2<f32> = vec2<f32>(1.0, 0.0);
let ROW_BASIS_Y: vec2<f32> = vec2<f32>(0.5, HALF_SQRT_3);

let unscaled_pos = pos.x * ROW_BASIS_X + pos.y * ROW_BASIS_Y;
return vec2<f32>(grid_width * unscaled_pos.x, ROW_BASIS_Y.y * grid_height * unscaled_pos.y);
return vec2<f32>(grid_width * (pos.x + pos.y / 2.0), grid_height * pos.y * 0.75);
}

fn get_mesh(v_index: u32, vertex_position: vec3<f32>) -> MeshOutput {
8 changes: 1 addition & 7 deletions src/render/shaders/row_odd_hex.wgsl
Original file line number Diff line number Diff line change
@@ -5,13 +5,7 @@

// Gets the screen space coordinates of the bottom left of an isometric tile position.
fn hex_row_tile_pos_to_world_pos(pos: vec2<f32>, grid_width: f32, grid_height: f32) -> vec2<f32> {
let SQRT_3: f32 = 1.7320508;
let HALF_SQRT_3: f32 = 0.8660254;
let ROW_BASIS_X: vec2<f32> = vec2<f32>(1.0, 0.0);
let ROW_BASIS_Y: vec2<f32> = vec2<f32>(0.5, HALF_SQRT_3);

let unscaled_pos = pos.x * ROW_BASIS_X + pos.y * ROW_BASIS_Y;
return vec2<f32>(grid_width * unscaled_pos.x, ROW_BASIS_Y.y * grid_height * unscaled_pos.y);
return vec2<f32>(grid_width * (pos.x + pos.y / 2.0), grid_height * pos.y * 0.75);
}

fn row_odd_to_axial(offset_pos: vec2<f32>) -> vec2<f32> {

0 comments on commit 759524f

Please sign in to comment.