DEV Community

Atomi J.D.
Atomi J.D.

Posted on

Forget GPUs: Let's Render a Spinning 3D Cube in a Console

By day, I manage large-scale software projects for a Fortune 500 company. By night, I get back to my roots. After decades of programming in C (since 1990) and C++, I decided to build my own programming language from scratch, just for the fun of it: jdBasic.

This led me to a classic nerdy challenge: could I render a solid, spinning 3D object using only the characters in a standard console window?

Challenge accepted. The result is pure, real-time, ASCII-based 3D rendering:

This isn't a pre-recorded animation. It's a program calculating 3D transformations and perspective for every frame. And it's all running in jdBasic.



The Tool: What is jdBasic?

I built jdBasic to fuse the simplicity of classic BASIC with the power of modern programming paradigms. It’s a hobby, but a serious one, written in C++20. Think of it as a language where FOR...NEXT loops can coexist with:

  • APL-Inspired Array Programming: This is the secret sauce for the cube. You can perform math on entire matrices in a single line, no loops required.
  • A Built-in AI Engine: It has first-class Tensor objects and automatic differentiation for building and training neural networks.
  • Modern Features: It has Maps (dictionaries), TRY...CATCH error handling, and can even be extended with C++ DLLs.

You can explore the interpreter's source code, dive into the documentation, and see more examples over at the official GitHub repository: https://github.com/AtomiJD/jdBasic


Deconstructing the Cube

The magic behind the animation boils down to four key steps applied in every frame:

  1. Define Geometry: We start with two matrices: one for the 8 (X, Y, Z) vertices of the cube, and another defining the 6 faces by referencing the vertex indices.
  2. Apply Rotation: Standard trigonometry rotation matrices are used to calculate the new position of every vertex as the cube spins. This is done with a single MATMUL (Matrix Multiplication) operation.
  3. Project to 2D: We simulate perspective to make the 3D object look right on a 2D screen. The simple rule is: objects farther away appear smaller. This entire calculation is "vectorized"—performed on all 8 vertices at once without a single FOR loop.
  4. Use the Painter's Algorithm: To make the cube solid and hide rear faces, we calculate the average depth of each face, sort them from back to front, and draw them in that order. This ensures that closer faces are drawn on top of farther ones.

The Full Code

Here is the complete source code. The power of jdBasic's array-oriented functions makes it surprisingly compact.

' ==========================================================
' == Fully Optimized ASCII 3D Cube
' ==========================================================

' --- Configuration Section ---
SCREEN_WIDTH = 40
SCREEN_HEIGHT = 20

' --- 1. Define Vertices, Faces, and Transformations ---
DIM Vertices[8, 3]
Vertices = RESHAPE([-1,-1,-1,  1,-1,-1,  1, 1,-1, -1, 1,-1, -1,-1, 1,  1,-1, 1,  1, 1, 1, -1, 1, 1], [8, 3])

DIM Faces[6, 4]
Faces = RESHAPE([4,5,6,7, 0,3,2,1, 0,1,5,4, 2,3,7,6, 1,2,6,5, 3,0,4,7], [6, 4])

DIM FaceChars$[6]
FaceChars$ = ["#", "O", "+", "=", "*", "."]

DIM ScalingMatrix[3, 3]
ScalingMatrix = [[1,0,0], [0,2,0], [0,0,1]]

' --- 2. Main Animation Loop ---
DIM ScreenBuffer[SCREEN_HEIGHT, SCREEN_WIDTH]
DIM ProjectedPoints[8, 2]
AngleX = 0 : AngleY = 0
FocalLength = 5

' --- 3. Functions for Rotation and drawing---
FUNC CreateRotationX(angle)
    RETURN [[1, 0, 0], [0, COS(angle), -SIN(angle)], [0, SIN(angle), COS(angle)]]
ENDFUNC

FUNC CreateRotationY(angle)
    RETURN [[COS(angle), 0, SIN(angle)], [0, 1, 0], [-SIN(angle), 0, COS(angle)]]
ENDFUNC

SUB DrawLine(x1, y1, x2, y2, char$)
    dx = ABS(x2 - x1) : dy = -ABS(y2 - y1)
    sx = -1: IF x1 < x2 THEN sx = 1
    sy = -1: IF y1 < y2 THEN sy = 1
    err = dx + dy
    DO
        IF x1 >= 0 AND x1 < SCREEN_WIDTH AND y1 >= 0 AND y1 < SCREEN_HEIGHT THEN ScreenBuffer[y1, x1] = char$
        IF x1 = x2 AND y1 = y2 THEN EXITDO
        e2 = 2 * err
        IF e2 >= dy THEN err = err + dy : x1 = x1 + sx
        IF e2 <= dx THEN err = err + dx : y1 = y1 + sy
    LOOP
ENDSUB

SUB FillFace(points, char$)
    y_min = MAX([0, MIN(SLICE(points, 1, 1))])
    y_max = MIN([SCREEN_HEIGHT - 1, MAX(SLICE(points, 1, 1))])
    FOR y = y_min TO y_max
        intersections = []
        FOR i = 0 TO 3
            p1 = SLICE(points, 0, i)
            p2 = SLICE(points, 0, (i + 1) MOD 4)
            IF p1[1] <> p2[1] AND ((y >= p1[1] AND y < p2[1]) OR (y >= p2[1] AND y < p1[1])) THEN
                intersections = APPEND(intersections, p1[0] + (y - p1[1]) * (p2[0] - p1[0]) / (p2[1] - p1[1]))
            ENDIF
        NEXT i
        IF LEN(intersections) >= 2 THEN
            FOR x = MAX([0, MIN(intersections)]) TO MIN([SCREEN_WIDTH - 1, MAX(intersections)])
                ScreenBuffer[y, x] = char$
            NEXT x
        ENDIF
    NEXT y
ENDSUB

CURSOR FALSE

F = 0

DO
    ' --- a) Rotate & Scale Vertices ---
    CombinedTransform = MATMUL(ScalingMatrix, MATMUL(CreateRotationY(AngleY), CreateRotationX(AngleX)))
    RotatedVertices = MATMUL(Vertices, CombinedTransform)

    ' --- b) Vectorized Perspective Projection ---
    X_col = SLICE(RotatedVertices, 1, 0)
    Y_col = SLICE(RotatedVertices, 1, 1)
    Z_col = SLICE(RotatedVertices, 1, 2)
    Perspective = FocalLength / (FocalLength - Z_col)
    ScreenX = (X_col * Perspective) * (SCREEN_WIDTH / 8) + (SCREEN_WIDTH / 2)
    ScreenY = (Y_col * Perspective) * (SCREEN_HEIGHT / 8) + (SCREEN_HEIGHT / 2)
    ProjectedPoints = STACK(1, INT(ScreenX), INT(ScreenY))

    ' --- c) Vectorized Painter's Algorithm ---
    FaceZCoords = RESHAPE(Z_col[Faces], [6, 4])
    FaceDepths = SUM(FaceZCoords, 1) / 4
    DrawOrder = GRADE(FaceDepths)

    ' --- d) Draw Faces from Back to Front ---
    ScreenBuffer = RESHAPE([" "], [SCREEN_HEIGHT, SCREEN_WIDTH])
    FOR i = 0 TO 5
        face_idx = DrawOrder[i,0]
        v_indices = SLICE(Faces, 0, face_idx)
        face_x_coords = SLICE(ProjectedPoints, 1, 0)[v_indices]
        face_y_coords = SLICE(ProjectedPoints, 1, 1)[v_indices]
        face_points = STACK(1, face_x_coords, face_y_coords)

        FillFace face_points, FaceChars$[face_idx]
        FOR p = 0 TO 3
            p1 = SLICE(face_points, 0, p)
            p2 = SLICE(face_points, 0, (p + 1) MOD 4)
            DrawLine p1[0], p1[1], p2[0], p2[1], "*"
        NEXT p
    NEXT i

    ' --- e) Render and Update ---
    CLS
    PRINT "Fully Optimized 3D Cube (Press any key)"
    PRINT FRMV$(ScreenBuffer)
    YIELD
    AngleX = AngleX + 0.05 : AngleY = AngleY + 0.08
    F = F + 1
LOOP UNTIL F > 200

CURSOR TRUE
Enter fullscreen mode Exit fullscreen mode

Try It Yourself

You can run this code right now in your browser. No installation needed.

  1. Go to the jdBasic Web REPL: https://jdbasic.org/live/index.html
  2. Type EDIT and press Enter.
  3. Paste the entire code block from above into the editor.
  4. Click the "Save & Close" button.
  5. Type RUN and press Enter.

Enjoy the show! Happy hacking.

Top comments (0)