In the atmosphere example we can change between Earth and Mars, but the
size of the sun does not change. This somewhat bothered me from an
astronomical accuarcy.
This commit introduces a new `SunDisk::MARS` constant to accurately
represent the sun’s apparent size when viewed from Mars. The
implementation updates the documentation and adds the corresponding
constant with an angular size of ~21 arcminutes (0.00615 radians).
Key modifications:
- Added `SunDisk::MARS` constant in `directional_light.rs`
- Updated documentation to reference the new Mars preset
- Modified `atmosphere` example to switch sun disk alongside atmosphere
presets
I tested this manually.
---------
Co-authored-by: Máté Homolya <mate.homolya@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
# Objective
Fix typos and other small issues in the documentation. I can drop the
changes to `bevy_reflect`'s and `bevy_anti_alias`'s crate descriptions
if it's a problem.
# Objective
When using the `SpotLight` component on an entity that already has the
`Camera` component, the `Frustum` component of the camera will be
overriden with the one create from the spotlight. This leads to
incorrect frustum culling, if both components are on the same entity.
There is no mention of this in the docs. This pr adds that.
I am not sure what the convention is when doing warnings in the docs. I
just did \*\*bold\*\*, but I can change that.
## Solution
Change docs
---------
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
Right now, visibility ranges are always resolved relative to the view.
This is incorrect for shadow maps in two ways:
1. Visibility ranges for meshes in directional light shadow maps should
be resolved relative to the camera that the cascades are associated
with.
2. Visibility ranges for meshes in point and spot light shadow maps
should be resolved relative to *some* camera.
To properly solve (2), this commit introduces the notion of a *shadow
LOD origin*. The shadow LOD origin is the point that visibility ranges
are relative to, when rendering views not associated with any camera.
Point and spot light shadow maps are currently not associated with a
camera, and therefore we need this extra notion in order to properly
evaluate visibility ranges.
(As a follow-up, we should introduce the notion of *own shadow maps*,
which will allow each camera to have separate shadow maps for point and
spot lights. That feature is however out of scope for *this* patch,
which simply seeks to make the existing semantics consistent.)
A new component, `ShadowLodOrigin`, has been added, which allows the
developer to customize the shadow LOD origin. In the absence of this
component, this PR implements a simple heuristic to determine the shadow
LOD origin: to prefer an origin that coincides with cameras that render
to a window. This heuristic should suffice in the vast majority of
cases, so developers will rarely have to manually use the
`ShadowLodOrigin` component.
A new field, `lod_view_world_position`, has been added to `View` to
supply the position of the shadow LOD origin to the GPU. This is much
simpler than introducing a new uniform or using immediates, as #24197
tried to do.
This commit is the proper fix for #23991. PR #24252 attempted to fix
this problem by reverting #23115. However, this didn't actually fix the
issue, because the semantics were still inconsistent. This commit
constitutes the correct fix for the issue. I verified that, after
un-reverting #23115 on top of this patch and modifying it to use the new
`lod_view_world_position`, that the issue reported in #23991 disappears.
# Objective
Fixes
https://github.com/bevyengine/bevy/issues/24084#issuecomment-4365309756
Also fixes `EnvironmentMapLight::rotation` if it is attached to a view.
## Solution
Store view environment map rotation in `LightProbesUniform` and remove
`EnvironmentMapUniform`
## Testing
The examples works:
```
cargo r --example pccm --features free_camera,https
cargo r --example light_probe_blending --features free_camera,https
cargo r --example rotate_environment_map --features pbr_multi_layer_material_textures
```
# Objective
Alternative to #24004.
https://github.com/bevyengine/bevy/pull/23288 adds ltc luts for rect
light support which implicitly requires `bevy_image/ktx2` and
`bevy_image/zstd` otherwise loading ltc luts will panic.
We either accept to always enable area light supoort (#24004), or add a
feature to opt out it (this PR).
## Solution
Gate ltc luts behind a feature and merge them to a texture array.
## Testing
`rect_light` example works.
---------
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
# Objective
- Should fix#23573 - I don’t have DX-12 so I can’t verify this, just
following orders. Someone can verify this is fixed!
## Solution
- Search and replace “29.0.1” with “29.0.3” if it is related to wgpu
(and checking that the version is available on crates)
- Fixes#23220.
## Testing
- Leaning on CI for this one personally
# Objective
- solid_color returned envmap lights that looked black
## Solution
- set intensity to 1.0
- also make sure to use proper color space (linear)
## Testing
- tested in reflection_probe example
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
# Objective
- Resolves a "strict ambiguity" from #23843.
- TL;DR, adding systems before this schedule with `Commands` could cause
these systems being ambiguous.
- A step for #23846.
## Solution
- Explicitly order these lighting bounds updates after the transform
propagation.
## Testing
- This removes a "strict ambiguity" from #23846.
# Objective
- Partially addresses #23688.
- Prevents use of `Skybox::default()` from causing errors.
## Solution
`Skybox::default()` is problematic because it contains an `Image` that
is not a valid skybox. ~~This change removes the `Default`
implementation and instead provides a `new()` function which takes the
image as a parameter (and also the brightness, which is practically
required).~~ This change makes the `image` field optional so that the
default `None` renders nothing.
Things we could do instead of this:
* Make `Skybox` not implement `Default`. I am informed this is a bad
idea.
* Create a default cubemap image for `default()` to use.
## Testing
Ran the `skybox` and `irradiance_volumes` examples.
# Objective
A lot of Bevy's built in types don't derive FromTemplate, which is what
makes things like asset handles "templatable" in BSN:
```rust
bsn! {
ImageNode { image: "path_to_image.png" }
}
```
## Solution
Derive `FromTemplate` for components that have handles.
In cases of fields with `Option<SOME_TEMPLATED_TYPE>`, I've added the
`#[template(OptionTemplate<XTemplate>)]` attribute, as these cases can't
use the blanket `Default + Clone` template implementation (which doesn't
do templating on the internal type). Later I'm likely to propose
something like `#[template(built_in)]` for standard "collection types"
to make this a little less boilerplatey.
# Objective
Enable the use of `NotShadowReceiver` in `bsn!` by implementing `Clone`
(`Default` is already implemented).
```rust
error[E0277]: the trait bound `NotShadowReceiver: Clone` is not satisfied
--> src/spawn_circle.rs:375:27
|
375 | world.spawn_scene(bsn! {
| ___________________________^
376 | | #SpawnCircle
377 | | SpawnCircle
378 | | SpawnEventToTrigger({self.event})
... |
394 | | )]
395 | | });
| |_________^ the trait `Clone` is not implemented for `NotShadowReceiver`
|
```
I also noticed `TransmittedShadowReceiver` would have the same issue, so
added `Clone` there as well.
---
An alternative would be to implement `FromTemplate` instead of `Default`
and `Clone`. I'm not sure which solution is preferred in general, but
these are Marker components so don't require any additional values.
# Objective
**Anecdotal feedback:**
- no floating origin support for implementing large-scale worlds, forked
Bevy's atmosphere at
https://github.com/philpax/veldera/blob/main/crates/bevy_pbr_atmosphere_planet/NOTICE.md
- no custom up axis support, resorted to using a custom sky shader for
flight simulator with Z up coordinate system. Bevy's atmosphere appears
tilted at a 90 degree angle with no way of changing it.
## Solution
- Atmosphere component can be spawned stand-alone
- AtmosphereSettings remains on camera
- A closest-to-camera heuristic is used to pick the primary atmosphere
to render. Deliberately no multi-atmosphere support to keep the scope of
this PR small and self contained. See
https://github.com/mate-h/bevy/pull/19 at an attempt.
- `scene_units_to_m` removed in favor of using `Transform`
- Z up now possible by offsetting the viewer position to the equator
- Floating origin systems now possible
- Simplify the `AtmosphereBuffer` / `AtmosphereData` structs to just use
the plain extracted `GpuAtmosphere` struct. this reduces the complexity
of the struct in the mesh view bindings. Since atmosphere settings is
coupled with the rendering pipeline of the atmosphere this makes sense
architecturally.
- We no longer hard code the offset to the north pole from the planet
center in places.
**Why not multi atmosphere:**
The atmosphere uses multiple LUTs (lookup textures) to accelerate the
rendering performance. Some of them are not view dependent:
- Transmittance LUT
- Multiple scattering LUT
- Scattering / density LUTs
These can be coupled and rendered for each atmosphere individually.
However the remainder of the pipeline is view dependent:
- Aerial View LUT
- Sky View LUT
- Render Sky pass
In raymarched rendering mode, these LUTs can be skipped and only the
render sky pass runs sampling on all of the atmospheres with a raymarch
in screen space.
Further, the Sky View LUT uses a local reference frame to concentrate
texel density along the horizon's local up axis. This in turn means it's
coupled with both a _specific_ atmosphere's local coordinates as well as
the view's transform matrix. We cannot consider rendering both
atmospheres into a single LUT for this reason. So it has to be unique
for each pair of (view, atmosphere). Given two views and two atmospheres
we would need 4 of these Sky View LUTs and at some point, raymarched
rendering will become the less expensive option.
Lastly the Render Sky pass needs to happen once per view, we cannot
realistically composite them in sequence with simple dual-source
blending as we do with the scene, this would result in incorrect
scattering integration. This in turn means we need to bind ALL of the
luts calculated previously so a single render sky pass and render aerial
view lut - perhaps making use of array textures. Rely on unified
volumetric ingegration in the raymarching loop: for each light,for each
atmosphere, attenuate inscattering and transmittance along the path
integral. It is suffice to say this change is overall _too complex_ for
the time being and is likely the reason Unreal Engine also do not
support multiple atmospheres. For context: our research is based heavily
on Sebastian Hillarie's work, one of the Unreal graphics engineers.
That being said about multiple atmospheres - I am thinking of this PR as
a segway into unified volumetrics in Bevy. that is: Render the FogVolume
and Atmosphere in a single pass! Making use of the frustum aligned voxel
grid "froxel" approach to accelerate the rendering. This would
drastically increase the performance for scenes wanting to make use of
both the atmosphere and local fog volumes.
## Testing
- Ran the `examples/3d/atmosphere.rs` example.
---
## Showcase
(example screenshot unchanged compared to main.)
```rs
// Spawn earth atmosphere
commands.spawn(Atmosphere::earth(earth_medium));
commands.spawn((
Camera3d::default(),
// Can be adjusted to change the rendering quality
AtmosphereSettings::default(),
));
```
---------
Co-authored-by: Emerson Coskey <emerson@coskey.dev>
# Objective
Fix errors when disabling gpu clustering. Fixes#23208.
## Solution
~Set cluster buffer bindings correctly.~ Allow writing cluster on cpu
buffer if gpu clustering is disabled.
## Testing
Tested mobile example on android.
# Objective
Communicate the fact that we care about not using v29.0.0, since 29.0.1
contains the limits fix discussed in #23277
## Testing
```
cargo run --example occlusion_culling
```
results in no warnings on macos
# Objective
Quite a few comments in the rendering code link to the filament spec.
The address changed from https://google.github.io/filament/Filament.html
to https://google.github.io/filament/Filament.md.html at some point.
There is an automated redirect set up, but fragment identifiers are
dropped during the redirect so section links no longer work.
## Solution
Find and replace :)
I also fixed two of the links that were broken.
wgpu update for v29.
I have tested on macos m1, m5, and windows. Linux testing is
appreciated.
- [x] before merge, naga_oil and dlss_wgpu need to be published, and the
patches referencing their respective PRs removed from the workspace
Cargo.toml
##### other PRs
- naga_oil: https://github.com/bevyengine/naga_oil/pull/134
- dlss_wgpu: https://github.com/bevyengine/dlss_wgpu/pull/27
##### Source of relevant changes
- `Dx12Compiler::DynamicDxc` no longer has `max_shader_model`
- https://github.com/gfx-rs/wgpu/pull/8607
- `Dx12BackendOptions::force_shader_model` comes from:
- https://github.com/gfx-rs/wgpu/pull/8984
- Allow optional `RawDisplayHandle` in `InstanceDescriptor`
- https://github.com/gfx-rs/wgpu/pull/8012
- Add `GlDebugFns` option to disable OpenGL debug functions
- https://github.com/gfx-rs/wgpu/pull/8931
- Add a DX12 backend option to force a certain shader model
- https://github.com/gfx-rs/wgpu/pull/8984
- Migrate validation from maxInterStageShaderComponents to
maxInterStageShaderVariables
- https://github.com/gfx-rs/wgpu/pull/8652
- gaps are now supported in bind group layouts
- https://github.com/gfx-rs/wgpu/pull/9034
- depth validation changed to option to match spec
- https://github.com/gfx-rs/wgpu/pull/8840
- SHADER_PRIMITIVE_INDEX is now PRIMITIVE_INDEX
- https://github.com/gfx-rs/wgpu/pull/9101
- Support for binding arrays of RT acceleration structures
- https://github.com/gfx-rs/wgpu/pull/8923
- Make HasDisplayHandle optional in WindowHandle
- https://github.com/gfx-rs/wgpu/pull/8782
- `QueueWriteBufferView` can no longer be dereferenced to `&mut [u8]`,
so use `WriteOnly`.
- https://github.com/gfx-rs/wgpu/pull/9042
- ~bevy_mesh currently has an added dependency on `wgpu`, can we move
`WriteOnly` to wgpu-types?~ (it is in wgpu-types now)
- Change max_*_buffer_binding_size type to match WebGPU spec (u32 ->
u64)
- https://github.com/gfx-rs/wgpu/pull/9146
- raw vulkan init `open_with_callback` takes Limits as argument now
- https://github.com/gfx-rs/wgpu/pull/8756
## Known Issues
There is currently one known issue with occlusion culling on macos,
which we've decided to disable on macos by checking the limits we
actually require. This makes it so that if wgpu releases a patch fix,
bevy 0.19 users will benefit from occlusion culling re-enabling for
them.
<details><summary>More details</summary>
On macos, the wpgu limits were changed to align with the spec and now
put the early and late GPU occlusion culling `StorageBuffer` limit at 8,
but we currently use 9. [Filed in wgpu
repo](https://github.com/gfx-rs/wgpu/issues/9287)
```
2026-03-19T01:37:10.771117Z ERROR bevy_render::error_handler: Caught rendering error: Validation Error
Caused by:
In Device::create_bind_group_layout, label = 'build mesh uniforms GPU late occlusion culling bind group layout'
Too many bindings of type StorageBuffers in Stage ShaderStages(COMPUTE), limit is 8, count was 9. Check the limit `max_storage_buffers_per_shader_stage` passed to `Adapter::request_device`
```
</details>
solari working on wgpu 29:
<img width="1282" height="752" alt="image"
src="https://github.com/user-attachments/assets/4744faec-65c0-4a72-93e1-34a721fc26d8"
/>
---------
Co-authored-by: atlv <email@atlasdostal.com>
Currently, Bevy handles meshes tagged with `NoCpuCulling` by simply
always adding them to `VisibleEntities`. This is, however, inefficient,
because `VisibleEntities` has to be repopulated with the entity IDs of
such meshes every frame and copied to the render world. When scaling to
millions of entities, this becomes a significant bottleneck.
This PR changes the visibility systems to ignore meshes with
`NoCpuCulling` entirely. Instead of being added to `VisibleEntities`,
the mesh extraction systems instead use standard ECS queries to iterate
over meshes with `NoCpuCulling` directly, in addition to any entities in
`VisibleEntities` that use CPU culling. For efficiency,
`RenderVisibleMeshEntities` now tracks mesh instances that are subject
to CPU culling and those that opted out of CPU culling in two separate
data structures. Note that this required changing the signatures of
`DirtySpecializations` methods to return a tuple of references instead
of a reference to a tuple. Although that change looks complicated, it's
actually just reshuffling to accommodate this slight type change, not a
change in logic.
On `bevy_city`, `check_visibility` takes a median of 1.19 ms, and
`check_dir_light_mesh_visibility` takes a median of 4.33 ms. With this
patch, these systems entirely disappear if `NoCpuCulling` is added to
every mesh.
<img width="2756" height="1800" alt="Screenshot 2026-02-22 020929"
src="https://github.com/user-attachments/assets/18048399-bcfd-4165-8491-8d126d73534e"
/>
Currently, Bevy clusters lights on the CPU. This is generally not
considered a best practice any longer, and it can be a bottleneck in
workloads like `many_lights`. Moreover, it prevents GPU systems like
[Hanabi] from creating clusterable objects such as lights and decals
without a round trip to the CPU.
This PR introduces GPU light clustering when supported by the hardware.
The algorithm is the same as the existing GPU light clustering, but
parallelized over all clusters, and the resulting on-GPU format for
clusters is unchanged. GPU light clustering uses the hardware rasterizer
for compute purposes as a way to automatically distribute workloads
within 2D axis-aligned bounding boxes without actually rendering any
pixels, a first for Bevy. The algorithm is as follows, with each step
corresponding to a raster or compute command:
1. *Z slicing*: We have a 3D cluster froxel grid of size WxHxD and seek
to rasterize D axis-aligned quads, each of size WxH, representing the
range of each clusterable object. In this compute phase, we generate D
indirect instances for each clusterable object for the subsequent
indirect draws.
2. *Count rasterization*: We use instanced indirect drawing to rasterize
each quad generated in step 1 to a viewport of size WxH, with color
writes disabled. Each rasterized fragment represents a cluster-object
pair. In the fragment shader, we check to see if the object intersects
the cluster, and, if it does, we atomically bump a counter corresponding
to the number of objects of the given type intersecting the cluster in
question. We don't record the ID of the object in this phase; we simply
count the number of objects.
3. *Local allocation*: Now that we know the number of objects of each
type in each cluster, we can proceed to allocate space in the clustered
object buffer for each clustered object list. To do this, we need to
perform a [*prefix sum*] operation so that each list is tightly packed
with the others. For example, if adjacent clusters have 2, 5, and 3
objects, they'll be allocated at offsets 0, 2, and 7 respectively. This
*local* step uses a [Hillis-Steele scan] in shared memory to compute the
prefix sum of each chunk of 256 clusters. We can't go beyond 256
clusters in this local step because 256 is the maximum workgroup size in
`wgpu`.
4. *Global allocation*: To deal with the fact that we can't calculate
prefix sums beyond 256 clusters in step 3, we employ this second step
that does a sequential loop over every 256-cluster chunk, propagating
the prefix sum. At the end of this step, every list of clustered objects
is allocated.
5. *Populate rasterization*: Finally, we issue an instanced indirect
draw command using the same parameters as step (2). We test each
cluster-object pair for intersection, and, if the test passes, we record
the ID of each clustered object into the correct space in the list,
using a scratch pad buffer of atomics to store the position of the next
object in each list.
The buffer of clustered objects has a fixed size and can overflow. We
detect this condition via asynchronous CPU readback and automatically
grow the buffer for subsequent frames. In this case, we also log a
message so that the developer can choose a larger initial buffer size
and avoid any incorrect frames. Additionally, like #22874, the automatic
clustering heuristics are dynamically adjusted from frame to frame, by
recording statistics on the GPU and using CPU readback to download them
back to the CPU for processing.
As part of this PR, I refactored clustered visibility so that clustered
objects go through the same `ViewVisibility` system as other objects,
instead of using `VisibleClusterableObjects`. This was a nice
simplification.
On the `many_lights` benchmark, with about 8,000 lights visible out of
100,000, this process takes approximately 0.099 ms on my NVIDIA GeForce
RTX 4070 Laptop GPU. The AMD Ryzen 9 8945HS CPU, however, takes 2.12 ms
to do the same task. The GPU version is therefore a 21x speedup.
`main` `assign_objects_to_clusters` time, 2.12 ms:
<img width="2756" height="1800" alt="Screenshot 2026-02-17 222757"
src="https://github.com/user-attachments/assets/66341ad2-96f2-4e4a-87ee-fe3462bc05de"
/>
GPU clustering GPU time, 0.099 ms:
<img width="2756" height="1800" alt="Screenshot 2026-02-17 222458"
src="https://github.com/user-attachments/assets/18e2e0ae-a946-4b80-b38a-0543e76ebc02"
/>
`main`, 5.71 ms median frame time, 175 FPS:
<img width="2756" height="1800" alt="Screenshot 2026-02-17 222243"
src="https://github.com/user-attachments/assets/111c8e22-414f-4ee1-95fa-d7cfe422c2ab"
/>
GPU clustering, 4.88 ms median frame time, 205 FPS:
<img width="2756" height="1800" alt="Screenshot 2026-02-17 222256"
src="https://github.com/user-attachments/assets/0a662e88-a1b9-49c8-8bab-cc12b46cd079"
/>
[Hanabi]: https://github.com/djeedai/bevy_hanabi
[*prefix sum*]: https://en.wikipedia.org/wiki/Prefix_sum
[Hillis-Steele scan]:
https://en.wikipedia.org/wiki/Prefix_sum#Algorithm_1:_Shorter_span,_more_parallel
## Alice's PM Note from @kfc35
Fixes https://github.com/bevyengine/bevy/issues/22957 and also fixes
https://github.com/bevyengine/bevy/issues/22904.
Right now, every frame, all specialization and queuing systems iterate
over all entities visible from a view and check to see whether they need
to be updated by consulting a set of change ticks and comparing them to
the current change ticks. To handle cases in which a mesh needs to be
removed from the bins, a separate final *sweep* pass then finds entities
that no longer exist and removes them manually from the bins. This
process is complex, error-prone, and slow, as it involves visiting all
visible entities multiple times every frame.
This PR changes the setup so that, instead of examining change ticks,
the visibility logic pushes the set of added and removed entities to
each view explicitly. The visibility system determines which meshes need
to be added and removed by first sorting the list of visible entities,
then performing an O(n) diff process on the last frame's visible
entities and this frame's visible entity list. The end result is that
the specialization and queuing systems only process the entities that
they need to every frame. If a mesh was visible last frame, remained
visible this frame, and didn't change its mesh or material, then it's
generally not examined at all. Not only is this significantly faster for
virtually all realistic scenes, but it's also much simpler.
In order to achieve the benefits of not examining every visible mesh
every frame, I made sorted render passes retained via an `IndexMap`.
This allows entities to be removed and added via random access while
still allowing the list to be sorted by distance. Note that I had to
remove the radix sort because `IndexMap` doesn't currently support that;
I believe the enormous speed benefits of this patch outweigh any minor
sorting regressions from this.
I tested this PR by running `scene_viewer` on a test scene with many
meshes and materials and implementing a material shuffler that randomly
switches the materials around. I tested the following cases:
* Moving the camera so that meshes become visible and invisible.
* Switching opaque materials on meshes.
* Moving meshes from opaque to alpha masked and vice versa.
* Moving meshes from binned render passes to sorted render passes (i.e.
transparent).
* All of the above while the meshes were off screen, then moving them on
screen to ensure that the changes took effect.
This PR brings the `specialize_shadows` time on the `bevy_city` demo
from 12.87 ms per frame to 0.1261 ms per frame, a 102x speedup. It
brings the `queue_shadows` time on the same demo from 12.34 ms per frame
to 0.1102 ms, a 111x speedup. Mean frame time goes from 50.16 ms to
23.26 ms, a 2.16x speedup.
`specialize_shadows` in `bevy_city` before and after:
<img width="2756" height="1800" alt="Screenshot 2026-02-14 180313"
src="https://github.com/user-attachments/assets/dbc3c68b-e0ec-424f-8085-87c0f5f41d3f"
/>
`queue_shadows` in `bevy_city` before and after:
<img width="2756" height="1800" alt="Screenshot 2026-02-14 180500"
src="https://github.com/user-attachments/assets/08f8e1bb-6ab4-47da-ae68-a80156d59caa"
/>
Frame graph of `bevy_city` before:
<img width="2756" height="1800" alt="Screenshot 2026-02-12 203324"
src="https://github.com/user-attachments/assets/d0807cee-23a2-4e14-be1a-7466b795ebfa"
/>
Frame graph of `bevy_city` after:
<img width="2756" height="1800" alt="Screenshot 2026-02-14 180506"
src="https://github.com/user-attachments/assets/b22acf0f-a6f9-432b-93d7-f8057c815b05"
/>
Right now, before assigning objects to clusters, if the
`dynamic_resizing` feature is enabled, `assign_objects_to_clusters`
traverses the list of clusterable objects processing bounding spheres,
and, if the `ClusterFarZMode::MaxClusterableObjectRange` feature is
enabled, `assign_objects_to_clusters` traverses the list of clusterable
objects processing bounding spheres again. This means that we
potentially traverse the list of clusterable objects 3 times per frame,
which has poor cache behavior. Worse, it means that we have to process
clusterable objects on CPU, adding significant CPU overhead even when
GPU clustering is enabled in my [branch]. In fact, this approach won't
work at all when we have the ability to spawn lights and decals on the
GPU without CPU involvement, because the CPU won't be aware of the index
counts and Z extents to begin with.
This patch changes `assign_objects_to_clusters` to instead traverse the
list once and gather relevant statistics as it does so. These statistics
are saved for the *next* frame and used as heuristics for those
features. This does mean that there might occasionally be a single frame
of inefficient PBR shading. However, the savings are significant--a 28%
speedup in `assign_objects_to_clusters`--and the heuristics are actually
*better*, as they can now use the *actual* results of culling instead of
relying on conservative bounding spheres for speed. Note also that this
patch should never result in an *incorrect* frame, only a single
potentially *less optimal* frame.
<img width="2756" height="1800" alt="Screenshot 2026-02-08 234232"
src="https://github.com/user-attachments/assets/e857c688-edb2-4297-a2e2-f16ff5fa52bb"
/>
[branch]: https://github.com/pcwalton/bevy/tree/gpu-clustering
The lighting code is designed such that lights that aren't visible in
any view shouldn't exist in the render world. Unfortunately, when the
render world was made retained, the light extraction code was never
fully updated to clean up components corresponding to lights that became
invisible. So Bevy is currently not enforcing that invariant. This
causes `many_lights` to become slower over time as more lights enter the
view and are extracted to the render world, while lights outside the
view aren't removed.
This PR fixes the issue in two ways:
1. Light extraction now properly accounts for the retained render world,
following the patterns that other objects like meshes use. Lights that
haven't changed from the previous frame aren't re-extracted and are
retained from frame to frame.
2. Visibility of lights and other clustered objects is now determined by
the same system that determines visibility of meshes. The
`GlobalVisibleClusterableObjects` side table is gone. To do this, I made
the existing `Sphere` type into a component that can be added in lieu of
`Aabb` to entities that are culled using spheres instead of AABBs. This
was surprisingly straightforward to add to the visibility code, as it
was already using spheres for quick rejection.
In addition to being a simplification, this PR increases the FPS of
`many_lights` from around 30 FPS to around 100 FPS on my Ryzen 9 8945HS,
moving it from CPU bound to strongly GPU bound. The profile was
dominated by `extract_lights` and `prepare_lights` before. Retained mode
drops `extract_lights` time from 8.9 ms plus 8.0 ms of commands
processing time to 0.4 ms, and `prepare_lights` time drops to
approximately 1.2 ms per frame (which further drops to 0.9 ms with my
extra PR #22846 applied).
`many_lights` on `main`:
<img width="2756" height="1800" alt="Screenshot 2026-02-07 101916"
src="https://github.com/user-attachments/assets/18ec7190-cb5b-41c8-8c6f-75ef13d683fb"
/>
`many_lights` in this PR:
<img width="2756" height="1800" alt="Screenshot 2026-02-07 101210"
src="https://github.com/user-attachments/assets/daf3d93f-efcc-4a2e-b728-da1f32e6ad85"
/>
Comparison of `prepare_lights` between `main` and this PR for
`many_lights`:
<img width="2756" height="1800" alt="Screenshot 2026-02-07 101234"
src="https://github.com/user-attachments/assets/c8c63685-a3df-4351-a7f3-d74fc40d505a"
/>
Comparison of `extract_lights` between `main` and this PR for
`many_lights`:
<img width="2756" height="1800" alt="Screenshot 2026-02-07 101248"
src="https://github.com/user-attachments/assets/68eb1618-a856-40eb-94bc-91f215d1dc4e"
/>
---------
Co-authored-by: Robert Swain <robert.swain@gmail.com>
Co-authored-by: atlv <email@atlasdostal.com>
# Objective
- yeet another `#![expect(missing_docs, reason = "Not all docs are
written yet, see #3492.")]` from a rendering crate i extracted
- yeet some stale code
## Solution
- document
## Testing
- ran lighting, deferred, shadow_biases, and a couple other examples.
looks fine.
note: review commit by commit
# Objective
- Adopts #10038 by @tim-blackbird
- Half of #19468 (no camera gizmo, just frustum)
- Does Part 3 of and fixes#13878
## Solution
I stand on the shoulders of giants and have updated #10038 to main, with
the following changes:
- The frustum gizmos are now immediate gizmos, not retained
- The current view’s frustum is drawn around the border of the screen as
a color like so (note the green border at the left, bottom, and right
edges of the screen).
<img width="1286" height="752" alt="Screenshot 2026-01-31 at 9 18 41 PM"
src="https://github.com/user-attachments/assets/7ed2b4db-1710-4be1-b6ca-00725d09944f"
/>
Also thanks to @RCoder01 for their github gist attached to the original
PR; my updates basically ended up being the same although code is more
or less in its proper place now.
## Testing
- I ran some scene_viewer gltf files in the bevy repo (e.g. `cargo run
--example scene_viewer --features "free_camera" --
assets/models/cubes/Cubes.glb`). I guess none have multiple cameras
though to cycle through though as far as I could tell? But at least this
shows you that toggling the frusta shows the faint border around the
screen.
- I ran the light_gizmos example `cargo run --example light_gizmos`.
Only `SpotLight` has gizmos drawn (`PointLight` and `DirectionalLight`
have components that wrap `Frustum` but not `Frustum` itself). It’s the
yellow gizmo in the following screenshot. Since there are dedicated
light gizmos, using a Frustum gizmo on a light seems unnecessary.
<img width="1278" height="740" alt="Screenshot 2026-01-31 at 9 36 54 PM"
src="https://github.com/user-attachments/assets/6e676bb8-32d4-4d90-9225-ee3a878745a6"
/>
- Like the original author, I modified the `split_screen` example to see
a camera frustum gizmo from one player on another player’s screen. I
removed players 3 and 4, added a frustum gizmo for player 2’s camera,
and moved player 1’s camera far enough so that you can see the frustum.
<img width="1274" height="740" alt="Screenshot 2026-01-31 at 9 09 46 PM"
src="https://github.com/user-attachments/assets/fff38469-9d00-46ba-9098-fdf8418b54fa"
/>
---
## Showcase
<details>
<summary>Modified `split_screen` example code</summary>
```rust
//! Renders four cameras to the same window to accomplish "split screen".
use std::f32::consts::PI;
use bevy::{
camera::Viewport, light::CascadeShadowConfigBuilder, prelude::*, window::WindowResized,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (set_camera_viewports, button_system))
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
));
commands.spawn(SceneRoot(
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
));
// Light
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadow_maps_enabled: true,
..default()
},
CascadeShadowConfigBuilder {
num_cascades: if cfg!(all(
feature = "webgl2",
target_arch = "wasm32",
not(feature = "webgpu")
)) {
// Limited to 1 cascade in WebGL
1
} else {
2
},
first_cascade_far_bound: 200.0,
maximum_distance: 280.0,
..default()
}
.build(),
));
// Cameras and their dedicated UI
for (index, (camera_name, camera_pos)) in [
("Player 1", Vec3::new(300.0, 300.0, -150.0)),
("Player 2", Vec3::new(150.0, 150., 50.0)),
]
.iter()
.enumerate()
{
let camera = commands
.spawn((
Camera3d::default(),
Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y),
Camera {
// Renders cameras with different priorities to prevent ambiguities
order: index as isize,
..default()
},
CameraPosition {
pos: index as u32 % 2,
},
ShowFrustumGizmo {
color: if index == 0 { Some(Color::NONE) } else { None },
},
))
.id();
// Set up UI
if index == 0 {
commands.spawn((
UiTargetCamera(camera),
Node {
width: percent(100),
height: percent(100),
..default()
},
children![
(
Text::new(*camera_name),
Node {
position_type: PositionType::Absolute,
top: px(12),
left: px(12),
..default()
},
),
buttons_panel(),
],
));
}
}
fn buttons_panel() -> impl Bundle {
(
Node {
position_type: PositionType::Absolute,
width: percent(100),
height: percent(100),
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
padding: UiRect::all(px(20)),
..default()
},
children![
rotate_button("<", Direction::Left),
rotate_button(">", Direction::Right),
],
)
}
fn rotate_button(caption: &str, direction: Direction) -> impl Bundle {
(
RotateCamera(direction),
Button,
Node {
width: px(40),
height: px(40),
border: UiRect::all(px(2)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BorderColor::all(Color::WHITE),
BackgroundColor(Color::srgb(0.25, 0.25, 0.25)),
children![Text::new(caption)],
)
}
}
#[derive(Component)]
struct CameraPosition {
pos: u32,
}
#[derive(Component)]
struct RotateCamera(Direction);
enum Direction {
Left,
Right,
}
fn set_camera_viewports(
windows: Query<&Window>,
mut window_resized_reader: MessageReader<WindowResized>,
mut query: Query<(&CameraPosition, &mut Camera)>,
) {
// We need to dynamically resize the camera's viewports whenever the window size changes
// so then each camera always takes up half the screen.
// A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
for window_resized in window_resized_reader.read() {
let window = windows.get(window_resized.window).unwrap();
let size = window.physical_size();
for (camera_position, mut camera) in &mut query {
camera.viewport = Some(Viewport {
physical_position: camera_position.pos * size,
physical_size: size,
..default()
});
}
}
}
fn button_system(
interaction_query: Query<
(&Interaction, &ComputedUiTargetCamera, &RotateCamera),
(Changed<Interaction>, With<Button>),
>,
mut camera_query: Query<&mut Transform, With<Camera>>,
) {
for (interaction, computed_target, RotateCamera(direction)) in &interaction_query {
if let Interaction::Pressed = *interaction {
// Since TargetCamera propagates to the children, we can use it to find
// which side of the screen the button is on.
if let Some(mut camera_transform) = computed_target
.get()
.and_then(|camera| camera_query.get_mut(camera).ok())
{
let angle = match direction {
Direction::Left => -0.1,
Direction::Right => 0.1,
};
camera_transform.rotate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, angle));
}
}
}
}
```
</details>
---------
Co-authored-by: devil-ira <justthecooldude@gmail.com>
Co-authored-by: Robert Swain <robert.swain@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Currently, if a fragment overlaps multiple reflection probes and/or
irradiance volumes, Bevy arbitrarily chooses one to provide diffuse
and/or specular light. This is unsightly. The standard approach is to
accumulate radiance and irradiance as a weighted sum. In most engines,
light probes have an artist-controllable *falloff* range, which causes
the weight of each probe to diminish gradually from the center of the
probe.
This PR implements both falloff and blending for light probes.
Reflection probes and irradiance volumes are blended using a weighted
sum. In the case of reflection probes, if the weights sum to less than
1.0, and an environment map is present on the camera, than the
environment map receives the remaining weight necessary to bring the
total weight up to 1.0. This is useful for reflection probes that
correspond to building interiors, to allow smooth transitions between
the indoor building and an exterior environment map when exiting the
building.
Falloff is specified as a fraction of the *interior* of each light probe
that applies gradual falloff, instead of specifying a distance *outside*
the light probe over which the influence diminishes. (See the
documentation comments in `LightProbe` for more detail.) The reason why
I chose to do it this way is that the voxel contents of an irradiance
volume would be ill-defined within the falloff range otherwise. Clamping
to the edge of the 3D voxel cube inside the falloff region (i.e.
extending the edge voxels out) is likely to be incorrect, and extending
the voxel region to encompass the falloff range plus the interior range
would complicate the calculations in the performance-critical PBR
shader.
A new example, `light_probe_blending`, has been added. This example
shows a reflective sphere that moves between two rooms, each of which
has a reflection probe with a falloff range, so the sphere smoothly
blends between the two. The user can pan and zoom the camera.
<img width="2564" height="1500" alt="Screenshot 2026-01-25 215214"
src="https://github.com/user-attachments/assets/67098769-8082-47c3-ae96-4124732b73f6"
/>
# Objective
- Another day, another step towards scene description outside of render
crates
## Solution
- Move Atmosphere to bevy_light.
## Testing
- atmosphere example looks good.
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
# Objective
- Does the latter half of #13945
- Does the first two thirds of #13878
- Finish some of what #13882 started
## Solution
- Moves `bevy_camera::primitives::HalfSpace` to
`bevy_math::primitives::HalfSpace` (first commit)
- Moves some of `bevy_camera::primitives::Frustum` to
`bevy_math::primitives::ViewFrustum` (second commit)
- I basically followed Jondolf’s directions on the two refactorings.
aabb, obb, and sphere stuff stays in bevy_camera (for now? If sphere is
refactored, it has been stressed to be done as a follow up )
## Testing
I ran the `lighting`, `2d_gizmos`, and `3d_gizmos` example just to make
sure the lights… and camera… (and action!) were still working.
Everything seems to be fine there compared to main. If there are any
other examples I should run to make sure things look good, let me know.
# Objective
- Skybox is a main world component for scene definition, it shouldnt be
in a rendering crate
## Solution
- Move it to bevy_light, alongside EnvironmentMapLight component sibling
which lets it influence lighting.
## Testing
- skybox example works
Upgrade to wgpu 28
> [!important]
> This can't merge until https://github.com/bevyengine/naga_oil/pull/132
does, and the dependency is updated from my fork to the release.
>
> Also requires wgpu 28 in dlss_wgpu:
https://github.com/bevyengine/dlss_wgpu/pull/17
> [!note]
> This does not enable mesh shaders, and neither does the naga_oil PR. I
chose to do an upgrade first, then go back and see about mesh shaders.
Here's a general list of changes and what I did. Commits are grouped by
feature except for the last one, which enabled solari when I ran the
solari example.
## MipmapFilterMode is split from FilterMode
- Split MipmapFilterMode from FilterMode #8314:
https://github.com/gfx-rs/wgpu/pull/8314
solution: implement From for `MipmapFilterMode`/`ImageFilterMode` since
the values are the same. The split was because the spec indicates they
are different types, even though they have the same values.
## Push Constants are now Immediates
- https://github.com/gfx-rs/wgpu/pull/8724
immediate_size is [a
u32](https://docs.rs/wgpu/28.0.0/wgpu/struct.PipelineLayoutDescriptor.html#structfield.immediate_size)
so use that instead of `PushConstantRange`
## Capabilities name changes
- https://github.com/gfx-rs/wgpu/pull/8671
Got new list from
https://github.com/gfx-rs/wgpu/blob/trunk/wgpu-core/src/device/mod.rs#L449
and copied it in.
## subgroup_{min,max}_size moved from Limits to AdapterInfo
- https://github.com/gfx-rs/wgpu/pull/8609
Update the limits to have the fields they have now, mirror the logic
from the other limits calls.
## multiview_mask
- https://github.com/gfx-rs/wgpu/pull/8206
set to None because we don't use it currently. Its vaguely for VR.
## error scope is now a guard
- https://github.com/gfx-rs/wgpu/pull/8685
retain guard and then pop() it later
---
I made one mistake during the PR, thinking set_immediates was going to
be the size of the immediates and not the offset. I'd like reviewers to
take a look at immediates offset and size sites specifically just in
case I missed something.
Here's a bunch of examples running
<img width="1470" height="1040" alt="screenshot-2025-12-24-at-16 47
22@2x"
src="https://github.com/user-attachments/assets/83dcf4c8-69f5-480a-b724-86598530f25a"
/>
<img width="1470" height="1040" alt="screenshot-2025-12-24-at-16 48
44@2x"
src="https://github.com/user-attachments/assets/46d897fa-1ab2-44ef-8055-fe2fce740dbc"
/>
<img width="1470" height="1040" alt="screenshot-2025-12-24-at-16 49
10@2x"
src="https://github.com/user-attachments/assets/6ae7a9bf-0473-4800-8dfc-233a6a41d6df"
/>
<img width="1470" height="1040" alt="screenshot-2025-12-24-at-16 49
31@2x"
src="https://github.com/user-attachments/assets/89f84a26-cfbd-4196-bca8-111c3d20ba7b"
/>
## Known Issues
> [!NOTE]
>
> There are no current known issues. Everything previous has been
solved.
- [x] enable extensions can not be written in naga oil in a way that
allows them to be used in composed modules.
Update: this was fixed by introducing a flag in naga-oil to force
shaders to allow ray queries in composed modules when using bevy_solari.
This is temporary and will be different in WESL-future.
<details><summary>old explanation</summary>
This is blocking solari from working on nvidia (solari runs successfully
on macos m1) because it needs `enable wgpu_ray_query;`. Putting the
declaration before `#define_import_path` means it gets stripped out
(afaict), and putting it after results in
```
error: expected global declaration, but found a global directive
┌─ embedded://bevy_solari/scene/raytracing_scene_bindings.wgsl:3:1
│
3 │ enable wgpu_ray_query;
│ ^^^^^^ written after first global declaration
│
= expected global declaration, but found a global directive
```
</details>
- [x] dlss_wgpu mixes apis which [causes panics
now](https://github.com/bevyengine/dlss_wgpu/pull/17#issuecomment-3690847524).
<details><summary>Previous notes as dlss_wgpu was being fixed
here</summary>
The wgpu release notes don't mention which PR this was introduced in,
only saying:
> Using both the wgpu command encoding APIs and
CommandEncoder::as_hal_mut on the same encoder will now result in a
panic.
It was caused by https://github.com/gfx-rs/wgpu/pull/8373 which claimed
to not know of any use cases
> With record on finish, the actual ordering on the command buffer is
deeply counter intuitive (all as_hal would come first) and I think it
additionally was just flat out broken in some ways
> -
https://discord.com/channels/691052431525675048/743663924229963868/1453786307099758683
Possible path forward is using multiple command buffers:
https://discord.com/channels/691052431525675048/743663924229963868/1453795633503670415
</summary>
---------
Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com>
At the moment, clustering is a three-step process:
1. `assign_objects_to_clusters` runs on all clusterable objects during
the `PostUpdate` schedule, creating lists of all clusterable objects in
each view.
2. During the extraction phase, `extract_clusters` runs on all views
that had clusters created for them, linearizing the clusters into a list
of `ExtractedClusterableObjectElement::ClusterHeader` commands followed
by other `ExtractedClusterableObjectElement`s, one for each object in
the cluster. Each `ExtractedClusterableObjectElement` specifies the
render world entity for the clusterable object.
3. In the render world, `prepare_clusters` processes all
`ExtractedClusterableObjectElement` commands to create the GPU buffers,
looking up each clustered object in the `GlobalClusterableObjectMeta`
table in order to translate from entity to index.
Unfortunately, there are two main problems with this:
a. Light probes don't have render world entities at all and are instead
tracked in `RenderViewLightProbes` components in the render world. Thus
step (2) silently fails for them.
b. The `GlobalClusterableObjectMeta` table only contains clustered
lights and decals, so even if light probes had render-world entities,
step (3) would still fail.
The end result is that the GPU ends up consulting bogus out-of-bounds
indices that may or may not actually refer to the light probe when
traversing clusters.
This PR fixes the issues:
* I extended `extract_clusters` to support light probes, by adding
`ExtractedClusterableObjectElement::ReflectionProbe` and
`ExtractedClusterableObjectElement::IrradianceVolume` variants. These
variants reference the *main* world entities for light probes, since no
render-world entities exist for them.
* When processing the new `ExtractedClusterableObjectElement` commands,
`prepare_clusters` uses the `RenderViewLightProbes` to find the index in
the reflection probe or irradiance volume table as appropriate and
supply it to the GPU. Note that this step might fail if a texture that
the light probe needs hasn't been loaded yet. In this case, an index of
-1 is stored, and the shader skips it. This isn't the optimum behavior;
ideally we wouldn't cluster such objects at all. However, it was a
minimally-invasive change.
* I renamed types that referenced clusterable objects to refer to
clusterable *lights* specifically if the types only dealt with lights,
to reduce confusion in the future.
* The `VisibleClusterableObjects` type is currently overloaded to both
serve as a component, in which case it contains *all* clusterable
objects associated with a view, and to serve as a container for the
objects associated with a cluster. Not only is this confusing, but it's
also wasteful, as there's bookkeeping that the type does that's not
needed when it's serving as a component. I split the type into the
component `VisibleClusterableObjects` and the helper structure
`ObjectsInCluster`, and encapsulated logic within each type in order to
make `assign_objects_to_clusters` easier to understand.
* The `gather_light_probes` system performed its own frustum culling on
light probes separately from the frustum culling that
`assign_objects_to_clusters` also does. This was wasteful and confusing,
especially since the frustum culling algorithms differed between the two
systems, so I simplified the logic so that `assign_objects_to_clusters`
fills out a table for `gather_light_probes` to use.
* `compute_radiances` in `environment_map.wgsl` was broken, as it
neglected to set `light_from_world` to the identity matrix when falling
back to the view environment map when a reflection probe wasn't found.
This would cause specular to vanish in some cases (e.g. in the
`reflection_probes` example). I fixed the problem.
This commit is a prerequisite for #22610, as multiple light probes are
too broken without it.
# Objective
- mgi388 pointed out on discord that gizmos depending on lights makes no
sense
- i agree, this is purely an artifact of how tangled our deps used to
be. but now its mostly untangled and this can be trivially fixed
## Solution
- invert the dep
## Testing
3d_gizmos runs and looks fine
# Objective
- Adopts and closes#22288
## Solution
- change the example to make it easier to tell if the reflection is
actually matching
- fix the assets because the reflection doesnt actually match
- throw the assets into the asset repo
https://github.com/bevyengine/bevy_asset_files/pull/7
## Testing
- running the example
---------
Co-authored-by: Patrick Walton <pcwalton@mimiga.net>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Bump version after release
This PR has been auto-generated
---------
Co-authored-by: Bevy Auto Releaser <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
# Objective
- Make visibility systems less slow.
- Saves as much as 1.2ms of CPU time on a (GPU bound) 6.7ms frame,
rendering caldera hotel.
- Fixes#22256
## Solution
- Replace the `EntityHashSet` with a component added directly to
entities. This still allows for correct change detection triggers on
visibility, but avoids hashing. This also enables parallel updates.
## Summary
- This PR lead to a rabbit hole that uncovered #22256
- This PR resolves the regression introduced since 0.17 released
- A more fair comparison is not to main (which has a regression), but to
0.17
- Compared to 0.17.3, this is a 27% speedup to the `PostUpdate` schedule
for caldera hotel on an M4 Max
<img width="558" height="251" alt="image"
src="https://github.com/user-attachments/assets/2d711e3c-b65e-4c3a-9d2a-a587f8f36aae"
/>
## Testing
- Many cubes, many foxes, caldera
- Yellow is new, red is old.
- Note the bimodality in the old traces, this was consistently
repeatable, and seems to have something to do with `EntityHashSet` and
`EntityHashMap`. Worth investigating that further, as I've seen that
bimodal behavior before, and blamed it on pcores vs ecores, but I
verified that is not happening.
- Summary: caldera hotel no longer exhibits bimodal performance with a
pathological mode that runs very slowly. Roughly comparing the ~90th
percentile performance, the optimized code is about 1.2ms faster. This
is particularly significant when you consider this is running on a
device that is already hitting 150fps (GPU bound), so it only has a
frame budget of 6.7ms.
- Notably, visibility checking was the last egregiously performing set
of systems in the common hot path of most bevy apps, and no longer stick
out as obviously slow in a frame profile:
A high level comparison of the change in CPU time by looking at just
PostUpdate between old(red) and new(yellow)
<img width="1738" height="512" alt="image"
src="https://github.com/user-attachments/assets/5252ff0f-d9f3-49d1-a3ce-309bd84d5485"
/>
### caldera hotel - all entities in view
For this test, I didn't touch the camera, so all entities were in view
at all times.
`check_visibility`
<img width="1262" height="397" alt="image"
src="https://github.com/user-attachments/assets/c80c470a-1548-42d2-838c-4e74525a6cb8"
/>
`reset_view_visibility`
<img width="1262" height="407" alt="image"
src="https://github.com/user-attachments/assets/7b550823-f832-41f1-9e42-20cab342b0f2"
/>
`mark_newly_hidden_entities_invisible`
This is 23us slower than the "fast mode" of the existing code on main,
but 180-250us *faster* than the weird "slow mode" on main. The new code
is significantly more stable, and does not exhibit the super slow mode.
<img width="1256" height="418" alt="image"
src="https://github.com/user-attachments/assets/b7cbf57d-a221-468a-9af8-3381981874b2"
/>
### caldera hotel - no entities in view
For this test, I immediately rotated the camera so the hotel was not in
view.
`check_visibility`
This is largely a wash. The old code is 2us faster, but this is likely
in the realm of noise.
<img width="1255" height="372" alt="image"
src="https://github.com/user-attachments/assets/2c795f72-ebea-4b35-ba19-a8eccd4c56fc"
/>
`reset_view_visibility`
This is a big win. The main peak is about 30us faster now, but the major
win is the worst case, which is nearly 500us faster.
<img width="1252" height="398" alt="image"
src="https://github.com/user-attachments/assets/f8a9d99f-a6e1-48db-8639-d52276dba9ab"
/>
`mark_newly_hidden_entities_invisible`
Same story as the other caldera comparison with everything in view, this
is a bit slower than the fast mode, but way faster than the slow mode on
main.
<img width="1248" height="400" alt="image"
src="https://github.com/user-attachments/assets/4564d94e-911a-4b3f-a584-c6e102ef2dd0"
/>
This commit expands the number of textures associated with each
clustered decal from 1 to 4. The additional 3 textures apply normal
maps, metallic-roughness maps, and emissive maps respectively to the
surfaces onto which decals are projected.
Normal maps are combined using the [*Whiteout* blending method] from
SIGGRAPH 2007. This approach was chosen because, subjectively, it
appeared better than the more complex [*reoriented normal mapping*
(RNM)] approach. Additionally, *Whiteout* normal map blending is
commutative and associative, unlike RNM, which is a useful property for
our decals, which are currently applied in an unspecified order. (The
fact that the order in which our decals are applied is unspecified is
unfortunate, but is a long-standing issue and should probably be fixed
in a followup.) In particular, commutativity is desirable because
otherwise one must specify which normal map is the *base* normal map and
which normal map is the *detail* normal map, but that's not a policy
decision that Bevy can unconditionally make, as decals aren't necessary
more detailed than the base normal map. (For instance, consider a bullet
hole decal embedded in a wall with a subtle rough texture; one might
reasonably argue that the base material's normal map is the detail map
and the bullet hole is the base map, even though the bullet hole's
normal map comes from a decal.)
Note that, with a custom material shader, it's possible for application
code to use the decal images for arbitrary other purposes. For example,
with a custom shader an application might use the metallic-roughness map
as a clearcoat map instead if it has no need for a metallic-roughness
map on a decal. And, of course, a custom material shader could adopt RNM
blending for decals if it wishes.
A new example, `clustered_decal_maps`, has been added. This example
demonstrates the new maps by spawning clustered decals with maps
randomly over time and projecting them onto a wall.
<img width="2564" height="1500" alt="Screenshot 2025-12-05 095953"
src="https://github.com/user-attachments/assets/255fca64-2b42-4794-a367-14336d023310"
/>
# Objective
For resources-as-components (#19731), structs mustn't doubly derive both
`Component` and `Resource`.
## Solution
Split `AmbientLight` in two: `AmbientLight` (the resource) and
`AmbientLightOverride` (the component).
## Testing
I initially made two structs `AmbientLightComponent` and
`AmbientLightResource`, and replaced every mention with the relevant one
to ensure that I didn't confuse the two.
## Notes
- I don't know if the names are correct. I kept the easiest name for the
resource, as that's the one most often used.
- I haven't provided any conversion methods as there are already plans
to replace the component variant with something else.
---------
Co-authored-by: atlv <email@atlasdostal.com>
# Objective
- `PointLight` and `SpotLight` are both `Copy`, but `DirectionalLight`
isn't.
## Solution
- Make it `Copy`. It's also a light, and the struct is comparable in
size to the others
## Testing
- CI
## Additional info
Noticed this while implementing a CPU estimate of Bevy's lighting for
the visibility calculations in a stealth system
# Objective
The 0.17 release candidate process has been started. We should be able
to start merging PRs scheduled for 0.18 now.
## Solution
Update the crate versions all to `0.18.0-dev`
# Objective
- #20434 is really cool, and deserves a bit of a showcase.
- Fixes#21125.
## Solution
- Write release notes.
- Also add a note to the docs that this plays nicely with bloom.
---------
Co-authored-by: Emerson Coskey <emerson@coskey.dev>
# Objective
- Decals are broken on web:
https://github.com/bevyengine/bevy/issues/19177
- webgl doesn't work at all, WebGPU requires non-MSAA
- Volumetric fog works only on WebGPU
- However, our users have no guidance about this!
## Solution
- Document the state of affairs.
- Fix the decal example to at least run on WebGPU
## Testing
- `bevy run --example decals --features webgpu web`
# Objective
- use meshes, lights, cameras without rendering
- this is a bug because without it, running a bevy app without
bevy_render instantly crashes on expected Assets resources not being
present
## Solution
- make LightPlugin and CameraPlugin be included without needing
rendering crates to add them
- extract MeshPlugin
## Testing
- disabled all rendering features and ran a bare app