I typically write a raytracer while attempting to stick to a small set of "best-practices" for the language I'm trying to learn. I also use this exercise from time-to-time to test new language features (for example C++20 ranges/coroutines recently).
I also recommend this same exercise to others through the book "The Ray Tracer Challenge" [1] by Jamis Buck, because it describes an entire implementation of a raytracer in pseudocode. Beginners mostly just need to plug-in their language of choice & still end up with a satisfying visual result. Experienced programmers can extend it by adding the ability to load non-trivial .obj models, which will necessarily motivate adding concurrency, bounding volumes, & other general performance improvements.
I'm planning on tackling it again in Elixir soon, which should be fun & interesting.
EDIT: Almost forgot to mention the free bonus chapters [2] that get you started on bounding volumes (AABB), soft area lights, and texture mapping.
[1] https://pragprog.com/titles/jbtracer/the-ray-tracer-challeng...
I'm currently going through this book and it's amazing. Any tips on where to go once I finish it?
[1] http://www.raytracerchallenge.com/#bonus
[2] https://raytracing.github.io/