Scene description
A structured scene file defines cameras, lights, geometry, transforms, and material parameters for the renderer.
Mission entry / 04
Developed as part of the 42 curriculum, miniRT implements core rendering concepts from scratch including ray-object intersections, transformations, lighting models, shadows, camera systems, and scene parsing. The project became a deep exploration of computational graphics, linear algebra, debugging methodology, and systems-level programming.


B.01
miniRT is a low-level ray tracing project focused on building a rendering pipeline from first principles. The page currently uses the IMU dossier as a structural template while reserving space for deeper rendering notes, diagrams, and visual debugging artifacts.
The final dossier can describe how scene descriptions become parsed objects, how rays are generated through camera space, and how intersections, lighting, shadows, and material decisions produce the final frame.
C.01
A structured scene file defines cameras, lights, geometry, transforms, and material parameters for the renderer.
The C parser converts text records into validated scene objects while catching malformed input before render time.
Camera parameters are transformed into primary rays, mapping each pixel into a direction through the virtual scene.
Rays are tested against supported primitives to determine closest hits, normals, and geometric visibility.
The renderer evaluates lighting, shadows, and color contribution before writing the final pixel output to the viewport.
D.01
Math is hard. I love math, but it is hard. It's even HARDER in C.
Creating a function to invert a 4x4 matrix, in C, using no external libraries, and ensuring every function is 25 lines long or shorter was perhaps one of the most frustrating things I've ever done in programming.
It would have MASSIVELY helped to take a step back in the beginning and think about how to model a 4x4 matrix in C.
The solution? Not a 16 long float array. It is also not a 4x4 2d array. For us, the cleverest solution was to reuse our vector type and then model matrices as an array of 4 vectors, allowing us to repurpose many of the vector operation functions when writing this huge function.
Cylinders are evil. Spheres lull you into a false sense of confidence because the math is elegant and symmetrical and everything feels almost reasonable. Then cylinders arrive and suddenly your renderer becomes a psychological experiment.
Infinite cylinders are one thing. Finite cylinders are another. Cylinder caps are another problem entirely. The commit messages read:
trynna fix cyl
followed shortly afterwards by:
ok cylinders work but smth still weird with shadows and weird circles on planes
and eventually:
something is very fucked
The worst bug occurred when cylinder caps and planes incorrectly intersected while occupying the same level in the world: rays hit the wrong object, shadows appeared where they shouldn't, and surfaces flickered between states depending on camera angle.
The eventual fix required reasoning about intersection sorting, treating caps as separate surfaces, and accounting for floating-point precision.
The eventual victory commit was appropriately subtle and restrained:
Fixed incorrect display of cylinder caps when coinciding with planes. FUCK YEAH WOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
A beautiful moment in software engineering.
At some point during development I discovered that my raytracer fundamentally disagreed with the concept of "looking upwards".
Looking horizontally? Fine.
Looking slightly upwards? Fine.
Looking directly up into the sky?
Immediate segfault.
For weeks there existed a genuinely hilarious state where the camera could look down from above, but if you attempted to look upwards from below the plane of the world, the renderer would implode while simultaneously displaying shadows that physically should not exist.
One commit message captured the emotional state perfectly:
you can't look up from below it will segfault when you look at the sky. also when you look up from below the plane you can see FUCKING SHADOWS! WHY ARE THEY THERE! FIGURE IT OUT.
The root cause ended up being the camera's orientation system and the infamous "up vector" problem. When the camera points perfectly vertically, many naïve camera implementations break because the cross products used to construct the camera basis collapse into degenerate cases.
In simple terms: my renderer literally lost its sense of direction when forced to look straight up.
The eventual solution required rebuilding the camera orientation logic to properly handle vertical edge cases and dynamically choose stable perpendicular vectors instead of assuming the world always has a nice obvious "up".
The final commit felt deeply satisfying:
camera can now point in all directions, including up and down, and render correctly
A sentence that sounds obvious until you've spent multiple evenings watching your renderer crash because somebody dared to observe the sky.
Planes are deceptive.
Mathematically, planes are extremely simple objects. In practice, they became one of the most frustrating parts of the entire renderer because they forced me to truly understand coordinate systems, transformations, and normals instead of just pretending I did.
Initially, planes only worked when aligned nicely with the world axes. The moment rotation entered the picture, reality collapsed.
One particularly painful discovery:
planes were in fact not working happy when rotated anything beyond their original way.
At another point I discovered that normals were effectively being transformed incorrectly twice:
okay fixed a double transform in world_normal_at function
which explained why lighting behaved like the renderer had suffered a concussion.
This entire section of the project forced me to properly understand why normals cannot simply be transformed like ordinary points or vectors, why inverse transpose matrices exist, and why graphics programmers become emotionally attached to linear algebra textbooks.
There was also the subtle issue of ensuring surfaces faced the correct side relative to both the eye and the light source. If this logic breaks, lighting suddenly appears on the wrong side of surfaces, shadows invert, and your renderer begins hallucinating.
Eventually the geometry system stabilised:
Planes now fully functional and normals work correctly, plane is always facing the side of the light + eye.
An incredibly satisfying sentence after days of staring at mathematically impossible shadows.
There is a very specific type of suffering unique to graphics programming where something looks wrong, but mathematically everything appears correct.
Shadows became that experience for me.
At first it was exciting:
SHADOWS WORKING
Then reality arrived.
Objects would shadow themselves. Tiny floating point inaccuracies caused rays to intersect surfaces they had literally just left. Random dark artifacts appeared on planes. Strange circular patterns emerged from nowhere. Looking at scenes from certain angles produced completely impossible lighting behaviour.
The most frustrating aspect was that these bugs often looked supernatural. Nothing crashes. Nothing throws errors. The renderer simply gaslights you visually.
You spend hours questioning your understanding of geometry, only to discover the issue was a tiny floating point comparison buried deep inside an intersection test.
Getting pixels onto the screen was only half the battle. The second half was teaching the renderer to understand an actual scene file instead of hardcoded objects.
This became its own engineering challenge entirely.
At first the strategy was basically brute force desperation:
try to just use the one function to get it from the file to the screen no loop no nothing just hardcode for 1
which honestly was the correct decision.
Because once parsing entered the project, everything became interconnected:
vectors colours matrices object types validation linked lists memory management transformations
A single malformed line in a scene file could silently corrupt the renderer in spectacular ways.
Over time the parser evolved from "please read one sphere correctly" into a surprisingly robust system capable of:
validating normalised vectors checking invalid colours rejecting malformed files handling comments detecting invalid FOVs ensuring scene requirements were satisfied
At one point the parser became so broken that we effectively deleted it and rebuilt large portions from scratch:
Deleted old parser and created a new one
which was painful, but also probably the moment the project matured architecturally.
The most satisfying part of the parser work was that it transformed the project from "graphics demo" into something that actually resembled a tiny rendering engine.
A text file could now describe an entire world.
One of the strangest parts of graphics programming is that the renderer can be completely mathematically wrong while still producing an image that looks vaguely believable.
This became painfully obvious while debugging intersection ordering.
The renderer would often hit the wrong object even though all the intersection calculations individually appeared correct. Rays would pass through nearby objects and select farther ones. Objects inside other objects would render incorrectly. Entire scenes would become monochrome because the renderer kept selecting the wrong surface to shade.
One particularly brutal bug produced this commit:
Fixed an issue causing the wrong intersection to be selected when viewing INSIDE an object and changing all objects to that colour causing monochrome scenes
which sounds absurd, but at the time felt completely impossible to debug.
There were also multiple fixes involving:
sorting intersections correctly rejecting negative t-values handling EPSILON comparisons choosing the closest valid hit
One especially subtle issue involved replacing:
tmp->t > 0
with:
tmp->t > EPSILON
which fixed an entire class of rendering artifacts caused by floating point precision errors and self intersections.
This entire area taught me that many rendering bugs are not dramatic crashes. They're tiny logical inaccuracies that slowly poison the correctness of the whole engine.
Raytracing becomes deeply cursed the moment the camera enters an object.
Outside surfaces are intuitive. Normals point outward. Lighting behaves predictably. Everything makes sense.
Then you move the camera inside a sphere and suddenly the renderer starts questioning reality.
At one point:
lighting inverted itself surfaces rendered incorrectly shadows appeared where they shouldn't hit selection broke entirely
One commit eventually documented the issue:
Rendering no longer is incorrect when viewing the inside of a sphere.
which was the result of far more suffering than that sentence suggests.
The root problem was that normals behave differently depending on whether the ray intersects the outside or inside of a surface. If you fail to detect this correctly, lighting calculations become inverted because the renderer thinks the surface is facing away from the camera.
This eventually forced me to properly understand:
surface orientation eye vectors normal inversion inside/outside tests dot products geometric winding
What looked initially like “why does this sphere look weird?” became one of the most educational parts of the entire renderer.
Before miniRT I thought lighting was mostly artistic.
Then I implemented it myself.
Suddenly every pixel became the result of dozens of tiny mathematical decisions:
ambient contribution diffuse reflection specular highlights surface normals reflection vectors light intensity object colour view direction
And if any single value is slightly wrong, the entire scene immediately looks cursed.
One particularly nasty issue came from accidentally applying light intensity scaling twice:
removed double intensity scaling in lighting calculation
which explained why certain scenes looked like they were being rendered inside a nuclear reactor.
Another issue came from calculating light vectors from slightly incorrect positions, causing impossible shadows and self-shadowing artifacts.
There was also the incredibly satisfying moment when Phong lighting first worked correctly:
OH MY GOD SHINY BALL FIRST TRY IAMEPIC
which genuinely felt magical at the time.
Because suddenly the renderer stopped looking like raw geometry and started looking like an actual world with depth, material response, and light interaction.
That was probably the first moment miniRT felt real.
miniRT was also my introduction to the very specific emotional experience of debugging large C programs without safety nets.
I should have learned from Minishell. A previous project where it was of PARAMOUNT importance that you were careful with memory.
I wasn't careful. When things broke, they really broke.
Sometimes the renderer crashed immediately.
Sometimes it rendered nonsense.
Sometimes it corrupted memory silently before detonating somewhere completely unrelated twenty seconds later.
At one point the renderer would crash simply because no objects were visible in the scene:
“MiniRT no longer crashes when viewing a scene where no objects are visible because they are behind the camera FOV.”
which is objectively hilarious in retrospect.
Memory leaks became another recurring battle. Every object, matrix, linked list node, parser structure, and intersection record had to be managed manually. Forgetting a single free somewhere deep in the render pipeline slowly poisoned the entire application.
One of the most satisfying commits in the whole repository was simply:
NO LEAKS | 100% WORKING BALLS | MONUMENTAL MILESTONE
which perfectly captures the emotional maturity of systems programmers after several consecutive hours inside Valgrind.
The project taught me that C gives you extraordinary control, but also absolutely zero mercy.
There are algorithmically difficult problems in programming.
And then there is trying to make a raytracer conform to Norminette.
At some point miniRT stopped being about graphics and became a full-scale war against function length limits, file structure rules, and line counts. Entire evenings disappeared into splitting perfectly functional code into increasingly fragmented pieces purely to satisfy formatting constraints.
One particularly emotionally stable commit message documented the state of affairs:
getting really fuckin fed up of this shit at the minute. Why in fucks name cant I do 26 lines
which, honestly, remains a fantastic question.
The frustrating part wasn't that the renderer was broken. The renderer worked. The issue was restructuring:
into forms acceptable to a style checker with the emotional warmth of a parking ticket.
At one point entire subsystems were being split into files like:
which felt less like software architecture and more like administrative paperwork.
And yet, strangely, the process did improve the project. Slightly. And I HATE to admit it.
Functions became more modular. Responsibilities became clearer. Code paths became easier to reason about.
I still maintain that 25 lines is a deeply unserious number for graphics programming though.
If you've made it this far down the page, firstly: thank you. Secondly, you deserve to see what software engineering actually looks like at 2:14am after your renderer has been psychologically dismantling you for six consecutive hours.
Portfolio projects often appear suspiciously clean in hindsight. miniRT was not clean. miniRT was war. I'm making this website as part of my efforts to clean up my general documentation of all the stuff I've ever made. Currently sitting in the living room at my families home in London, as my brother and Dad play Wii golf. If by some miracle anyone reads this far down I have a treat for you.
Somewhere between matrix inversions, phantom shadows, exploding normals, and cylinders behaving like interdimensional objects, the commit history slowly transformed from technical documentation into what can only reasonably be described as emotional telemetry.
You've already seen many of the recovered artefacts from the front lines, here are a few more:
POWER BABY. PARSER INTEGRATED. ROTATIONS ROTATED. LEAKS PLUGGED. COME AT ME BITCH
And, perhaps most importantly:
The little boy from Rosario, Argentina. On behalf of every little boy wearing his shirt, Messi on a million backs, Messi for a million flashbulbs. One kick of the ball, one kick of the football. He's done it before.
which, for reasons I still cannot fully explain, appears immediately before a fix for normalised orientation vectors.
miniRT taught me a lot about rendering, linear algebra, floating point precision, debugging, systems programming, and software architecture.
It also taught me that all sufficiently advanced graphics projects eventually become partially spiritual experiences.
And finally, for those truly committed enough to reach the bottom of the dossier, one final unfiltered transmission recovered from the depths of the Norminette arc:
getting really fuckin fed up of this shit at the minute. Why in fucks name cant I do 26 lines, what french bastard decided that thats going to be the limit. Its one light you bagutte eating prick. If the project fuckin works, it fuckin works. If one more bellend comes at me telling me how to parse my program I am going to render the more gargantuous cylinder and ram it into his arse at a 45 degree angle. RECTALLY INSERT IT. I've had enough, I have been so unbelievably generous with these pricks. THE AMOUNT of people who hand in absolute sheer and utter RUBBISH. And I understand. I listen. I let them learn, and fix, and pass. EVEN THOUGH THEY SHOULDNT. If I am penalised for my kindness and human nature tomorrow I will absolutely lose my shit. Cannot wait to build a rocket and send it to the moon. Or France.
Every single bit of info you've read this far is 100% real and was experience, felt, and maybe not quite processed. Feel free to check out the project below. Commit messages included. Thank you and goodbye.
E.01