The Whispering Mills

By Xuanyi Lyu
🏆 Highlight Submission

This project is an 8-second ray-traced animation of a pastoral scene featuring two papercraft windmills. I started with a static 3D model and built a complete rendering pipeline around it. The animation transitions from day to night, showing windmill blades that spin during the day and gradually stop as darkness falls, drifting clouds that give way to a starlit sky, and dynamic lighting with shadows. A slow camera dolly brings the viewer closer to the scene. The animation runs at 1920×1080 resolution and 60 fps, with background music.

What I Built

The core challenge was taking a static mesh and bringing it to life. I implemented several major features to make this happen:

1. Mesh Loading and Rendering

I wrote a custom OBJ/MTL loader (src/obj_loader.{h,cpp}) that handles vertex positions, normals, texture coordinates, materials, and texture images. The loader supports polygon triangulation and resolves texture paths relative to the model directory. For rendering, I built a ray tracer that samples textures (nearest-neighbor interpolation) and computes Blinn-Phong shading with directional lighting.

2. BVH Acceleration

To make ray tracing practical for a high-poly model (11197 triangles), I implemented a bounding volume hierarchy (src/bvh.{h,cpp}). The BVH uses median splits along the longest axis and iterative traversal. This brought render times down from minutes per frame to a few seconds.

3. Windmill Animation

The windmill blades needed to rotate realistically without drifting or clipping through the structure. I grouped triangles by their texture names (e.g., “wing”) and computed a separate pivot point and rotation axis for each fan. The pivot is the centroid of the fan’s vertices, and the axis is estimated using PCA (the eigenvector with smallest variance). Each frame, I apply a Rodrigues rotation to the blade vertices and normals, then rebuild the BVH so shadows and intersections stay correct. The rotation speed smoothly decelerates as night approaches—the blades spin at full speed during the day, then gradually slow down through dusk and come to a complete stop at night. This logic is in src/main.cpp.

4. Day-Night Transition and Sky

The animation smoothly transitions from day to night over its duration. The sky is rendered as an emissive background plane that changes appearance based on the time of day. During daytime, clouds drift across the sky by scrolling the texture UV coordinates over time (emissive_uv_offset). As night falls, the sky fades into a deep blue-purple gradient with procedurally generated stars. The stars are positioned using a hash function applied to the original UV coordinates, so they remain fixed in place rather than drifting with the clouds. Each star has a soft Gaussian falloff to avoid aliasing artifacts at high frame rates. I added separate exposure and gain controls for emissive materials to keep the sky bright without overexposing the ground.

5. Lighting and Shadows

I implemented dynamic lighting that changes throughout the animation. During the day, a directional sun provides the main illumination, with hard shadows cast using shadow rays through the BVH. As the sun sets, its intensity decreases and its color shifts from white to warm orange. At night, a cool-toned moon takes over as the primary light source. To avoid overly dark shadows, I added a small ambient term that transitions from warm daytime colors to cooler nighttime tones, plus an optional “sky fill” that adds soft light to upward-facing surfaces. The final image goes through a filmic tone mapper (Hable/Uncharted 2 curve) to compress the dynamic range. All of this is in src/scene.cpp.

6. Camera and Output

The camera performs a slow dolly-in over the 8-second animation. I render at 1920×1080 and 60 fps, writing each frame as a PPM file. A simple bitmap font renderer (src/image.{h,cpp}) draws the title card overlay for the first 2.5 seconds. At the end of the run, the program prints the ffmpeg command to encode the frames into an MP4.

To help find the right camera angles, I also built an interactive preview tool (preview.cpp) that uses GLFW to open a window where I can adjust all camera parameters with keyboard controls and see the result in real time.

7. Performance

Rendering is multithreaded (one thread per row chunk). On my machine, each frame takes a few seconds. The BVH is rebuilt every frame to account for the moving windmill blades, but the cost is acceptable thanks to the simple median-split construction.

What I Contributed

The 3D model and textures are from an external source (see Acknowledgements), but all the code and animation logic are my own work:

Acknowledgements