How Do Computers Render?
A computer is just an array of pixels (or a small square that holds a single color). For example, an array of 1280 pixels wide by 720 pixels tall would make 921,600 individual pixels that each store a color!
Color is typically represented by a (Red, Green Blue) component, and when working with images, it could also have a transparency component known as (Alpha) with 24 (RGB) or 32 (RGBA) bits. In both cases, 8 bits are used to store each component.
So each component can be from 0 to 255! (i.e. Red = [255, 0, 0])
Software Rendering
One method of drawing images to a screen is through software rendering. With this method, each pixel is individually set and calculated by the CPU. This means that all rotations, scaling, and translations, along with lighting, and other features need to be done pixel by pixel. As a result, software rendering is much less popular in the games industry because it is extremely slow due to the CPU. However, the PyGame library uses it, and it works amazingly for small games!
In the past, games were made with software rendering such as Doom! The 3D calculations were done on each block and each pixel. Quite an impressive feat!
Hardware Rendering
But now most complex applications use hardware for rendering! This utilizes the GPU and graphics drivers to do most of the heavy lifting.
Graphics APIs
Each computer and graphics driver is different, and thus the graphics driver manufacturer (i.e. NVIDIA) defines rendering to the screen. Regardless, they all use the same commands to draw objects to the screen. Most Windows computers use DirectX, Mac users use Metal, and APIs such as Vulkan and OpenGL are made to be cross-platform, meaning that they can run on most operating systems.
Vertex Buffers
The GPU also has memory, where you can store data, which can be positions, colors, texture coordinates, normals, etc.
The first step in rendering is to tell the GPU what to draw. This is done by specifying data in buffers, and telling the GPU to use that buffer to render. A Vertex Buffer (VBO) stores each individual vertex to be drawn (position, color, texture coordinates, etc). A set of vertices can create a "primitive" shape that the GPU can draw. So for example, a triangle (the most used primitive) requires 3 vertices.
Meshes
Triangles are used to construct almost everything. For example, a rectangle can be constructed from 2 triangles next to each other! A cube needs 12 triangles (2 triangles per face x 6 faces)! A series of these primitives (almost always triangles) is known as a mesh, and it defines the shape. Check out these rabbit meshes with varying degrees of realism!
Shaders
Unfortunately, rendering is more complicated than just that. The GPU itself does not know what to do with the data from the vertex buffers. Special programs called "Shaders" run on the GPU taking in that data, and telling the GPU what to do with it (i.e. drawing to the screen).
Shaders can also take in "parameters" (uniforms in OpenGL) that can alter the output of the shader.
Vertex Shaders
Vertex shaders are relatively simple. They convert the vertices (usually position with other data) to screen coordinates which are usually -1 to 1 in both dimensions. Take a look at the example below.
Depending on the screen resolution, the triangle will look differently, because the coordinates are normalized.
Fragment Shaders
Fragment shaders run per pixel on screen. They take in the interpolated output of the vertex shader (position & normals for lighting calculations, texture coordinates for textures, etc). Through lighting or any other calculations, the color of the pixel can be outputted, and the vertex will be drawn to the screen.
Textures & Texture Coordinates
To draw a texture (image) in 3D graphics, the texture must be first loaded from the CPU memory and stored in the GPU memory as a texture.
In the Vertex Buffer, the coordinates of the texture must be specified per vertex. However the value must be normalized from (0-1), meaning that the top right pixel would be (1, 1) and the bottom left (0, 0) in OpenGL as seen below.
The fragment shader can be supplied with a handle to the texture in GPU memory, and using the interpolated texture coordinates, a pixel by pixel texture can be drawn.
Rendering Techniques
Batch Rendering
If the mesh type is the same (all textures, all cubes, all circles etc), it is much more efficient to render all of the meshes in 1 draw call than to render each of them individually. As a result, 1 large mesh (batch) is created to store all of the smaller meshes, and rendered each frame in 1 draw call! Extremely efficient for voxel engines (like Minecraft) or 2D games (all 2D textures)!
Cameras & Projections
At its core, graphics programming is just using math (matrices and vectors) to manipulate and render data on screen. Thus to represent a world, there are 2 types of projections: the simple 2D orthographic projection, and the 3D perspective projection.
As in the image, the perspective projection draws things that are farther away smaller, while orthographic projections completely ignore depth and the 3D look.
The camera position, and look at matrix must also be defined (Don't worry about this! This is linear algebra which you will likely encounter at university)! With all of this, a projection matrix is created (for orthographic vs perspective) and a view matrix is created (for camera positions, angles, etc)
VSync
Most computers have a fixed refresh rate (typically running at 60 frames / second), meaning that the game does not need to render at high FPS such as 100. Therefore, by enabling VSync, the game will by default synchronize with the video / screen at that fixed FPS.
Final Thoughts
Graphics programming is a completely separate field from pure game programming. Yet, it can be extremely rewarding to see cool 3D lighting in scenes that you programmed from scratch!
If you are ever interested in learning 3D graphics, OpenGL is highly recommend for beginners. This webiste Learn OpenGL does a marvelous job at teaching OpenGL and core graphics fundamentals, with projects and scenes that look extremely realistic!