Hello Scene — All the pretty little things
Wait, what? Well, yah, why not?
One of the joys I’ve had as a programmer over the years has been to read some paper, or some article, and try out the code for myself. Well, ray tracing has been a love of mine since the early 90s, when I first played with POV Ray.
Back in the day, Peter Shirley introduced ray tracing to an audience of eager programmers through the book: Ray Tracing in One Weekend. There were two subsequent editions that followed, exploring various optimizations and improvements. For the purposes of my demo scene here, I wanted to see how hard it was to integrate, and how big the program would be. So, here’s what the integration looks like:
#include "scene_final.h" #include "gui.h" scene_final mainScene; void onFrame() { if (!mainScene.renderContinue()) { recordingStop(); halt(); } } void setup() { setCanvasSize(mainScene.image_width, mainScene.image_height); mainScene.renderBegin(); recordingStart(); }
Just your typical demo scene, using the gui.h approach. I implement setup(), to create the screen size, initializer the raytrace renderer, and begin the screen recording.
In ‘onFrame()’, I tell the renderer to continue, as it will render only one scanline at a time on the canvas. It will return false when there are no more lines to be rendered, and that’s when I just stop the program. How did I get the screen capture? Just comment out that ‘halt()’ for one fun, then take a screen snapshot.
I did have to make two alterations to the original Raytrace Weekend code. One was to the scene renderer. I had to split out the initialization code (for convenience), and I had to break the ‘render()’ into two parts, the ‘begin()’ and ‘continue()’.
class scene { public: hittable_list world; hittable_list lights; camera cam; double aspect_ratio = 1.0; int image_width = 100; int image_height = 100; int samples_per_pixel = 10; int max_depth = 20; color background = color(0, 0, 0); int fCurrentRow = 0; public: void init(int iwidth, double aspect, int spp, int maxd, const color& bkgd) { image_width = iwidth; aspect_ratio = aspect; image_height = static_cast<int>(image_width / aspect_ratio); samples_per_pixel = spp; max_depth = maxd; background = bkgd; } bool renderContinue() { if (fCurrentRow >= image_height) return false; int j = image_height - 1 - fCurrentRow; color out_color; for (int i = 0; i < image_width; ++i) { color pixel_color(0, 0, 0); for (int s = 0; s < samples_per_pixel; ++s) { auto u = (i + random_double()) / (image_width - 1); auto v = (j + random_double()) / (image_height - 1); ray r = cam.get_ray(u, v); pixel_color += ray_color(r, max_depth); } fit_color(out_color, pixel_color, samples_per_pixel); //write_color(std::cout, pixel_color, samples_per_pixel); gAppSurface->copyPixel(i, fCurrentRow, PixelRGBA(out_color[0], out_color[1], out_color[2])); } fCurrentRow = fCurrentRow + 1; return true; } void renderBegin() { cam.initialize(aspect_ratio); std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; } private: color ray_color(const ray& r, int depth) { hit_record rec; // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); // If the ray hits nothing, return the background color. if (!world.hit(r, interval(0.001, infinity), rec)) return background; scatter_record srec; color color_from_emission = rec.mat->emitted(r, rec, rec.u, rec.v, rec.p); if (!rec.mat->scatter(r, rec, srec)) return color_from_emission; if (srec.skip_pdf) { return srec.attenuation * ray_color(srec.skip_pdf_ray, depth-1); } auto light_ptr = make_shared<hittable_pdf>(lights, rec.p); mixture_pdf p(light_ptr, srec.pdf_ptr); ray scattered = ray(rec.p, p.generate(), r.time()); auto pdf_val = p.value(scattered.direction()); double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); color color_from_scatter = (srec.attenuation * scattering_pdf * ray_color(scattered, depth-1)) / pdf_val; return color_from_emission + color_from_scatter; } };
These are the guts of the ray tracer, with the private ‘ray_color()’ function doing the brunt of it. But, I’m not really dissecting how the ray tracer works, just what was required to incorporate it into my demo scene.
Right there in ‘renderContinue()’, you can see how we go from whatever the raytracer was doing before (writing out a .ppm file), to converting the color to something we can throw into our canvas
fit_color(out_color, pixel_color, samples_per_pixel); //write_color(std::cout, pixel_color, samples_per_pixel); gAppSurface->copyPixel(i, fCurrentRow, PixelRGBA(out_color[0], out_color[1], out_color[2]));
The ‘fit_color’ routine takes the oversaturated color value the ray tracer had created, and turns it into a RGB in the range of 0..255. We then simply copy that to the canvas with copyPixel(). The effect this will have is to very slowly refresh the application window every time a single line is ray traced. With this particular image, it is slower than watching grass grow. This image took several hours (8) to render on my 5 year old i7 based desktop machine. Even if you imagined it took half that time, it’s still slow. There are ways to speed it up, but that’s another story.
What I’m interested in are a couple of things, how big is that program, and where’s the movie?
This little demo program is 173kilo bytes in size. Just think about that. Go to a typical web page, and the banner image might be bigger than that. Given that our machines, even cell phones, comes with Gigabytes of RAM, who cares how big a program is? Well, small size still means more efficient, if you’ve chosen proper algorithms. I like the challenge of small, because it means I’m parsimonious. Using as little external dependencies as possible. This also means that when I want to port to another platform, beyond Windows, I have less baggage to carry around.
This points to another design point.
I’m using C/C++ here. That’s not the only language I ever use, but it’s ok for these demos. I’m a big fan of C#, as well as my favorite LuaJIT. Of course you can also just use Javascript and browsers, but here we are. You’ll also notice in my usage of the language you don’t see a lot of memory management. You don’t typically see new/delete. That’s not because I’m using some garbage collection system. It’s because of a careful choice of data structures, calling conventions, and object lifetime management. Most things are held on ‘the stack’, because they’re temporary. Then, things like the canvas object, are initialized internally, so the programmer doesn’t have to worry about how that’s occuring, and doesn’t need to manage any associated memory.
I like this. It gives me a relatively easy programming API without forcing me to deal with memory management, which is easily the biggest bug generator when using this particular language. This is great for short demos. It’s a lot harder to maintain with more serious involved applications, although I’d argue it can be done with proper composition and super tight adherence to a coding methodology. Not realistic with large teams of programmers though.
OK, so small size, simple to write code, simple to integrate stuff you see on the internet. What about the movie?
scene_full movie
There you go. 8 hours of rendering, condensed down to a few seconds for your viewing pleasure. In this particular case, since the renderer is updating the canvas every frame, each frame is a single scan line. As there are 800 vertical lines, there are 800 frames. You can pick whatever frame rate you like to display at whatever speed.
If you were really clever, and had a few machines laying around, you’d create a render farm, and make an animated short with motion, each machine rendering a single frame, and then ultimately stitching it all together. But, on my meager dev box, I just get this little movie, and that’s my demo.
This just goes to show you. If you see something interesting out there in the graphics world, maybe a new line drawing algorithm, or a real-time renderer, it’s not hard to try those things out when you’ve got the proper setup. Of course, there are other frameworks out there, like SDL, or Qt, which do this kind of thing. If you look at them, just see how big they are, how complex their build environment is, and how much framework you must learn to do basic things. If they’re ok for you, then go that route. If they’re a bit much, then you might pursue this method, which is fairly minimal.
Next time around, screen capture, for fun and profit.