In [computer graphics](graphics.md) 3D rendering is the process of computing images which represent a projected view of 3D objects through a virtual camera.
There are many methods and [algorithms](algorithm.md) for doing so differing in many aspects such as computation complexity, implementation complexity, realism of the result, representation of the 3D data, limitations of viewing and so on. If you are just interested in the [realtime](realtime.md) 3D rendering used in [gaymes](game.md) nowadays, you are probably interested in [GPU](gpu.md)-accelerated 3D [rasterization](rasterization.md) with [APIs](api.md) such as [OpenGL](opengl.md) and [Vulkan](vulkan.md).
As most existing 3D "[frameworks](framework.md)" are [harmful](harmful.md), a [LRS](lrs.md) programmer is likely to write his own 3D rendering system that suits his program best, therefore we should list some common methods of achieving 3D. Besides that, it's just pretty interesting to see what there is in the store.
A very important realization of a graphics programmer is that **3D rendering is to a great extent about [faking](cheating.md)** (especially the mainstream realtime 3D) -- it is an endeavor that seeks to produce something that looks somehow familiar to HUMAN sight specifically and so even though the methods are mathematical, the endeavor is really an [art](art.md) in the end, not dissimilar to that of a magician who searches for "smoke and mirrors" [hacks](hacking.md) to produce illusions for the audience. Reality is infinitely complex, we use nothing else but [approximations](approximation.md) and neglecting that rely on assumptions about human sight such as "60 FPS looks like smooth movement to human eye", "infrared spectrum is invisible", "humans can't tell a mirror reflection is a bit off", "inner corners are usually darker than flat surfaces", "no shadow is completely black because light scatters in the atmosphere" etc. Really 3D graphics is nothing but searching for what looks [good enough](good_enough.md), and deciding this relies on a SUBJECTIVE judgement of a human (and sometimes every individual). In theory -- if we had infinitely powerful computers -- we would just program in a few lines of electromagnetic equations and run the precise simulation of light propagating in 3D environment to produce an absolutely realistic result, but though some methods try to come close to said approach, we simply won't ever have infinitely powerful computers. For this we have to resort to a bit more [ugly](ugly.md) approach of identifying specific notable real-life phenomena individually (for example [caustics](caustic.md), [Fresnel](fresnel.md), mirror reflections, refractions, [subsurface scattering](subsurface_scattering.md), metallicity, [noise](noise.md), [motion blur](motion_blur.md) and myriads of others) and addressing each one individually with special treatment, many times correcting and masking our imperfections (e.g. applying [antialiasing](antialiasing.md) because we dared to use a simplified model of light sampling, applying texture filtering because we dared to only use finite amount of memory for our data, applying [postprocessing](postprocessing.md) etc.).
**Rendering spectrum**: The book *Real-Time Rendering* mentions that methods for 3D rendering can be seen as lying on a spectrum, one extreme of which is *appearance reproduction* and the other *physics simulation*. Methods closer to trying to imitate the appearance try to simply focus on imitating the look of an object on the monitor that the actual 3D object would have in real life, without being concerned with *how* that look arises in real life (i.e. closer to the "faking" approach mentioned above) -- these may e.g. use image data such as photographs; these methods may rely on lightfields, photo [textures](texture.md) etc. The physics simulation methods try to replicate the behavior of light in real life -- their main goal is to solve the **[rendering equation](rendering_equation.md)**, still only more or less [approximately](approximation.md) -- and so, through internally imitating the same processes, come to similar visual results that arise in real world: these methods rely on creating 3D geometry (e.g. that made of triangles or voxels), computing light reflections and [global illumination](global_illumination.md). This is often easier to program but more computationally demanding. Most methods lie somewhere in between these two extremes: for example [billboards](billboard.md) and [particle systems](particle_system.md) may use a texture to represent an object while at the same time using 3D quads (very simple 3D models) to correctly deform the textures by perspective and solve their visibility. The classic polygonal 3D models are also usually somewhere in between: the 3D geometry and [shading](shading.md) are trying to simulate the physics, but e.g. a photo texture mapped on such 3D model is the opposite appearance-based approach ([PBR](pbr.md) further tries to shift the use of textures more towards the *physics simulation* end).
With this said, let's not take a look at possible **classifications** of 3D rendering methods. As seen, there are many ways:
- by **order**:
- **object order**: The method iterates on objects and draws object by object, one after another. This results in pixels being drawn to "random" places on the screen and possibly already drawn pixels being [overdrawn](overdraw.md) with new pixels (though this can be further reduced). Typically requires a [frame buffer](frame_buffer.md) and [double buffering](double_buffering.md), often also [z-buffer](z_buffer.md) (or [sorting](sorting.md)), i.e. requires a lot of memory. This method is also a bit ugly but typically also faster than the alternative, so it is prevailing nowadays.
- **image order**: The method iterates on screen pixels, typically going pixel by pixel from left to right, top to bottom, deciding the color of each pixel independently. May be easier to program and require less memory (no frame buffer is needed, see e.g. [frameless rendering](frameless.md)), however though [parallelism](parallelism.md) is applicable here (many pixels may potentially be independently computed in parallel, speeding up rendering), the algorithms used (e.g. [path tracing](path_tracing.md)) often have to expensively simulate light behavior and so performance is still an issue.
- by **speed**:
- **[realtime](realtime.md)**: Able to render at interactive [FPS](fps.md), typically used in [games](game.md) etc.
- **[offline](offline.md)**: Spends a lot of time (even many minutes) on rendering each frame with the goal to produce output of extreme quality, typically used to render 3D movies etc.
- by **relative limitation**:
- **primitive/"pseudo3D"/2.5D/...**: Older methods that produce 3D views but had great limitations e.g. in camera degrees of freedom or possible environment geometry that was usually limited to a "2D sector map" (see e.g. [Doom](doom.md)).
- **full/"true" 3D**: The "new" way of 3D rendering that allows freely rotating camera, arbitrary 3D geometry etc. Though this still has limitations (as any computer approximation of reality), many people just call this the "true" 3D.
- by **approach** (sides of above mentioned rendering spectrum):
- **appearance based**: Focuses on achieving desired appearance by any means necessary, faking, "cheating", not trying to stay physically correct. This is typically faster.
- **[physics](physics.md) simulation** (see also [physically based rendering](pbr.md)): Focuses on simulating the underlying physics of reality with high correctness so that we also get a very realistic result.
- by **main method/algorithm** (see also the table below):
- **rasterization**: Appearance based object order methods further based on a relatively simple algorithm capable of drawing (rasterizing) a simple geometric shape (such as a triangle) which we then use to draw the whole complex 3D scene (composed of great many of triangles).
- **ray casting/tracing**: Physics simulation image order methods further based on tracing paths of light in a manner that's closer to reality.
- ...
- by **3D data** ([vector](vector.md) vs [raster](raster.md) classification applies here just as in 2D graphics):
- **triangle meshes** (vector, and other boundary representations)
- **[voxels](voxel.md)** (raster, and potentially other volumetric representations)
- **[software rendering](sw_rendering.md)**: Rendering only with [CPU](cpu.md). This is typically slower as a CPU mostly performs sequential computation, eliminating the possible parallelism optimization, however the approach is more [KISS](kiss.md) and [portable](portablity.md).
- **[GPU](gpu.md) accelerated**: Making use of specialized graphics rendering hardware (GPU) that typically uses heavy parallelism to immensely speed up rendering. While this is the mainstream, extremely fast way of rendering, it is also greatly [bloated](bloat.md) while often being an [overkill](overkill.md) that greatly complicates programming and makes programs less [portable](portability.md), less [future proof](future_proof.md) etc.
- **[hybrids](hybrid.md)**: Methods may be combined and/or lie in between different extremes, for example we may see a rasterizer 3D renderer that uses ray tracing to add detail (shadows, reflections, ...) to the scene, we may see renderers that allow triangle meshes as well as voxels etc. { One nice hybrid looking engine is e.g. [Chasm: The Rift](chasm_the_rift.md). ~drummyfish }
Finally a table of some common 3D rendering methods follows, including the most simple, most advanced and some unconventional ones. Note that here we talk about methods and techniques rather than algorithms, i.e. general approaches that are often modified and combined into a specific rendering algorithm. For example the traditional triangle rasterization is sometimes combined with raytracing to add e.g. realistic reflections. The methods may also be further enriched with features such as [texturing](texture.md), [antialiasing](antialiasing.md) and so on. The table below should help you choose the base 3D rendering method for your specific program.
- *2.5D*: primitive 3D, often called [pseudo 3D](pseudo_3d.md) or fake 3D, having significant limitations e.g. in degrees of freedom of the camera
- *off*: slow method usually used for offline (non-realtime) rendering (even though they indeed may run in real time e.g. with the help of powerful GPUs)
- *IO* vs *OO*: [image order](image_order.md) (rendering by pixels) vs [object order](object_order.md) (rendering by objects)
|[triangle rasterization](rasterization.md)|*OO*, traditional in GPUs |
|[voxel space rendering](voxel_space.md) |*OO 2.5D*, e.g. Comanche |
|[wireframe rendering](wireframe.md) |*OO*, just lines |
TODO: Rescue On Fractalus!
TODO: find out how build engine/slab6 voxel rendering worked and possibly add it here (from http://advsys.net/ken/voxlap.htm seems to be based on raycasting)
TODO: VoxelQuest has some innovative voxel rendering, check it out (https://www.voxelquest.com/news/how-does-voxel-quest-work-now-august-2015-update)
If you're a complete noob and are asking what the essence of 3D is or just how to render simple 3Dish pictures for your game without needing a [PhD](phd.md), here's the very basics. Yes, you can use some 3D engine such as [Godot](godot.md) that has all the 3D rendering preprogrammed, but you you'll surrender to [bloat](bloat.md), you won't really know what's going on and your ability to tinker with the rendering or optimizing it will be basically zero... AND you'll miss on all the [fun](fun.md) :) So here we just foreshadow some concepts you should start with if you want to program your own 3D rendering.
The absolute basic thing in 3D is probably **[perspective](perspective.md)**, or the concept which says that "things further away look smaller". This is basically the number one thing you need to know and with which you can make simple 3D pictures, even though there are many more effects and concepts that "make pictures look 3D" and which you can potentially study later (lighting, shadows, [focus and blur](depth_of_field.md), [stereoscopy](stereo.md), [parallax](parallax.md), visibility/obstruction etc.). { It's probably possible to make something akin "3D" even without perspective, just with [orthographic](ortho.md) projection, but that's just getting to details now. Let's just suppose we need perspective. ~drummyfish }
If you don't have rotating camera and other fancy things, perspective is actually mathematically very simple, you basically just **divide the object's size by its distance from the viewer**, i.e. its Z coordinate (you may divide by some multiple of Z coordinate, e.g. by 2 * Z to get different [field of view](fov.md)) -- the further away it is, the bigger number its size gets divided by so the smaller it becomes. This "dividing by distance" ultimately applies to all distances, so in the end even the details on the object get scaled according to their individual distance, but as a first approximation you may just consider scaling objects as a whole. Just keep in mind you should only draw objects whose Z coordinate is above some threshold (usually called a *near plane*) so that you don't divide by 0! With this "dividing by distance" trick you can make an extremely simple "3Dish" renderer that just draws [sprites](sprite.md) on the screen and scales them according to the perspective rules (e.g. some space simulator where the sprites are balls representing planets). There is one more thing you'll need to handle: **[visibility](visibility.md)**, i.e. nearer objects have to cover the further away objects -- you can do this by simply [sorting](sorting.md) the objects by distance and drawing them back-to-front ([painter's algorithm](painters_algorithm.md)).
Here is some "simple" [C](c.md) code that demonstrates perspective and draws a basic animated wireframe cuboid as ASCII in terminal:
```
#include <stdio.h>
#define SCREEN_W 50 // ASCII screen width
#define SCREEN_H 22 // ASCII screen height
#define LINE_POINTS 64 // how many points for drawing a line
#define FOV 8 // affects "field of view"
#define FRAMES 30 // how many animation frames to draw
char screen[SCREEN_W * SCREEN_H];
void showScreen(void)
{
for (int y = 0; y <SCREEN_H;++y)
{
for (int x = 0; x <SCREEN_W;++x)
putchar(screen[y * SCREEN_W + x]);
putchar('\n');
}
}
void clearScreen(void)
{
for (int i = 0; i <SCREEN_W*SCREEN_H;++i)
screen[i] = ' ';
}
// Draws point to 2D ASCII screen, [0,0] means center.
int drawPoint2D(int x, int y, char c)
{
x = SCREEN_W / 2 + x;
y = SCREEN_H / 2 + y;
if (x >= 0 && x <SCREEN_W&&y>= 0 && y <= SCREEN_H)
screen[y * SCREEN_W + x] = c;
}
// Divides coord. by distance taking "FOV" into account => perspective.
int perspective(int coord, int distance)
{
return (FOV * coord) / distance;
}
void drawPoint3D(int x, int y, int z, char c)
{
if (z <= 0)
return; // at or beyond camera, don't draw
drawPoint2D(perspective(x,z),perspective(y,z),c);
}
int interpolate(int a, int b, int n)
{
return a + ((b - a) * n) / LINE_POINTS;
}
void drawLine3D(int x1, int y1, int z1, int x2, int y2, int z2, char c)
{
for (int i = 0; i <LINE_POINTS;++i)//drawafewpointstoformaline
**PRO TIP**: It also help if you learn a bit about [photography](photo.md) because 3D usually tries to simulate [cameras](camera.md) and 3D programmers adopt many terms and concepts from photography. At least learn the very basics such as [focal length](focal_length.md), [pinhole camera](pinhole_camera.md), the "exposure triangle" (shutter speed, [aperture](aperture.md), [ISO](iso.md)) etc. You should know how focal length is related to [FOV](fov.md), what the "f number" means, how to use exposure settings to increase or decrease things like [motion blur](motion_blur.md) and [depth of field](depth_of_field.md), what [HDR](hdr.md) means etc.
You may have come here just to learn about the typical realtime 3D rendering used in today's [games](game.md) because aside from research and niche areas this kind of 3D is what we normally deal with in practice. This is what this section is about.
Nowadays "game 3D" means a [GPU](gpu.md) accelerated 3D [rasterization](rasterization.md) done with rendering [APIs](api.md) such as [OpenGL](opengl.md), [Vulkan](vulkan.md), [Direct3D](d3d.md) or [Metal](metal.md) (the last two being [proprietary](proprietary.md) and therefore [shit](shit.md)) and higher level engines above them, e.g. [Godot](godot.md), [OpenSceneGraph](osg.md) etc. The methods seem to be evolving to some kind of rasterization/[pathtracing](pathtracing.md) hybrid, but rasterization is still the basis.
This mainstream rendering uses an [object order](object_order.md) approach (it blits 3D objects onto the screen rather than determining each pixel's color separately) and works on the principle of **triangle rasterization**, i.e. 3D models are composed of triangles (or higher polygons which are however eventually broken down into triangles) and these triangles are projected onto the screen according to the position of the virtual camera and laws of [perspective](perspective.md). Projecting the triangles means finding the 2D screen coordinates of each of the triangle's three vertices -- once we have thee coordinates, we draw (rasterize) the triangle to the screen just as a "normal" 2D triangle (well, with some asterisks).
Furthermore things such as [z-buffering](z_buffer.md) (for determining correct overlap of triangles) and [double buffering](double_buffering.md) are used, which makes this approach very memory ([RAM](ram.md)/[VRAM](vram.md)) expensive -- of course mainstream computers have more than enough memory but smaller computers (e.g. [embedded](embedded.md)) may suffer and be unable to handle this kind of rendering. Thankfully it is possible to adapt and imitate this kind of rendering even on "small" computers -- even those that don't have a GPU, i.e. with pure [software rendering](sw_rendering.md). For this we e.g. replace z-buffering with [painter's algorithm](painters_algorithm.md) (triangle sorting), drop features like [perspective correction](perspective_correction.md), [MIP mapping](mip_mapping.md) etc. (of course quality of the output will go down).
Also additionally there's a lot of [bloat](bloat.md) added in such as complex [screen space](screen_space.md) shaders, [pathtracing](pathtracing.md) (popularly known as *raytracing*), [megatexturing](megatexturing.md), [shadow rendering](shadow.md), [postprocessing](postprocessing.md), [compute shaders](compute_shader.md) etc. This may make it difficult to get into "modern" 3D rendering. Remember to [keep it simple](kiss.md).
On PCs the whole rendering process is hardware-accelerated with a [GPU](gpu.md) (graphics card). GPU is a special hardware capable of performing many operations in [parallel](parallelism.md) (as opposed to a [CPU](cpu.md) which mostly computes sequentially with low level of parallelism) -- this is ideal for graphics because we can for example perform mapping and drawing of many triangles at once, greatly increasing the speed of rendering ([FPS](fps.md)). However this hugely increases the [complexity](complexity.md) of the whole rendering system, we have to have a special [API](api.md) and [drivers](driver.md) for communication with the GPU and we have to upload data (3D models, textures, ...) to the GPU before we want to render them. [Debugging](debugging.md) gets a lot more difficult. So again, this is [bloat](bloat.md), consider avoiding GPUs.
GPUs nowadays are no longer just focusing on graphics, but are kind of a general device that can be used for more than just 3D rendering (e.g. [crypto](crypto.md) mining, training [AI](ai.md) etc.) and can no longer even perform 3D rendering completely by themselves -- for this they have to be programmed. I.e. if we want to use a GPU for rendering, not only do we need a GPU but also some extra code. This code is provided by "systems" such as [OpenGL](opengl.md) or [Vulkan](vulkan.md) which consist of an [API](api.md) (an [interface](interface.md) we use from a [programming language](programming_language.md)) and the underlying implementation in a form of a [driver](driver.md) (e.g. [Mesa3D](mesa3d.md)). Any such rendering system has its own architecture and details of how it works, so we have to study it a bit if we want to use it.
The important part of a system such as OpenGL is its **rendering [pipeline](pipeline.md)**. Pipeline is the "path" through which data go through the rendering process. Each rendering system and even potentially each of its version may have a slightly different pipeline (but generally all mainstream pipelines somehow achieve rasterizing triangles, the difference is in details of how they achieve it). The pipeline consists of **stages** that follow one after another (e.g. the mentioned mapping of vertices and drawing of triangles constitute separate stages). A very important fact is that some (not all) of these stages are programmable with so called **[shaders](shader.md)**. A shader is a program written in a special language (e.g. [GLSL](glsl.md) for OpenGL) running on the GPU that processes the data in some stage of the pipeline (therefore we distinguish different types of shaders based on at which part of the pipeline they reside). In early GPUs stages were not programmable but they became so as to give a greater flexibility -- shaders allow us to implement all kinds of effects that would otherwise be impossible.
Let's see what a typical pipeline might look like, similarly to something we might see e.g. in OpenGL. We normally simulate such a pipeline also in [software renderers](sw_rendering.md). Note that the details such as the coordinate system [handedness](handedness.md) and presence, order, naming or programmability of different stages will differ in any particular pipeline, this is just one possible scenario:
1. Vertex data (e.g. 3D [model space](model_space.md) coordinates of triangle vertices of a 3D model) are taken from a vertex buffer (a GPU memory to which the data have been uploaded).
2.**Stage: [vertex shader](vertex_shader.md)**: Each vertex is processed with a vertex shader, i.e. one vertex goes into the shader and one vertex (processed) goes out. Here the shader typically maps the vertex 3D coordinates to the screen 2D coordinates (or [normalized device coordinates](ndc.md)) by:
- multiplying the vertex by a [model matrix](model_matrix.md) (transforms from [model space](model_space.md) to [world space](world_space.md), i.e. applies the model move/rotate/scale operations)
- multiplying by [view matrix](view_matrix.md) (transforms from [world space](world_space.md) to [camera space](camera_space.md), i.e. takes into account camera position and rotation)
- multiplying by [projection matrix](projection_matrix.md) (applies perspective, transforms from [camera space](camera_space.md) to [screen space](screen_space.md) in [homogeneous coordinates](homogeneous_coordinates.md))
3. Possible optional stages that follow are [tessellation](tessellation.md) and geometry processing ([tessellation shaders](tessellation_shader.md) and [geometry shader](geometry_shader.md)). These offer possibility of advanced vertex processing (e.g. generation of extra vertices which vertex shaders are unable to do).
4.**Stage: vertex post processing**: Usually not programmable (no shaders here). Here the GPU does things such as [clipping](clipping.md) (handling vertices outside the screen space), primitive assembly and [perspective divide](perspective_divide.md) (transforming from [homogeneous coordinates](homogeneous coordinates.md) to traditional [cartesian coordinates](cartesian_coordinates.md)).
5.**Stage: [rasterization](rasterization.md)**: Usually not programmable, the GPU here turns triangles into actual [pixels](pixel.md) (or [fragments](fragment.md)), possibly applying [backface culling](backface_culling.md), [perspective correction](perspective_correction.md) and things like [stencil test](stencil_test.md) and [depth test](dept_test.md) (even though if fragment shaders are allowed to modify depth, this may be postpones to later).
6.**Stage: pixel/fragment processing**: Each pixel (fragment) produced by rasterization is processed here by a [pixel/fragment shader](fragment_shader.md). The shader is passed the pixel/fragment along with its coordinates, depth and possibly other attributes, and outputs a processed pixel/fragment with a specific color. Typically here we perform [shading](shading.md) and [texturing](texture.md) (pixel/fragment shaders can access texture data which are again stored in texture buffers on the GPU).
7. Now the pixels are written to the output buffer which will be shown on screen. This can potentially be preceded by other operations such as depth tests, as mentioned above.