Page MenuHome

Blue noise pixel correlation in cycles
Needs ReviewPublic

Authored by Joseph Eagar (joeedh) on Jun 2 2019, 7:56 AM.

Details

Reviewers
None
Group Reviewers
Cycles
Summary

This is an implementation of https://www.arnoldrenderer.com/research/dither_abstract.pdf. The idea is simple: path tracers (including cycles) decorrelate pixels by offsetting per-pixel prng samplers by noise. The idea of the paper is instead of using white noise for this, a blue noise mask is used instead. To control how correlated and "blue" the noise is I've added a "Coherence" option that scales the resolution of the final prng offset. The lower the resolution, the fewer distinct noise groups will be in the final render and the more "blue" the noise will look.

Anyway, here's an example image without blue noise:

And here's with blue noise:

The blue noise mask itself is a 64x64 16-bit greyscale image, it's compiled into a C file in cycles/util/util_bluenoise_mask.cpp, with some definitions in cycles/kernel/kernel_bluenoise_mask.h. If you want to generate the mask, there's a script to do so in release/datafiles/bluegen. I've packaged my blue noise mask generator into an npm module; the script in bluegen will install it and generate a mask (it uses the void-cluster method of generating 2d blue noise). If you want to play with the mask generator, you'll have to clone the browser version from here:

https://github.com/joeedh/StochasticScreenGenerator.git

. . .but note that the void cluster code is a small part of a much bigger research project. So, messy research code. :)

Diff Detail

Repository
rB Blender

Event Timeline

Joseph Eagar (joeedh) edited the summary of this revision. (Show Details)Jun 2 2019, 7:59 AM

Note that we also have D2149. It would be interesting to see renders with 1, 4, 64, 1024 samples to see how it converges and how it compared to the reference. If it works well enough it would be nice to have this in 2.81.

I'm somewhat surprised this patch works at all because of the following code that we have (for Sobol in this case):

uint tmp_rng = cmj_hash_simple(dimension, rng_hash);
shift = tmp_rng * (1.0f / (float)0xFFFFFFFF);
return r + shift - floorf(r + shift);

That is, we hash the per-pixel rng_hash value with the dimension. I would expect this to destroy the blue noise properties?

It might be useful to find a way to hide the repetition in the 64x64 pattern, since in some cases it can become visible.

Looks great,

What are the benefits of this over the @Lukas Stockner (lukasstockner97) dithered sobol implementation?

  • Added an option to ignore coherency setting in non-bluenoise mode.
  • Added a 128x128 blue noise mask.
  • Updated defaults. Seems the setting of coherency that leads to quickest convergence differs from scene to scene.

By the way, technically speaking there should be multiple blue noise masks, one for each sampling dimension. I did write code to generate these, but integrating them into cycles seemed like a larger change then I'd like to make (at the moment there's one per-pixel offset for all the dimensions). Even one mask works extremely well, but I am curious how big of a difference multiple masks might make with, say, objects with subsurface scattering with motion blur and depth of field, or other extreme cases like that.

Looks great,
What are the benefits of this over the @Lukas Stockner (lukasstockner97) dithered sobol implementation?

Do you mean this patch? https://developer.blender.org/D2149

Looking at it real quick, it looks like his patch is better, more comprehensive but possibly slower. His patch works within the SOBOL implementation, but the basic method actually sits above the specific sampling generator. That said, I'd have to build the patch to see if it's higher quality or not.

My gut instinct is that his diff would probably perform better than mine if I replaced his mask data with mine (I have quite a bit of experience generating these things, it's one of my research interests). He used a simulated annealing process to generate his mask, but in my experience is that tends to produce worse results than a good void-cluster implementation. I'd have to check though.

By the way, I added a bigger mask, so that should help with tiling artifacts. If a 128x128 mask isn't enough, a 256x256 certainly would be (though that might take a day or so to generate).