Repository navigation
Assignment 01_Raytracer
Released: Oct 1, 2026 Due: Oct 30, 11:59 pm, to be submitted through MS Teams
In this assignment you extend the ray tracer from the recursive ray tracing recitation into a renderer capable of producing visually compelling images, using the Cornell Box as a reference scene. You will:
- Complete a core set of features that everyone implements.
- Choose extra features from a a set of features.
- Design and render an original art scene [optional; completing this part will earn bonus points].
The goal is correct light transport first, and good-looking images second. A beautiful image built on wrong physics will lose marks. A correct but plain image will not get full marks for the art scene.
The assignment project is called cornellbox. I have provided some necessary helper code and input files in the directory cornellbox. You may use that code for your assigment or build using your recitation code from the raytrace. You may restructure the code as you see fit.

Your first goal is to render the Cornell box below. It is the image everyone is graded against, and it exercises every other core requirement. Build the scene first, then add C2 to C6 until it renders correctly.
Save it as cornell.png: resolution 800 × 800, supersampling at least 4 × 4, maximum recursion depth 5.
Same setup as Recitation 02: an eye point and an image plane given by its lower left corner (llc) and upper right corner (urc).
| Parameter | Value |
|---|---|
| Eye position | (0, 1, 3.8) |
| Image plane lower left corner (llc) | (-0.364, 0.636, 2.8) |
| Image plane upper right corner (urc) | (0.364, 1.364, 2.8) |
| Background color (rays that miss) | black (0, 0, 0) |
The image plane is 1 unit in front of the eye, so the camera looks along -z at the centre of the box.
To place the camera anywhere and aim it in any direction, see feature F2.
The box is 2 units on each side: x from -1 to 1, y from 0 to 2, z from -1 to 1. The front (z = 1) is open and faces the camera. Build each wall from two triangles. Normals point into the box.
| Surface | Corners | Normal | Material |
|---|---|---|---|
| Floor (y = 0) | (-1,0,-1), (1,0,-1), (1,0,1), (-1,0,1) | (0, 1, 0) | white |
| Ceiling (y = 2) | (-1,2,-1), (-1,2,1), (1,2,1), (1,2,-1) | (0, -1, 0) | white |
| Back wall (z = -1) | (-1,0,-1), (-1,2,-1), (1,2,-1), (1,0,-1) | (0, 0, 1) | white |
| Left wall (x = -1) | (-1,0,1), (-1,2,1), (-1,2,-1), (-1,0,-1) | (1, 0, 0) | red |
| Right wall (x = 1) | (1,0,-1), (1,2,-1), (1,2,1), (1,0,1) | (-1, 0, 0) | green |
| Light panel (y = 1.99) | (-0.3,1.99,-0.3), (0.3,1.99,-0.3), (0.3,1.99,0.3), (-0.3,1.99,0.3) | (0, -1, 0) | emissive |
Split each quad (A, B, C, D) into triangles (A, B, C) and (A, C, D).
The light panel is only there so the light is visible in the image. It is not shaded: any camera or secondary ray that hits it returns white (1, 1, 1) directly. Do not test it in shadow rays.
| Sphere | Center | Radius | Material |
|---|---|---|---|
| Mirror | (-0.45, 0.35, -0.35) | 0.35 | mirror |
| Glass | (0.45, 0.35, 0.35) | 0.35 | glass |
Both spheres rest on the floor. The mirror sphere sits back left and the glass sphere sits front right.
A point light at (0, 1.95, 0) with intensity I = (1, 1, 1), just below the light panel. Ambient intensity I_a = (1, 1, 1).
Notation follows Marschner & Shirley: k_a ambient, k_d diffuse, k_s specular, p Phong exponent, k_m mirror reflectance, n index of refraction.
| Material | k_a | k_d | k_s | p | k_m | n |
|---|---|---|---|---|---|---|
| White | 0.1 · k_d | (0.73, 0.73, 0.73) | 0 | n/a | 0 | n/a |
| Red | 0.1 · k_d | (0.63, 0.06, 0.05) | 0 | n/a | 0 | n/a |
| Green | 0.1 · k_d | (0.14, 0.45, 0.09) | 0 | n/a | 0 | n/a |
| Mirror | 0 | (0.05, 0.05, 0.05) | (0.5, 0.5, 0.5) | 200 | (0.9, 0.9, 0.9) | n/a |
| Glass | 0 | 0 | (0.5, 0.5, 0.5) | 200 | Fresnel (C3) | 1.5 |
The walls are Lambertian (no specular term). The glass has no diffuse color: its appearance comes entirely from reflection and refraction.
When your render is correct you should see: the red and green walls reflected in the mirror sphere, a flipped and magnified view of the room through the glass sphere, a brighter reflective rim near the edges of the glass sphere (Fresnel), and hard shadows from both spheres on the floor (see Fig. 1).
- Trace reflected rays for reflective materials and refracted rays for transparent materials.
- Use Snell's law for the refraction direction. Each transparent material has an index of refraction
n(for example, glass 1.5, water 1.33). - Track whether a ray is entering or leaving an object by checking the sign of
d · n. Flip the normal and swap the indices when leaving. - Cap the recursion depth. Make the maximum depth a parameter (default 5).
- Offset secondary ray origins by a small epsilon along the ray direction or normal to avoid self-intersection ("shadow acne").
-
Use Schlick's approximation to decide how much light is reflected versus refracted:
R(θ) = R0 + (1 - R0)(1 - cos θ)^5, whereR0 = ((n1 - n2) / (n1 + n2))^2 -
Blend the reflected and refracted colors with weights
Rand1 - R. -
Your glass should look noticeably more mirror-like at grazing angles, especially near the edges of a sphere.
- When the term under the square root in Snell's law is negative, there is no refracted ray. Return full reflection instead.
- This matters for rays leaving glass at steep angles. Without it you will see black rings inside glass spheres.
- Implement ray-triangle intersection using barycentric coordinates and Cramer's rule, as in Marschner & Shirley Section 4.4.2.
- A hit is valid when β ≥ 0, γ ≥ 0, β + γ ≤ 1, and t lies in the valid ray interval.
- Each triangle carries its own material, so the left wall can be red and the right wall green.
- Implement jittered supersampling: split each pixel into an
n × ngrid and shoot one ray through a random point in each cell. - Average the results. Make
na parameter. - Show a before/after comparison of an edge in your report.
Each feature carries 5, 10 or 15 marks, depending on how much work it takes. Choose features whose marks add up to 20. Marks beyond 20 are not counted, so there is no benefit in doing more.
| Feature | Marks |
|---|---|
| F1. Soft shadows from an area light | 10 |
| F2. Look-at camera with a field of view | 5 |
| F3. Checkerboard floor | 5 |
| F4. Glossy reflection | 10 |
| F5. Colored glass with Beer-Lambert absorption | 5 |
| F6. Triangle meshes and a BVH | 15 |
| F7. Thin-film interference | 10 |
For example: F6 and F3 (15 + 5), F1 and F4 (10 + 10), or F7, F2 and F5 (10 + 5 + 5).
For each feature, submit one render that isolates it clearly (named feature_<name>.png), and explain in your report how you implemented it.
Some of these features are covered in upcoming recitations. You are welcome to integrate that code, but the implementation in your ray tracer and the explanation in your report must be your own.
Replace the point light with an area light. Use the light panel from C1 (the 0.6 × 0.6 square at y = 1.99) as the light.
- Split the panel into an
n × ngrid and pick one random point in each cell (jittered sampling, the same idea as C6). Makena parameter; 4 × 4 is a good default. - For each sample point, trace a shadow ray and compute the diffuse and specular terms as if a point light sat there.
- Average the results over all samples. Add the ambient term once, not once per sample.
A shading point that sees the whole panel is fully lit, one that sees none of it is in umbra, and one that sees part of it gets part of the light: the penumbra. Your shadows should be sharp near the contact point with the floor and softer further away.
Expect render time to go up several times, since every shading point now traces n × n shadow rays.
Test render: the Cornell box with the area light, next to the point light version from C1.

The C1 camera only works when the image plane is parallel to the xy-plane and the eye looks along -z. Replace it with a general camera that can be placed anywhere and aimed at anything. It is described by:
| Parameter | Meaning |
|---|---|
| eye | position of the camera |
| lookAt | the point the camera aims at |
| up | a rough "up" direction, usually (0, 1, 0) |
| θ | vertical field of view in degrees |
The look-at point is a point, not a direction. The viewing direction is the vector from the eye to it: lookAt - eye. Describing a camera by eye, look-at point, up vector and field of view is common in computer graphics. OpenGL's gluLookAt and most 3D modelling tools use the same convention.
Camera frame (Marschner & Shirley Section 4.3). Build three perpendicular unit vectors:
-
w = (eye - lookAt) / |eye - lookAt|points backwards, away from what the camera sees. -
u = (up × w) / |up × w|points to the right. -
v = w × upoints up in the image.
From field of view to llc and urc. Put the image plane at distance d = 1 in front of the eye. Its half-height h follows from the field of view:
h = d · tan(θ / 2)
and its half-width is h · (image width / image height), which is just h for a square image. A wider angle (larger θ) shows more of the scene, like a wide-angle lens.
In the C1 camera, the corners were given directly. For the Cornell box, θ = 40° gives h = tan 20° ≈ 0.364, which is where the C1 values llc = (-0.364, 0.636, 2.8) and urc = (0.364, 1.364, 2.8) come from.
With a general frame, the corners are no longer axis-aligned, so write them with u, v and w:
llc = eye - d·w - h_w·u - h·v
urc = eye - d·w + h_w·u + h·v
where h_w is the half-width. The pixel at row i, column j is then
pixelPos = llc + (2 h_w) · x · u + (2 h) · y · v
with x = (j + 0.5) / width and y = 1 - (i + 0.5) / height (row 0 is the top). Replace the 0.5 with your jittered offsets from C6.
Test render: two images. First, check your camera with eye = (0, 1, 3.8), lookAt = (0, 1, 0), up = (0, 1, 0), θ = 40°. It must match your C1 render. Second, render the Cornell box from a new viewpoint of your choice, and state the eye, look-at point and field of view in your report. This camera is also the easiest way to frame your art scene.
Give the floor a procedural checkerboard texture. Compute the colour from the 3D hit point, not from a stored image: for a floor in the xz-plane, alternate between two colours based on whether floor(x / s) + floor(z / s) is even or odd, where s is the square size (for example, s = 0.25 gives an 8 × 8 board). Use the checker colour as k_d (and k_a) in Blinn-Phong.
Test render: the Cornell box with a checkerboard floor, so the pattern also shows in the mirror and through the glass sphere.

A perfect mirror reflects each ray in exactly one direction. Real metals are slightly rough, so their reflections are blurred. Model this with a roughness parameter a per material (Marschner & Shirley Section 13.4.4):
- Compute the mirror direction
ras in C2. - Build an orthonormal basis around
r: two unit vectorsuandvperpendicular torand to each other (Section 2.4.6). - Pick two random numbers ξ1, ξ2 in [0, 1) and perturb the direction inside a small square of side
a:r' = r + (-a/2 + ξ1 a) u + (-a/2 + ξ2 a) v, then normalize. - If
r'points below the surface (r' · n < 0), try again or fall back tor.
With a = 0 you get the ordinary mirror back. Larger a gives blurrier reflections.
Trace only one glossy ray per hit. Averaging happens through your C6 pixel samples: each of the n × n primary rays picks its own random glossy direction. If you instead traced several glossy rays at every hit, the number of rays would multiply at each bounce and your render would take far too long. You will likely need more pixel samples (for example 8 × 8) to keep the blur smooth.
Test render: replace the two C1 spheres with three spheres of radius 0.28 in a row at (-0.6, 0.28, -0.1), (0, 0.28, -0.1) and (0.6, 0.28, -0.1). Use the mirror material from C1 with roughness 0.05, 0.2 and 0.5 from left to right. The difference is easier to see if you also turn on the checkerboard floor (F3).

Instead of tinting glass with a flat color, attenuate light by the distance it travels inside the object (Beer's law, Marschner & Shirley Section 13.1):
T = exp(-σ · d)
where σ is a per-channel absorption coefficient and d is the path length inside the medium. Thick parts of an object look darker and richer in color than thin parts. This applies only to transparent materials. Opaque surfaces such as the mirror sphere have no inside path, so they are not affected.
Choosing σ. Give the glass material a coefficient σ, one value per channel, in units of 1 / (scene unit). A large value absorbs that channel quickly. σ = 0 is clear glass. For amber, absorb blue strongly and red hardly at all, for example σ = (R 0.1, G 0.6, B 3.0). Remember the code stores colours in BGR order.
As a sanity check, a ray straight through the centre of the C1 glass sphere travels d = 0.7, so it keeps about 93% of its red, 66% of its green and 12% of its blue.
Where to apply it.
- When a ray hits a glass surface from the inside (the same test you use in C2 to flip the normal), it has just travelled a distance
d = tthrough the glass, wheretis the hit distance (with a normalized direction). - Multiply everything that hit returns (reflected, refracted and highlight together) by
T, channel by channel. That colour is the light that travelled back along the inside segment, so all of it was absorbed along the way. - Do nothing when a ray hits glass from the outside. That ray travelled through air.
- Rays that bounce around inside through total internal reflection are attenuated again on each inside segment, automatically.
A common mistake is to attenuate at the entering hit instead. At that point you do not yet know how far the ray will travel inside.
Shadows stay as in the core: the glass still casts an opaque shadow.
Test render: the C1 Cornell box with σ added to the glass sphere, next to your C1 render. The sphere should turn amber, deeper in colour near the centre, where light passes through the most glass, and lighter towards the rim.

Replace the two spheres with the Stanford bunny, a scanned mesh of 69,451 triangles. Rendering it by testing every triangle for every ray is far too slow, so you will build a bounding volume hierarchy (BVH) to speed up intersection (Marschner & Shirley Section 12.3).
Provided files (on Teams):
-
assets/bunny_cornell.obj: the Stanford bunny (Stanford 3D Scanning Repository), already scaled, rotated and moved to stand on the floor in the middle of the Cornell box. Use it as is. -
ObjLoader.h: a small header with one function,loadOBJ(filename, vertices, faces). It fills a list of vertices and a list of triangles, each triangle being three vertex indices (starting at 0). Put it next tomain.cpp.
The provided cornellbox/CMakeLists.txt already defines ASSETS_DIR as the full path to cornellbox/assets, so the program finds the file no matter where you run it from. Open it as std::string(ASSETS_DIR) + "/bunny_cornell.obj".
Step 1: load the mesh. Turn each face into one of your C5 triangles. Compute its normal from the vertex order: n = (b - a) × (c - a), normalized. Give the bunny a diffuse material of your choice (no mirror, no glass). A scanned mesh is not always perfectly oriented, so when shading, flip the normal if it points away from the incoming ray (n · d > 0).
Step 2: build the BVH. Put all triangles (bunny and walls) into a binary tree of axis-aligned bounding boxes:
- Each node stores a box that encloses all its triangles.
- To build a node: compute the box of its triangles. If it has 4 or fewer triangles, make it a leaf. Otherwise, find the longest axis of the box, sort the triangles by their centroid along that axis (
std::nth_elementis enough), split them at the median into two halves, and build a child node from each half. - Store the nodes in a
std::vectorand refer to children by index.
Step 3: traverse the BVH.
- Ray-box test: use the slab method (Section 12.3.1). For each axis, compute where the ray enters and leaves the two planes of the box, keep the largest entry and the smallest exit, and report a hit if entry ≤ exit. Precompute
1 / donce per ray. - Closest hit: start at the root. If the ray misses a node's box, or only reaches it beyond the closest hit found so far, skip the node and everything below it. At a leaf, test its triangles with your C5 function. Otherwise visit both children.
- Shadow rays only need to know whether anything blocks the light, so return as soon as one blocker is found.
Timing. Render the bunny scene at 200 × 200 with 1 sample per pixel, once with the BVH and once without (testing every triangle). Report both times in your report. Expect a speed-up of several hundred times. The full 800 × 800 render with 4 × 4 samples is only practical with the BVH.
Test render: the Cornell box with the bunny in place of the spheres, 800 × 800, 4 × 4 samples, plus the timing table.

Replace the glass sphere with a floating soap bubble that shows rainbow-coloured bands. The colours come from interference: light reflects from both the outer and inner surface of a very thin film of soapy water, and the two reflected waves add up or cancel depending on the film thickness and the wavelength. This feature is not covered in class, so the hints below give you everything you need.
Scene. Keep the C1 box and mirror sphere. Replace the glass sphere with a bubble of radius 0.35 centred at (0.4, 0.8, 0.3). Film index of refraction n = 1.33.
Hint 1: one wavelength per channel. A ray tracer normally ignores wavelength. Here, compute the reflectance separately for each colour channel, using one wavelength each: λ = 650 nm for red, 550 nm for green, 450 nm for blue. Remember the code stores colours in BGR order.
Hint 2: film thickness. The film is only a few hundred nanometres thick, so you never trace rays through it. It is just a number that varies over the surface. A bubble drains downwards, so make it thin at the top and thick at the bottom. Let h go from 0 at the bottom of the sphere to 1 at the top:
h = (y - (c_y - r)) / (2r), d = 900 + (150 - 900) · h (in nm)
A small wobble, for example adding 0.08 sin(9x + 5z) to h before using it, makes the bands less perfectly horizontal and more natural.
Hint 3: angle inside the film. At a hit, make the normal face the incoming ray (flip it if n · d > 0, since rays hit the bubble from outside and from inside). Then cos θi = -d · n. The angle inside the film follows from Snell's law:
cos θt = sqrt(1 - (1 - cos² θi) / n²)
Hint 4: Fresnel amplitudes. At one air-film surface, the Fresnel amplitude coefficients for the two polarizations are
rs = (cos θi - n cos θt) / (cos θi + n cos θt)
rp = (n cos θi - cos θt) / (n cos θi + cos θt)
These are the exact Fresnel equations that Schlick's approximation (C3) simplifies. You need both, because the interference formula below uses amplitudes, not reflectances.
Hint 5: phase difference and interference. The wave reflected from the inner surface travels an extra distance through the film, which shifts its phase by
δ = 4π · n · d · cos θt / λ
For a film with air on both sides, the reflectance for one polarization is
R(r, δ) = 2r² (1 - cos δ) / (1 + r^4 - 2r² cos δ)
Compute it with r = rs and with r = rp, and average the two (sunlight and lamps are unpolarized). Do this for each of the three wavelengths to get a reflectance per channel. Check: when δ is a multiple of 2π, R = 0 (the waves cancel); in between, colours appear.
Hint 6: reflection and transmission. The film is too thin to absorb light, so the transmitted part is T = 1 - R, per channel. Because the two sides of the film are parallel, the transmitted ray does not bend: it continues in the same direction. So at each bubble hit:
- reflected colour: trace the mirror direction (C2),
- transmitted colour: trace the same direction, starting just past the surface (
point + ε · d), - result:
R * reflected + T * transmitted, channel by channel (cwiseProductin Eigen), plus a Blinn-Phong highlight if you like.
A ray passing through the bubble hits it twice (front and back), and each hit gets its own thickness, so the colours from both sides mix, just like a real bubble.
Hint 7: shadows. A bubble lets almost all light through, so skip it in your shadow rays (the same way you skip the light panel). Otherwise a black disc appears on the floor under the bubble.
Expect subtle colours, strongest near the rim, where the angle is large. That is what real bubbles look like.
Test render: the Cornell box with the mirror sphere and the bubble, 800 × 800, 4 × 4 samples. Also include a close-up crop of the bubble in your report.

Design and render one original scene at 1920 × 1080. Use as many of your features as you like. Put effort into composition, lighting, and materials.
Save it as art.png and give it a title.
If you need ideas, here are some themes. You are free to ignore them.
-
Still life in glass: bottles, lenses, and a prism on a wooden table.

-
Harbour at dusk: a reflective water plane, boats built from primitives, a warm sky.

-
Desk toys: a Newton's cradle, marbles, a glass paperweight.

Provided meshes. The assets/art/ folder on Teams has simple meshes you can load with ObjLoader.h:
| File | Description |
|---|---|
wine_bottle.obj |
hollow glass bottle, 1.0 tall, standing on y = 0 |
wine_glass.obj |
wine glass, 0.8 tall, standing on y = 0 |
lens.obj |
biconvex lens, radius 0.3, axis along y (rotate it to stand it up) |
prism.obj |
triangular prism, side 0.4, length 0.5 |
dome.obj |
solid glass half-ball (paperweight), radius 0.25 |
boat_hull.obj |
small boat hull with a deck, length 2 along x |
cylinder.obj, cone.obj, box.obj
|
unit primitives: scale them into posts, masts, towers, tables and bases |
The glass meshes are closed, with normals pointing out of the glass, so your C2 refraction works on them directly. The hollow bottle and wine glass have an inner and an outer surface, like real glassware.
Tips for the art scene.
- Placing a mesh: after loading, scale, rotate and translate each vertex before you build the triangles, then compute the normals from the moved vertices.
- Smooth shading (optional): the curved meshes have shared vertices. Averaging the face normals at each vertex and blending them across each triangle with the barycentric coordinates from C5 makes glass look much smoother. Use the flat triangle normal to decide whether a ray is entering or leaving glass.
- Flat bases: if a flat mesh base lies exactly in the plane of your table, rays cannot tell which surface is closer and you get speckles. Put the table a tiny bit lower (for example y = -0.003).
- Wide images: the F2 camera handles the 16:9 shape through the aspect ratio. With the C1 camera, make your image plane 16:9 as well.
- Glass needs something to refract: glass against a black background looks black. A light backdrop behind glass objects shows off refraction much better.
- Render time: test at a small size (for example 480 × 270, 1 sample) and only render the final image at full size.
We will hold a class vote on your best rendered images. The top two receive a small bonus (see marking). With your permission, selected images may be shown on the course or department webpage.
Submit a short report (report.pdf, about 2 to 4 pages) containing:
-
A list of the menu features you implemented.
-
For each feature: a short explanation of your approach, the test render, and any known problems.
-
The anti-aliasing before/after comparison.
-
A table of render times for the Cornell box and art scene, with resolution, samples per pixel, and maximum recursion depth. Include your CPU and RAM specs.
-
The title of your art scene and a sentence or two about it.
-
AI declaration: which parts you used AI tools for and the prompts you used, as required by the course AI policy. If you did not use any, say so.
-
References: use the ACM Transactions on Graphics (ACM TOG) reference style. Examples:
Journal article:
Matthias Zwicker, Hanspeter Pfister, Jeroen van Baar, and Markus Gross. 2001. Surface splatting. Proceedings of SIGGRAPH 2001, 371–378.
Book:
Tomas Akenine-Möller, Eric Haines, Naty Hoffman, Angelo Pesce, Michał Iwanicki, and Sébastien Hillaire. 2018. Real-Time Rendering (4th ed.). CRC Press.
Conference paper:
Tero Karras, Samuli Laine, and Timo Aila. 2019. A style-based generator architecture for generative adversarial networks. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 4401–4410.
Website / online resource:
Matt Pharr, Wenzel Jakob, and Greg Humphreys. 2023. Physically Based Rendering: From Theory to Implementation (4th ed.). Available at https://pbr-book.org/. Accessed September 2026.
For in-text citations, use the author-year style, for example:
[Pharr et al. 2023]orPharr et al. [2023].
Submit one zip file on MS Teams named lastname_A1.zip containing:
lastname_A1/
├── cornellbox/ source code and CMakeLists.txt (no build folder)
├── renders/
│ ├── cornell.png
│ ├── art.png
│ └── feature_<name>.png one per menu feature
└── report.pdf
Your code must build with the course CMake setup. Do not include the build folder, the external libraries, or the meshes provided with the assignment. If you use any other meshes that your code needs to run, put them in cornellbox/assets/ and include them in your submission. If such a mesh is too large to include, share it through a link in your report.
| Component | Marks |
|---|---|
| Core C1: Cornell box render | 15 |
| Core C2 to C6 (correctness) | 50 |
| Feature menu | 20 |
| Report | 10 |
| Code quality (readable, commented, builds cleanly) | 5 |
| Total | 100 |
| Art scene (creativity, composition, use of features) bonus | +5 |
| Class vote bonus (top two) | +5 |
The core implementation is worth 80 marks. The remaining 20 marks can be earned by implementing additional features. Each feature carries a different number of marks, so you may choose the feature(s) you wish to implement, provided they add up to a maximum of 20 marks.
To receive credit for your implementation, you must successfully complete the demonstration and viva. During the demonstration, you will be asked questions about your code, implementation choices, and the graphics concepts involved. The goal is to verify that you understand what you have implemented, rather than simply that the program produces the expected output.
If your understanding cannot be demonstrated convincingly during the first attempt, you may be asked to repeat the demonstration one more time after reviewing the relevant concepts. There will be no third attempt.
You are welcome to use AI tools as a learning and development aid. However, make sure that you understand the underlying concepts and any code you incorporate into your solution. Before using AI-generated code, take the time to understand how and why it works, then implement and test it yourself. This will be particularly important during the demonstration and viva, where you will be expected to explain your implementation and answer questions about it.
- Get the core right before anything else. Every menu feature builds on a correct Cornell box.
- Render small while debugging. Use 200 × 200 with no supersampling until things look right.
- Build in Release mode for final renders. Debug builds can be 10 to 50 times slower.
- Visualize intermediate values. Render normals as colors, or render only the Fresnel term, when something looks wrong.
- Start early. Features like soft shadows and glossy reflection multiply your render time. Leave time for final renders.
This assignment builds on the ray tracer assignment from the previous offering of this course. Its requirements, features and marking scheme were modified by the course instructor, and the physics, the reference solution and the reference renders were checked by the instructor. In preparing the wiki page and supporting material, LLMs (Claude and ChatGPT) were used as assistants: for drafting and editing parts of the text, and for generating some of the helper code (for example ObjLoader.h) and the procedural meshes in assets/art/. All AI-assisted material was reviewed, tested and revised before release.
The Stanford bunny (bunny_cornell.obj) is from the Stanford 3D Scanning Repository, rescaled and repositioned for this assignment. The notation and methods follow Marschner and Shirley, Fundamentals of Computer Graphics.
This is also a fair model for your own use of AI tools: they can help, but you remain responsible for understanding, testing and explaining everything you submit. Declare your use in your report as described in Section 5.