rndr
A 3D engine in Rust that renders on the CPU. I wrote all of the vector and matrix math myself.
What it is
rndr is a small 3D engine in Rust. It draws triangles on the CPU and has simple physics: ray casting, collisions and rigid bodies.
I used no math library and did almost no research. I wrote the vectors, the 3×3 matrices and the matrix inverse myself. SDL2 only shows the final pixels in a window. Meshes load from STL files.
Coordinates and math
The world has forward, to the left and up. Angles are in degrees.
V3 is a 3D vector with the dot product, the cross product, the length and the element-wise (Hadamard) product. M3x3 stores its three columns, so a matrix times a vector is a sum of the columns:
The inverse comes from the cofactors and the determinant. If is smaller than the float epsilon, the matrix has no inverse and the caller skips the case:
Transforms
An object has a position and a rotation around , and . The rotation matrix applies roll, then pitch, then yaw:
where and are the sine and the cosine. A vertex of a mesh moves into the world with .
Camera
The camera has the same transform. Its rotation turns the base vectors into its forward , right and up vectors. These three vectors are the rows of the camera matrix. Thus one matrix product gives the coordinates of a point relative to the camera:
The perspective projection is a pinhole. The screen is at an offset from the pinhole, and similar triangles give the screen position:
is kept as the depth. If is smaller than 0.01, the division is skipped, because a very small makes lines millions of pixels long. If one vertex of a triangle is nearer than the near plane (), the whole triangle is rejected. There is no clipping.
Rasterization
The triangles of a mesh are projected and rasterized in parallel with Rayon. Each triangle returns a list of pixel writes, and the main thread applies them.
For a triangle on the screen, the rasterizer visits each pixel in the bounding box of the triangle, clamped to the screen. The area of a triangle comes from its corners:
splits into three smaller triangles. If is inside, their areas add up to the full area:
The same areas give the barycentric weights of :
The weights interpolate the position, the normal, the color and the depth of the three vertices, for example . The weights are calculated on the screen, so the interpolation is not perspective-correct. Large triangles near the camera show it.
The interpolated values go to the fragment shader. A shader is a trait with one method. It gets the position of the pixel in the world and in the mesh, and it changes the output depth and color. A new shader does not change the rasterizer.
The pixel buffer keeps a depth for each pixel. A write happens only if its depth is not larger than the stored depth.
Ray casting
A point on the triangle is . A point on the ray is . When they are equal, the result is a 3×3 linear system:
I invert the matrix and get , and in one product. The ray hits the triangle if , , and . With , the same interpolation as in the rasterizer gives the position and the normal of the hit.
For a sphere with center and radius , the condition is a quadratic in :
The normal at the hit is the direction from the center to the hit. A ray against the scene returns the hit with the smallest .
Collision detection
Mesh against mesh uses the ray cast. The center of a mesh is the average of its vertices. For each vertex , in parallel, a ray starts at in the direction of , with a maximum distance of . If the ray hits the other mesh before it gets to the vertex, part of the other mesh is inside this mesh. The hit is the contact point, and the inverse of the hit normal is the contact normal.
Two spheres touch if the distance between the centers is at most . The contact point is halfway between the centers.
Each pair of objects is checked once per tick, so the check is in the number of objects.
Rigid bodies
A rigid body has a mass , a diagonal inertia , a linear velocity and an angular velocity . Each tick is one explicit Euler step:
The angular velocity is added directly to the rotation angles.
For a collision with normal , each body has an offset from its center to the contact point. Its momentum at the contact point is
This splits into a normal part and a tangent part:
Friction uses the Coulomb model. If the tangent momentum is small, static friction stops it. Otherwise dynamic friction slows it:
The impulses exchange the normal momentum of the two bodies and apply the friction:
Each impulse changes the linear velocity, and its torque changes the angular velocity. The division by is element-wise:
A body can lock its movement or its rotation. A locked body ignores the matching change.
What was hard
The camera matrix. I tried to derive it without the internet and used brute force for a long time. The answer was simple: the rows of the matrix are the forward, right and up vectors of the camera.
That was a good lesson for my ego. I wanted to derive everything myself. Most knowledge is a compression that other people already did, and it is rational to use it.