Hexagon TileMap Fog of War
This shader can be used to display a “fog of war” over a hexagonal (pointy-top, odd-r) TileMap. Add the below code to a ShaderMaterial on the tilemap itself (whose origin should be (0, 0)), and set the Hex Size uniform in the editor.
The shader uses a “visibility mask”: an image the same size as your TileMap, in cells. Each pixel controls the color that will be overlaid onto the corresponding cell (by cell index) in your TileMap. Since “fog of war” generally changes as the player interacts with the world, you’ll want to generate this texture in code, in most cases. Ensure that you set the “Visibility Mask” and “Visibility Mask Size”(size of the image) uniforms when the fog of war changes.
This shader could be adapted to 3.x, but TileMaps work very differently there, so it might be a bit of work. At the very least, you’ll need to change uniform sampler2D visibility_mask : filter_nearest;
to just uniform sampler2D visibility_mask;
and ensure the image you generate has nearest filtering set. world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0, 1.0)).xy;
in the vertex function needs to use WORLD_MATRIX
instead of MODEL_MATRIX
as well.
Shader code
shader_type canvas_item;
uniform vec2 hex_size;
uniform sampler2D visibility_mask : filter_nearest;
uniform vec2 visibility_mask_size;
varying vec2 world_pos;
const float godot_approx_sqrt3 = 1.75;
// Much of this is based on https://www.redblobgames.com/grids/hexagons/
ivec2 axial_to_oddr(ivec2 hex) {
int col = hex.x + (hex.y - (hex.y & 1)) / 2;
int row = hex.y;
return ivec2(col, row);
}
// From https://observablehq.com/@jrus/hexround
ivec2 axial_round(vec2 hex) {
float x_grid = round(hex.x);
float y_grid = round(hex.y);
float x_diff = hex.x - x_grid;
float y_diff = hex.y - y_grid;
float dx = round(x_diff + 0.5 * y_diff) * step(y_diff * y_diff, x_diff * x_diff);
float dy = round(y_diff + 0.5 * x_diff) * (1.0 - step(y_diff * y_diff, x_diff * x_diff));
return ivec2(int(x_grid + dx), int(y_grid + dy));
}
ivec2 pixel_to_hex(vec2 pixel) {
float hex_radius = hex_size.y / 2.0;
float q = (godot_approx_sqrt3 / 3.0 * pixel.x - 1.0 / 3.0 * pixel.y) / hex_radius;
float r = (2.0 / 3.0 * pixel.y) / hex_radius;
return axial_round(vec2(q, r));
}
vec4 visibility_from_cell(ivec2 cell) {
// Convert axial coordinates to odd-r coordinates
ivec2 oddr_cell = axial_to_oddr(cell);
// Normalize coordinates for visibility mask texture
vec2 clamped_coord = clamp(vec2(oddr_cell) / visibility_mask_size + 0.00001, vec2(0.0), vec2(1.0));
return texture(visibility_mask, clamped_coord);
}
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0, 1.0)).xy;
}
void fragment() {
COLOR = texture(TEXTURE, UV);
vec2 corrected_pos = world_pos - hex_size / 2.0;
ivec2 hex_cell = pixel_to_hex(corrected_pos);
COLOR *= visibility_from_cell(hex_cell);
}