# Objective
Fixed#19035. Fixed#18882. It consisted of two different bugs:
- The allocations where being incremented even when a Data binding was
created.
- The ref counting on the binding was broken.
## Solution
- Stopped incrementing the allocations when a data binding was created.
- Rewrote the ref counting code to more reliably track the ref count.
## Testing
Tested my fix for 10 minutes with the `examples/3d/animated_material.rs`
example. I changed the example to spawn 51x51 meshes instead of 3x3
meshes to heighten the effects of the bug.
My branch: (After 10 minutes of running the modified example)
GPU: 172 MB
CPU: ~700 MB
Main branch: (After 2 minutes of running the modified example, my
computer started to stutter so I had to end it early)
GPU: 376 MB
CPU: ~1300 MB
# Objective
after #15156 it seems like using distinct directional lights on
different views is broken (and will probably break spotlights too). fix
them
## Solution
the reason is a bit hairy so with an example:
- camera 0 on layer 0
- camera 1 on layer 1
- dir light 0 on layer 0 (2 cascades)
- dir light 1 on layer 1 (2 cascades)
in render/lights.rs:
- outside of any view loop,
- we count the total number of shadow casting directional light cascades
(4) and assign an incrementing `depth_texture_base_index` for each (0-1
for one light, 2-3 for the other, depending on iteration order) (line
1034)
- allocate a texture array for the total number of cascades plus
spotlight maps (4) (line 1106)
- in the view loop, for directional lights we
- skip lights that don't intersect on renderlayers (line 1440)
- assign an incrementing texture layer to each light/cascade starting
from 0 (resets to 0 per view) (assigning 0 and 1 each time for the 2
cascades of the intersecting light) (line 1509, init at 1421)
then in the rendergraph:
- camera 0 renders the shadow map for light 0 to texture indices 0 and 1
- camera 0 renders using shadows from the `depth_texture_base_index`
(maybe 0-1, maybe 2-3 depending on the iteration order)
- camera 1 renders the shadow map for light 1 to texture indices 0 and 1
- camera 0 renders using shadows from the `depth_texture_base_index`
(maybe 0-1, maybe 2-3 depending on the iteration order)
issues:
- one of the views uses empty shadow maps (bug)
- we allocated a texture layer per cascade per light, even though not
all lights are used on all views (just inefficient)
- I think we're allocating texture layers even for lights with
`shadows_enabled: false` (just inefficient)
solution:
- calculate upfront the view with the largest number of directional
cascades
- allocate this many layers (plus layers for spotlights) in the texture
array
- keep using texture layers 0..n in the per-view loop, but build
GpuLights.gpu_directional_lights within the loop too so it refers to the
same layers we render to
nice side effects:
- we can now use `max_texture_array_layers / MAX_CASCADES_PER_LIGHT`
shadow-casting directional lights per view, rather than overall.
- we can remove the `GpuDirectionalLight::skip` field, since the gpu
lights struct is constructed per view
a simpler approach would be to keep everything the same, and just
increment the texture layer index in the view loop even for
non-intersecting lights. this pr reduces the total shadowmap vram used
as well and isn't *much* extra complexity. but if we want something less
risky/intrusive for 16.1 that would be the way.
## Testing
i edited the split screen example to put separate lights on layer 1 and
layer 2, and put the plane and fox on both layers (using lots of
unrelated code for render layer propagation from #17575).
without the fix the directional shadows will only render on one of the
top 2 views even though there are directional lights on both layers.
```rs
//! Renders two cameras to the same window to accomplish "split screen".
use std::f32::consts::PI;
use bevy::{
pbr::CascadeShadowConfigBuilder, prelude::*, render:📷:Viewport, window::WindowResized,
};
use bevy_render::view::RenderLayers;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(HierarchyPropagatePlugin::<RenderLayers>::default())
.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>>,
) {
let all_layers = RenderLayers::layer(1).with(2).with(3).with(4);
// 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))),
all_layers.clone()
));
commands.spawn((
SceneRoot(
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
),
Propagate(all_layers.clone()),
));
// Light
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_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(),
RenderLayers::layer(1),
));
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_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(),
RenderLayers::layer(2),
));
// Cameras and their dedicated UI
for (index, (camera_name, camera_pos)) in [
("Player 1", Vec3::new(0.0, 200.0, -150.0)),
("Player 2", Vec3::new(150.0, 150., 50.0)),
("Player 3", Vec3::new(100.0, 150., -150.0)),
("Player 4", Vec3::new(-100.0, 80., 150.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: UVec2::new((index % 2) as u32, (index / 2) as u32),
},
RenderLayers::layer(index+1)
))
.id();
// Set up UI
commands
.spawn((
UiTargetCamera(camera),
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
},
))
.with_children(|parent| {
parent.spawn((
Text::new(*camera_name),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.),
left: Val::Px(12.),
..default()
},
));
buttons_panel(parent);
});
}
fn buttons_panel(parent: &mut ChildSpawnerCommands) {
parent
.spawn(Node {
position_type: PositionType::Absolute,
width: Val::Percent(100.),
height: Val::Percent(100.),
display: Display::Flex,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.)),
..default()
})
.with_children(|parent| {
rotate_button(parent, "<", Direction::Left);
rotate_button(parent, ">", Direction::Right);
});
}
fn rotate_button(parent: &mut ChildSpawnerCommands, caption: &str, direction: Direction) {
parent
.spawn((
RotateCamera(direction),
Button,
Node {
width: Val::Px(40.),
height: Val::Px(40.),
border: UiRect::all(Val::Px(2.)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BorderColor(Color::WHITE),
BackgroundColor(Color::srgb(0.25, 0.25, 0.25)),
))
.with_children(|parent| {
parent.spawn(Text::new(caption));
});
}
}
#[derive(Component)]
struct CameraPosition {
pos: UVec2,
}
#[derive(Component)]
struct RotateCamera(Direction);
enum Direction {
Left,
Right,
}
fn set_camera_viewports(
windows: Query<&Window>,
mut resize_events: EventReader<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 resize_event in resize_events.read() {
let window = windows.get(resize_event.window).unwrap();
let size = window.physical_size() / 2;
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, &ComputedNodeTarget, &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
.camera()
.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));
}
}
}
}
use std::marker::PhantomData;
use bevy::{
app::{App, Plugin, Update},
ecs::query::QueryFilter,
prelude::{
Changed, Children, Commands, Component, Entity, Local, Query,
RemovedComponents, SystemSet, With, Without,
},
};
/// Causes the inner component to be added to this entity and all children.
/// A child with a Propagate<C> component of it's own will override propagation from
/// that point in the tree
#[derive(Component, Clone, PartialEq)]
pub struct Propagate<C: Component + Clone + PartialEq>(pub C);
/// Internal struct for managing propagation
#[derive(Component, Clone, PartialEq)]
pub struct Inherited<C: Component + Clone + PartialEq>(pub C);
/// Stops the output component being added to this entity.
/// Children will still inherit the component from this entity or its parents
#[derive(Component, Default)]
pub struct PropagateOver<C: Component + Clone + PartialEq>(PhantomData<fn() -> C>);
/// Stops the propagation at this entity. Children will not inherit the component.
#[derive(Component, Default)]
pub struct PropagateStop<C: Component + Clone + PartialEq>(PhantomData<fn() -> C>);
pub struct HierarchyPropagatePlugin<C: Component + Clone + PartialEq, F: QueryFilter = ()> {
_p: PhantomData<fn() -> (C, F)>,
}
impl<C: Component + Clone + PartialEq, F: QueryFilter> Default for HierarchyPropagatePlugin<C, F> {
fn default() -> Self {
Self {
_p: Default::default(),
}
}
}
#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)]
pub struct PropagateSet<C: Component + Clone + PartialEq> {
_p: PhantomData<fn() -> C>,
}
impl<C: Component + Clone + PartialEq> std::fmt::Debug for PropagateSet<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PropagateSet")
.field("_p", &self._p)
.finish()
}
}
impl<C: Component + Clone + PartialEq> Eq for PropagateSet<C> {}
impl<C: Component + Clone + PartialEq> std:#️⃣:Hash for PropagateSet<C> {
fn hash<H: std:#️⃣:Hasher>(&self, state: &mut H) {
self._p.hash(state);
}
}
impl<C: Component + Clone + PartialEq> Default for PropagateSet<C> {
fn default() -> Self {
Self {
_p: Default::default(),
}
}
}
impl<C: Component + Clone + PartialEq, F: QueryFilter + 'static> Plugin
for HierarchyPropagatePlugin<C, F>
{
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
update_source::<C, F>,
update_stopped::<C, F>,
update_reparented::<C, F>,
propagate_inherited::<C, F>,
propagate_output::<C, F>,
)
.chain()
.in_set(PropagateSet::<C>::default()),
);
}
}
pub fn update_source<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<(Entity, &Propagate<C>), (Changed<Propagate<C>>, Without<PropagateStop<C>>)>,
mut removed: RemovedComponents<Propagate<C>>,
) {
for (entity, source) in &changed {
commands
.entity(entity)
.try_insert(Inherited(source.0.clone()));
}
for removed in removed.read() {
if let Ok(mut commands) = commands.get_entity(removed) {
commands.remove::<(Inherited<C>, C)>();
}
}
}
pub fn update_stopped<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
q: Query<Entity, (With<Inherited<C>>, F, With<PropagateStop<C>>)>,
) {
for entity in q.iter() {
let mut cmds = commands.entity(entity);
cmds.remove::<Inherited<C>>();
}
}
pub fn update_reparented<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
moved: Query<
(Entity, &ChildOf, Option<&Inherited<C>>),
(
Changed<ChildOf>,
Without<Propagate<C>>,
Without<PropagateStop<C>>,
F,
),
>,
parents: Query<&Inherited<C>>,
) {
for (entity, parent, maybe_inherited) in &moved {
if let Ok(inherited) = parents.get(parent.parent()) {
commands.entity(entity).try_insert(inherited.clone());
} else if maybe_inherited.is_some() {
commands.entity(entity).remove::<(Inherited<C>, C)>();
}
}
}
pub fn propagate_inherited<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<
(&Inherited<C>, &Children),
(Changed<Inherited<C>>, Without<PropagateStop<C>>, F),
>,
recurse: Query<
(Option<&Children>, Option<&Inherited<C>>),
(Without<Propagate<C>>, Without<PropagateStop<C>>, F),
>,
mut to_process: Local<Vec<(Entity, Option<Inherited<C>>)>>,
mut removed: RemovedComponents<Inherited<C>>,
) {
// gather changed
for (inherited, children) in &changed {
to_process.extend(
children
.iter()
.map(|child| (child, Some(inherited.clone()))),
);
}
// and removed
for entity in removed.read() {
if let Ok((Some(children), _)) = recurse.get(entity) {
to_process.extend(children.iter().map(|child| (child, None)))
}
}
// propagate
while let Some((entity, maybe_inherited)) = (*to_process).pop() {
let Ok((maybe_children, maybe_current)) = recurse.get(entity) else {
continue;
};
if maybe_current == maybe_inherited.as_ref() {
continue;
}
if let Some(children) = maybe_children {
to_process.extend(
children
.iter()
.map(|child| (child, maybe_inherited.clone())),
);
}
if let Some(inherited) = maybe_inherited {
commands.entity(entity).try_insert(inherited.clone());
} else {
commands.entity(entity).remove::<(Inherited<C>, C)>();
}
}
}
pub fn propagate_output<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<
(Entity, &Inherited<C>, Option<&C>),
(Changed<Inherited<C>>, Without<PropagateOver<C>>, F),
>,
) {
for (entity, inherited, maybe_current) in &changed {
if maybe_current.is_some_and(|c| &inherited.0 == c) {
continue;
}
commands.entity(entity).try_insert(inherited.0.clone());
}
}
```
# Objective
Spot light shadows are still broken after fixing point lights in #19265
## Solution
Fix spot lights in the same way, just using the spot light specific
visible entities component. I also changed the query to be directly in
the render world instead of being extracted to be more accurate.
## Testing
Tested with the same code but changing `PointLight` to `SpotLight`.
# Objective
Fixes#19150
## Solution
Normally the `validate_cached_entity` in
86cc02dca2/crates/bevy_pbr/src/prepass/mod.rs (L1109-L1126)
marks unchanged entites as clean, which makes them remain in the phase.
If a material is changed to an `alpha_mode` that isn't supposed to be
added to the prepass pipeline, the specialization system just
`continue`s and doesn't indicate to the cache that the entity is not
clean anymore.
I made these invalid entities get removed from the pipeline cache so
that they are correctly not marked clean and then removed from the
phase.
## Testing
Tested with the example code from the issue.
# Objective
Fixes#18945
## Solution
Entities that are not visible in any view (camera or light), get their
render meshes removed. When they become visible somewhere again, the
meshes get recreated and assigned possibly different ids.
Point/spot light visible entities weren't cleared when the lights
themseves went out of view, which caused them to try to queue these fake
visible entities for rendering every frame. The shadow phase cache
usually flushes non visible entites, but because of this bug it never
flushed them and continued to queue meshes with outdated ids.
The simple solution is to every frame clear all visible entities for all
point/spot lights that may or may not be visible. The visible entities
get repopulated directly afterwards. I also renamed the
`global_point_lights` to `global_visible_clusterable` to make it clear
that it includes only visible things.
## Testing
- Tested with the code from the issue.
# Objective
Fixes#19027
## Solution
Query for the material binding id if using fallback CPU processing
## Testing
I've honestly no clue how to test for this, and I imagine that this
isn't entirely failsafe :( but would highly appreciate a suggestion!
To verify this works, please run the the texture.rs example using WebGL
2.
Additionally, I'm extremely naive about the nuances of pbr. This PR is
essentially to kinda *get the ball rolling* of sorts. Thanks :)
---------
Co-authored-by: Gilles Henaux <ghx_github_priv@fastmail.com>
Co-authored-by: charlotte <charlotte.c.mcelwain@gmail.com>
# Objective
Fixes#18843
## Solution
We need to account for the material being added and removed in the
course of the same frame. We evict the caches first because the entity
will be re-added if it was marked as needing specialization, which
avoids another check on removed components to see if it was "really"
despawned.
There's still a race resulting in blank materials whenever a material of
type A is added on the same frame that a material of type B is removed.
PR #18734 improved the situation, but ultimately didn't fix the race
because of two issues:
1. The `late_sweep_material_instances` system was never scheduled. This
PR fixes the problem by scheduling that system.
2. `early_sweep_material_instances` needs to be called after *every*
material type has been extracted, not just when the material of *that*
type has been extracted. The `chain()` added during the review process
in PR #18734 broke this logic. This PR reverts that and fixes the
ordering by introducing a new `SystemSet` that contains all material
extraction systems.
I also took the opportunity to switch a manual reference to
`AssetId::<StandardMaterial>::invalid()` to the new
`DUMMY_MESH_MATERIAL` constant for clarity.
Because this is a bug that can affect any application that switches
material types in a single frame, I think this should be uplifted to
Bevy 0.16.
Fixes#18809Fixes#18823
Meshes despawned in `Last` can still be in visisible entities if they
were visible as of `PostUpdate`. Sanity check that the mesh actually
exists before we specialize. We still want to unconditionally assume
that the entity is in `EntitySpecializationTicks` as its absence from
that cache would likely suggest another bug.
The goal of `bevy_platform_support` is to provide a set of platform
agnostic APIs, alongside platform-specific functionality. This is a high
traffic crate (providing things like HashMap and Instant). Especially in
light of https://github.com/bevyengine/bevy/discussions/18799, it
deserves a friendlier / shorter name.
Given that it hasn't had a full release yet, getting this change in
before Bevy 0.16 makes sense.
- Rename `bevy_platform_support` to `bevy_platform`.
A clippy failure slipped into #18768, although I'm not sure why CI
didn't catch it.
```sh
> cargo clippy --version
clippy 0.1.85 (4eb161250e 2025-03-15)
> cargo run -p ci
...
error: empty line after doc comment
--> crates\bevy_pbr\src\light\mod.rs:105:5
|
105 | / /// The width and height of each of the 6 faces of the cubemap.
106 | |
| |_^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#empty_line_after_doc_comments
= note: `-D clippy::empty-line-after-doc-comments` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::empty_line_after_doc_comments)]`
= help: if the empty line is unintentional remove it
help: if the documentation should include the empty line include it in the comment
|
106 | ///
|
```
# Objective
- Improve the docs for `PointLightShadowMap` and
`DirectionalLightShadowMap`
## Solution
- Add example for how to use `PointLightShadowMap` and move the
`DirectionalLightShadowMap` example from `DirectionalLight`.
- Match `PointLight` and `DirectionalLight` docs about shadows.
- Describe what `size` means.
---------
Co-authored-by: Robert Swain <robert.swain@gmail.com>
# Objective
Fixes#16896Fixes#17737
## Solution
Adds a new render phase, including all the new cold specialization
patterns, for wireframes. There's a *lot* of regrettable duplication
here between 3d/2d.
## Testing
All the examples.
## Migration Guide
- `WireframePlugin` must now be created with
`WireframePlugin::default()`.
Currently, `RenderMaterialInstances` and `RenderMeshMaterialIds` are
very similar render-world resources: the former maps main world meshes
to typed material asset IDs, and the latter maps main world meshes to
untyped material asset IDs. This is needlessly-complex and wasteful, so
this patch unifies the two in favor of a single untyped
`RenderMaterialInstances` resource.
This patch also fixes a subtle issue that could cause mesh materials to
be incorrect if a `MeshMaterial3d<A>` was removed and replaced with a
`MeshMaterial3d<B>` material in the same frame. The problematic pattern
looks like:
1. `extract_mesh_materials<B>` runs and, seeing the
`Changed<MeshMaterial3d<B>>` condition, adds an entry mapping the mesh
to the new material to the untyped `RenderMeshMaterialIds`.
2. `extract_mesh_materials<A>` runs and, seeing that the entity is
present in `RemovedComponents<MeshMaterial3d<A>>`, removes the entry
from `RenderMeshMaterialIds`.
3. The material slot is now empty, and the mesh will show up as whatever
material happens to be in slot 0 in the material data slab.
This commit fixes the issue by splitting out `extract_mesh_materials`
into *three* phases: *extraction*, *early sweeping*, and *late
sweeping*, which run in that order:
1. The *extraction* system, which runs for each material, updates
`RenderMaterialInstances` records whenever `MeshMaterial3d` components
change, and updates a change tick so that the following system will know
not to remove it.
2. The *early sweeping* system, which runs for each material, processes
entities present in `RemovedComponents<MeshMaterial3d>` and removes each
such entity's record from `RenderMeshInstances` only if the extraction
system didn't update it this frame. This system runs after *all*
extraction systems have completed, fixing the race condition.
3. The *late sweeping* system, which runs only once regardless of the
number of materials in the scene, processes entities present in
`RemovedComponents<ViewVisibility>` and, as in the early sweeping phase,
removes each such entity's record from `RenderMeshInstances` only if the
extraction system didn't update it this frame. At the end, the late
sweeping system updates the change tick.
Because this pattern happens relatively frequently, I think this PR
should land for 0.16.
Due to the preprocessor usage in the shader, different combinations of
features could cause the fields of `StandardMaterialBindings` to shift
around. In certain cases, this could cause them to not line up with the
bindings specified in `StandardMaterial`. This resulted in #18104.
This commit fixes the issue by making `StandardMaterialBindings` have a
fixed size. On the CPU side, it uses the
`#[bindless(index_table(range(M..N)))]` feature I added to `AsBindGroup`
in #18025 to do so. Thus this patch has a dependency on #18025.
Closes#18104.
---------
Co-authored-by: Robert Swain <robert.swain@gmail.com>
PR #17898 disabled bindless support for `ExtendedMaterial`. This commit
adds it back. It also adds a new example, `extended_material_bindless`,
showing how to use it.
# Objective
Add web support to atmosphere by gating dual source blending and using a
macro to determine the target platform.
The main objective of this PR is to ensure that users of Bevy's
atmosphere feature can also run it in a web-based context where WebGPU
support is enabled.
## Solution
- Make use of the `#[cfg(not(target_arch = "wasm32"))]` macro to gate
the dual source blending, as this is not (yet) supported in web
browsers.
- Rename the function `sample_sun_illuminance` to `sample_sun_radiance`
and move calls out of conditionals to ensure the shader compiles and
runs in both native and web-based contexts.
- Moved the multiplication of the transmittance out when calculating the
sun color, because calling the `sample_sun_illuminance` function was
causing issues in web. Overall this results in cleaner code and more
readable.
## Testing
- Tested by building a wasm target and loading it in a web page with
Vite dev server using `mate-h/bevy-webgpu` repo template.
- Tested the native build with `cargo run --example atmosphere` to
ensure it still works with dual source blending.
---
## Showcase
Screenshots show the atmosphere example running in two different
contexts:
<img width="1281" alt="atmosphere-web-showcase"
src="https://github.com/user-attachments/assets/40b1ee91-89ae-41a6-8189-89630d1ca1a6"
/>
---------
Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
## Objective
Fix #18714.
## Solution
Make sure `SkinUniforms::prev_buffer` is resized at the same time as
`current_buffer`.
There will be a one frame visual glitch when the buffers are resized,
since `prev_buffer` is incorrectly initialised with the current joint
transforms.
Note that #18074 includes the same fix. I'm assuming this smaller PR
will land first.
## Testing
See repro instructions in #18714. Tested on `animated_mesh`,
`many_foxes`, `custom_skinned_mesh`, Win10/Nvidia with Vulkan,
WebGL/Chrome, WebGPU/Chrome.
## Objective
Fix motion blur not working on skinned meshes.
## Solution
`set_mesh_motion_vector_flags` can set
`RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN` after specialization has
already cached the material. This can lead to
`MeshPipelineKey::HAS_PREVIOUS_SKIN` never getting set, disabling motion
blur.
The fix is to make sure `set_mesh_motion_vector_flags` happens before
specialization.
Note that the bug is fixed in a different way by #18074, which includes
other fixes but is a much larger change.
## Testing
Open the `animated_mesh` example and add these components to the
`Camera3d` entity:
```rust
MotionBlur {
shutter_angle: 5.0,
samples: 2,
#[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding: Default::default(),
},
#[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
Msaa::Off,
```
Tested on `animated_mesh`, `many_foxes`, `custom_skinned_mesh`,
Win10/Nvidia with Vulkan, WebGL/Chrome, WebGPU/Chrome. Note that testing
`many_foxes` WebGL requires #18715.
# Objective
Make all feature gated bindings consistent with each other
## Solution
Make the bindings of fields gated by `pbr_specular_textures` feature
consistent with the other gated bindings
# Objective
- Cleanup
## Solution
- Remove completely unused weak_handle
(`MESH_PREPROCESS_TYPES_SHADER_HANDLE`). This value is not used
directly, and is never populated.
- Delete multiple loads of `BUILD_INDIRECT_PARAMS_SHADER_HANDLE`. We
load it three times right after one another. This looks to be a
copy-paste error.
## Testing
- None.
# Objective
My ecosystem crate, bevy_mod_outline, currently uses `SetMeshBindGroup`
as part of its custom rendering pipeline. I would like to allow for
possibility that, due to changes in 0.16, I need to customise the
behaviour of `SetMeshBindGroup` in order to make it work. However, not
all of the symbol needed to implement this render command are public
outside of Bevy.
## Solution
- Include `MorphIndices` in re-export list. I feel this is morally
equivalent to `SkinUniforms` already being exported.
- Change `MorphIndex::index` field to be public. I feel this is morally
equivalent to the `SkinByteOffset::byte_offset` field already being
public.
- Change `RenderMeshIntances::mesh_asset_id()` to be public (although
since all the fields of `RenderMeshInstances` are public it's possible
to work around this one by reimplementing).
These changes exclude:
- Making any change to the `RenderLightmaps` type as I don't need to
bind the light-maps for my use-case and I wanted to keep these changes
minimal. It has a private field which would need to be public or have
access methods.
- The changes already included in #18612.
## Testing
Confirmed that a copy of `SetMeshBindGroup` can be compiled outside of
Bevy with these changes, provided that the light-map code is removed.
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
# Objective
Fixes#17872
## Solution
This should have basically no impact on static scenes. We can optimize
more later if anything comes up. Needing to iterate the two level bin is
a bit unfortunate but shouldn't matter for apps that use a single
camera.
# Objective
Fixes#17986Fixes#18608
## Solution
Guard against situations where an extracted mesh does not have an
associated material. The way that mesh is dependent on the material api
(although decoupled) here is a bit unfortunate and we might consider
ways in the future to support these material features without this
indirect dependency.
# Objective
Unlike for their helper typers, the import paths for
`unique_array::UniqueEntityArray`, `unique_slice::UniqueEntitySlice`,
`unique_vec::UniqueEntityVec`, `hash_set::EntityHashSet`,
`hash_map::EntityHashMap`, `index_set::EntityIndexSet`,
`index_map::EntityIndexMap` are quite redundant.
When looking at the structure of `hashbrown`, we can also see that while
both `HashSet` and `HashMap` have their own modules, the main types
themselves are re-exported to the crate level.
## Solution
Re-export the types in their shared `entity` parent module, and simplify
the imports where they're used.
# Objective
As of bevy 0.16-dev, the pre-existing public function
`bevy::pbr::setup_morph_and_skinning_defs()` is now passed a boolean
flag called `skins_use_uniform_buffers`. The value of this boolean is
computed by the function
`bevy_pbr::render::skin::skins_use_uniform_buffers()`, but it is not
exported publicly.
Found while porting
[bevy_mod_outline](https://github.com/komadori/bevy_mod_outline) to
0.16.
## Solution
Add `skin::skins_use_uniform_buffers` to the re-export list of
`bevy_pbr::render`.
## Testing
Confirmed test program can access public API.
# Objective
The flags are referenced later outside of the VERTEX_UVS ifdef/endif
block. The current behavior causes the pre-pass shader to fail to
compile when UVs are not present in the mesh, such as when using a
`LineStrip` to render a grid.
Fixes#18600
## Solution
Move the definition of the `flags` outside of the ifdef/endif block.
## Testing
Ran a modified `3d_example` that used a mesh and material with
alpha_mode blend, `LineStrip` topology, and no UVs.
# Objective
Requires are currently more verbose than they need to be. People would
like to define inline component values. Additionally, the current
`#[require(Foo(custom_constructor))]` and `#[require(Foo(|| Foo(10))]`
syntax doesn't really make sense within the context of the Rust type
system. #18309 was an attempt to improve ergonomics for some cases, but
it came at the cost of even more weirdness / unintuitive behavior. Our
approach as a whole needs a rethink.
## Solution
Rework the `#[require()]` syntax to make more sense. This is a breaking
change, but I think it will make the system easier to learn, while also
improving ergonomics substantially:
```rust
#[derive(Component)]
#[require(
A, // this will use A::default()
B(1), // inline tuple-struct value
C { value: 1 }, // inline named-struct value
D::Variant, // inline enum variant
E::SOME_CONST, // inline associated const
F::new(1), // inline constructor
G = returns_g(), // an expression that returns G
H = SomethingElse::new(), // expression returns SomethingElse, where SomethingElse: Into<H>
)]
struct Foo;
```
## Migration Guide
Custom-constructor requires should use the new expression-style syntax:
```rust
// before
#[derive(Component)]
#[require(A(returns_a))]
struct Foo;
// after
#[derive(Component)]
#[require(A = returns_a())]
struct Foo;
```
Inline-closure-constructor requires should use the inline value syntax
where possible:
```rust
// before
#[derive(Component)]
#[require(A(|| A(10))]
struct Foo;
// after
#[derive(Component)]
#[require(A(10)]
struct Foo;
```
In cases where that is not possible, use the expression-style syntax:
```rust
// before
#[derive(Component)]
#[require(A(|| A(10))]
struct Foo;
// after
#[derive(Component)]
#[require(A = A(10)]
struct Foo;
```
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François Mockers <mockersf@gmail.com>
# Objective
- The prepass pipeline has a generic bound on the specialize function
but 95% of it doesn't need it
## Solution
- Move most of the fields to an internal struct and use a separate
specialize function for those fields
## Testing
- Ran the 3d_scene and it worked like before
---
## Migration Guide
If you were using a field of the `PrepassPipeline`, most of them have
now been move to `PrepassPipeline::internal`.
## Notes
Here's the cargo bloat size comparison (from this tool
https://github.com/bevyengine/bevy/discussions/14864):
```
before:
(
"<bevy_pbr::prepass::PrepassPipeline<M> as bevy_render::render_resource::pipeline_specializer::SpecializedMeshPipeline>::specialize",
25416,
0.05582993,
),
after:
(
"<bevy_pbr::prepass::PrepassPipeline<M> as bevy_render::render_resource::pipeline_specializer::SpecializedMeshPipeline>::specialize",
2496,
0.005490916,
),
(
"bevy_pbr::prepass::PrepassPipelineInternal::specialize",
11444,
0.025175499,
),
```
The size for the specialize function that is generic is now much
smaller, so users won't need to recompile it for every material.
# Objective
- Fixes https://github.com/bevyengine/bevy/issues/11682
## Solution
- https://github.com/bevyengine/bevy/pull/4086 introduced an
optimization to not do redundant calculations, but did not take into
account changes to the resource `global_lights`. I believe that my patch
includes the optimization benefit but adds the required nuance to fix
said bug.
## Testing
The example originally given by
[@kirillsurkov](https://github.com/kirillsurkov) and then updated by me
to bevy 15.3 here:
https://github.com/bevyengine/bevy/issues/11682#issuecomment-2746287416
will not have shadows without this patch:
```rust
use bevy::prelude::*;
#[derive(Resource)]
struct State {
x: f32,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, update)
.insert_resource(State { x: -40.0 })
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::linear_rgb(0.0, 1.0, 0.0))),
));
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
commands.spawn(Camera3d::default());
}
fn update(mut state: ResMut<State>, mut camera: Query<&mut Transform, With<Camera3d>>) {
let mut camera = camera.single_mut().unwrap();
let t = Vec3::new(state.x, 0.0, 10.0);
camera.translation = t;
camera.look_at(t - Vec3::Z, Vec3::Y);
state.x = 0.0;
}
```
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
# Objective
- Fixes https://github.com/bevyengine/bevy/issues/18332
## Solution
- Move specialize_shadows to ManageViews so that it can run after
prepare_lights, so that shadow views exist for specialization.
- Unfortunately this means that specialize_shadows is no longer in
PrepareMeshes like the rest of the specialization systems.
## Testing
- Ran anti_aliasing example, switched between the different AA options,
observed no glitches.
# Objective
For materials that aren't being used or a visible entity doesn't have an
instance of, we were unnecessarily constantly checking whether they
needed specialization, saying yes (because the material had never been
specialized for that entity), and failing to look up the material
instance.
## Solution
If an entity doesn't have an instance of the material, it can't possibly
need specialization, so exit early before spending time doing the check.
Fixes#18388.
# Objective
Now that #13432 has been merged, it's important we update our reflected
types to properly opt into this feature. If we do not, then this could
cause issues for users downstream who want to make use of
reflection-based cloning.
## Solution
This PR is broken into 4 commits:
1. Add `#[reflect(Clone)]` on all types marked `#[reflect(opaque)]` that
are also `Clone`. This is mandatory as these types would otherwise cause
the cloning operation to fail for any type that contains it at any
depth.
2. Update the reflection example to suggest adding `#[reflect(Clone)]`
on opaque types.
3. Add `#[reflect(clone)]` attributes on all fields marked
`#[reflect(ignore)]` that are also `Clone`. This prevents the ignored
field from causing the cloning operation to fail.
Note that some of the types that contain these fields are also `Clone`,
and thus can be marked `#[reflect(Clone)]`. This makes the
`#[reflect(clone)]` attribute redundant. However, I think it's safer to
keep it marked in the case that the `Clone` impl/derive is ever removed.
I'm open to removing them, though, if people disagree.
4. Finally, I added `#[reflect(Clone)]` on all types that are also
`Clone`. While not strictly necessary, it enables us to reduce the
generated output since we can just call `Clone::clone` directly instead
of calling `PartialReflect::reflect_clone` on each variant/field. It
also means we benefit from any optimizations or customizations made in
the `Clone` impl, including directly dereferencing `Copy` values and
increasing reference counters.
Along with that change I also took the liberty of adding any missing
registrations that I saw could be applied to the type as well, such as
`Default`, `PartialEq`, and `Hash`. There were hundreds of these to
edit, though, so it's possible I missed quite a few.
That last commit is **_massive_**. There were nearly 700 types to
update. So it's recommended to review the first three before moving onto
that last one.
Additionally, I can break the last commit off into its own PR or into
smaller PRs, but I figured this would be the easiest way of doing it
(and in a timely manner since I unfortunately don't have as much time as
I used to for code contributions).
## Testing
You can test locally with a `cargo check`:
```
cargo check --workspace --all-features
```