How to create a hex grid using shaders in Godot engine
Πως να δημιουργήσετε ένα εξαγωνικό πλέγμα χρησιμοποιώντας shaders στη μηχανή Godot
Godot is an advanced, open-source engine for developing 2D and 3D games. This article explores how to implement a hexagonal grid using custom shaders. A shader is a specialized program that processes data within the rendering pipeline. We will utilize fragment shaders (a.k.a. pixel shaders) to calculate the color and attributes of individual pixels.
Το Godot είναι μια προηγμένη μηχανή ανοιχτού κώδικα, για την ανάπτυξη παιχνιδιών 2Δ και 3Δ. Αυτό το άρθρο εξετάζει πώς να υλοποιήσετε ένα εξαγωνικό πλέγμα χρησιμοποιώντας προσαρμοσμένους κώδικες σκίασης (shaders). Ένας κώδικας σκίασης (shader) είναι ένα εξειδικευμένο πρόγραμμα που επεξεργάζεται δεδομένα εντός της ροής εργασιών απόδοσης γραφικών (rendering pipeline). Θα χρησιμοποιήσουμε κώδικες σκίασης τμημάτων, fragment shaders (γνωστοί και ως κώδικες σκιάσης εικονοστοιχείων, pixel shaders) για να υπολογίσουμε το χρώμα και τις ιδιότητες μεμονωμένων εικονοστοιχείων.
The implementation of the mathematical logic of hexagons in shader code is often considered a fairly complex challenge, but in reality, it is quite manageable. Understanding hex-based shaders starts with a shift in perspective: we have to move away from the square cartesian grid (x, y) and toward axial coordinates. In a normal grid, we move along axes at 90° angles. In a hex grid, those axes are at 60°. To make this work in code, we transform our standard orthogonal UV space into a skewed hexagonal grid.
Η υλοποίηση της μαθηματική λογικής των εξαγώνων, σε κώδικες σκίασης, θεωρείται συχνά μια αρκετά περίπλοκη πρόκληση αλλά στην πραγματικότητα είναι αρκετά διαχειρίσιμη. Η κατανόηση των shaders που βασίζονται σε εξάγωνα ξεκινά με μια αλλαγή προοπτικής: πρέπει να φύγουμε από το τετράγωνο καρτεσιανό πλέγμα (x, y) και να πάμε σε αξονικές συντεταγμένες (axial coordinates). Σε ένα κανονικό πλέγμα, κινούμαστε κατά μήκος αξόνων σε γωνίες 90°. Σε ένα εξαγωνικό πλέγμα, αυτοί οι άξονες βρίσκονται στις 60°. Για να λειτουργήσει αυτό στον κώδικα, μετασχηματίζουμε τον τυπικό ορθογώνιο χώρο UV σε ένα λοξό εξαγωνικό πλέγμα.
shader_type canvas_item;
const float grid_scale = 70.0;
const float line_thickness = 0.02;
const vec4 line_color = vec4(0.0, 0.0, 0.0, 1.0);
/************************************************************************
* Calculates a hexagonal grid mask. The math works by skewing the *
* coordinate system so that rectangular tiles align into a staggered *
* hexagonal pattern. I.e. we convert the (scaled) rectangular UV space *
* into a skewed axial hex coordinate system. *
************************************************************************/
float hex(vec2 p) {
// 1. Skew the X-axis to account for hex width
// This aligns the grid points to a 60-degree orientation.
// We replace sin(PI / 3.0) = sqrt(3)/2 with its literal value 0.86602540.
// Since shaders run every frame for every pixel,
// pre-calculating constants is a good optimization.
p.x /= 0.86602540; // sin(PI / 3.0) = sqrt(3)/2 = 0.86602540
// 2. Stagger every other column to create the 'honeycomb' offset
p.y += floor(p.x) * 0.5;
// 3. Mirror the coordinates within each cell (0.0 to 0.5)
// This allows us to calculate distance from the center easily.
p = abs(fract(p) - 0.5);
// 4. Calculate Hexagonal Distance Field
// 1.0 is the threshold for the edge.
// max(px * 1.5 + py, py * 2.0) defines the boundary of a hexagon.
float distance_to_edge = abs(1.0 - max(p.x * 1.5 + p.y, p.y * 2.0));
// 5. Anti-aliasing using Screen-Space Derivatives
// fwidth makes the lines look crisp regardless of zoom level or scale.
float edge_smoothing = fwidth(distance_to_edge);
// Return the grid line mask: 1.0 at the line, 0.0 inside the cell
return 1.0 - smoothstep(line_thickness - edge_smoothing, line_thickness + edge_smoothing, distance_to_edge);
}
void fragment() {
// 1. Sample the original texture
vec4 original_color = texture(TEXTURE, UV);
// 2. Setup UVs for the hex grid
vec2 grid_uv = UV;
// Automatic aspect ratio correction using built-in texture size
// TEXTURE_PIXEL_SIZE is (1/width, 1/height)
grid_uv.x *= TEXTURE_PIXEL_SIZE.y / TEXTURE_PIXEL_SIZE.x;
// Apply scale
grid_uv *= grid_scale;
// 3. Calculate the grid lines
float grid_intensity = hex(grid_uv);
// 4. Composite the lines over the texture
// We use line_color where grid is 1, original_color where grid is 0
// We multiply grid_intensity by line_color.a to allow for transparent lines
vec3 final_rgb = mix(original_color.rgb, line_color.rgb, grid_intensity * line_color.a);
// 5. Apply original alpha
COLOR = vec4(final_rgb, original_color.a);
}