- Nim 100%
| imgs | Adding diffuse light demo. | |
| src | Adding diffuse light demo. | |
| tests |
pos3d -> vec3
|
|
| .gitignore | Adding notes in PDF | |
| config.nims | Adding Ch2 example ray tracing | |
| copper.nimble | Removing dependencies. | |
| notes.pdf | Adding notes about PPM | |
| README.org | Adding notes about PPM | |
Computer graphics from scratch
Toy implementation of a ray tracing and a rasterizing in the Nim Programming Language. No external dependencies. Done just to learn about computer graphis.
Build
nimble build
the run ./bin/copper. Or execute both steps with
nimble run
The first time dependencies will be installed, so it will take longer. List all available tasks with
nimble tasks
Basics
Colors
Colors on a screen can be generated using a combination of red, green and blue. In a computer, each of this components is represented as a number. In the following implementation, a byte per component is used. The three bytes combined give a total of 2ドル^{24}$ colors. A color depth of 24. This format is known as 888. To reduce memory usage, 555 and 565 formats are sometimes used.
Colors can be thought as elements of a three dimentional vector space. As such, they can be added and multiplied by some scalar. As the values are bounded, to prevent overflows the values after each operation are clamped between 0 and 255.
Canvas
A canvas has pixels. It also has a dimention, although that may be infered from the pixel's data strucuture.
The only primitive needed is a putPixel(x, y, color), where x,y are the coordinates. In computer graphics, the coordinate systems used is centered at the origin. The conversion from an array style coordinate would be
\[p_{x} = \frac{w}{2} + a_{x}, p_{y} = \frac{h}{2} + a_{y}\]
where, $a_{i}$ are the "array coordinates" and $p_{i}$ the "pixel coordinates".
Creating primitives
Originally, I was goind to use treeform/pixie to manipulate images. In other examples, people used sdl or some other library. But I found a video from Tsoding, who used images inPPM Format because it is so simple it can be implemented in a few lines of code without any external library.
The whole specification is in the link, but the important parts are
- The header format is
<format> <width> <height> <max>. Spaces may be replaced with any whitespace.P6is the format for binary color images. And with 888 RGB images, a max value of 255 is enough. - The rest of the file's bytes are the pixels, three (or six if
maxif greater than 255) bytes per pixel. They will be rendered first from left to right, them from top to bottom, the same way a two dimentional array is layed in memory.
Using this format, is was simple enough to create custom RGB colors, a canvas, a putPixel primitive and a procedure to save and visualize images.
Raytracing
To transform a scene into an image, every section of the scene must be translated into a pixel. Modeling light dinamics is quite difficult in the general setting (photon mapping). A very simple alternative is to reverse the problem. Suppose rays of light come out of the viewer eyes. Then, simply find the nearest point in the scene intersected by the ray.
- Fix a view point.
-
For every pixel on the canvas
- Determine the nearest intersecting point corresponding the the pixel.
- Determine the color of that point.
- Paint the pixel with that color.
As a side note, all the pseudocode asumes a canvas and scene as global variables.
Perspective
The scene is a restricted portion of the 3D space. For now, suppose it is parallel to XY and a fixed distance 1 from the origin. So finding the correspondin point in the scene is just a scaling.
\[s_{x} = p_{x}\frac{s_{w}}{w}, s_{y} = p_{y}\frac{s_{h}}{h}, s_{z} = 1\]
where $p_{i}$ are the scene points. In a more code-like presentation
toSceneCoords(px, py, distance=1) {
return (px*scene.w/canvas.w, py*scene.h/canvas.h, distance)
}
Then, the raytracing steps can be rewritter as
- Fix a view point.
-
For every pixel on the canvas
- Determine the corresponding point on the scene.
- Determine the color seen through that scene point.
- Paint the pixel with that color.
For 1, lets fix the view point at the origin. 2.1 just consiste of applying toSceneCoords and 2.3 is just a call to putPixel. 2.2 still requires a little more math.
Intersections
A very simple form is a sphere. All points $p$ on a sphere satisfy that $\|p-c\|^{2} = r^{2},ドル where $c$ is the center of the sphere and $r$ is its radius. Rays are lines. All the points on a line satisfy $p = o + t(v - o)$ for some $t,ドル where $o, v$ are points on the line.
Finding an intersection of the line and sphere is finding a point that satisfies both equations. Replacing the second equation in the first one, we get
\begin{align*} \|o + t(v-o) - c\|^2 &= \|(o-c) + t(v-o)\|^2 \\ &= \|o-c\|^2 + 2t\|o-c\|\|v-o\| + t^2\|v-o\|^2 \\ &= r^2 \end{align*}that can be solver for $t$ using the quadratic equation. Finding the intersections as pseudocode would then be
findIntersections(sphere, origin, point) {
a = |point-origin|^2
b = 2 * |origin- sphere.center|*|point-origin|
c = |origin - sphere.center|^2 + sphere.radius^2
return findRoots(a, b, c)
}
Only positive intersection (intersections in front of the view point) should be considered. Then to find the color of the closest sphere (2.2)
findColor(origin, point) {
color = scene.backGroundColor
closest = inf
for sphere in scene {
intersenctions = findIntersections(sphere, origin, point)
for solution in intersections {
if 0 <= solution < closest {
closest = solution
color = sphere.color
}
}
}
return color
}
Final considerations
We have all the points program the raytracing. A more code like presentation of the steps would be
rayTrace(origin) { // step 1
for x in canvas.xrange { // step 2.0
for y in canvas.yrange { // step 2.0
point = toSceneCoords(x, y) // step 2.1
color = findColor(origin, point) // step 2.2
putPixel(x, y, color) // step 2.3
}
}
}