jgayfer / bevy_light_2d

General purpose 2D lighting for the Bevy game engine.
MIT License
128 stars 5 forks source link

Light occlusion #2

Closed jgayfer closed 1 week ago

jgayfer commented 2 months ago

I'd like to add the option to add light occluders, allowing us to have shadows where light is occluded.

The initial version of this feature will likely include only basic occluder shapes (square and/or circle) and hard shadows, but we can look to improve upon this later as needed.

FreddyWordingham commented 2 months ago

Hi @jgayfer,

Thank you for this package. I'd like to understand more about achieving a light occlusion effect.

Is your planned approach to:

  1. Add an Occluder component
  2. Extract Occluder entities to send position and shape data to the GPU uniform, along with the light position and intensity data you are already extracting
  3. Transform occluder positions from world space to screen space within the shader
  4. Calculate the shadow regions in screen space

Alternatively, could you read the depth map from a pre-pass and calculate shadows based on that information? This method might be simpler (if there's a sampleable depth map in 2D) but would require careful placement of sprites along the z-axis.

Thanks!

jgayfer commented 2 months ago

@FreddyWordingham That's roughly how I've been thinking about it!

Effectively a pre-pass shader to compute an SDF texture that we can use with ray marching in the main lighting pass to see if there is an occluder between a given vertex and a point light. This shader toy is roughly how I hope to achieve the effect (though the SDF field would be computed up front, and I'd likely start with hard shadows).

That being said, I'm very new to the world of rendering, and am learning a lot as I go.

I'm admittedly not familiar with depth maps / buffers (though it's been on my radar to learn about). I'm not entirely sure how the z values would map to shadows. Interested to hear more about your idea to help me better understand.

FreddyWordingham commented 2 months ago

@jgayfer thanks very much for your reply. The shader toy is a great example

My comment about using a depth map might be a little naive - I'm not sure the Bevy 2D camera implementation actually supports a depth map like it does in the 3D physically based rendering pipeline. My hope was that computing the SDF texture in a pre-pass could be skipped, using the depth texture instead. If the occluding objects were at z=1.0, and the floor was z=0.0, you could check for any values of z=1.0 between two points to determine if an area is occluded. This is probably a bad idea anyway as it would mean having to place sprites in the world at certain heights.

Thanks again for writing bevy_light_2d, it's been a very helpful example of "Extracing" world components for the GPU pipeline, and using that information in a post-processing shader

jgayfer commented 2 months ago

@FreddyWordingham Ah! I see what you're saying.

Some thoughts with that approach:

  1. We likely wouldn't know if a given object should occlude light (or if a given pixel is from an object at all).
  2. We wouldn't know the shape of the object, so we wouldn't be able to make use of an existing SDF (which means ray marching is off the table, which I believe is somewhat required for decent performance).

Happy to be proven wrong, but that's what comes to mind.


I'm glad you're found the crate useful!

Creating a working and useful library was honestly a bonus. A big motivation for me was to create something that is cleanly laid out and easy to follow for someone new to custom render passes (especially on the shader side). I struggled a lot to get up to speed, and there aren't a ton of resources out there.

FreddyWordingham commented 2 months ago

Hi @jgayfer I had a bash at implementing a CircularOccluder2d component and adding it to your pipeline. I'm absolutely sure you're way ahead of me and how this can be done better (particularly in the shader). If it's of any use please find the fork here, and the diff here.

Screenshot 2024-06-13 at 15 47 44

cargo run --example occluder shows a single large point light source in the centre, with a small circular occluder moving horizontally, back and forth, just above the light.

Currently, I'm seeing stippling in my bevy_light_2d implementation [above]. Although, I'm not seeing the same issue in my playground project [below]. So I've got some discontinuity to track down!

Screenshot 2024-06-13 at 15 56 05

Scene contains a spider, wondering around with a light source attached to it. There is an (invisible) circular occluder in the centre of the tilemap). The green tiles show the ground the spider can walk on, and the yellow box shows the spiders spawn point.


Thanks again for your work

jgayfer commented 2 months ago

@FreddyWordingham Wow, very nice! I hadn't considering doing it without computing the SDF texture up front.

Do you want to open this up as a draft PR? Will make it a touch easier to review and test (plus make it a bit more visible).

FreddyWordingham commented 2 months ago

Absolutely :)

I'll try and sort out the source of the stippling issue before I open the PR.

I'll look into computing the SDF texture in a node before the main lighting pass, as I'm sure it will be much more performant with a greater number of occluders

jgayfer commented 2 months ago

The SDF texture very likely would perform a fair bit better, but I don't think I'd say no to your approach as an initial version. It's a net improvement (though I'd want to make sure it at least performs half decently).

One limitation with WebGL2 is we can't use compute shaders, which is likely the preferred way to create the SDF texture. This means we may still want to fall back to the "naive" approach of computing signed distances inline (as the current proposal does).