Skip to content

Instantly share code, notes, and snippets.

@jonas1ara
Last active March 9, 2026 05:44
Show Gist options
  • Select an option

  • Save jonas1ara/a768aebb37fe7131636438423366cdd9 to your computer and use it in GitHub Desktop.

Select an option

Save jonas1ara/a768aebb37fe7131636438423366cdd9 to your computer and use it in GitHub Desktop.
Cube in F#
open System
let mutable A, B, C = 0.0, 0.0, 0.0
let mutable cubeWidth = 20.0
let width, height = 160, 44
let zBuffer = Array.create (width * height) 0.0
let buffer = Array.create (width * height) ' '
let backgroundASCIICode = '.'
let distanceFromCam = 100.0
let mutable horizontalOffset = 0.0
let K1 = 40.0
let incrementSpeed = 0.6
let mutable x, y, z = 0.0, 0.0, 0.0
let mutable ooz = 0.0
let mutable xp, yp, idx = 0, 0, 0
let calculateX i j k =
j * (sin A) * (sin B) * (cos C) - k * (cos A) * (sin B) * (cos C) +
j * (cos A) * (sin C) + k * (sin A) * (sin C) + i * (cos B) * (cos C)
let calculateY i j k =
j * (cos A) * (cos C) + k * (sin A) * (cos C) -
j * (sin A) * (sin B) * (sin C) + k * (cos A) * (sin B) * (sin C) -
i * (cos B) * (sin C)
let calculateZ i j k =
k * (cos A) * (cos B) - j * (sin A) * (cos B) + i * (sin B)
let calculateForSurface cubeX cubeY cubeZ ch =
x <- calculateX cubeX cubeY cubeZ
y <- calculateY cubeX cubeY cubeZ
z <- calculateZ cubeX cubeY cubeZ + distanceFromCam
ooz <- 1.0 / z
xp <- int (float width / 2.0 + horizontalOffset + K1 * ooz * x * 2.0)
yp <- int (float height / 2.0 + K1 * ooz * y)
idx <- xp + yp * width
if idx >= 0 && idx < width * height then
if ooz > zBuffer.[idx] then
zBuffer.[idx] <- ooz
buffer.[idx] <- ch
let drawFrame () =
buffer.[0 .. width * height - 1] <- Array.init (width * height) (fun _ -> backgroundASCIICode)
Array.fill zBuffer 0 (width * height) 0.0
cubeWidth <- 20.0
horizontalOffset <- -2.0 * cubeWidth
for cubeX in -cubeWidth .. incrementSpeed .. cubeWidth do
for cubeY in -cubeWidth .. incrementSpeed .. cubeWidth do
calculateForSurface cubeX cubeY -cubeWidth '@'
calculateForSurface cubeWidth cubeY cubeX '$'
calculateForSurface -cubeWidth cubeY -cubeX '~'
calculateForSurface -cubeX cubeY cubeWidth '#'
calculateForSurface cubeX -cubeWidth -cubeY ';'
calculateForSurface cubeX cubeWidth cubeY '+'
cubeWidth <- 10.0
horizontalOffset <- 1.0 * cubeWidth
for cubeX in -cubeWidth .. incrementSpeed .. cubeWidth do
for cubeY in -cubeWidth .. incrementSpeed .. cubeWidth do
calculateForSurface cubeX cubeY -cubeWidth '@'
calculateForSurface cubeWidth cubeY cubeX '$'
calculateForSurface -cubeWidth cubeY -cubeX '~'
calculateForSurface -cubeX cubeY cubeWidth '#'
calculateForSurface cubeX -cubeWidth -cubeY ';'
calculateForSurface cubeX cubeWidth cubeY '+'
cubeWidth <- 5.0
horizontalOffset <- 8.0 * cubeWidth
for cubeX in -cubeWidth .. incrementSpeed .. cubeWidth do
for cubeY in -cubeWidth .. incrementSpeed .. cubeWidth do
calculateForSurface cubeX cubeY -cubeWidth '@'
calculateForSurface cubeWidth cubeY cubeX '$'
calculateForSurface -cubeWidth cubeY -cubeX '~'
calculateForSurface -cubeX cubeY cubeWidth '#'
calculateForSurface cubeX -cubeWidth -cubeY ';'
calculateForSurface cubeX cubeWidth cubeY '+'
Console.SetCursorPosition(0, 0)
for k in 0 .. width * height - 1 do
match k % width with
| 0 -> Console.WriteLine()
| _ -> Console.Write(buffer.[k])
A <- A + 0.05
B <- B + 0.05
C <- C + 0.01
System.Threading.Thread.Sleep(10)
while true do
drawFrame()
@jonas1ara
Copy link
Author

jonas1ara commented Mar 6, 2026

Cube

Cube

ASCII 3D cube renderer written in F#. It draws three spinning cubes in real time using Euler rotations, perspective projection, and a z-buffer.

How it works

1. 3D Rotation (Euler matrices)

Each point (i, j, k) of the cube is rotated in 3D space using three Euler angles: A (X-axis), B (Y-axis), and C (Z-axis). The combined rotation produces the transformed coordinates:

x' =  j·sin(A)·sin(B)·cos(C)  −  k·cos(A)·sin(B)·cos(C)
    + j·cos(A)·sin(C)          +  k·sin(A)·sin(C)
    + i·cos(B)·cos(C)

y' =  j·cos(A)·cos(C)          +  k·sin(A)·cos(C)
    − j·sin(A)·sin(B)·sin(C)  +  k·cos(A)·sin(B)·sin(C)
    − i·cos(B)·sin(C)

z' =  k·cos(A)·cos(B)  −  j·sin(A)·cos(B)  +  i·sin(B)

This corresponds to the matrix multiplication Rz(C) · Ry(B) · Rx(A) applied to the vector (i, j, k).

let calculateX i j k =
    j * (sin A) * (sin B) * (cos C) - k * (cos A) * (sin B) * (cos C) +
    j * (cos A) * (sin C) + k * (sin A) * (sin C) + i * (cos B) * (cos C)

2. Perspective Projection

Once the point is rotated, distanceFromCam is added to z' to push it away from the camera. Perspective projection then converts 3D coordinates to 2D screen pixels using:

ooz = 1 / z              (inverse of depth)

xp = width/2  + offset + K1 · ooz · x' · 2
yp = height/2           + K1 · ooz · y'

K1 acts as the focal length: the larger it is, the more zoom is applied. Multiplying by ooz (instead of dividing by z) produces the perspective effect — objects further away appear smaller.

ooz <- 1.0 / z
xp <- int (float width / 2.0 + horizontalOffset + K1 * ooz * x * 2.0)
yp <- int (float height / 2.0 + K1 * ooz * y)

3. Z-buffer (depth sorting)

To ensure closer faces occlude farther ones, a z-buffer is used: an array parallel to the character buffer that stores the highest ooz (inverse of z) seen so far for each pixel. Since ooz is larger for closer points, a character is only written if it exceeds the stored value.

if ooz > zBuffer.[idx] then
    zBuffer.[idx] <- ooz
    buffer.[idx] <- ch

4. The Six Faces

Each face is generated by iterating over a 2D grid and fixing the third coordinate to the cube's extreme value (±cubeWidth). Each face is assigned a distinct ASCII character for visual distinction:

Face Fixed coordinate Character
Front cubeZ = −cubeWidth @
Right cubeX = +cubeWidth $
Left cubeX = −cubeWidth ~
Back cubeZ = +cubeWidth #
Bottom cubeY = −cubeWidth ;
Top cubeY = +cubeWidth +

5. Animation

At the end of each frame the angles are incremented slightly, producing continuous rotation:

A <- A + 0.05   // X-axis rotation
B <- B + 0.05   // Y-axis rotation
C <- C + 0.01   // Z-axis rotation

All three cubes (sizes 20, 10, and 5) are rendered into the same shared buffer, separated horizontally via horizontalOffset.

Credits

This F# implementation is a port of the original C version of the ASCII 3D cube renderer created by tarantino07.
All credit for the original idea, structure, and mathematical approach goes to the author of:

https://github.com/tarantino07/cube.c

@WillEhrendreich
Copy link

Brilliant. Love it.

@jonas1ara
Copy link
Author

jonas1ara commented Mar 9, 2026

Thanks friend, it's fun to disconnect from work and play with F#.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment