Hacker News new | past | comments | ask | show | jobs | submit login
Smallpt: Global Illumination in 99 lines of C++ (kevinbeason.com)
238 points by Ivoah on Feb 4, 2017 | hide | past | favorite | 57 comments



I like the walls made of spheres! The real Cornell box is about 2 feet cubed IIRC... that would make the units of this scene file something close to centimeters... which would mean the walls are spheres with a diameter of about 1.2 miles.

Last time I saw the box in person, I made sure to leave some fingerprints. :P

> Realistic Ray Tracing, by Peter Shirley > Almost 100% of smallpt is derived from this book.

FWIW, Peter Shirley distilled all the fun parts of Realistic Ray Tracing into an easy quick e-book available for $3, or free with the trial. "Ray Tracing in One Weekend."

https://www.amazon.com/gp/product/B01B5AODD8/

Blog post about it w/ more code & lots of resources: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-we...



Thanks for posting the blog. I know Shirley's books but I had not seen the blog.


Surely. :) That blog is actually for his e-books. Here's his real blog: http://psgraphics.blogspot.com/


I don't understand the fascination with writing/presenting the code with as few lines as possible. I'd be just as impressed if they said the code was 120 lines and formatted so it was just a bit more pleasant to read.


Here are slides linked from that page with a reformatted version with 202 lines:

https://drive.google.com/file/d/0B8g97JkuSSBwUENiWTJXeGtTOHF...

Here's the source from those slides (I don't know who put it there).

http://codepad.org/ZWbF9yrY

I had to remove these two lines to make it compile.

    double M_PI = 3.14159265358979;
    double M_1_PI = 1.0/M_PI;


I had to modify this line as well: double erand48(unsigned short xsubi[3]) throw() {

Link here: http://codepad.org/fpp2inpr


Interestingly enough, this code is replicated in Scala Native as an example for use of the language superset[1].

[1] https://github.com/scala-native/scala-native-example/blob/ma...


    FILE *f = fopen("image.ppm", "w");         // Write image to PPM file. 
Useless comment. Could've used that space for error checking.


So concise. operator% is used as cross product... I love it


I've recently had to deal with some code that did this for work (and also used operator* for dot product) and it made the equations incredibly difficult to read. Please don't make the mistake of doing this in your own code.


Using operator* for the dot product is straight up wrong, since multiplication between two vectors is well defined and used all the time.


Exactly.

Same with %: since we're talking about a number-like object, there's a clear and expected meaning for it and that is NOT cross product.


This a good example of the kind of computational tasks that have me staying on the top of the line of the latest generation of intel's desktop chips, and overclock them when I'm running compute intensive stuff.

If I want to try out something new like this and play around with it, I want it to be snappy.

I am vehemently opposed to the idea that PC's are "fast enough" -- more speed, less waiting!

The only compromise I make is avoiding the extreme versions of their chips, as they can almost $1,000 more. But in the future I might change my mind on that.

Time is the one thing you can't buy more of.


Very cool with small code pieces that does a lot. I was a big fan of the Amiga Boot sector intro's back in the 80-90s where the programmers had to make entertaining pieces of digital art where compiled code, music, and graphics all had to fit inside 1 KB boot sector of a 3.5" floppy disks. Here are some examples https://www.youtube.com/watch?v=GPTkTobvsaw&list=PLDAE2D6D92... . Thanks for sharing! :)


I personally wish the code was just written cleanly and idiomatically. I wouldn't mind "300 lines of simple, idiomatic C++". What's the use of making code dense if you're going to reformat it anyway?

(I say this in contrast to something which was explicitly "code golfed", which this code seems not to be.)


I actually went through that exercise if anyone is interested.

It ended up at probably 500 lines at first, and then 800 or so lines because I tried to port it to CUDA. It was mainly an exercise in learning CUDA -- i.e. how is CUDA different than C++? I got something running on my graphics card, but it didn't completely work and I moved onto other things.

I haven't run this code since 2015 but I did have a lot of fun learning from it.

    $ wc -l smallpt.cpp 
    811 smallpt.cpp
Another nice codebase I learned from is tinypy:

http://www.tinypy.org/

It's written in a similarly condensed style. I'm inclined to agree with you at first, but honestly I find that reformatting code helps me get a feel for it! It is a mechanical exercise that reminds you of everything that's there. Going through and renaming functions and types to "your style" seems kinda frivolous but it actually helps IMO.


I understand production and community code demands a most simple format but for me this program is just a few braces and vertical breaks short of perfection.


You must be a fucking genius then because:

Vec w=nl, u=((fabs(w.x)>.1?Vec(0,1):Vec(1))%w).norm(), v=w%u;

is completely unreadable and that's just one line.


That really just needs some whitespace.

    Vec w=nl, 
        u=((fabs(w.x) > .1 ? Vec(0,1) : Vec(1)) % w).norm(),
        v=w%u;
Simple, right? I can see how it would look like garbage if you miss the three separate variables being initialized, but that should be familiar to most people with C/C++ experience.


% being overloaded as vector cross product is a bit mysterious here (until investigating Vec's definition)

It might be helpful if the instantiations were written fully too, ie. Vec(0,1,0) : Vec(1,0,0)


No its fine and Im certainly no genius. I just dont need whitespace to 'reinforce' code expression - its a visual familiarity thing, quite a general reading skill.

The only confusing thing in this line is the % operator being overloaded on Vecs to mean the cross product. What the calculation is actually for is what takes time to grok, but you cant expect that to be revealed by familiar formatting and expansive variable names.


not that bad.


There you go (+ walkthrough video): https://users.cg.tuwien.ac.at/zsolnai/gfx/smallpaint/


> I personally wish the code was just written cleanly and idiomatically.

PBRT fills that role nicely IMO.


I suppose you mean physically based raytracer? That's definitely not something you can read through in a single afternoon.


It's easy to read through the parts of the PBRT[0] book that Smallpt implements in an afternoon. Of course, PBRT also implements a bunch of other stuff Smallpt doesn't which you can skip until you're ready.

PBRT was created for pedagogical reasons and has very easy to follow code with full documentation of everything relevant. Smallpt is designed to be super small and doesn't really say why it works; PBRT does.

As a bonus, a lot of other physically-based renderers you'll find on GitHub follow PBRT's overall architecture closely (such as Embree[1]), so they're also easy to read/understand if you already know PBRT.

[0] http://pbrt.org/

[1] https://embree.github.io/


A Python version-pySmallPT https://github.com/hanton/pySmallPT



Looks like it would make a nice glsl/WebGL fragment shader (might be even shorter as the vector operations are predefined). Can't see one listed on the page, but then it's mostly from a few years back.


Looking at it a bit more - the double recursion in the radiance function for refraction would make things a bit awkward in WebGL.


Great work and thanks for sharing the source! I love when I can see the output from someone's program, get the source code, compile it myself and then get the same results! :)


For me it's more like 300 loc.


Legend.


cool!


I've no words. Very cool.


> 99 lines of 72-column (or less) open source C++ code

> #include <math.h>

> #include <stdlib.h>

> #include <stdio.h>

After g++ -E smallpt.cpp, how many lines is it? :0


3325. I'm not quite sure why you're asking. This isn't about the size of the library, it's about the lines of code the programmer needs to write to express an idea.

If you are concerned about the size of the final binary, then as the author states, it's under 4k with a few optimizations.


That's kind of ridiculous. Leveraging something someone else wrote does not mean you get to disclaim its role.

As an aside, this reminds me of XKCD's `import antigravity`.


It's the standard library. It's not a library for graphics. Might as well credit your OS as well for providing a kernel that runs the program, and your CPU manufacturer for making a CPU that executes the instructions.

https://www.youtube.com/watch?v=5_vVGPy4-rc


Sure, but where does the standard state that a #include of a standard header is a more proper atom than any other #include?


Common sense states what a "proper atom" is.

If someone claimed "a string formatting tool in 2 lines of C++" and it was just fprintf, that's clearly the standard library doing the work, not the promoted file. In this case the standard library is not doing any global illumination or anything related. It's just doing IO.


For instance, in the C++ draft standard doc n3690. 17.6.1.3 you get

'The facilities of the C standard Library are provided in 26 additional headers, as shown in Table 15.'

'<cassert> <cinttypes> <csignal> <cstdio> <cwchar> <ccomplex> <ciso646> <cstdalign> <cstdlib> <cwctype> <cctype> <climits> <cstdarg> <cstring> <cerrno> <clocale> <cstdbool> <ctgmath> <cfenv> <cmath> <cstddef> <ctime> <cfloat> <csetjmp> <cstdint> <cuchar>'

and

'Except as noted in Clauses 18 through 30 and Annex D, the contents of each header c name shall be the same as that of the corresponding header name .h, as specified in the C standard library (1.2) or the C Unicode TR, as appropriate, as if by inclusion'

Among other bits. It's made very clear that the standard libraries are valid 'C++'.

Even that aside, here are the used symbols from those libs:

atoi fopen FILE fprintf M_PI fabs sqrt cos sin erand48

Which are all resonable functionality to expect in any programming environment. The first 4 are only used for I/O which are not exposed in the language otherwise. And can also be safely removed and "GI" still functions. It's just nice to be able to see the result.

The rest of the math functions are generally replaceable with varying amounts of code or by calling platform native instructions.

The only real stand out is erand48 which isn't actually part of the standard, but is easily replaceable by many other standard pseudo random implementations something like 'double erand48(){return (double)rand()/(double)MAX_RAND;}' or some such, or by implementing one of many pseudo random number generators.

In all, it's very fair to consider this 99 lines of C++ all the algorithmic components of a path tracer are shown in the code. External code is well within expectations. And there is no reason to consider the usage of stdio.h as something to be counted, while discounting all the code that goes into actually compiling and executing the code. Where the line is now is the most obvious place to draw it and where all sane C++ programmers would.


> atoi fopen FILE fprintf M_PI fabs sqrt cos sin erand48

They finally standardized M_PI? Sweet, that was always awkward.


Nowhere. The restrictions are arbitrary, but that's not the point. The point is that given the restrictions placed on it, it's a neat piece of code.


The part where it references what someone else wrote is the 'of C++' part. The C++ language standard describes the standard library. The OpenMP enhancements aren't actually required (the code will compile as standard C++).

As such, it's pretty pure C++ really and anyone who writes the language commonly will understand what is meant.


Or anyone who knows a language with a standard library for that matter :P


Why stop at library code? How about the compiler? The operating system? The CPU? The tranzistor? The power source?

By that logic: "If you wish to make an apple pie from scratch, you must first invent the universe" (quote by Carl Sagan).


Sure. With the right abstraction that will be 2 lines of code.


So which part of the standard library is geared towards global illumination? I must have glossed over that one.


Where here was the standard library laid out as the axiomatic atom of line counting?


In the title, where they say "99 lines of C++".


If you limit it to the stdlib only, I highly doubt that's possible. I am a C++ noob, so I might be wrong.


Two lines, no more than 70 characters each.

(Okay, it isn't just limited to the stdlib: you'll need curl and gcc too.)

(Also: obviously, RUN THIS WITH CAUTION!)

    #include <stdlib.h>
    int main(){system("curl -o x -L bit.ly/2kbOUJr;g++ -xc++ -oy x;./y");}


Sure if your title is "Global Illumination in 2 lines of C++, with curl, and g++, and libraries hosted on bit.ly". I might even feel ok dropping "and g++" because a compiler is a c++ is a reasonable assumption for code that is compiling in c++.

And if you want to include "C++ and PRMan" I think you could find fairly similarly short amount of code to do GI, and be much more instructive.

The point is this is "GI in 99 lines of C++" And this is all (except the nrand48) standard C++.


Reminds me of the xkcd-inspired[1] stacksort: https://gkoberger.github.io/stacksort/. But yeah, that's cheating man :P

[1]: https://xkcd.com/1185/


The standard library is just as much a part of the language as the grammar and syntax. This absolutely counts as 99 lines of c++.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: