This is a submission for the MLH x DEV Writing Challenge
What I Built
I didn't build one big app. I spent the last few months building a lot of small graphics programs in C/C++, mostly to understand what's going on underneath.
I'm a computer engineering student, and I picked graphics because mistakes are visible. A pointer bug in a console program gives you a vague crash. A wrong matrix in a graphics program gives you a cube that's inside out, stretched, or gone.
What I worked on:
- Native Windows apps with Win32, including setting up an OpenGL rendering context by hand
- FreeGLUT + OpenGL: primitives, cubes, pyramids, cylinders, grids
- Transformations, orthographic and perspective projection, and
gluLookAt() - Depth testing, double buffering, text rendering, and L-Systems
- My first steps into CUDA and OpenCL
I started wanting to draw shapes. I ended up wanting to know how they get on the screen.
It Starts With a Triangle
My first program looked like this:
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex3f(1.0f, -1.0f, 0.0f);
glEnd();
A Colored Triangle on a black window. Then the questions started.
Why is (0.5, 0.0, 0.0) on the right? I never told OpenGL my window size. By default, the visible range is -1 to 1 and gets mapped to the viewport. My numbers weren't pixels.
Why didn't my mouse click line up with my drawing? Win32 puts the origin at the top-left with Y going down. OpenGL's Y goes up:
float x = 2.0f * mouseX / windowWidth - 1.0f;
float y = -2.0f * mouseY / windowHeight + 1.0f; // flip Y
That's when I realized one point can have several coordinates depending on the space you ask in. A lot of graphics is moving between those spaces.
(glBegin/glEnd is legacy OpenGL. I started with it because it makes the concepts easy to see, not because it's how modern renderers work.)
Transformations Are Matrix Multiplication
glPushMatrix();
glTranslatef(2.0f, 0.0f, 0.0f);
glRotatef(angle, 0.0f, 1.0f, 0.0f);
drawCube();
glPopMatrix();
When I swapped translate and rotate, my cube started orbiting instead of spinning in place. Order matters because matrix multiplication isn't commutative. I knew that from class, but I'd never actually seen it.
Projection and Cameras: There Is No Camera
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (double)width / height, 0.1, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(0, 3, 8, 0, 0, 0, 0, 1, 0);
Orthographic projection keeps sizes constant. Perspective makes distant things smaller because the visible region is a frustum. If you forget the aspect ratio, everything looks squashed.
The biggest surprise: OpenGL doesn't have a camera. gluLookAt moves and rotates the whole world the opposite way so the eye sits at the origin. Moving the camera right and moving the world left are the same thing.
The Pipeline
My code turned out to be only the start of a pipeline: transform vertices, clip, map to the viewport, rasterize, depth test, write pixels.
Two settings made it real. My first 3D cube had back faces drawn over front faces until I added depth testing:
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glEnable(GL_DEPTH_TEST);
And double buffering (glutSwapBuffers()) stopped the flicker of seeing a frame drawn half-finished.
Doing the Win32 setup by hand (pixel format, wglCreateContext, wglMakeCurrent) showed me what FreeGLUT had been hiding. Abstraction stopped being just a word.
Loops Changed How I Think About Geometry
A grid is a loop, not dozens of typed vertices:
glBegin(GL_LINES);
for (int i = -10; i <= 10; ++i) {
glVertex3f(i, 0, -10); glVertex3f(i, 0, 10);
glVertex3f(-10, 0, i); glVertex3f( 10, 0, i);
}
glEnd();
Geometry became the output of a computation. Cylinders became sine and cosine. L-Systems pushed this further: rewrite a string with rules, then let a turtle draw it, where [ and ] push and pop state, just like the matrix stack. The string grows exponentially, so I hit performance limits fast.
Going Lower
My questions slowly moved from graphics to the machine. Why does calling glVertex3f per vertex feel wasteful? Where do my vertices actually live? At 60 FPS I get about 16.6 ms per frame, so everything has to fit in that.
Win32 and OpenGL taught me the same trade-off: higher-level APIs are faster to write, lower-level ones give control, and control comes with responsibility.
CPU to GPU
Rendering means doing the same operation on lots of independent data. A CPU loop handles it one element at a time:
for (int i = 0; i < n; ++i) c[i] = a[i] + b[i];
In CUDA, each element gets a thread:
__global__ void add(const float* a, const float* b, float* c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) c[i] = a[i] + b[i];
}
The loop disappeared and the index came from where the thread sits. I now ask "which parts are independent?" before "how do I write the loop?" CUDA also made memory transfers a real concern, since copying data can cost more than computing on it.
I've recently started on OpenCL, which targets more kinds of devices but needs a lot more setup. I'm still at the reading-and-getting-confused stage.
Demo
The demo shows primitives, coordinate spaces, transformations, orthographic vs perspective views, camera movement with gluLookAt, loop-generated patterns, simple 3D scenes, on-screen text, and an L-System growing each iteration.
The interesting part isn't the final image. It's flipping one line, like turning off depth testing or reversing a rotation, and knowing why the output changed.
Partner Technologies
This work was built with native C/C++, Win32, OpenGL/FreeGLUT, and early CUDA/OpenCL experiments. None of those are MLH partner technologies, and I didn't use any for this project.
Hackathon Experience
This was a slower kind of build than a typical hackathon project. No feature list or deadline, just following one question into the next. Still, the habit I'd take into any hackathon is the same: build something small, break it, and find out exactly why.
What's Next
Modern OpenGL with shaders and buffers, how GPUs actually execute work, profiling instead of guessing, and CUDA/OpenCL on a problem where parallelism really helps.
I used to think graphics was the part of programming that makes things look nice. Every layer I explored, from coordinates to matrices to the pipeline to the hardware, led to another layer underneath. Right now, it's the most practical way I've found to see where math, software, and hardware meet.
Top comments (0)