miniRT Β· handbook
🧭 explorer
42 common core Β· defense handbook

miniRT / the ray tracer, explained

A complete field guide to this codebase: how a .rt file becomes pixels, what every file does, the math behind every intersection, and the answers to the questions an evaluator will actually ask. For the clickable version of the code β€” call graph, guided trails, annotations β€” open the interactive explorer πŸ§­.

00 Β· orientationBig picture

miniRT is a CPU ray tracer. For every pixel of an 800Γ—600 window it shoots a ray from the camera through that pixel into the scene, finds the nearest object the ray hits, and computes a color there from ambient light, diffuse (Lambert) light and shadows β€” plus specular highlights, multiple colored lights and a procedural checkerboard in the bonus. The image is drawn once into an MLX image buffer and re-rendered only when the camera moves.

scene.rt srcs/parserread line-by-line β†’ tokenized β†’ dispatched by identifier β†’ validated hard (any error: Error\n + exit 1)
β”‚β–Ό
t_scene includes/miniRT.hambient + camera + light list + object list (tagged unions)
β”‚β–Ό
render loop srcs/rendercamera basis once per frame Β· 4 anti-aliasing rays per pixel via get_ray(u,v)
β”‚β–Ό
intersection srcs/intersectnearest hit over the object list β†’ t_hit (point, ray-facing normal, material)
β”‚β–Ό
shading srcs/shadingambient + Ξ£ lights (diffuse [+ specular]) gated by shadow rays, clamped to [0,1]
β”‚β–Ό
pixels β†’ X11 window srcs/windowmlx_put_pixel writes the image buffer; one mlx_put_image_to_window per frame

The one data structure to know: t_scene

includes/miniRT.h:184

typedef struct s_scene {
    t_ambient  ambient;    /* ratio + color (exactly one "A") */
    t_camera   camera;     /* pos + dir + fov (exactly one "C") */
    t_light    *lights;    /* linked list: 1 in mandatory, N in bonus */
    t_object   *objects;   /* linked list of tagged unions */
    int        has_ambient, has_camera, has_light;
} t_scene;

t_object (miniRT.h:153) is a tagged union: an enum type + an anonymous union { sphere; plane; cylinder; cone; triangle; } + a t_material + next. One malloc per object, no nested allocations. Everything hangs off t_app (miniRT.h:208), a stack variable in main that owns the MLX state and the scene.

The two binaries & the linking trick

Volunteer this in defense make builds miniRT, make bonus builds miniRT_bonus. There is no #ifdef BONUS anywhere. Both binaries share COMMON_SRCS; the behavioral difference comes purely from linking variant files that export the same symbols.
SymbolMandatory fileBonus fileDifference
dispatchparser/dispatch.cparser/dispatch_bonus.cbonus adds co (cone) and tr (triangle)
check_countparser/parse_opts.cparser/parse_opts_bonus.cstrict != count vs < (allows optional material tokens)
parse_materialparser/parse_opts.c (no-op stub)parser/parse_opts_bonus.cparses per-object ks / shininess / … / checkerboard
parse_lightparser/parse_light.cparser/parse_light_bonus.csingle light (duplicate rejected) vs multi-light list
hit_objectintersect/hit_object.cintersect/hit_object_bonus.cbonus dispatches to cone/triangle too
shadeshading/shade.cshading/shade_bonus.cbonus adds specular, per-light color, checkerboard

So the shared object parser (parse_objects.c) behaves strictly in the mandatory binary and extensibly in the bonus binary just because a different check_count/parse_material got linked in. This is the most elegant design decision in the project.

Constants that matter

includes/miniRT.h:22-26

ConstantValueRole
EPSILON1e-6minimum accepted ray parameter t; rejects self-hits and grazing hits
WIDTH Γ— HEIGHT800 Γ— 600fixed window size
MAX_DEPTH8vestigial β€” ray_color takes a depth but never recurses (no reflection/refraction). Be honest about this if asked.
SHADOW_BIAS1e-4offset for shadow-ray origins (anti-acne, Β§07)

01 Β· orientationFile map

Every source file and what lives inside it. Files tagged BONUS are linked only into miniRT_bonus.

srcs/ entry point
main.carg check β†’ parse_scene β†’ mlx_setup β†’ render β†’ hook registration β†’ mlx_loop. Also home of error_exit (writes Error\n + message to stderr, exit 1).
srcs/math/ 4 files
vec3.cconstructors + add / sub / scale / negate β€” all by value, no allocation
vec3_bis.clen, norm (zero-length guard β†’ returns zero vector, not NaN), near_zero
vec3_ops.cdot, cross, reflect (bonus specular only), ray_at = O + tD
quad.cshared quadratic solver: quad_solve (discriminant), root1/root2, quad_nearest
srcs/parser/ 15 files
parse_scene.centry: .rt check, open, hand-rolled read_line (1-byte reads, 1 MiB cap), line loop, final A/C/L presence check
dispatch.cidentifier β†’ parser table (A C L sp pl cy), # comment skip, unknown-identifier error
dispatch_bonus.cBONUSsame + co and tr
parse_ambient.cA ratio r,g,b β€” ratio ∈ [0,1], duplicate rejected
parse_camera.cC pos dir fov β€” FOV strictly (0,180), duplicate rejected
parse_light.csingle L; sets attenuation kc=1, kl=0, kq=0
parse_light_bonus.cBONUSmulti-light list append; keeps light color. ⚠ forgets kc/kl/kq β€” see Β§12
parse_objects.csp pl cy + new_object/append_object; diameter Γ· 2; cylinder cap centers precomputed
parse_objects_bonus.cBONUSco (apex/axis/radius/height) and tr (normal precomputed, degenerate rejected)
parse_opts.cmandatory check_count (strict) + parse_material no-op stub
parse_opts_bonus.cBONUSlenient count + real material tail parser (ks…checker_size)
material.cdefault_material: ka .1, kd .9, ks .3, shininess 32, checker off
parse_num.cft_strtod: sign + integer + fraction accumulation (no scientific notation)
parse_num2.cvalid_num strict grammar + next_component (exactly-3-values enforcer)
parse_utils.cparse_double/ratio/color/vec3/normal β€” range checks live here
tok_utils.csplit_line whitespace splitter, free_tokens, token_count
free_scene.cwalks and frees both linked lists (called from the close handler)
srcs/intersect/ 9 files
intersect.cintersect_scene: nearest-hit loop over the object list
sphere.csphere quadratic + set_face_normal (inside/outside flip)
plane.cdenominator test, two-sided normal from the sign of dot(dir, n)
cylinder.chit_body (tries both roots), cap_plane/hit_cap (disk tests), min-of-three combiner
cylinder_utils.cthe perpendicular-projection quadratic, height clamp, body normal, cap-hit fill
cone_bonus.cBONUScone body root selection + base cap
cone_utils_bonus.cBONUScone_quad (kΒ² slope terms), height/apex clamp, gradient normal
triangle_bonus.cBONUSMΓΆller–Trumbore with early barycentric exits
hit_object.c / _bonus.ctype dispatch β€” the entire mandatory/bonus split on this side
srcs/render/ 3 files
camera.corthonormal axes (up-hint trick), FOV β†’ viewport basis, get_ray
render.cpixel loops, 2Γ—2 supersampling, ray_color, sky-gradient background
translation.cget_cam_right + move_camera (WASD)
srcs/shading/ 9 files
shade.cmandatory combine: ambient + Ξ£ (shadowed ? 0 : diffuse), clamp
shade_bonus.cBONUS+ checker, + specular, contribution Γ— light color per light
ambient.cobject βŠ— (A_color Γ— ratio) β€” the fixed formula
diffuse.cLambert: max(0, NΒ·L) Γ— brightness Γ— attenuation
specular_bonus.cBONUSPhong: white Γ— ks Γ— brightness Γ— (RΒ·V)^shininess
shadow.cbiased shadow ray + strictly-between-point-and-light test
attenuation.c1 / (kc + klΒ·d + kqΒ·dΒ²), clamped ≀ 1 (neutral in mandatory)
checker_bonus.cBONUS3D solid checker: floor-cell parity β†’ color inversion; material-copy pointer trick
color.c[0,1] color ops: scale, add, multiply, clamp, lerp
srcs/window/ 3 files
mlx_init.cmlx_setup: init β†’ window β†’ image β†’ data addr, each NULL-checked; starts locked
mlx_hooks.cmouse (scroll = FOV zoom), keys (ESC/SPACE/WASD), close (full teardown), expose (re-blit)
mlx_utils.cmlx_put_pixel: ARGB packing + direct buffer write
everything else support
includes/miniRT.hall structs, constants, prototypes (mandatory)
includes/miniRT_bonus.hbonus-only prototypes; includes miniRT.h
libft/own libc subset (split, strlen, memset, isspace…) β€” vendored, own Makefile
mlx_linux/vendored MiniLibX for X11 β€” the allowed graphics library
scenes/mandatory demo scenes; scenes/bonus/ for the bonus binary (Β§10)
Makefiletwo targets sharing COMMON_SRCS; builds mlx + libft first; -Wall -Wextra -Werror

02 Β· orientationLife of a ray

This is the story to tell an evaluator who says β€œexplain how your program works.”

  1. main srcs/main.c:15 β€” checks argc == 2, calls parse_scene, mlx_setup, render, registers the hooks (mouse; KeyPress=2; DestroyNotify=17; Expose=12) and enters mlx_loop.
  2. Parse (Β§03) β€” the .rt file is read line by line, tokenized on whitespace, dispatched by identifier (A C L sp pl cy [+ co tr]), each element validated hard. Any error β†’ Error\n + message on stderr, exit 1. Result: a fully populated t_scene.
  3. Render (Β§06) β€” build_camera_basis converts pos/dir/FOV into a viewport 1 unit in front of the camera. For each pixel, 4 sub-pixel rays (2Γ—2 anti-aliasing) are generated by get_ray(u, v) and averaged.
  4. Intersect (Β§05) β€” ray_color β†’ intersect_scene walks the object list, calls the per-shape intersector via hit_object, keeps the hit with the smallest t. A t_hit carries the point, a unit normal that always faces the ray, and a pointer to the object's material.
  5. Shade (Β§07/Β§08) β€” shade = ambient + for each light: if not in_shadow, add diffuse (bonus: + specular, Γ— light color). Clamp to [0,1].
  6. Write pixel β€” mlx_put_pixel packs r,g,b (Γ—255.999) into an int and writes it directly into the image buffer at addr + yΒ·line_len + xΒ·(bpp/8).
  7. After the double loop, one mlx_put_image_to_window blits the frame. Keys/scroll mutate the camera and re-run steps 3–7; ESC or the window cross destroy everything, free_scene, exit(0).

On a miss, the background is a vertical white→blue gradient render.c:19-29 — an aesthetic choice, not black; one line to change.

03 Β· subsystemParser

srcs/parser/ β€” entry: parse_scene(file, scene), parse_scene.c:83

End-to-end flow

  1. Extension check parse_scene.c:51-61, 87-88 β€” needs len >= 4 and the last 3 chars == ".rt" (shortest valid name is a.rt; case-sensitive).
  2. open(file, O_RDONLY) :89-91 β€” failure β†’ error. A directory named foo.rt opens fine, but the first read() returns βˆ’1 (EISDIR), caught at :37-38 as β€œread error”.
  3. ft_memset(scene, 0, …) :92 β€” zeroes the has_* flags and NULLs the lists.
  4. Line loop read_all :63-81 β€” read_line β†’ split_line β†’ dispatch β†’ free the line and tokens immediately.
  5. Post-conditions :95-96 β€” missing any of A/C/L β†’ error. A scene with zero objects is legal (renders only the background).

read_line β€” the hand-rolled GNL

parse_scene.c:23-49

Mallocs a 4096-byte buffer and reads the fd one byte at a time until \n, EOF, or 4095 chars. Return contract:

  • returns the line if i > 0 || ret > 0 β€” the ret > 0 part means a bare \n returns an empty string, not NULL, so blank lines don't terminate parsing (they produce zero tokens and are skipped at :77);
  • returns NULL only at true EOF with nothing read.

The 1 MiB size cap parse_scene.c:16-21 Β· commit ab79a0f: bump_total() counts each byte actually consumed into a line (called after the newline-break, so newlines don't count) and errors past 1 048 576 bytes. History: the old version over-counted and rejected a legitimate 10 000-object scene; the fix counts only bytes really read.

Defense phrasing β€œWe cap cumulative scene content at 1 MiB, counted byte-by-byte, so huge-but-legitimate scenes pass and an infinite-file bomb is rejected cleanly.”

Tokenization & dispatch

  • split_line tok_utils.c:52-76 β€” two-pass whitespace splitter. Whitespace is ft_isspace = space + ASCII 9–13, so CRLF files parse fine (the \r is stripped).
  • dispatch dispatch.c:23-43 / dispatch_bonus.c:40-54 β€” first token β†’ parser. A first token starting with # skips the line (comment support). Unknown identifier β†’ Error\nunknown identifier: <tok>, exit 1. Inline comments after data are not supported (they become extra tokens).

Number parsing β€” three layers

  1. valid_num parse_num2.c:28-44 β€” strict grammar before conversion: optional single sign, β‰₯1 digit, optional . + β‰₯1 digit, end of string. Rejects: "", "+", "1.", ".5", "++5", "1.2.3", "1e5", "0x1F".
  2. ft_strtod parse_num.c:61-74 β€” sign + integer part + fraction part. Deliberately no scientific notation.
  3. Range checks β€” parse_double parse_utils.c:15-28 enforces |v| ≀ 1 000 000; parse_ratio :30-38 enforces [0,1].

Triples go through next_component parse_num2.c:46-71 β€” the trickiest function in the parser:

if (!last && **s == ',')
    *s += 1;
else if (!last || **s != '\0')
    error_exit("expected exactly three comma separated values");

Read it as: not-last β‡’ a comma must follow (consume it); last β‡’ end-of-string must follow. Every malformed shape β€” 1,2 Β· 1,2,3,4 Β· 1,,3 Β· 1,2, β€” falls into the error. The 64-byte local buf with its i < 63 copy guard means a 200-digit component just stops copying, leaves *s on a char that is neither , nor \0, and hits the same clean error β€” no overflow possible.

  • Colors parse_utils.c:40-58 β€” each component must be an integer in [0,255] (v != (int)v rejects 12.5), then is normalized to [0,1] by Γ·255. All downstream color math is linear [0,1]; conversion back to 0–255 happens only at pixel-write time.
  • Orientation vectors parse_utils.c:70-81 β€” each component in [βˆ’1,1], vector must not be ~zero (vec3_near_zero), then re-normalized with vec3_norm. So 0.5,0.5,0 is accepted and canonicalized rather than rejected β€” deliberate tolerance for rounding (e.g. 0.577,0.577,0.577); say so up front.

Element parsers

IdFileRules
Aparse_ambient.c:15-263 tokens; ratio ∈ [0,1]; duplicate rejected via has_ambient
Cparse_camera.c:15-274 tokens; FOV strictly ∈ (0,180); duplicate rejected
L (mand.)parse_light.cduplicate rejected; sets attenuation kc=1, kl=0, kq=0 (:31-33) β†’ always exactly 1.0
L (bonus)parse_light_bonus.cappends to the list β€” multiple lights; keeps the color. ⚠ does NOT set kc/kl/kq β€” Β§12 bug #1
sp pl cyparse_objects.c:42-92new_object malloc β†’ geometry β†’ default_material(color) + parse_material (no-op in mandatory) β†’ tail-append. Sphere & cylinder divide the diameter by 2 (:49, :79), reject ≀ 0. Cylinder precomputes top/bottom_center = center Β± axisΒ·h/2 (:85-88) β€” the .rt center is the mid-height point.
coparse_objects_bonus.c:15-33co apex axis radius height color β€” a radius, not a diameter (own documented format)
trparse_objects_bonus.ctr v0 v1 v2 color β€” normal precomputed at parse time: normalize(cross(v1βˆ’v0, v2βˆ’v0)) (:52-54); degenerate/collinear rejected (:55-56)

Bonus per-object material tail parse_opts_bonus.c:21-44, appended after the color: [ks] [shininess] [refl] [transp] [ior] [checkerboard 0/1] [checker_size]. Defaults from default_material material.c:15-31: ka .1, kd .9, ks .3, shininess 32, refl/transp 0, ior 1, checker off, size 1. Of these, only ks, shininess, checkerboard, checker_size are actually consumed by shading β€” the rest are forward-looking storage (admit it if asked).

Memory management

  • Objects/lights: singly linked lists of individually malloc'd nodes, tail-append.
  • Normal teardown: free_scene free_scene.c:15-36 walks both lists; called from the window-close path.
  • On a parse error mid-file: error_exit main.c:33-39 writes Error\n + message to stderr and exit(1) β€” nothing is freed, but everything already allocated is still referenced from live stack frames, so valgrind reports it as β€œstill reachable”, not β€œdefinitely lost”.
Defense phrasing β€œOn malformed input we exit immediately; no memory is lost β€” 0 bytes definitely lost under valgrind. On the normal path everything is freed by free_scene in the close handler.” Run valgrind on a bad file before the defense so you can quote the numbers.

04 Β· subsystemMath toolkit

srcs/math/ β€” all vec3 functions are by-value: no pointers, no allocation

FunctionFileNotes
vec3, add, sub, scale, negatevec3.cnegate is used everywhere to flip normals toward the ray
vec3_lenvec3_bis.c:15√(x²+y²+z²)
vec3_normvec3_bis.c:20guards Γ·0: len < 1e-12 β†’ returns the zero vector (detectable, not NaN)
vec3_near_zerovec3_bis.c:30all comps < 1e-8; rejects zero orientations & degenerate triangles
vec3_dotvec3_ops.c:16the workhorse: quadratics, NΒ·L, projections, face tests
vec3_crossvec3_ops.c:22camera basis, triangle normal, MΓΆller–Trumbore
vec3_reflectvec3_ops.c:31v βˆ’ 2(vΒ·n)n; only used by bonus specular
ray_atvec3_ops.c:37origin + tΒ·dir
Key invariant Ray directions are normalized (camera.c:73), so t is a true world-space distance β€” nearest-hit comparisons and the shadow max-distance check depend on it.

The shared quadratic solver

math/quad.c Β· t_quad in miniRT.h:169

Sphere, cylinder and cone are all atΒ² + bt + c = 0. The t_quad struct is shared scratch space β€” the extra fields d f m n root exist so helpers stay under Norm limits without recomputing: the cylinder caches its projected vectors in d/f, the cone caches axis dot products in m/n.

  • quad_solve quad.c:20-27 β€” disc = bΒ² βˆ’ 4ac; negative β†’ miss (return 0); else cache sqrt_disc.
  • quad_root1 = (βˆ’b βˆ’ √disc)/2a β€” the near root (entry point).
  • quad_root2 = (βˆ’b + √disc)/2a β€” the far root (exit; the relevant one when the ray starts inside the object, since root1 is then behind the origin).
  • quad_nearest quad.c:46-55 β€” try root1, accept if t β‰₯ EPSILON; else root2; else miss. One call handles outside rays, inside rays, and objects behind the camera.
Why t β‰₯ EPSILON and not t β‰₯ 0 Floating point. A secondary ray starting on a surface would re-hit that same surface at t β‰ˆ 1e-16; requiring t β‰₯ 1e-6 discards those self-hits β€” otherwise the render shows β€œshadow acne” speckle.

05 Β· subsystemIntersections

srcs/intersect/

Dispatch & nearest hit

  • hit_object hit_object.c:15-24 β€” if/else on obj->type β†’ sphere/plane/cylinder. The bonus version hit_object_bonus.c:15-28 adds cone and triangle. This one-file swap is the entire mandatory/bonus split on the intersection side.
  • intersect_scene intersect.c:15-37 β€” walks the list, closest starts at 1e15, keeps hits with tmp.t < closest, copies the winning t_hit out whole. O(n) per ray β€” no BVH; fine at miniRT scale.

Every intersector fills the full t_hit: t, point, unit normal that always faces the ray, mat (a pointer into the object β€” no copy), front_face.

Sphere

sphere.c

Math. P on sphere ⟺ |P βˆ’ C|Β² = rΒ². Substitute P = O + tD, let oc = O βˆ’ C:

(DΒ·D)Β·tΒ² + 2(ocΒ·D)Β·t + (ocΒ·oc βˆ’ rΒ²) = 0

Code mapping sphere.c:34-52: oc at :40, a = dot(dir,dir) :41 (would be 1.0 for unit dirs β€” computed anyway so the formula stays correct for any caller), b = 2Β·dot(oc,dir) :42, c = dot(oc,oc) βˆ’ rΒ² :43, then quad_solve + quad_nearest :44, fill hit :46-50.

Normal flipping β€” set_face_normal sphere.c:17-29: the outward normal is normalize(P βˆ’ C); if dot(ray.dir, outward) < 0 the ray sees the outside β†’ keep it, front_face = 1; otherwise the ray hit from inside (origin inside the sphere, quad_nearest returned root2) β†’ negate, front_face = 0. Shading always receives a normal pointing against the incoming ray (the β€œRay Tracing in One Weekend” convention).

Plane

plane.c

Solve (O + tD βˆ’ p0)Β·n = 0 β†’ t = ((p0 βˆ’ O)Β·n) / (DΒ·n).

  • denom = dot(dir, normal) :21; |denom| < EPSILON β†’ parallel β†’ miss (:22) β€” also protects the division.
  • t < EPSILON rejected (:26).
  • Two-sided (:31-35): denom < 0 β‡’ the ray faces the stored normal β†’ keep it; denom > 0 β‡’ the ray comes from behind β†’ return the negated normal. Same β€œnormal faces the ray” invariant, derived directly from denom's sign (no extra dot product β€” denom is dot(dir, n)).

Cylinder β€” the defense centerpiece

cylinder.c + cylinder_utils.c

Decomposition: infinite tube clipped by height + top disk + bottom disk, tested independently; smallest valid t wins (intersect_cylinder cylinder.c:81-103, min-reduction starting at INFINITY).

(a) Body quadratic β€” cylinder_quadratic cylinder_utils.c:18-34

P is on the infinite tube (center C, unit axis A, radius r) ⟺ the component of P βˆ’ C perpendicular to the axis has length r. Instead of expanding a messy scalar formula, the code projects the axis component out first:

q->d = dir βˆ’ (dirΒ·A)A;      /* direction, axis part removed          */
q->f = oc  βˆ’ (ocΒ·A)A;       /* origin offset likewise (oc = O βˆ’ C)   */
a = dΒ·d;   b = 2Β·(dΒ·f);   c = fΒ·f βˆ’ rΒ²;

i.e. an ordinary circle quadratic in the plane perpendicular to the axis. Edge case at :28: a β‰ˆ 0 β‡’ ray parallel to the axis β‡’ it can never cross the wall β†’ return 0 (the caps still get tested). Also protects the Γ·2a divisions.

(b) Height clamp β€” valid_body_hit cylinder_utils.c:39-49

proj = (P βˆ’ C)Β·A is the signed distance along the axis from the (mid-height) center; valid iff βˆ’h/2 ≀ proj ≀ h/2 and t β‰₯ EPSILON.

(c) Root selection with clipping β€” hit_body cylinder.c:18-37

Crucial subtlety You cannot just take the nearest positive root. The near root may hit the infinite tube outside the height range while the far root is inside (tilted views; camera inside). So root1 is tried and, only if rejected, root2 β€” each validated with the height clamp. Same pattern in the cone.

(d) Caps β€” hit_cap cylinder.c:56-76

Via cap_plane (:42-54): top uses top_center/+axis, bottom uses bottom_center/βˆ’axis; then plane intersection + disk membership |P βˆ’ cap_center|Β² ≀ rΒ² (:71-73 β€” squared, no sqrt). set_cap_hit cylinder_utils.c:84-93 flips the cap normal toward the ray like everything else. The cap centers were precomputed at parse time.

(e) Body normal β€” body_normal cylinder_utils.c:55-64

Project the hit point onto the axis, subtract: normalize(P βˆ’ (C + projΒ·A)) β€” the radial vector at that height, perpendicular to the axis by construction.

(f) Camera inside the cylinder

scenes/cylinder_inside.rt proves it. root1 < 0 β†’ rejected; root2 (the wall you're looking at) accepted; check_body_hit cylinder_utils.c:66-79 sees dot(dir, outward) > 0 β†’ front_face = 0, normal negated β†’ the interior wall shades correctly.

β€œWall or cap β€” how do you know?” You never need a flag: each of the three tests fills its own normal (radial vs Β±axis) and the min-t wins; downstream shading only consumes hit->normal.

Cone (bonus)

cone_bonus.c + cone_utils_bonus.c

Stored as apex + axis (apex→base) + base radius + height; slope k = radius/height = tan(half-angle) cone_utils_bonus.c:27.

Body quadratic β€” cone_quad :20-36: P on cone ⟺ |perp(Pβˆ’apex)|Β² = kΒ²Β·((Pβˆ’apex)Β·A)Β². Same perpendicular-decomposition trick, with m = dirΒ·A, n = ocΒ·A cached in the quad struct:

a = dβŠ₯Β·dβŠ₯ βˆ’ kΒ²mΒ²;
b = 2Β·(dβŠ₯Β·ocβŠ₯ βˆ’ kΒ²mn);
c = ocβŠ₯Β·ocβŠ₯ βˆ’ kΒ²nΒ²;

The βˆ’kΒ²(…) terms are exactly what turns a cylinder into a cone: the allowed radius grows linearly with axial distance.

Height clamp & the double-cone trap β€” check_cone_body :54-74: proj = n + tΒ·m (algebraically (Pβˆ’apex)Β·A, but two dot products cheaper); valid iff 0 ≀ proj ≀ height.

Classic evaluator trap proj < 0 discards the mirror cone above the apex β€” the quadratic describes a double cone and this test removes the phantom half. Know it cold.

Base cap: one disk at apex + axisΒ·height cone_bonus.c:34-35, same plane+disk test; there is no apex-end cap (it's a point).

Body normal β€” cone_body_normal cone_utils_bonus.c:42-52: normalize(perp βˆ’ kΒ²Β·projΒ·axis) where perp is the radial vector. Why the βˆ’kΒ²Β·projΒ·axis term: the surface is the zero set of F(P) = |perp|Β² βˆ’ kΒ²Β·projΒ²; the normal is its gradient βˆ‡F ∝ perp βˆ’ kΒ²Β·projΒ·axis. A purely radial normal would shade like a cylinder.

Triangle (bonus) β€” MΓΆller–Trumbore

triangle_bonus.c

Solve the linear system O + tD = v0 + uΒ·e1 + vΒ·e2 (e1 = v1βˆ’v0, e2 = v2βˆ’v0) by Cramer's rule via scalar triple products; hit iff u β‰₯ 0, v β‰₯ 0, u+v ≀ 1 (barycentric containment). No quadratic.

tri_solve :25-50: h = dir Γ— e2, det = e1Β·h β€” |det| < EPSILON β‡’ ray parallel to the triangle's plane β‡’ miss. The sign of det is kept: > 0 front face, < 0 back face (triangles are two-sided; the precomputed normal is just flipped by det's sign at render time :63-67 β€” no per-ray cross product). u is computed and range-checked before v (early exit), then t = (e2Β·(sΓ—e1))/det, rejected below EPSILON.

06 Β· subsystemCamera & render loop

srcs/render/

Camera basis

camera.c

Pinhole model with the viewport at distance 1 in front of the camera:

  • half_w = tan((fov Β· Ο€/180) / 2) :42 β€” FOV is horizontal, per subject. Trig: opposite/adjacent with adjacent = 1.
  • half_h = half_w Β· (HEIGHT/WIDTH) :43 β€” aspect ratio keeps pixels square, spheres round.
  • Soutenance fix commit 228f5f3 β€” it used to be the other way around (half_h = tan(…), FOV effectively vertical); the fix swapped the two lines.
  • horizontal/vertical = full spanning vectors; lower_left = pos + dir βˆ’ rightΒ·half_w βˆ’ upΒ·half_h :47-50.

Orthonormal axes β€” get_camera_axes camera.c:21-31:

if (fabs(dir.y) < 0.99)  world_up = (0,1,0);  else  world_up = (0,0,1);
right = norm(cross(dir, world_up));
up    = norm(cross(right, dir));

Edge case: a camera looking straight up/down would make cross(dir, (0,1,0)) the zero vector (normalizing = Γ·0) β€” the 0.99 guard swaps the hint to (0,0,1). up is re-derived from right Γ— dir so the basis is exactly orthogonal even when dir isn't perpendicular to the hint.

Ray generation β€” get_ray camera.c:64-75: target = lower_left + uΒ·horizontal + vΒ·vertical; dir = normalize(target βˆ’ origin). (u,v) ∈ [0,1]Β².

Render loop

render.c

  • render :78-93 β€” basis built once per frame; double loop y then x; one mlx_put_image_to_window at the end (:92).
  • 2Γ—2 supersampling β€” render_pixel :58-76: 4 rays per pixel at sub-pixel offsets 0.25 / 0.75 in each axis, averaged (Γ—0.25 at :75). Stratified anti-aliasing; total 800Γ—600Γ—4 = 1.92 M primary rays per frame (why keypress re-renders visibly lag).
  • u/v mapping β€” get_sample_color :45-56: u = px/WIDTH, v = 1 βˆ’ py/HEIGHT β€” the y-flip: image memory row 0 is the top of the window but viewport v grows upward from lower_left.
  • ray_color :34-43 β€” returns black if depth <= 0 but never recurses or decrements β€” MAX_DEPTH is scaffolding for a reflection bounce loop that was never enabled. Honest answer if asked.

Camera movement

translation.c + window/mlx_hooks.c

  • W/S move along Β±dir, A/D strafe along Β±right (recomputed with the same up-hint logic, translation.c:18-27), speed 0.5, then full re-render.
  • Mouse scroll (buttons 4/5): FOV βˆ“5Β°, clamped to [1,179], re-render.
  • SPACE toggles app->is_locked (movement/zoom ignored while locked). The camera starts locked (is_locked = 1 in mlx_setup) β€” press SPACE first when demoing movement.
  • Arrow keys are defined in the header but not handled β€” only WASD works (Β§12).

07 Β· subsystemShading β€” mandatory

srcs/shading/

shade shade.c:17-34: final = ambient + Ξ£ lights (in_shadow ? 0 : diffuse), then color_clamp. (void)ray; at :22 β€” no specular in mandatory. The loop is list-shaped but the mandatory parser guarantees exactly one light.

Ambient β€” know this formula, it was a graded fix

ambient.c:15-21 Β· commit cc27a56

amb = color_scale(ambient->color, ambient->ratio);   /* A color Γ— ratio  */
return color_multiply(hit->mat->color, amb);         /* βŠ— object color   */

The old version multiplied the object color in twice and dampened by ka = 0.1, making shadows nearly black and ignoring the A color. Numeric sanity check on scenes/min_shadow.rt: lit β‰ˆ 150 vs shadowed β‰ˆ 7 β‰ˆ 0.05 Γ— 150 (ratio 0.05). βœ”

Diffuse β€” Lambert

diffuse.c:15-27

to_light  = norm(light->pos βˆ’ hit->point);
intensity = max(0, dot(N, L)) Β· brightness Β· attenuation(light, point);
return color_scale(hit->mat->color, intensity);

dot(N,L) = cos ΞΈ: full strength when the surface faces the light, zero at grazing or facing away (the max(0, Β·)). Notes: mandatory never multiplies by light->color (subject: unused in mandatory) and the material kd is parsed but never applied β€” brightness is the only diffuse gain.

Shadows

shadow.c

make_shadow_ray :16-28 offsets the origin point + SHADOW_BIASΒ·normal + SHADOW_BIASΒ·to_light (bias 1e-4). Why: without it, the shadow ray re-hits its own surface at t β‰ˆ 1e-15 β†’ every point thinks it's shadowed β†’ β€œshadow acne” speckle. Offsetting along both normal and light direction covers grazing geometry. in_shadow :30-41 reuses the same intersect_scene:

if (intersect_scene(shadow_ray, scene, &tmp))
    return (tmp.t < light_dist - SHADOW_BIAS);

Only a hit strictly between point and light blocks; correct because intersect_scene returns the nearest hit, and t is world-distance because directions are unit length.

Attenuation

attenuation.c:15-25

1 / (kc + klΒ·d + kqΒ·dΒ²), clamped ≀ 1. The mandatory parser sets kc=1, kl=0, kq=0 β†’ always exactly 1.0 (the subject has no falloff; the machinery is present but neutral). ⚠ The bonus parser forgets to set these β€” Β§12 bug #1.

Color pipeline

color.c + window/mlx_utils.c

All colors are doubles in [0,1] end-to-end (color_scale/add/multiply/clamp/lerp). color_clamp at the end of shade is required because several additive terms (multi-light + specular) can exceed 1.0, and 1.2 Γ— 255 = 306 would bleed into the neighboring channel via the bit-shift packing. mlx_put_pixel mlx_utils.c:18-29 packs (int)(c Γ— 255.999) per channel β€” Γ—255.999 maps 1.0 β†’ 255 without hitting 256 β€” and writes *(unsigned int *)(addr + yΒ·line_len + xΒ·(bpp/8)).

08 Β· subsystemShading β€” bonus additions

shade_bonus.c Β· specular_bonus.c Β· checker_bonus.c

shade, bonus version shade_bonus.c:17-38:

apply_checker(hit, &tmp);                       /* may repoint hit->mat  */
final = ambient_light(hit, &scene->ambient);
while (light) {
    if (!in_shadow(scene, hit, light))
        final += color_multiply(diffuse + specular, light->color);
    light = light->next;
}
return color_clamp(final);

Four differences vs mandatory: checkerboard first, a real multi-light loop, Phong specular, and each light's contribution tinted by its own color commit b3f6ba6 β€” the color β€œwas always parsed but never used”; a red light on a white sphere now gives β‰ˆ (152, 51, 51). Ambient is deliberately not tinted by point lights.

Phong specular

specular_bonus.c:20-38 β€” true reflect-vector Phong, not Blinn half-vector

R = vec3_reflect(βˆ’to_light, normal);          /* v βˆ’ 2(vΒ·n)n     */
V = norm(βˆ’ray.dir);
spec = pow(max(0, dot(R, V)), shininess) Β· ks Β· brightness;
return white Γ— spec;

The highlight base is white β€” it reflects the light source, not the object (that's why a red ball gets a white glint); it becomes light-tinted by the per-light multiply above. ks = 0.3, shininess = 32 by default, per-object overridable.

Checkerboard β€” 3D solid procedural texture (not UV)

checker_bonus.c

sum = floor(p.x/size) + floor(p.y/size) + floor(p.z/size);
if (sum % 2)  color = 1 βˆ’ color;   /* photographic negative of the cell */

Space is diced into size-cubes; cell parity picks the original color or its inverse. floor() (not cast truncation) keeps cells consistent across negative coordinates; C's % on negatives yields βˆ’1, which is truthy, so alternation continues correctly through the origin. Works on every primitive because it only needs the 3D hit point. Trade-off to defend: a sphere looks β€œcarved from a checkered block,” not latitude/longitude tiles like UV mapping would give.

The pointer trick β€” great β€œexplain this” material apply_checker checker_bonus.c:26-38 copies the material into shade's stack variable tmp, inverts the copy's color if parity is odd, and repoints hit->mat at tmp. Safe only because hit never outlives shade's frame. If checkerboard is off or checker_size ≀ 0, it returns before touching anything β€” that guard (:28) also defuses the parser's missing checker_size validation.

09 Β· subsystemWindow & MLX

srcs/window/

mlx_setup

mlx_init.c:15-29

Straight-line init, NULL-checked at every step: mlx_init() (:17 β€” failure β†’ Error\nmlx_init failed, exit 1; verified to fail cleanly with no DISPLAY, no segfault), mlx_new_window(800, 600, "miniRT") (:20), mlx_new_image (:23), mlx_get_data_addr (:26 β€” fills bpp, line_len, endian). Ends with app->is_locked = 1 (:28) β€” the camera starts LOCKED; press SPACE before demoing movement!

mlx_put_pixel

mlx_utils.c:18-29

argb = ((int)(r*255.999) << 16) | ((int)(g*255.999) << 8) | (int)(b*255.999);
dst  = mlx->addr + y * line_len + x * (bpp / 8);
*(unsigned int *)dst = argb;
  • line_len = padded stride in bytes per row β€” the X server may align rows, so hardcoding WIDTH*4 could shear the image. Likewise bpp/8 vs a literal 4.
  • Γ—255.999 maps 1.0 β†’ 255 without ever producing 256.
  • Why an image buffer and not mlx_pixel_put? mlx_pixel_put is one X11 round-trip per pixel (480 000 per frame); we write RAM and blit once. Also, pixels drawn before the window is mapped are lost β€” an image persists and the Expose handler re-puts it.

Events

main.c:24-28 + mlx_hooks.c β€” the magic numbers are X11 protocol event codes

HookEventMaskHandler
mlx_hook(win, 2, 1L<<0, …)KeyPressKeyPressMaskkeys
mlx_hook(win, 17, 0, …)DestroyNotifynone neededred cross
mlx_hook(win, 12, 1L<<15, …)ExposeExposureMaskre-blit
mlx_mouse_hookButtonPressβ€”scroll zoom
  • key_handler mlx_hooks.c:42-68 β€” ESC β†’ close_handler; SPACE toggles is_locked (prints πŸ”’/πŸ”“); when locked everything else is ignored; WASD move at speed 0.5 + re-render.
  • close_handler mlx_hooks.c:70-78 β€” both ESC and the red cross funnel here: mlx_destroy_image β†’ mlx_destroy_window β†’ mlx_destroy_display (added in the soutenance commit; kills an X connection leak) β†’ free(mlx.ptr) β†’ free_scene β†’ exit(0).
  • expose_handler :80-84 β€” re-puts the stored image, so the window survives minimize/overlap without re-raytracing, and the first frame isn't lost if the put happens before the window is mapped.

10 Β· operationsScenes guide

Mandatory format: A ratio r,g,b Β· C pos dir fov Β· L pos brightness r,g,b Β· sp pos diameter color Β· pl point normal color Β· cy center axis diameter height color.

scenes/ β€” run with ./miniRT

SceneShows / when to use
full_mandatory.rtplane + sphere + cylinder + light β€” the main demo
sphere.rtdecimal values, tilted cylinder (axis 0,0,1) β€” float parsing
cylinder_inside.rtcamera at the cylinder's center β€” inside-surface normals
cylinder_side.rt / cylinder_top.rtbody from the side; caps from above
floor_shadow.rt / min_shadow.rthard shadow; min_shadow has ambient 0.05 (near-black shadow β€” the ambient-formula proof)
two_planes.rtplanes visible from the correct side (two-sided planes)
wall.rtvertical plane behind a sphere
ten_objs.rtseveral objects β€” closest-hit correctness

scenes/bonus/ β€” run with ./miniRT_bonus

SceneShows
showcase.rtone-stop bonus demo: 2 colored lights, cone, triangle, checkerboard plane with the full material tail
cone_triangle.rtcone apex-down + flat and vertical triangles
specular_test.rt / material_test.rtspheres with ks .9/.5/0 and shininess 256/8/32 β€” the Phong highlight varying
atelier.rt / nocturne.rtart scenes, multi-light (warm key + cool fill) β€” the best-looking demos

Bonus syntax by example (from showcase.rt)

L  -8,10,-6  0.8  255,80,80                    # colored light (color IS used in bonus)
co 4,-3,3  0,1,0  2.5  6  60,200,120  0.5 32   # cone: apex axis RADIUS height color [ks shin]
tr -1,4,7  -3,1,7  1,1,7  220,180,60           # triangle: three vertices + color
pl 0,-3,0  0,1,0  200,200,200  0.4 64 0 0 1 1 2
#  tail: ks=0.4 shininess=64 refl=0 transp=0 ior=1 checkerboard=1 size=2

The bonus scenes also demonstrate # comments and blank lines being accepted.

11 Β· operationsBuild & project health

zero warnings no relink norminette clean no forbidden functions

  • Makefile: make β†’ miniRT, make bonus β†’ miniRT_bonus, flags -Wall -Wextra -Werror -ggdb; builds mlx_linux and libft first; links -lmlx -lft -lXext -lX11 -lm. Verified: both targets compile with zero warnings, and re-running make does not relink (β€œNothing to be done”) β€” a standard evaluator check. Objects depend on both headers (Makefile:109), so header edits rebuild correctly.
  • Norminette 3.3.59: fully clean on includes/, srcs/, libft/.
  • Forbidden functions: clean. nm -u on both binaries minus MLX/libc internals leaves only open close read write malloc free exit and math (sqrt tan, + floor pow in bonus, allowed via -lm). The suspicious-looking rest (calloc getenv gethostname shm* strlen strncmp puts) all come from inside MiniLibX (X shared memory, getenv("DISPLAY")) β€” zero hits in project code. Number parsing is a homemade ft_strtod, not libc atof.

The soutenance commit (228f5f3) β€” β€œwhat did you fix recently?”

  1. mlx_destroy_display on close β€” X connection leak fixed.
  2. The Expose handler β€” the window survives minimize; the image is re-put if the first draw beats the window mapping.
  3. FOV made horizontal, as the subject defines (camera.c).
  4. A cylinder bug where a ray whose only hit was the bottom cap was lost (best not updated) β€” fixed in cylinder.c.
  5. read_line read a byte before checking read's return β€” an uninitialized read when handed a directory β€” fixed.
  6. The 1 MB scene cap (then refined by ab79a0f to count only bytes actually read, and c026715 to fit the norm's 25-line limit).

12 Β· operationsKnown weaknesses β€” read before the defense

#1 Β· Real bug β€” uninitialized attenuation in the bonus

parse_light_bonus.c:22-30 mallocs each light and sets pos/brightness/color/next but never sets kc, kl, kq (the mandatory parser sets them at parse_light.c:31-33). diffuse_light β†’ attenuation() (attenuation.c:21) reads all three on every shading sample β‡’ read of uninitialized heap memory in every miniRT_bonus frame. In practice fresh pages are often zero β†’ 1/0 = inf β†’ clamped to 1.0, so it looks fine β€” but valgrind ./miniRT_bonus scene.rt prints β€œConditional jump … depends on uninitialised value”, and running valgrind is a standard evaluator move. Fix = copy the 3 init lines from parse_light.c into parse_light_bonus.c.

  1. MAX_DEPTH / depth is dead scaffolding β€” no recursion (no reflection/refraction). Say it plainly: β€œhook for a bounce loop; rays terminate at first hit.”
  2. ka, kd, reflectivity, transparency, ior, roughness are parsed/stored but never used in shading; only ks/shininess/checker fields are consumed.
  3. Error paths don't free β€” but everything stays reachable (Β§03); verify with valgrind and quote the numbers.
  4. Performance: 4 rays/pixel, full re-render per keypress, linear object list. A 10k-object scene is slow β€” that's why the 1 MiB parse cap exists.
  5. mlx_put_pixel assumes 32 bpp little-endian (*(unsigned int *)dst; endian stored but unused) β€” always true on evaluation Linux/X11.
  6. Cone a β‰ˆ 0 (ray parallel to the slant) has no explicit guard like the cylinder's: roots become Β±inf and are rejected by the proj range check β€” implicit but harmless; know it.
  7. Lines > 4095 chars are split by read_line; the continuation chunk fails as β€œunknown identifier” β€” clean failure, no overflow.
  8. Camera within ~8Β° of vertical (the 0.99 threshold) switches the up-hint β€” can cause a visible roll change between two nearly-vertical cameras; cosmetic.
  9. Plane t = 0 rejected by EPSILON β€” a camera exactly on a plane doesn't see it; by design.
  10. The checkerboard token is a truncating cast (0.9 β†’ 0) and checker_size ≀ 0 is accepted by the parser (but neutralized in apply_checker) β€” defensible as β€œdocumented format: 0/1 and a positive size.”
  11. README vs reality: the header defines KEY_UP/DOWN/LEFT/RIGHT (miniRT.h:34-37) and the README advertises arrow keys, but key_handler only acts on WASD (mlx_hooks.c:62). The README also mentions reflection/transparency β€” parsed-but-unimplemented material fields. Either align the README before the defense or don't demo those.
  12. error_exit after parsing (e.g. mlx_new_window failure) exits without free_scene β€” OS-reclaimed and still-reachable, but know it exists. Also mlx_get_data_addr's return is not NULL-checked (mlx_init.c:26).
  13. The camera starts locked (is_locked = 1, mlx_init.c:28) β€” not a bug, but forget to press SPACE during the demo and movement β€œdoesn't work.”

13 Β· defenseQ&A drill

Tap a question, answer out loud, then reveal. Use the buttons to open or close a whole group.

Parsing

What happens with a duplicate A / C / L?
Rejected via the has_ambient/has_camera flags (parse_ambient.c:17, parse_camera.c:17). Duplicate L: rejected in mandatory (parse_light.c:19), allowed in bonus β€” multiple lights is a bonus feature.
What if I feed you 1,2, β€” or 1,2,3,4 β€” or 1..2 β€” or ++5?
All exit with Error\n: an empty component fails valid_num; an extra component trips the !last || **s != '\0' guard in next_component; 1..2 and ++5 fail the number grammar.
RGB of 256 or 12.5? Negative diameter? FOV 180?
parse_color enforces integer [0,255]; sphere/cylinder reject ≀ 0 diameter/height; FOV must be strictly inside (0,180) (parse_camera.c:24).
Camera orientation 0,0,0?
Rejected β€” vec3_near_zero check in parse_normal (parse_utils.c:78).
A non-normalized orientation like 1,1,1?
Components must each be in [βˆ’1,1]; then the vector is re-normalized with vec3_norm. Justification: the subject says the input is a normalized vector; we canonicalize instead of punishing rounding (e.g. 0.577,0.577,0.577).
No .rt extension? A directory? An unreadable file? Empty file? Missing L?
Extension check (parse_scene.c:87); open failure (:90); a directory passes open but the first read returns EISDIR β†’ β€œread error” (:37); an empty or comment-only file β†’ β€œscene missing mandatory element (A, C, or L)” (:95-96).
How do you read lines without get_next_line?
Our own read_line: 1-byte reads into a 4096 buffer until \n/EOF, with a cumulative 1 MiB content cap (bump_total). Only allowed functions: open/read/malloc/free/write/exit.
Why is exit-on-error OK for memory?
On malformed input we exit(1) immediately; every allocation is still referenced from live stack frames, so valgrind shows 0 bytes definitely lost (β€œstill reachable” only). The normal path frees everything via free_scene in the close handler.
Where is the bonus code? How does the shared parser accept extra tokens only in the bonus?
The linking trick (Β§00): both binaries share the object parser, but the bonus links a lenient check_count and a real parse_material instead of the strict counter and the no-op stub. Same symbols, different files, zero #ifdefs.
Diameter or radius?
sp/cy take a diameter per the subject (Γ·2 at parse_objects.c:49, 79); the bonus cone takes a radius per its own documented format (printed in its error message).
Do you support comments and blank lines?
Yes β€” lines whose first token starts with # are skipped (dispatch.c:27) and blank lines produce zero tokens. Inline comments after data are not supported (extra-token error).

Math & intersections

What does the discriminant mean?
For atΒ² + bt + c = 0, disc = bΒ² βˆ’ 4ac (quad.c:22). Negative β†’ the ray's line never touches the surface β†’ miss. Zero β†’ tangent graze (one root). Positive β†’ the line crosses twice: entry (root1) and exit (root2).
Why compare t against EPSILON instead of 0?
Floating point: a ray spawned on a surface can re-intersect its own surface at t β‰ˆ 1e-16. Requiring t β‰₯ 1e-6 (quad.c, plane.c, cylinder_utils.c, cone, triangle) discards those self-hits β€” without it the render shows shadow-acne speckle.
Why compute a = dot(dir,dir) if the direction is normalized (a = 1)?
Defensive correctness: the formula stays valid for any direction vector; it costs one dot product and protects against a future caller that doesn't normalize.
Why must ray directions be normalized at all?
So t is a metric distance: nearest-hit comparisons across objects, the shadow light-distance check, and attenuation all assume t in world units (normalized at camera.c:73).
What happens when a ray is parallel to a plane?
denom = dot(dir, n) β‰ˆ 0 β†’ the division would blow up; we return a miss when |denom| < EPSILON (plane.c:21-23). Geometrically the ray never crosses the plane (or lies inside it β€” also treated as no visible hit).
Derive the cylinder intersection.
A point is on the tube iff its distance to the axis is r. Remove the axis component from D and from Oβˆ’C (cylinder_utils.c:23-26) β€” what remains is a plain circle quadratic in the perpendicular plane: a = dβŠ₯Β·dβŠ₯, b = 2Β·dβŠ₯Β·fβŠ₯, c = fβŠ₯Β·fβŠ₯ βˆ’ rΒ². Finite height: reject roots whose axial projection leaves [βˆ’h/2, +h/2]. Caps: two disks β€” plane hit + |P βˆ’ cap_center| ≀ r. Take the min of the three.
How do you know whether you hit the wall or a cap?
We never need a flag: body, top and bottom are tested independently, each fills its own normal (radial via body_normal, Β±axis for caps), and intersect_cylinder keeps the smallest t. Shading only consumes hit->normal.
Why does hit_body try both roots instead of quad_nearest?
A root valid on the infinite tube can be invalid on the finite one: the near root may land above/below the height range while the far root lands on the visible wall (tilted views, camera inside). Each root is validated with the height clamp before falling through (cylinder.c:26-35). Same pattern for the cone.
What if the camera is inside a sphere or cylinder?
root1 < 0 is rejected; root2 (the exit surface) is used. The front-face test (dot(dir, outward) < 0) fails, so the normal is negated to point back at the ray (sphere.c:17-29, cylinder_utils.c:73-77) and interior walls shade correctly β€” demo scenes/cylinder_inside.rt.
Cone: why can the ray hit β€œabove the apex”?
The algebraic cone is a double cone; the quadratic doesn't know about the apex. proj = n + tΒ·m < 0 identifies hits on the mirror half and rejects them (cone_utils_bonus.c:61-62).
Why does the cone normal contain a kΒ² term?
The surface is |perp|Β² βˆ’ kΒ²projΒ² = 0; the normal is its gradient ∝ perp βˆ’ kΒ²Β·projΒ·axis (cone_utils_bonus.c:50-51). A pure radial normal would be a cylinder normal and the shading would look flat-walled.
Explain MΓΆller–Trumbore in one sentence.
It solves the 3Γ—3 linear system O + tD = v0 + uΒ·e1 + vΒ·e2 by Cramer's rule using scalar triple products, rejecting as soon as a barycentric coordinate (u, then v, then u+v) leaves [0,1] β€” and the determinant's sign tells front from back face for free (triangle_bonus.c:25-50).
Where is the triangle's normal computed?
Once, at parse time β€” normalize(cross(v1βˆ’v0, v2βˆ’v0)) (parse_objects_bonus.c:52-54); render time only flips it by det's sign. Degenerate triangles are refused at parse (vec3_near_zero, :55).
How is the closest object chosen?
Linear scan of the object list in intersect_scene (intersect.c:25-35), tracking the minimum t. O(objects) per ray β€” no BVH/spatial structure, acceptable at miniRT scale.

Camera, render & shading

How does FOV change the image?
Viewport half-width = tan(fov/2) at unit distance (camera.c:42): a larger FOV β†’ wider viewport β†’ more scene per pixel β†’ zoom-out with perspective distortion at the edges; 179Β° β‰ˆ fisheye, 1Β° β‰ˆ telephoto. It's the horizontal angle; height follows from the aspect ratio.
Do you handle the camera looking straight up?
Yes β€” camera.c:25-28 switches the up-hint to (0,0,1) when |dir.y| β‰₯ 0.99, avoiding a zero cross product.
Explain your lighting model / Phong.
Three additive terms: ambient = object βŠ— (A_color Γ— ratio) (ambient.c:19-20); diffuse = object Γ— max(0, NΒ·L) Γ— brightness (diffuse.c:20-26) β€” Lambert's cosine law; specular (bonus) = white Γ— ks Γ— brightness Γ— (RΒ·V)^shininess (specular_bonus.c:31-37), where R is the light direction mirrored about the normal. Sum per light, gated by the shadow test, clamped at the end.
What is shadow acne and how do you avoid it?
Self-intersection at t β‰ˆ 0 from floating-point error when a shadow ray starts exactly on the surface. Avoided by offsetting the origin by SHADOW_BIAS (1e-4) along normal + light direction (shadow.c:23-25), and by all intersectors rejecting t < EPSILON.
Why do you clamp colors?
Several additive terms (multiple lights + specular) can exceed 1.0; unclamped values overflow the Γ—255 conversion (1.2Γ—255 = 306 bleeds into the wrong channel via the bit-shift packing). color_clamp at the end of shade guarantees each channel maps to 0-255.
Why multiply object color by light color?
Component-wise multiplication models physical absorption: a surface reflects each wavelength proportionally to its albedo, and the incoming light only contains its own spectrum. Red light (1,0,0) on a green object (0,1,0) β†’ black β€” physically right.
What happens if brightness > 1?
The parser rejects it (must be in [0.0, 1.0], parse_light*.c). Even if it entered, the final clamp bounds the pixel.
Where is the anti-aliasing?
render.c:58-76 β€” 2Γ—2 stratified supersampling: 4 rays per pixel at offsets 0.25/0.75, averaged.
Why is the void blue instead of black?
An aesthetic sky gradient in background() (render.c:19-29) β€” one line to make it black.
Why does the mandatory part ignore the light's color?
The subject states the L color field is unused in the mandatory part; the bonus applies it (shade_bonus.c:33).

MLX, events & build

Why write into an image instead of mlx_pixel_put?
One X request per pixel = 480 000 round-trips per frame vs one memory write per pixel + one blit. Also, pixels drawn before the window maps are lost, while an image persists (re-put by the Expose handler).
What is line_len β€” why not WIDTH*4?
Bytes per row including padding β€” the X server may align rows; hardcoding WIDTH*4 can shear the image. Same reason for bpp/8 instead of a literal 4.
What are 2, 17, 12 and the masks in mlx_hook?
X11 event types (KeyPress, DestroyNotify, Expose) with their event masks (1L<<0 KeyPressMask, 1L<<15 ExposureMask; DestroyNotify needs mask 0 β€” it's delivered regardless).
ESC vs the red cross?
Both funnel into close_handler (mlx_hooks.c:70): destroy image β†’ window β†’ display β†’ free display ptr β†’ free_scene β†’ exit(0).
What if X isn't running / mlx_init fails?
NULL-checked at mlx_init.c:18; prints Error\nmlx_init failed to stderr, exit 1 β€” verified, no segfault.
Why doesn't the window go blank when I minimize or cover it?
The Expose handler re-blits the stored image without re-raytracing.
Any relink?
No β€” a second make does nothing (β€œNothing to be done”).

14 Β· defenseThe final 24 hours

  1. Hour 0-1 β€” read Β§00 + Β§02 twice. Draw the pipeline from memory once.
  2. Hour 1-3 β€” parser. Open parse_scene.c and follow one line of a real scene through read_line β†’ split_line β†’ dispatch β†’ parse_sphere with this guide beside you. Rehearse next_component out loud.
  3. Hour 3-6 β€” intersections. Re-derive on paper: the sphere quadratic, plane t, the cylinder perpendicular decomposition. Then read the code and match each line to your derivation. The cylinder is the most-asked topic β€” overlearn it.
  4. Hour 6-8 β€” camera + render. Recompute half_w for FOV = 70 on paper; explain the y-flip and the 4-ray averaging.
  5. Hour 8-10 β€” shading. Learn the ambient formula (it was a fix β€” evaluators love β€œwhy did this change”). Walk shade_bonus with a two-light scene.
  6. Hour 10-11 β€” run everything: make re && make bonus, every scene in scenes/ and scenes/bonus/, valgrind on a good and a bad scene, norminette.
  7. Hour 11-12 β€” decide about Β§12 bug #1 (fix it or prepare to own it).
  8. Remaining β€” drill Β§13 with a partner or a rubber duck; sleep.

Live-demo checklist

  • ./miniRT scenes/full_mandatory.rt β€” the main demo.
  • ./miniRT scenes/cylinder_inside.rt β€” camera inside a cylinder works.
  • ./miniRT scenes/min_shadow.rt β€” hard shadow + correct ambient in shadow.
  • Bad inputs ready to type: ./miniRT, ./miniRT x.txt, a scene with 1,2,, RGB 256, FOV 180, duplicate C, missing L.
  • ./miniRT_bonus scenes/bonus/showcase.rt (+ the others) for the bonus tour.
  • Press SPACE to unlock the camera before demoing WASD/scroll; demo WASD only β€” arrow keys are not handled.
  • valgrind --leak-check=full output pre-captured for a good and a bad run (and settle Β§12 bug #1 first β€” valgrind on the bonus flags it today).

15 Β· defenseGlossary

TermMeaning here
RayP(t) = O + tΒ·D β€” origin O, unit direction D; t is the world-space distance along it.
DiscriminantbΒ² βˆ’ 4ac of a quadratic; its sign says whether the ray's line misses (βˆ’), grazes (0), or crosses (+) the surface.
NormalUnit vector perpendicular to the surface at the hit point; in this code it always faces against the incoming ray.
front_face1 if the ray hit the surface from outside, 0 from inside (normal was flipped).
Lambert / diffuseMatte reflection: intensity ∝ cos θ = max(0, N·L). View-independent.
Phong specularGlossy highlight: (RΒ·V)^shininess with R = reflection of the light direction about N. View-dependent.
AmbientConstant fill light approximating indirect bounce; here object βŠ— (A_color Γ— ratio).
Shadow acneSpeckle caused by a shadow ray re-hitting its own surface at t β‰ˆ 0; cured by bias + EPSILON.
AttenuationDistance falloff 1/(kc + klΒ·d + kqΒ·dΒ²); neutral (1.0) in the mandatory part.
Barycentric coords(u, v) weights locating a point inside a triangle; containment ⟺ u β‰₯ 0, v β‰₯ 0, u+v ≀ 1.
MΓΆller–TrumboreRay–triangle test solving the barycentric system directly with scalar triple products.
SupersamplingAnti-aliasing by averaging several rays per pixel (here 2Γ—2).
Tagged unionA struct holding an enum tag + a union of shape structs β€” one type for all objects.
FOVHorizontal field of view; viewport half-width = tan(fov/2) at unit distance.
Solid textureA pattern computed from the 3D hit point itself (our checkerboard), no UV unwrapping.
MiniLibX42's tiny X11 wrapper: window, image buffer, event hooks. The only allowed graphics lib.