The joy of building a ray tracer, for fun, in Rust
blog.singleton.io
blog.singleton.io
FWIW, python 3 doesn't do the same anymore:
$ python2.7 -c "print(800 / 600)"
1
$ python3 -c "print(800 / 600)"
1.3333333333333333Python 2.2 added "from __future__ import division".
1/2 == 1
This makes no sense at all. Has this ever been the case?> The classic division operator makes it hard [in a dynamic language like Python] to write numerical expressions that are supposed to give correct results from arbitrary numerical inputs ... Another way to look at this is that classic division makes it difficult to write polymorphic functions that work well with either float or int arguments; all other operators already do the right thing. No algorithm that works for both ints and floats has a need for truncating division in one case and true division in the other.
Here's a presentation van Rossum gave in OSCOC 2001 about the issue, using: (see https://legacy.python.org/doc/essays/ppt/oscon2001/oscon2001... )
def velocity(distance, time):
return distance/time
In Python 2.0, this is incorrect, unless you could guarantee that at least one of distance and time was not a integer. The correct solution is something like: def velocity(distance, time):
return distance/(time*1.0)
while other valid-seeming solutions are subtly broken: x = float(x) # Broken if x is complex
x = x+0.0 # Broken if x is -0.0
Why you would pass in a complex number, I've no idea. But perhaps the imaginary component is 0? >>> 50/(30+0j)
(1.6666666666666667+0j)
In C this isn't a problem because the "double distance" and "double time" in the argument list ensure the algorithm is dealing with doubles.In addition, though the PEP doesn't say it, there's some influence from the Alice programming language, which started in the 1990s and built on Python. Quoting "Alice: Lessons Learned from Building a 3D System For Novices" at http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.43.... :
> Although we resisted changing the Python implementation, our user testing data forced us to make two changes. First, we modified Python’s integer division, so users could type 1/2 and have it evaluate to 0.5, rather than zero. Second, we made Python case insensitive. Over 85% of our users made case errors and many continued to do so even after learning that case was significant.
Feedback from Alice helped influence van Rossum's decision for changing division. As for "Case Insensitivity", see slide 44 from the OSCON presentation, with that as the title and "[ducks :-]" as the sole content.
https://github.com/mschaef/rust-rt
After spending as much time lately as I have in Scala and Python, my immediate reaction to Rust was quite positive. Python has performance issues from 1985 and the build ecosystem seems borderline chaotic. (Made me seriously miss Maven, etc.) Scala seems a lot like C++ - an amazing intellectual accomplishment, a great place to spend all of your time, but not so good as a part time language. (I spend a bunch of my time in Scala mentally expanding out shorthand notation the way I might be mentally macroexpanding in a Lisp.)
Rust, in contrast, seems to have struck a nice balance between expressive power and runtime performance. Expressively, it has a lot of what I like about Scala with a syntax that makes more sense to my C-style upbringing. Performance seems to be everything I'd expect from the fully compiled language that it is. (In terms of performance, there's no way Python would've let me get away with some of what I got away with in my Rust ray tracer.)
Given that the language gave such a positive initial impression, the questions I still have are more about what it feels like in the large. ie: Working with a significantly sized team jointly on a codebase that might last 1, 5, 10 or more years. (Even then, I'm pretty optimistic.)
Next step: add global illumination. Rayon is the key here, and having access to more cpu cores.
Ray Tracing And Global Illumination (2003)
https://digitalcommons.unf.edu/cgi/viewcontent.cgi?article=1...
Do you know of good open source real time (preferably gpu based) ray tracing engines out there? That can be used for games? I see a bunch of lists but want to get your expert opinion.
E.g. https://awesomeopensource.com/projects/raytracing/real-time
https://raytracing.github.io/books/RayTracingInOneWeekend.ht...
For example all Rust's loops are expressions. This isn't very useful in many cases, but breaking an ordinary loop can yield a value, so e.g. you can loop checking all the integers until you find one you like, and then return it as the value of the loop. Nice.
Coming from python, i feel doing something like this:
let x = for v in iter {
if cond(v) {
break v;
}
} else {
default_v
}
would be nice, but this was explicitly rejected by the core team. So for now anyway, only `loop` can break with value.In your example code, I see there's an else clause on the for loop which is presumably special syntax so as to provide the value if it doesn't break, but that feels pretty clumsy to me and I'm not sure there are many cases where it's more readable than what I'd do now.
If I could see a nice way to do it without such clumsy special syntax, I'd favour this, but with the else syntax (or some equivalent) it feels like it doesn't pull its weight.
[1]: https://github.com/fiddlerwoaroof/lisp-sandbox/blob/master/r...
If I were to start writing a new renderer, the first thing I'd do is to hook it up to an external image viewer over some protocol. These days, I find myself liking TEV (https://github.com/Tom94/tev) a lot as a simple open-source image viewer that supports this and most other basic features that I'd want. See the links in the README for Python and Rust implementations of its protocol.
anecdotally, in my cpu based ray-tracers (i have one in c++ and python so far) , i have found that SDL based rendering causes noticeable slowdowns to become quite useless after the initial novelty wears off.
My CPU-based ray tracer uses the code I linked. SDL_LockTexture, go wide with threads writing into the locked buffer, SDL_UnlockTexture. If I skip the actual ray tracing, I get 1400 FPS uploading a 1024x1024 image.
There are two PPM formats, both very simple: http://netpbm.sourceforge.net/doc/ppm.html
For example, the header
printf("P6\n%lu %lu\n255\n", width, height);
followed by width*height triplets of raw bytes representing the RGB values of each pixel.p.s. the Earth is also rotating the wrong direction!
1. https://www.doomworld.com/forum/topic/71128-so-is-doom-a-ray...
Among the trickery being used is raycasting, used in early 3D games like Doom, which is a kind of 2D ray tracing that works by column instead of by pixel. Real time ray tracing techniques, in particular signed distance field raymarching is a staple of the demoscene, this is made possible by using mathematically defined objects.
For prerendered graphics (ex: Pixar movies), the dominant technique is path tracing, it is a randomized variant of ray tracing that produces a noisy image that is progressively refined. It is even more costly than raytracing, but is much better at global illumination.
Regarding Pixar, they actually avoided ray tracing until they decided they really did need accurate reflections for the movie Cars. The reason is that traditional rendering is memory parallel: you can render a scene that won't fit in memory on a single computer by spreading the scene across a cluster. With ray tracing, the memory access patterns aren't predictable, so you have to have the whole scene fit in memory on every compute node. This doesn't matter for games because games don't divide the graphics computation across multiple machines.
pub trait Scatterable { fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option<Ray>, Srgb)>; }
I don't know rust but familiar with the Option type. Why would you not create a type that composes a Ray and a Srgb value, something like this?
struct RayColour/RayWithSrgb/etc { Ray, Srgb }
Seems like it would be a nicer API.
- None
- Some((None, Srgb))
- Some((Some(Ray), Srgb))
Only the last one is equivalent to the struct you described
Edit:
fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option<Ray>, Srgb)>
The function takes a `Ray` as input, so you cannot render light without a ray. The return value might be one of the cases listed above.The `None` case might be something like a black hole: a ray hits the object and nothing happens.
The `Some((None, Srgb))` case would be a non-reflective surface, it changes the color if hit by a ray but does not reflect the ray further.
The `Some((Some(Ray), Srgb))` case would be a reflective surface, that changes the color and reflects the ray.
Again not sure about the exact semantics and I didn't read the blog post yet, but that would be my guess.
https://raytracing.github.io/books/RayTracingInOneWeekend.ht...
I don't think there are actually any cases in which you can usefully do something with the color but without the ray (the reason that the ray output is optional is that the ray might be absorbed rather than reflected, but in this case I think its color becomes meaningless, or becomes the equivalent of (0.0, 0.0, 0.0)). However, someone implementing this based on the tutorial might not think of it that way, because the different features in question were added separately at different times.
In the original C++ example in the tutorial, there is a boolean return value indicating whether there is an output ray or not, which I think corresponds to the Some case for the option type here. As there is ultimately only one boolean, not two, and as material implementations are expected to set both the ray and the color when scattering occurs, I think it's correct that you could combine the vector and color return values into a single struct wrapped in a single option type, at least with the implementation strategy that the tutorial is suggesting.
I worked through about 90% of the guide in Python and then about 60% in Rust; this article definitely makes me want to pick it up again.
Edit:
> The `Some((None, Srgb))` case would be a non-reflective surface, it changes the color if hit by a ray but does not reflect the ray further.
While this feels like a plausible guess, I don't believe it actually aligns with the strategy suggested in the tutorial. See section 8:
https://raytracing.github.io/books/RayTracingInOneWeekend.ht...
Diffuse materials (non-specular reflection) are implemented by having them scatter incident light in a random direction, possibly with some attenuation and some change of color. The change of color, though, is only meaningful when rays are scattered. See also section 9.3
https://raytracing.github.io/books/RayTracingInOneWeekend.ht...
(talking about how you can either scatter every ray and attenuate its intensity, or scatter a fraction of rays with no attenuation but absorb some rays at random, with the same statistical result on the output)