Hydroxide banner

Hydroxide

11 devlogs
26h 33m 45s

A physically accurate CPU path tracer written in Rust. Compiles to WASM.

Features include:

  • Scene editor on web build!
  • Path tracing with global illumination
  • Two-level BVH acceleration (O(log n) intersection)
  • Glass, mirror, diffuse ma…

A physically accurate CPU path tracer written in Rust. Compiles to WASM.

Features include:

  • Scene editor on web build!
  • Path tracing with global illumination
  • Two-level BVH acceleration (O(log n) intersection)
  • Glass, mirror, diffuse materials
  • Mesh loading (STL)
  • Multithreaded via Rayon
  • Progressive WASM renderer
  • Russian roulette termination
This project uses AI

Github copilot for inline completions. AI also used for writing some of the WASM toolchain glue code.

Demo Repository

Loading README...

btz

Shipped this project!

Hours: 8.41
Cookies: 🍪 251
Multiplier: 24.83 cookies/hr

I’ve added some cooler editor capabilities, and improved despeckling, both in the root cause and with a filter. The fact that this is possible in real time on the cpu in the browser makes this pretty cool.

You can now properly compose scenes, more than just moving spheres around. User-uploaded meshes are big. I also published to cargo!

This was my first big Rust project, so I’m really proud of how it’s turned out and am excited to share this with the world :)

btz

Polish and wgpu research

I’m thinking that I might do the great wgpu rewrite after all, but idk. Have to look into some more resources. Research time ofc is not counted in hackatime. Otherwise, I made the ui a bit better. I think I’ll probably reship now and see if I ever end up porting to the gpu.

It basically means rewriting all of my business logic, but in one giant file, and writing a bunch of glue code.

If you’re reading these devlogs, good on you! Here’s a video of navigating the ui, might help you if you’re stuck.

The exporting and local bridge do theoretically work, but TBD if voters are going to like that.
Anyways, figured I might as well ship these hours in case I never do end up finishing the great wgpu port

0
btz

Proper scene editor

Relatively light since I already got the serde working, just populating that through.

Also added rotation / scaling support for meshes, and enabled custom meshes!

Gizmos add a lot to the usability.

Finally there’s the native export bridge, which is fun, but a bit finicky. We’ll see how usable it is.

Also I published on crates.io! That’s fun

0
btz

Housekeeping + serialization

Seperated some monolithic files.

I also knocked off basically all of the little things off of the roadmap, like blue noise and stuff

Then, I’m working on scene serialization. This way you can edit scenes without recompiling, and maybe even take scenes from the editor, download them, and run rendering locally!

Used serde and bincode to do this. After some triangle optimization + gzipping, it’s a 10mb file when the source is ~80mb, so I did a better job than the STL file format.

Finally, I added despeckling! Especially noticeable in the dragon mesh. I’ll try to fix the root cause eventually, but masking it for now is really good.

Attachment
Attachment
0
btz

Shipped this project!

Hours: 6.66
Cookies: 🍪 183
Multiplier: 27.4 cookies/hr

I’m really proud of what I’ve made, because I implemented the renderer itself. And, rather than just doing some simple projection math to generate these images, it actually physically simulates how photons would behave to generate these images.

I’ve taken this project to the next level in terms of interactivity by adding the scene editor for web, which was really sick.

NEE was also quite an interesting optimization to add, and gives you more bang for your buck.

I’m really proud of making this, since it’s my first big Rust project.

btz

Added scene editor

The ship feedback repeatedly showed that one of the biggest actionable feature asks was more interactivity. So I made a proper scene editor for the wasm build!

Object picking + outlining is surprisingly free since I’ve built all the infra around tracing anyways.

Lots of glue code to be written in wasm.rs and in js.

Anyways, this takes the web demo interactivity to the next level.

Attachment
Attachment
0
btz

Post-ship

Added colored lighting. This made the image really noisy, and require a lot more samples to get a good result. I cranked them up for this render, but also worked on some optimizations:

Simple Next Event Estimation (not faster, but gets better results with fewer samples)
Memory layout with Rayon (better progress bar accuracy)
Better memory layout divisions with rayon (Cache locality… Made my code 28% faster!)

Attachment
0
btz

Shipped this project!

Hours: 11.49
Cookies: 🍪 331
Multiplier: 28.83 cookies/hr

I built a path tracer!
I learned a lot. This was my first complex Rust project, and I had to manage lots of different toolchain things.
Optimization was also a really big element of this project, and it was cool to develop with that in mind.
I also learned a lot of math, and learned how path tracing works! Huge thanks to the Ray Tracing in One Weekend series.

Overall, I’ve made a product that I’m really proud of.

btz

Deploy to web

This is the devlog I used AI the most for. There are all sorts of black-magic toolchain things that you need to do.

In the end, the shell script has to manually patch things with sed to allow for proper imports. I also had to pin an arbitrary nightly version of rustc.

yeah… quite something.

In terms of optimization, the big one here is the illusion of speed. By that I mean progressive rendering. If it loads instantly for the user and it’s responsive, they’ll watch everything render in in real time and be entertained. If it’s 6000 ms to see anything respond to input, they’ll think it’s broken and click off.

Attachment
0
btz

BVH fr

This is where optimization and OOP design merge. So I wanted my BVH to work over any Hittable (thus requiring the type Arc). On large meshes however, this leads to insane heap churn (also triangles aren’t Hittable and don’t own materials). In the end, I implemented a MeshBVH. The type design is sufficiently different that it’s worth the technical debt of code duplication.

I also optimized BVH building for MeshBVH, where the root owns all the objects and bvh nodes just take indices of a Vec.

Finally, I made some beautiful renders! This is the final scene.

Last roadmap item is really just building for WASM. BVH is a magical performance improvement, and with progressive rendering, it’ll actually be snappy.

Attachment
0
btz

BVH

Yes, one feature gets its own devlog. The Raytracing in One Weekend book calls it “by far the most difficult and involved part.”

It’s great to be able to collide with objects in O(log n) time instead of O(n). For the 100 spheres benchmark, this has sped it up 10x again, down to 10.285 ms! (±0.7 ms)

No new picture because it looks literally the same, but it’s a lot faster. Between this and Rayon, it’s now two orders of magnitude faster!

There was another optimization suggested, where you split on the longest axis, instead of a random one, but I found that it was actually 15% slower, so I didn’t do it. Empirical testing!

The one thing is that the flamegraph’s now all choppy because of Rayon.

At this point, my code’s too fast for the testing harness. 2ms is too noisy, so I’ll make harder tests to bench against. Anyways, if you see the ms number go up after this devlog, that’s why.

Attachment
0
btz

Easy roadmap items

random spheres test scene render
Switch to Möller–Trumbore for triangles
optimize more: gamma correction, russian roulette termination, bench hardness (timing + correctness)
optimize more: bench hardness (timing + correctness)
Add rayon finally!

Moller-trumbore saves many cycles on triangles, gamma correction is for correctness, roulette uses fewer cycles AND more bounces, and bench finally lets me quantify these things

And yeah, using multiple cores made it go 10x faster

In terms of benchmarking, I now have very concrete, statistically-significant automatic tests to point to

render balls            time:   [137.77 ms 140.26 ms 142.21 ms]
                        change: [−90.620% −90.360% −90.126%] (p = 0.00 < 0.05)
                        Performance has improved.

render cube             time:   [41.857 ms 42.394 ms 42.942 ms]
                        change: [−90.613% −90.429% −90.297%] (p = 0.00 < 0.05)
                        Performance has improved.

Here’s a cool high-resolution render. And now I finally have real CI tooling, like a mature programmer (lint + regression test + benchmark???)

Next to do:
BVH (this is the other big optimization)
lights
progressive rendering

Attachment
Attachment
0
btz

Devlog 2: Adding stuff

I added most of the things on the roadmap: Shiny things, dielectric (glass)… and that’s it.

I also added ray-triangle intersection! So I can now render meshes, like a cube. I got ambitious and wanted to render a teapot, but 10k triangles with no BVH is NOT the move. So I’ll implement BVH down the line

The other thing I did was restructure the code. 500-line world.rs was getting pretty unweildy, so I learned Rust project management and made a proper folder tree

TODO:
Switch to Möller–Trumbore for triangles
ADD BVH!
add cool lights

Attachment
Attachment
0
btz

Devlog one– wrote the raytracer

This will be the most boilerplate-heavy devlog.
I made a Vec3 class in it’s own module, and then promptly decided to put all of my other raytracing business logic in world.rs. In there, there’s Camera, Material, Lambertian, HitRecord, Hittable, Sphere, HittableList (BVH later), and World !
I’m also using the image crate because I couldn’t be bothered to make file IO, and the no-dependencies bragging rights cannot be that good.

My initial image that came out was just a flat red circle. Then I realized that I had symmetric shading everywhere, so I added the giant floor sphere.

aand the initial render times SUCKED! 8.598s to render frames at this crappy 400x300 resolution.

Rather than guessing or asking AI, I just actually ran perftests with flamegraph. The quickest win is to build for release.
RNG was weirdly taking pretty long (in the hot path), so I switched to fastrng
I also started using squared distances as comparison instead.
The final unexpected thing was that tan() was being called in the hot path when I literally could have just cached it. So that’s what I did.

Now it renders in just 500ms!
Roadmap:
add planes, add glass, add checkerboard, add lights, optimize more
I’ve also attached the current flamegraph.

Attachment
Attachment
0