Building a Graphics Enginecreated: 2021.12.28. 18:19 · modified: 2021.12.30. 10:07 · license
Around the start of the fall semester, I suddenly became interested in computer graphics. I had a vague idea that objects were represented as triangular surfaces and that each triangle was projected onto a plane to determine which pixels to display. But I knew nothing about the geometry required for projection, or about textures and lighting. While looking for a resource that would help me learn these things as I built a graphics engine myself, I came across a YouTube tutorial that seemed perfect. It used no graphics libraries such as OpenGL; its only interface to the computer screen was a function that printed a character corresponding to an individual pixel at a specified position. The tutorial had four parts, covering everything from basic projective geometry to textures. It was exactly the guide I wanted, with one problem: the pixel-printing function belonged to the Windows console API and worked only on Windows. Since I use a MacBook, I needed to find a replacement. Xcode might have let me develop a console application with similar functionality, but with the semester about to begin, I wasn't up for digging into a new development tool. So I put the project on hold.
This week, after the semester ended, I returned to the project. Sitting down to work on it again gave me a new goal: this time, I would build a graphics engine using only what I already knew, without consulting any outside resources. Understanding the principles mattered more to me than optimizing performance, so I chose Python, which would let me turn ideas into code quickly. To draw pixels, I could simply create and manipulate a two-dimensional array of characters large enough to fill the console, then print it. I got straight to coding.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from time import sleep, time class XY: def __init__(self, x, y): self.x = x self.y = y WIDTH, HEIGHT = 100, 30 SCREEN_BASE = [[' ' for _ in range(WIDTH)] for _ in range(HEIGHT)] SCREEN = SCREEN_BASE.copy() def clear(): SCREEN = SCREEN_BASE.copy() def fill(xy): SCREEN[HEIGHT-1-xy.y][xy.x] = '▇' def render(pixels): clear() for pixel in pixels: fill(pixel) output = '
'.join(''.join(col for col in row) for row in SCREEN) print(output) for i in range(30): render([XY(3*i, i)]) sleep(0.1) | cs |
I had made a basic coordinate class and a drawing function. I tried updating the display about once every 0.1 seconds to draw a line with a slope of 1/3.
Okay. There were a couple of minor issues: even though I used the Unicode full block character, the pixels had different widths and heights, and there was spacing between the lines. I decided to leave that for later.
Now it was time to define a triangle and calculate the pixel positions for its vertices. First, I needed to work out the geometry of the projection.
I had a feeling there must be an elegant way to project things using transformation matrices, but I had long forgotten the introductory linear algebra I took at the end of my first year (not that I'd understood it back then, either), and I couldn't work it out from scratch. So I started by defining the coordinates of the camera and the point, the distance between the camera and the screen, and two vectors specifying the camera's viewing direction and tilt. I could find where the line through c and d intersected the screen plane, then calculate the pixel's distance from the center of the screen. From there, the vector from the center to the pixel and w would let me determine the pixel's angle relative to the screen's rightward direction. The calculations looked as though they would get messy, but I didn't mind as long as I got the result I needed.
Wait—how did the equation of a line in three dimensions go again? My original resolve not to look anything up vanished, and I quickly grew tired of trying to remember, so I checked Wikipedia. The moment I saw the equation, it all came back to me. Still, it was a bit of a shock that I couldn't immediately recall something so basic. Knowledge you don't use really does fade frighteningly fast. Just minutes after resuming the project, I realized where I stood and decided to study the geometry I needed, one step at a time.
Quaternions, Interpolation and Animation
To be continued.
Subscribe to my newsletter to receive an email when a new post is published.
Next: January 2022
Previous: Rome, My Dream