Generating triangular/hexagonal coordinates (xyz)
Categories:
Generating Triangular and Hexagonal Coordinates (XYZ)

Explore the mathematical foundations and practical implementations for converting between axial/cube coordinates and 3D Cartesian (XYZ) coordinates for triangular and hexagonal grids.
Hexagonal and triangular grids are fundamental structures in various fields, from game development and urban planning to scientific simulations. Unlike square grids, their unique geometry offers different adjacency rules and movement patterns. However, working with these grids often requires converting their native coordinate systems (like axial or cube coordinates) into standard 3D Cartesian (XYZ) coordinates for rendering, physics, or integration with other systems. This article delves into the mathematics behind these conversions and provides practical code examples.
Understanding Hexagonal Coordinate Systems
Hexagonal grids can be represented using several coordinate systems, each with its own advantages. The most common are axial coordinates (q, r) and cube coordinates (x, y, z). Cube coordinates are particularly elegant because they maintain the property that x + y + z = 0, simplifying many calculations. Axial coordinates are a projection of cube coordinates onto a 2D plane.
There are two primary orientations for hexagonal grids: 'pointy-top' and 'flat-top'. The choice of orientation affects the conversion formulas to XYZ. We will focus on the 'pointy-top' orientation, which is common in many applications.
graph TD A[Hexagonal Grid] --> B{Coordinate System?} B -->|Axial (q, r)| C[Axial to Cube Conversion] B -->|Cube (x, y, z)| D[Cube to XYZ Conversion] C --> D D --> E[3D Cartesian (XYZ)]
Flowchart of hexagonal coordinate conversion process.
Hexagonal Axial to Cube Conversion
Before converting to XYZ, it's often easiest to first convert axial coordinates (q, r) to cube coordinates (x, y, z). The relationship is straightforward:
x = q
z = r
y = -x - z
(due to the x + y + z = 0 invariant)
This conversion ensures that the cube coordinate system is consistent and simplifies subsequent calculations.
def axial_to_cube(q, r):
x = q
z = r
y = -x - z
return x, y, z
# Example usage:
cube_coords = axial_to_cube(1, 2) # q=1, r=2
print(f"Axial (1, 2) -> Cube {cube_coords}")
Python function for converting axial to cube coordinates.
Hexagonal Cube to XYZ Conversion (Pointy-Top)
Once you have cube coordinates (x, y, z), converting them to 3D Cartesian (XYZ) coordinates requires defining the size of your hex tiles. Let size
be the distance from the center of a hex to one of its vertices. The width of a hex is 2 * size
, and the height is sqrt(3) * size
.
For a pointy-top hex grid, the XYZ coordinates can be calculated as follows:
X = size * (sqrt(3) * x + sqrt(3)/2 * z)
Y = size * (3/2 * z)
Z = 0
(for a 2D grid, or a chosen height for 3D stacking)
Note that Y
here represents the vertical axis on the screen, and Z
would be depth if you were stacking hexes. If you're working in a typical 3D engine where Y is up, you might swap Y and Z or adjust accordingly. For simplicity, we'll assume a 2D plane embedded in 3D space, so Z is constant.
function hex_to_pixel_pointy_top(q, r, size) {
const x = q;
const z = r;
// y = -x - z; // Not directly used in this pixel conversion, but good to remember
const pixelX = size * (Math.sqrt(3) * x + Math.sqrt(3) / 2 * z);
const pixelY = size * (3 / 2 * z);
const pixelZ = 0; // Assuming a 2D plane for now
return { x: pixelX, y: pixelY, z: pixelZ };
}
// Example usage:
const hexSize = 10;
const pixelCoords = hex_to_pixel_pointy_top(1, 2, hexSize);
console.log(`Hex (1, 2) -> Pixel (${pixelCoords.x.toFixed(2)}, ${pixelCoords.y.toFixed(2)}, ${pixelCoords.z.toFixed(2)})`);
JavaScript function for converting pointy-top hex axial coordinates to XYZ pixels.
Generating Triangular Coordinates
Triangular grids are closely related to hexagonal grids. A hexagonal grid can be thought of as being composed of six equilateral triangles meeting at a central point. Conversely, a triangular grid can be formed by subdividing a hexagonal grid.
One common way to represent triangular grids is using barycentric coordinates or by adapting hexagonal coordinate systems. Each triangle in a grid can be uniquely identified by its orientation (up or down) and its position within a larger hexagonal or rhombic structure.
For simplicity, we can derive triangular coordinates from hexagonal coordinates. Each hex (q, r) can be split into six triangles. The center of the hex is (X, Y, Z) as calculated above. The vertices of the hex can then be found by adding offsets based on the size
and orientation. Each triangle would then be defined by the hex center and two adjacent vertices.
graph TD A[Hexagonal Grid] --> B{Subdivide Hex} B --> C1[Triangle 1] B --> C2[Triangle 2] B --> C3[Triangle 3] B --> C4[Triangle 4] B --> C5[Triangle 5] B --> C6[Triangle 6] C1 --> D[Triangular XYZ] C2 --> D C3 --> D C4 --> D C5 --> D C6 --> D
Conceptual flow for deriving triangular coordinates from hexagonal grids.
using System;
using UnityEngine; // Example for Unity, adjust for other engines
public static class HexGridUtils
{
public static Vector3 HexToWorld(int q, int r, float hexSize)
{
float x = hexSize * (Mathf.Sqrt(3) * q + Mathf.Sqrt(3) / 2 * r);
float y = hexSize * (3f / 2f * r);
float z = 0; // Assuming 2D plane, adjust for 3D stacking
return new Vector3(x, y, z);
}
// This function would return the 6 vertices of a hex, from which triangles can be formed.
public static Vector3[] GetHexVertices(int q, int r, float hexSize)
{
Vector3 center = HexToWorld(q, r, hexSize);
Vector3[] vertices = new Vector3[6];
for (int i = 0; i < 6; i++)
{
float angle_deg = 60 * i + 30; // Pointy-top hex starts with vertex at 30 degrees
float angle_rad = Mathf.PI / 180 * angle_deg;
vertices[i] = new Vector3(
center.x + hexSize * Mathf.Cos(angle_rad),
center.y + hexSize * Mathf.Sin(angle_rad),
center.z
);
}
return vertices;
}
// Example: Get the center of an 'up' triangle within a hex
public static Vector3 GetUpTriangleCenter(int q, int r, float hexSize, int triangleIndex)
{
Vector3 hexCenter = HexToWorld(q, r, hexSize);
Vector3[] hexVertices = GetHexVertices(q, r, hexSize);
// For a specific triangle (e.g., triangleIndex 0-5), calculate its centroid
// This is a simplified example, actual triangle indexing might vary
if (triangleIndex < 0 || triangleIndex >= 6) return Vector3.zero;
Vector3 v1 = hexVertices[triangleIndex];
Vector3 v2 = hexVertices[(triangleIndex + 1) % 6];
// Centroid of the triangle formed by hexCenter, v1, v2
return (hexCenter + v1 + v2) / 3f;
}
}
// Usage example (in a MonoBehaviour):
/*
public class HexGridTest : MonoBehaviour
{
public float hexSize = 1f;
void Start()
{
Vector3 worldPos = HexGridUtils.HexToWorld(0, 0, hexSize);
Debug.Log($"Hex (0,0) World Pos: {worldPos}");
Vector3[] vertices = HexGridUtils.GetHexVertices(0, 0, hexSize);
for (int i = 0; i < vertices.Length; i++)
{
Debug.Log($"Vertex {i}: {vertices[i]}");
}
Vector3 triCenter = HexGridUtils.GetUpTriangleCenter(0, 0, hexSize, 0);
Debug.Log($"Triangle 0 Center: {triCenter}");
}
}
*/
C# functions for converting hex coordinates to world space and deriving triangle centers.
Inverse Conversion: XYZ to Hex/Triangular Coordinates
Converting from XYZ back to hexagonal (q, r) or triangular coordinates is also crucial, especially for mouse picking or collision detection. This process typically involves reversing the steps:
- Pixel to Fractional Hex: Convert the XYZ point to a fractional (floating-point) axial or cube coordinate.
- Rounding: Round the fractional coordinates to the nearest integer hex coordinates.
For hexes, the rounding process for cube coordinates is particularly robust. After converting pixel (X, Y) to fractional cube (fx, fy, fz), you can round them using a specific algorithm that ensures round(fx) + round(fy) + round(fz) = 0
.
import math
def pixel_to_hex_pointy_top(pixelX, pixelY, size):
# Inverse of hex_to_pixel_pointy_top
q_float = (pixelX * math.sqrt(3)/3 - pixelY / 3) / size
r_float = (pixelY * 2/3) / size
# Convert fractional axial to fractional cube
x_float = q_float
z_float = r_float
y_float = -x_float - z_float
# Rounding to integer cube coordinates
rx = round(x_float)
ry = round(y_float)
rz = round(z_float)
x_diff = abs(rx - x_float)
y_diff = abs(ry - y_float)
z_diff = abs(rz - z_float)
if x_diff > y_diff and x_diff > z_diff:
rx = -ry - rz
elif y_diff > z_diff:
ry = -rx - rz
else:
rz = -rx - ry
# Convert back to axial if needed
return int(rx), int(rz) # q = x, r = z
# Example usage:
hexSize = 10
pixelX_test = 17.32 # Example pixel X
pixelY_test = 30.00 # Example pixel Y
hex_coords_back = pixel_to_hex_pointy_top(pixelX_test, pixelY_test, hexSize)
print(f"Pixel ({pixelX_test:.2f}, {pixelY_test:.2f}) -> Hex {hex_coords_back}")
Python function for converting XYZ pixels back to pointy-top hex axial coordinates.