Boson Engine

Publish Date

In real life, our eyes can see because of all the photons shooting into them. In a computer, we don't have that many resources to simulate an almost infinite number of photons radiated by a light source, let alone do so for tens of thousands of lights. Luckily, we do have a trick up our sleeve -- Since we only want photons that hit the camera, we can just simulate them; in other words, we can shoot rays out of the camera to produce the exactly same result. Voila! This does oddly resemble the early misconception of how eyes can see, though.

C++ Build

Just Some Sketches

In the beginning, a rough picture was painted, with me drawing inspiration from donut.c, which is a famous 3D torus renderer. From the start, I'd encountered numerous issues in C++, such as the obligatory duplicate object error and expensive copying. The idea was that we could shoot a ray from our virtual screen in the math model, apply a ray step vector to it, determine if it collided with something, and then get the dot product of the collision surface's normal and our sunlight vector as the illumination index, which sounded easy... I spent one hour debugging why every pixel returned the same illumination index, and it turned out to be a typecasting issue (you can never be more careful!)

Linear Algebra Recap

"Wait... stop right here! Give me some math for your renderer!" I know you are going to ask me this, so don't worry. In the renderer, three four-by-four matrices are used, respectively the view matrix, the model matrix, and the local matrix. Note that the local matrix has been evaluated into a vector. When we nudge our camera, the view matrix gets updated. With some basic multiplications, we can apply offsets with the W axis that is a constant, and apply scales with the diagonal elements.

Vf=[xoyxozxowxoxyyozyowyoxzoyzzowzoxwoywozww]ViV_f = \left[\begin{matrix} x & o_{yx} & o_{zx} & o_{wx} \\ o_{xy} & y & o_{zy} & o_{wy} \\ o_{xz} & o_{yz} & z & o_{wz} \\ o_{xw} & o_{yw} & o_{zw} & w \end{matrix}\right] \cdot V_i

For determining the collision, we can use a heuristic algorithm. Given a triangle consisting of three vertices v1,v2,v3v_1, v_2, v_3 and a ray position pp, such that

n1=(v1v)×(v2v)n2=(v2v)×(v3v)n3=(v3v)×(v1v)n_1 = (v_1 - v) \times (v_2 - v) \\ n_2 = (v_2 - v) \times (v_3 - v) \\ n_3 = (v_3 - v) \times (v_1 - v)

, when any of them are zero, or when n1^n2^||\hat{n_1} \cdot \hat{n_2}|| and n2^n3^||\hat{n_2} \cdot \hat{n_3}|| are both one, we can conclude that the ray has collided with the triangle. The next step is to calculate the illumination index, which is the dot product of the normal of the triangle and the sunlight vector. This is only valid in an "ideal" world, where the computer does not suffer from floating point errors. In reality, we have to use a threshold (denoted as ϵ\epsilon) to determine if a value is close enough to one or zero.

The Good Part

With a triangle successfully rendered, my goal was clear -- I wanted to render more triangles; hence, I jumped to work, but first thing first, I had to do some housekeeping! For one thing, matrices and vectors were all hardcoded with std structs which were not really readable. For another, everything was unorganized, crammed into a single main.cpp file that was starting to look like a tin filled with sardines. I split it into several files and added namespaces to create a more semantic environment. For linear algebra, with some TMP (Template Meta-Programming), matrices could come in different sizes without sacrificing performance as well as vectors. Now, what was left seemed like an ECS system waiting to be implemented...

Rust Build

As I looked forward to marking this project by an ECS system, little did I foresee the maintenance issues of my engine. Intertwined definitions and declarations everywhere, making it difficult to fix a certain issue without turning it into a whack-a-mole; thus, I thought about rewriting the engine. Coincidentally, I was learning about Rust, so I ported Boson to Rust. One thing particular in Rust is that I no longer have to worry about references/pointers anymore, since the compiler is smart enough to validate my code thanks to the lifetime concept. Another is that Rust feels like C, but in a more robust (not elegant) way. It's not like C++, which is a bunch of different libraries and backward-compatibility glued together; instead, it gives off a feeling similar to C -- concise and simple. These reasons propelled me to develop a version of Boson in Rust, which can be found here.