Skip to content

Instantly share code, notes, and snippets.

@singularitti
Last active November 19, 2025 10:23
Show Gist options
  • Select an option

  • Save singularitti/68c274ac4e75042aaae9a7f03f65ce7f to your computer and use it in GitHub Desktop.

Select an option

Save singularitti/68c274ac4e75042aaae9a7f03f65ce7f to your computer and use it in GitHub Desktop.
Perspective rendering of M×N×P cube grids in Makie.jl #Julia #3D #visualization
using GLMakie
using GeometryBasics: Point3f, QuadFace, Mesh, Point3, Vec3
"""
axis_colored_cube_base(c_fb, c_tb, c_lr; size=1.0)
Build base geometry for a single cube whose three pairs of axis-aligned faces
share colors according to the Cartesian axes.
This is a low-level helper that constructs the vertex positions, face indices,
and per-vertex color data for one cube. The cube is axis-aligned, with edge
length `size`, and its corners spanning `[0, size]³` in local coordinates.
The three color arguments control the faces as follows:
- `c_fb`: front / back faces (±Y direction).
- `c_tb`: top / bottom faces (±Z direction).
- `c_lr`: left / right faces (±X direction).
Each face gets a uniform color by duplicating its four corner vertices so that
per-vertex colors can be used to emulate per-face colors.
# Arguments
- `c_fb`: color for the front/back faces (any `Makie`-compatible color, e.g. `:tomato`).
- `c_tb`: color for the top/bottom faces (e.g. `:steelblue`).
- `c_lr`: color for the left/right faces (e.g. `:seagreen`).
- `size::Real=1.0`: edge length of the cube in local coordinates.
# Returns
A tuple `(vertices, faces, colors)` where:
- `vertices::Vector{Point3f}`: vertex positions of the cube, with 4 distinct
vertices per face (24 total).
- `faces::Vector{QuadFace{Int}}`: quad faces referencing `vertices`.
- `colors::Vector`: per-vertex colors matching the length of `vertices`.
# Examples
```julia
julia> verts, faces, colors = axis_colored_cube_base(:tomato, :steelblue, :seagreen; size=1.0);
julia> length(verts), length(faces), length(colors)
(24, 6, 24)
```
# See also
[`cube_grid`](@ref).
# Implementation
To assign distinct colors to each face in `GLMakie.mesh`, the color buffer must
have the same length as the vertex buffer. GLMakie interprets `color` as
per-vertex colors when it is a vector, so a single “face color” can only be
achieved by giving the same color to all vertices of that face.
This function therefore duplicates the 4 vertices of each face, rather than
sharing vertices across faces. The resulting cube has 6 faces × 4 vertices =
24 vertices, with a matching 24-element color vector. This keeps the data
simple and makes it easy to translate or otherwise transform the entire cube
geometry without worrying about color indexing.
Internally, vertex coordinates are stored as `Point3f` (`Float32`) since Makie
uploads geometry to the GPU in single precision. Callers may freely work in
`Float64`; only the plotting layer needs `Float32`.
"""
function axis_colored_cube_base(c_fb, c_tb, c_lr; size::Real=1.0)
s = Float32(size)
base_verts = Point3f[]
faces = QuadFace{Int}[]
colors = Any[] # Makie will convert these to concrete color types
# helper: add one quad face with 4 vertices and a single color
function add_face!(p1::Point3f, p2::Point3f, p3::Point3f, p4::Point3f, col)
start = length(base_verts) + 1
push!(base_verts, p1, p2, p3, p4)
push!(faces, QuadFace(start, start + 1, start + 2, start + 3))
return append!(colors, (col for _ in 1:4)) # per-vertex colors
end
# ±Z faces (bottom/top) → c_tb
add_face!(Point3f(0, 0, 0), Point3f(s, 0, 0), Point3f(s, s, 0), Point3f(0, s, 0), c_tb) # bottom
add_face!(Point3f(0, 0, s), Point3f(s, 0, s), Point3f(s, s, s), Point3f(0, s, s), c_tb) # top
# ±Y faces (front/back) → c_fb
add_face!(Point3f(0, 0, 0), Point3f(s, 0, 0), Point3f(s, 0, s), Point3f(0, 0, s), c_fb) # front (y = 0)
add_face!(Point3f(0, s, 0), Point3f(s, s, 0), Point3f(s, s, s), Point3f(0, s, s), c_fb) # back (y = s)
# ±X faces (left/right) → c_lr
add_face!(Point3f(0, 0, 0), Point3f(0, s, 0), Point3f(0, s, s), Point3f(0, 0, s), c_lr) # left (x = 0)
add_face!(Point3f(s, 0, 0), Point3f(s, s, 0), Point3f(s, s, s), Point3f(s, 0, s), c_lr) # right (x = s)
return base_verts, faces, colors
end
"""
cube_grid(M, N, P;
cube_size=1.0,
gap=0.2,
color_front_back=:tomato,
color_top_bottom=:steelblue,
color_left_right=:seagreen,
azimuth=45,
elevation=45,
zoom=8.0)
Render an ``M \\times N \\times P`` grid of small cubes with axis-colored faces in a 3D Makie figure.
Construct a `Figure` containing an `LScene` which displays a regular Cartesian
grid of cubes. Each cube is rendered with three distinct color pairs aligned
with the Cartesian axes, using `axis_colored_cube_base` as the geometric
template.
The camera is positioned using spherical angles `azimuth` and `elevation`
around the center of the cube block, and `zoom` controls the distance of the
camera from the center.
# Arguments
- `M::Integer`: number of cubes along the X direction.
- `N::Integer`: number of cubes along the Y direction.
- `P::Integer`: number of cubes along the Z direction.
- `cube_size::Real=1.0`: edge length of each cube.
- `gap::Real=0.2`: spacing between adjacent cubes.
- `color_front_back=:tomato`: color for the ``\\pm Y`` faces of every cube.
- `color_top_bottom=:steelblue`: color for the ``\\pm Z`` faces of every cube.
- `color_left_right=:seagreen`: color for the ``\\pm X`` faces of every cube.
- `azimuth::Real=45`: azimuthal camera angle in degrees around the Z axis.
- `elevation::Real=45`: camera elevation angle in degrees above the XY plane.
- `zoom::Real=8.0`: distance of the camera eye from the center of the cube block.
# Returns
A `Figure` object containing a single `LScene` with the rendered cube grid.
# Examples
```julia
julia> fig = cube_grid(3, 3, 3);
julia> display(fig) # in the REPL or a notebook, this opens a 3D interactive view
```
Change the facet colors:
```julia
julia> fig = cube_grid(3, 3, 3;
color_front_back = :orange,
color_top_bottom = :rebeccapurple,
color_left_right = :goldenrod);
julia> display(fig)
```
Adjust camera orientation:
```julia
julia> fig = cube_grid(4, 4, 2;
azimuth = 30,
elevation = 70,
zoom = 12.0);
julia> display(fig)
```
# See also
[`axis_colored_cube_base`](@ref), [`mesh`](@ref), [`LScene`](@ref).
# Implementation
The grid is built in two stages:
1. `axis_colored_cube_base` precomputes the geometry and per-vertex colors for
a single cube at the origin, spanning `[0, cube_size]³`. Colors are stored
per vertex to satisfy GLMakie’s requirement that color buffers and vertex
buffers have the same length when using per-vertex colors.
2. `cube_grid` loops over all `(i, j, k)` indices in the `M × N × P` grid,
translates the base cube’s vertices by a vector
```julia
(i * (cube_size + gap),
j * (cube_size + gap),
k * (cube_size + gap))
```
and calls `mesh!` with the translated vertices and the precomputed faces and
colors. Each cube is therefore rendered as its own mesh, but all meshes
share the same face connectivity and color pattern.
Using explicit vertex translation instead of per-instance model matrices keeps
the code straightforward and avoids potential backend differences in how
instance transforms are handled, while still being perfectly adequate for
moderate grid sizes.
Internally, geometry is stored as `Point3f` (`Float32`), which is what Makie
uses on the GPU. Users are free to compute positions in `Float64` and cast at
the plotting boundary if desired.
# Extended help
The choice to duplicate vertices per face and to store colors per vertex is
driven directly by GLMakie’s rendering model:
- When `color` is a vector, it is interpreted as per-vertex colors, and its
length must match the number of vertices.
- There is no built-in notion of “per-face color” for polygonal meshes; that
behavior must be emulated by giving all vertices of a given face the same
color.
This implementation also separates the concerns of geometry construction
(`axis_colored_cube_base`) from scene assembly and camera control
(`cube_grid`). Callers who need more control can reuse the base cube data to
build different kinds of 3D visualizations (e.g. voxel fields, occupancy
grids, or custom per-cube color schemes) without changing the low-level
geometry logic.
"""
function cube_grid(
M,
N,
P;
cube_size=1.0,
gap=0.2,
color_front_back=:tomato,
color_top_bottom=:steelblue,
color_left_right=:seagreen,
azimuth=45.0,
elevation=45.0,
zoom=8.0,
)
fig = Figure(; size=(800, 600))
lscene = LScene(fig[1, 1]; show_axis=false)
s = lscene.scene
# base cube data: we will translate vertices for each instance
base_verts, faces, colors = axis_colored_cube_base(
color_front_back, color_top_bottom, color_left_right; size=cube_size
)
# place cubes on an M×N×P grid
for i in 0:(M - 1), j in 0:(N - 1), k in 0:(P - 1)
x = i * (cube_size + gap)
y = j * (cube_size + gap)
z = k * (cube_size + gap)
# translate base vertices to new position
verts = [Point3f(v[1] + x, v[2] + y, v[3] + z) for v in base_verts]
mesh!(lscene, Mesh(verts, faces); color=colors)
end
# camera control
az = deg2rad(azimuth)
el = deg2rad(elevation)
ex = zoom * cos(el) * cos(az)
ey = zoom * cos(el) * sin(az)
ez = zoom * sin(el)
eyepos = Point3(ex, ey, ez)
lookat = Point3((M - 1) / 2, (N - 1) / 2, (P - 1) / 2)
upvec = Vec3(0, 0, 1)
Makie.update_cam!(s, eyepos, lookat, upvec)
return fig
end
using GLMakie
using GeometryBasics: Rect, Point2
"""
square_grid(M, N;
block_size=1.0,
gap=0.2,
block_color=:lightgreen)
Render an `M × N` grid of square blocks in **pure 2D**, with all axes,
ticks, labels, and spines removed for a clean visualization surface.
# Arguments
- `M, N`:
Number of blocks along the X and Y directions.
- `block_size`:
Side length of each square block (default: `1.0`).
- `gap`:
Separation distance between neighboring blocks (default: `0.2`).
- `block_color`:
Color applied uniformly to all squares (default: `:lightgreen`).
# Description
This function creates a minimalist 2D layout that displays a rectangular
grid of square blocks. It uses a fully hidden Makie `Axis`, disabling
ticks, labels, spines, and grid lines for a clean drawing canvas.
Each square is constructed as a `Rect` placed in the XY-plane at positions
determined by `block_size` and `gap`.
This renderer is ideal for applications such as:
- layout visualization
- occupancy grids
- cellular automaton states
- discretized 2D domains
- block diagrams
- tile-based representations
# Returns
A Makie `Figure` containing the rendered 2D block grid.
"""
function square_grid(M, N;
block_size = 1.0,
gap = 0.2,
block_color = :lightgreen)
fig = Figure(size = (800, 600))
ax = Axis(fig[1, 1])
# Hide all axis decorations (modern Makie versions)
ax.xticksvisible[] = false
ax.yticksvisible[] = false
ax.xticklabelsvisible[] = false
ax.yticklabelsvisible[] = false
ax.xgridvisible[] = false
ax.ygridvisible[] = false
ax.xlabelvisible[] = false
ax.ylabelvisible[] = false
ax.titlevisible[] = false
ax.subtitlevisible[] = false
ax.leftspinevisible[] = false
ax.rightspinevisible[] = false
ax.topspinevisible[] = false
ax.bottomspinevisible[] = false
# Draw each square
for i in 0:M-1, j in 0:N-1
x = i * (block_size + gap)
y = j * (block_size + gap)
rect = Rect(Point2(x, y), block_size, block_size)
poly!(ax, rect, color = block_color)
end
return fig
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment