Skip to content

Conversation

mate-h
Copy link
Contributor

@mate-h mate-h commented May 2, 2025

Objective

Currently Bevy's atmosphere solution does not extend to include the PBR pipeline, it is merely a composited effect on top of the scene using blend operations. The goal of this pull request is to provide realistic, physically-based lighting to any geometry in an outdoor scene, covering a vast number of use cases for game developers.

Solution

We achieve this using a combination of the following features:

  • Environment map light reflections include the atmosphere at all roughness levels
  • Volumetric shadowing through the atmosphere for opaque geometry
  • Space views using a custom origin point and raymarched rendering
  • Small bonus: added parameters for Martian atmosphere preset using scientific data

More specific implementation details:

  • Copy atmospheric functions to PBR pipeline and compute new directional light color.
  • Added new specialization key for when the atmosphere is present, and a new shader def in the main PBR pipeline
  • Added blue noise texture, added parser 16bit texture parser function into a missing implementation
  • Created a spherical coordinate system where the observer's position is relative to the planet surface instead of relying on just the altitude parameter "r". this needs to be followed up by supporting the transform component on the atmosphere.
  • Added a rendering_method parameter to the AtmosphereSettings which includes more precise volumetric shadows and enables space views
  • Added an origin parameter to the Atmosphere which controls the observers position, enabling space views for using the raymarched rendering method
  • Added a new SunLight component that controls the size of the sun disk rendered in the atmosphere and defaulted it to the mean value taken from scientific data

Shared ray-marching function

Shared ray-marching function in the atmosphere shader, except for the aerial-view LUT. For the Aerial-view LUT we have two nested for loops, and we march the ray further and further for each depth slice so it's slightly different.

The shared ray-marching function has the following signature:

struct RaymarchResult {
    inscattering: vec3<f32>,
    transmittance: vec3<f32>,
}

fn raymarch_atmosphere(
    pos: vec3<f32>,
    ray_dir: vec3<f32>,
    t_max: f32,
    sample_count: f32,
    uv: vec2<f32>,
    jitter: bool,
    ground: bool
) -> RaymarchResult

Since WGSL has no function overloading (see open issue), the caller has to supply all arguments. I also wanted to specifically avoid using a struct as an input to make the resulting called code more concise, at the sacrifice of readability. Because the function signature can easily be looked up, I didn't think this was an issue.
Caller code example:

#import bevy_pbr::atmosphere::raymarch_atmosphere;

let result = raymarch_atmosphere(world_pos, ray_dir, t_max, sample_count, uv, jitter, ground, shadow);

Proposal: additional parameter to control whether to apply the shadowing term for computing volumetric shadows.

PBR Directional light

PBR directional lights are occluded by the transmittance through the atmosphere, getting tinted orange, then red when the sun is low over the horizon.

  • First, we specify a pipeline key for the atmosphere
  • Bind the atmosphere buffer and the transmittance texture to the PBR pipeline.
  • Implemented the pbr_lighting.wgsl file's directional_light function to extend it to the atmospheric transmittance.

Generated Environment Map Light

In order to generate an environment map, we place a light probe into the world and attach the AtmosphereEnvironmentMapLight component to it. Given it's transform in the world, we create an environment map at that location, by running the atmosphere shader ray-marching for each cube face. Note how we need to create a separate view (2d storage texture array) and re-interpret it at runtime, using the create view function.

ECS flow:

  • The entity starts with LightProbe and AtmosphereEnvironmentMapLight
  • AtmosphereEnvironmentMapLight is extracted with ExtractComponentPlugin
  • prepare_atmosphere_probe_components adds FilteredEnvironmentMapLight and AtmosphereEnvironmentMap
  • create_environment_map_from_prefilter adds EnvironmentMapLight based on the FilteredEnvironmentMapLight

Single Pass downsampling (SPD) pipeline:

  • takes the 512x512 Cubemap as an input, and creates 9 MIP levels.
  • each level is progressively down-sampled.
  • It is actually broken into two passes due to architectural limitations.
  • largely based on Jasmine's code from github

Pre-filtering pipeline: composed of multiple Radiance Map (specular mips) generation passes, followed by the irradiance map pass (diffuse).
The pre-filtering pipeline is largely based on these articles:

Render Graph:
render-graph

Testing

  • Test scene has been updated to include a range of light probes with different roughness levels and metallic values
  • Added Minimal camera, sun position and exposure controller
  • Switch between Earth and Mars atmosphere using 1 and 2 keys
  • Added help text similar to other examples

Showcase

showcase-image

Usage:

// Spawn a new light probe to generate an environment map for the atmosphere
commands.spawn((
    LightProbe,
    AtmosphereEnvironmentMapLight::default(),
    // The translation controls where the light probe is placed,
    // and the scale controls the extents of where it will affect objects in the scene.
    Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::splat(100.0)),
));

Notes for reviewers

In it's current state, this PR includes a lot of code that may not need to be merged into the main branch. Therefore I am looking for feedback which changes to eliminate before proceeding any more work on it.

Given that all this work is done, but how large this pull request is may make it difficult to review all at once. I'm also looking for feedback whether it is better/more efficient to split it into smaller ones, for example:

  1. PBR directional light
  2. SPD + Environment lighting
  3. Volumetric shadowing and mars

Next Steps

  • create a table comparing the ground, jitter, and shadow parameters per-render pass and screenshots that show the results
  • fix deferred rendering with the atmosphere
  • dispatch separate pass for rendering a sky-view lut for the environment and profile the improvement in gpu time
  • create a graphic that shows the new ECS components and their relationships
  • support the transform component in the atmosphere to set the scene origin
  • use shared raymarch function in aerial view lut as well.
  • support larger environment maps above 512x512 resolution per cubeface (more mips)

@mate-h
Copy link
Contributor Author

mate-h commented May 2, 2025

More robust test scene includes camera and sun controller(with right click) and switching between earth, mars, raymarched vs default rendering:
atmosphere.rs.txt

@jnhyatt
Copy link
Contributor

jnhyatt commented May 3, 2025

This seems to be several changes under one umbrella. Do you think it's at all feasible to split this into multiple PRs? It'd be much easier to review piecewise but I understand it might well be a lot of work. No worries if that's the case

@mate-h
Copy link
Contributor Author

mate-h commented May 3, 2025

It's definitely feasible the only reason I didn't, is because I am not certain that will ultimately be less work to review. But i could start by splitting out the PBR directional light part since it's fairly isolated. But the new ray-marching function is used throughout, so that part might be more challenging. How many smaller pieces would make sense?

@jnhyatt
Copy link
Contributor

jnhyatt commented May 3, 2025

Eh, I guess not all that important after my first pass. It's honestly pretty well-structured and somewhat interrelated. I'll trust you're right about what's less review work. If you want to break it up, I think the light probe stuff looks pretty well-isolated, and the directional light stuff like you said. But again, I'm no longer convinced it'd be better like that.

@JMS55 JMS55 self-requested a review May 3, 2025 01:23
@mate-h
Copy link
Contributor Author

mate-h commented May 3, 2025

After some discussion I decided it's best to de-couple the SPD and pre-filtering pipeline changes in a separate PR, but keeping everything atmosphere-related here, and in the other PR I will still reference this one for the description and use-case.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Contentious There are nontrivial implications that should be thought through S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 5, 2025
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label May 5, 2025
@alice-i-cecile alice-i-cecile added this to the 0.17 milestone May 5, 2025
Copy link
Contributor

github-actions bot commented May 5, 2025

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@atlv24 atlv24 modified the milestones: 0.17, 0.18 Jul 8, 2025
github-merge-queue bot pushed a commit that referenced this pull request Jul 23, 2025
# Objective

This PR implements a robust GPU-based pipeline for dynamically
generating environment maps in Bevy. It builds upon PR #19037, allowing
these changes to be evaluated independently from the atmosphere
implementation.

While existing offline tools can process environment maps, generate mip
levels, and calculate specular lighting with importance sampling,
they're limited to static file-based workflows. This PR introduces a
real-time GPU pipeline that dynamically generates complete environment
maps from a single cubemap texture on each frame.

Closes #9380 

## Solution

Implemented a Single Pass Downsampling (SPD) pipeline that processes
textures without pre-existing mip levels or pre-filtered lighting data.

Single Pass Downsampling (SPD) pipeline:
- accepts any square, power-of-two cubemap up to 8192 × 8192 per face
and generates the complete mip chain in one frame;
- copies the base mip (level 0) in a dedicated compute dispatch
(`copy_mip0`) before the down-sampling pass;
- performs the down-sampling itself in two compute dispatches to fit
within subgroup limits;
- heavily inspired by Jasmine's prototype code.

Pre-filtering pipeline:
- generates the specular Radiance Map using bounded-VNDF GGX importance
sampling for higher quality highlights and fewer fireflies;
- computes the diffuse Irradiance Map with cosine-weighted hemisphere
sampling;
- mirrors the forward-/reverse-tonemap workflow used by TAA instead of
exposing a separate *white-point* parameter;
- is based on the resources below together with the “Bounded VNDF
Sampling for Smith-GGX Reflections” paper.

The pre-filtering pipeline is largely based on these articles:
-
https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/
- https://bruop.github.io/ibl/
-
https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf

> The forward-/reverse-tonemap trick removes almost all fireflies
without the need for a separate white-point parameter.

Previous work: #9414

## Testing

The `reflection_probes.rs` example has been updated:

- The camera starts closer to the spheres so the reflections are easier
to see.
- The GLTF scene is spawned only when the reflection probe mode is
active (press Space).
- The third display mode (toggled with Space) shows the generated
cubemap chain.
- You can change the roughness of the center sphere with the Up/Down
keys.

## Render Graph

Composed of two nodes and a graph edge:
```
Downsampling -> Filtering
```

Pass breakdown:
```
dowsampling_first_pass -> dowsampling_second_pass ->
radiance_map_pass -> irradiance_map_pass
```

<img width="1601" height="2281" alt="render-graph"
src="https://github.com/user-attachments/assets/3c240688-32f7-447a-9ede-6050b77c0bd1"
/>

---

## Showcase
<img width="2564" height="1500" alt="image"
src="https://github.com/user-attachments/assets/56e68dd7-9488-4d35-9bba-7f713a3e2831"
/>


User facing API:
```rust
commands.entity(camera)
    .insert(GeneratedEnvironmentMapLight {
        environment_map: world.load_asset("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
        ..default()
    });
```

## Computed Environment Maps
To use fully dynamic environment maps, create a new placeholder image
handle with `Image::new_fill`, extract it to the render world. Then
dispatch a compute shader, bind the image as a 2d array storage texture.
Anything can be rendered to the custom dynamic environment map.
This is already demonstrated in PR #19037 with the `atmosphere.rs`
example.

We can extend this idea further and run the entire PBR pipeline from the
perspective of the light probe, and it is possible to have some form of
global illumination or baked lighting information this way, especially
if we make use of irradiance volumes for the realtime aspect. This
method could very well be extended to bake indirect lighting in the
scene.
#13840 should make this possible!

## Notes for reviewers

This PR no longer bundles any large test textures.

---------

Co-authored-by: atlas <email@atlasdostal.com>
@mate-h
Copy link
Contributor Author

mate-h commented Aug 2, 2025

Work has been continued on a different set of Git branches.
mate-h#10
I will be opening separate , smaller PRs to land this feature.

@mate-h mate-h closed this Aug 2, 2025
@github-project-automation github-project-automation bot moved this to Done in Rendering Aug 2, 2025
@mate-h mate-h removed this from Rendering Aug 2, 2025
@mate-h mate-h removed this from the 0.18 milestone Aug 2, 2025
@atlv24 atlv24 mentioned this pull request Aug 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants