Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Caustic rendering demo #274

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

danieldjohnson
Copy link
Collaborator

Based on https://benedikt-bitterli.me/tantalum/, this demo generates caustics by tracing the path of light through a 2D scene. All updates are done in the Accum writer effect, which should mean that this example can be sped up using parallelism.

Uses a custom antialiasing function that computes line integrals within bilinearly-smoothed pixels, so the rendered outputs correspond fairly accurately to the amount of light passing through each pixel.

Example output:

Some implementation notes:

  • I needed atan2 but that's not a builtin yet, so I found a cheap approximation. But this should probably be a builtin? I imagine it's just a matter of hooking it up to LLVM in the right way.
  • I ran into Can't write effect-polymorphic higher-order functions #267 while trying to write effect-polymorphic helper functions, so right now it's written without those helper functions.
  • It's surprisingly annoying to index into small matrices and vectors, and there's a lot of visual noise (e.g. m.(1@_).(1@_)). I ended up mostly using tuples instead. Maybe this is another argument toward having a more powerful indexing syntax.

Based on https://benedikt-bitterli.me/tantalum/, this demo generates caustics
by tracing the path of light through a 2D scene. All updates are done in the
`Accum` writer effect, which should mean that this example can be sped up
using parallelism.
@google-cla google-cla bot added the cla: yes label Oct 20, 2020
@@ -48,6 +48,7 @@ code {

.plot-img {
width: 80%;
image-rendering: pixelated;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it makes sense to turn this on globally. This makes images crisp when they get scaled up, which was useful for figuring out antialiasing and such (since the images are always upscaled to 80% width).

Copy link
Collaborator

@apaszke apaszke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generally looks good. Some asks form me would be:

  1. There's a lot of inline math with no explanation nor reference. It would be good to factor those out or at least add some text. Otherwise I think it will be difficult for people to follow this example, and that's their purpose after all.
  2. There are a lot of int<->index casts. I wonder if we could eliminate some of those? Otherwise it doesn't seem to be a great selling point for Dex?

examples/caustics.dx Outdated Show resolved Hide resolved
def rasterLineSegmentInFourPixels (height:Int)?-> (width:Int)?-> (ref:Type)?->
(_: VSpace out)?=>
(pixel_x: Int) -- 0 <= pixel_x < width - 1
(pixel_y: Int) -- 0 <= pixel_y < height - 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those invariants are exactly the Fin width and Fin height invariants, so maybe take those as arguments? The unsafe call sites can still do the check, but the safe ones can skip them (and I think you have those below!)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting. They are actually Fin (width-1). Would it make sense to represent the canvas size as Unit|Fin width_minus_1 in this case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Maybe? But then we would have to change all the other functions (drawIntoCanvas, rasterLineSegment, etc) to accept non-Fin-based tables. I'm not sure if that's a net improvement or not. What do you think?

(also, technically, this function works fine if you give it things out of bounds, it just does nothing in that case. So it's not strictly speaking an invariant.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I'm just a little concerned with the number of index<->int casts in this code. I would consider that an usual case for Dex code, and it will likely cause us to deoptimize a bunch of stuff. I'd rather remove as many of them as we can.

examples/caustics.dx Outdated Show resolved Hide resolved
det = m.(0@_).(0@_) * m.(1@_).(1@_) - m.(0@_).(1@_) * m.(1@_).(0@_)
(1.0/det) .* [ [m.(1@_).(1@_), -m.(0@_).(1@_)]
, [-m.(1@_).(0@_), m.(0@_).(0@_)]
]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just support table unpacking syntax? You could do

[[a, b], [c, d]] = m

and then do math on the individual elements

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea! Yeah, that would clean up a lot of stuff.

examples/caustics.dx Outdated Show resolved Hide resolved
@danieldjohnson
Copy link
Collaborator Author

I agree that this isn't as well documented as it could be; I was mostly focused on trying to get something that worked. I'll take another pass through it and try to make it more readable.

Regarding the int <-> index casts, it seems like there's a few different reasons I'm doing that in this code:

  1. to unpack small fixed-size tables (table unpacking syntax would make this less visually cluttered, but I'm not sure it would change the behavior?)
  2. to dynamically render shapes into specific locations in a larger table (this is pretty fundamental to how the rendering algorithm works)
  3. to iterate over dynamically-sized ranges of integers

I suppose there are other ways of accomplishing 3 (like a while loop) but I don't have a mental model of what the performance characteristics are. For 2, I'm not sure there's much that I can do to remove those. Is dynamically indexing a table based on math something that Dex isn't expected to do well? If so, maybe this isn't a good choice of example.

]

-- TODO: deduplicate with https://github.com/google-research/dex-lang/pull/276
def atan2 (y:Float) (x:Float) :Float =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, I forgot you already added an atan2 function. The one I used is more expensive, but follows the TF and XLA implementations. I think its derivation is pretty close to the one you used, but with a higher-degree polynomial. I don't know if the XLA people had a good reason for their choice - perhaps it should depend on the floating-point precision being used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Do what XLA does" is probably a better justification for an implementation than "use the first thing you see on stack overflow" anyways!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants