Last active
November 19, 2025 10:23
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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