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);
}
Tags
fog of war, hex, hexagon, tilemap, visibility
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

Fog of war with alpha cut off as white color

Barycentric Hexagon for Spatial Nodes

Barycentric Hexagon

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments