Bevy version and features
0.19.0; still present on main (crates/bevy_pbr/src/render/pbr_functions.wgsl, crates/bevy_pbr/src/deferred/mod.rs). Reproduced with default features.
[Optional] Relevant system information
cargo 1.96.0 (30a34c682 2026-05-25), stable
- Arch Linux (kernel 7.1.3-arch2-2), Wayland session (repro captured under Xwayland)
AdapterInfo { name: "NVIDIA GeForce RTX 5090", vendor: 4318, device: 11141, device_type: DiscreteGpu, device_pci_bus_id: "0000:01:00.0", driver: "NVIDIA", driver_info: "610.43.03", backend: Vulkan, subgroup_min_size: 32, subgroup_max_size: 32, transient_saves_memory: false }
Not setup-specific: the loss is a shader-math term, view-angle dependent, and reproduces identically regardless of SSAO/SSR/tonemapping settings.
What you did
Rendered a scene containing emissive StandardMaterials (no clearcoat) with DefaultOpaqueRendererMethod::deferred():
use bevy::core_pipeline::prepass::{DeferredPrepass, DepthPrepass};
use bevy::pbr::DefaultOpaqueRendererMethod;
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(DefaultOpaqueRendererMethod::deferred())
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.1, 0.05, 0.02),
emissive: Color::srgb(1.0, 0.3, 0.12).to_linear() * 5.0,
..default()
})),
Transform::from_xyz(0.0, 1.0, 0.0),
));
commands.spawn((
Camera3d::default(),
bevy::camera::Hdr,
DepthPrepass,
DeferredPrepass,
bevy::post_process::bloom::Bloom::NATURAL,
Msaa::Off,
Transform::from_xyz(0.0, 20.0, 30.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
What went wrong
- Expected: deferred and forward rendering produce the same lighting for the same scene; the emissive cube glows at full brightness (and feeds bloom) either way.
- Actually happened: under deferred rendering, emissive surfaces render roughly 25x dimmer than under forward (view-angle dependent), killing bloom with them. The same scene with the
DefaultOpaqueRendererMethod::deferred() line removed renders the cube at full brightness.
Measured on the repro above (sRGB screenshot of the cube face, defaults otherwise): forward (253, 157, 120), deferred (122, 38, 20) — the unclipped green/blue channels are ~20-25x dimmer in linear terms.
Additional information
Cause
apply_pbr_lighting darkens emission for clearcoat materials (pbr_functions.wgsl):
#ifdef STANDARD_MATERIAL_CLEARCOAT
emissive_light = emissive_light * (0.04 + (1.0 - 0.04) * pow(1.0 - clearcoat_NdotV, 5.0));
#endif
Two problems compound:
-
The formula multiplies by the Fresnel term itself instead of by the energy the coat transmits. The glTF KHR_materials_clearcoat spec's emission section — the same document the code comment above the line cites — defines it as coated_emission = emission * (1 - clearcoat * clearcoat_fresnel), about 0.96 face-on for a full-strength coat. The current code multiplies by fresnel itself, about 0.04 face-on, i.e. it keeps only the light the coating reflects back inward and discards the ~96% that should be transmitted. It also ignores clearcoat strength entirely, so a material with clearcoat: 0.0 is darkened as if fully coated.
-
The deferred lighting pipeline defines STANDARD_MATERIAL_CLEARCOAT unconditionally (deferred/mod.rs, DeferredLightingLayout::specialize), since it cannot know per-pixel whether the G-buffer material had clearcoat. Forward rendering only compiles the block in for materials that actually use clearcoat, which is why the bug hides there: with the def gated, materials without clearcoat never execute the broken line.
So under deferred, every emissive surface takes the broken path and loses ~96% of its emission face-on.
I verified the G-buffer round-trip is lossless by dumping pbr_input.material.emissive directly from the deferred lighting pass — the unpacked value matches forward exactly; the loss happens only at this multiply.
Suggested fix
#ifdef STANDARD_MATERIAL_CLEARCOAT
let clearcoat_fresnel = 0.04 + (1.0 - 0.04) * pow(1.0 - clearcoat_NdotV, 5.0);
emissive_light = emissive_light * (1.0 - clearcoat * clearcoat_fresnel);
#endif
With this change the deferred repro matches the forward render pixel-exactly on the emissive cube face, clearcoat: 0.0 materials are unaffected by the def being always-on, and full-strength clearcoat materials get the spec's mild Fresnel darkening in forward too (the current formula is wrong there as well, just masked for uncoated materials by the def gating).
Workaround
Patching the shader line above in a vendored bevy_pbr (via [patch.crates-io]) fully restores parity with forward.
Related
This is a 0.19 regression introduced by #23359 (which fixed #23284, clearcoat not visible with deferred renderer). Before that PR the deferred G-buffer discarded clearcoat entirely and the deferred lighting shader did not define STANDARD_MATERIAL_CLEARCOAT, so the broken emission multiply was never compiled into the deferred path and deferred emissives rendered at full brightness. #23359 correctly packs clearcoat into the G-buffer but defines STANDARD_MATERIAL_CLEARCOAT unconditionally in the deferred lighting shader, which put every deferred pixel onto the pre-existing broken formula. The formula bug itself predates #23359 and also affects forward rendering for materials that actually use clearcoat; the unconditional def just made it universal under deferred.
Bevy version and features
0.19.0; still present on
main(crates/bevy_pbr/src/render/pbr_functions.wgsl,crates/bevy_pbr/src/deferred/mod.rs). Reproduced with default features.[Optional] Relevant system information
cargo 1.96.0 (30a34c682 2026-05-25), stableAdapterInfo { name: "NVIDIA GeForce RTX 5090", vendor: 4318, device: 11141, device_type: DiscreteGpu, device_pci_bus_id: "0000:01:00.0", driver: "NVIDIA", driver_info: "610.43.03", backend: Vulkan, subgroup_min_size: 32, subgroup_max_size: 32, transient_saves_memory: false }Not setup-specific: the loss is a shader-math term, view-angle dependent, and reproduces identically regardless of SSAO/SSR/tonemapping settings.
What you did
Rendered a scene containing emissive
StandardMaterials (no clearcoat) withDefaultOpaqueRendererMethod::deferred():What went wrong
DefaultOpaqueRendererMethod::deferred()line removed renders the cube at full brightness.Measured on the repro above (sRGB screenshot of the cube face, defaults otherwise): forward
(253, 157, 120), deferred(122, 38, 20)— the unclipped green/blue channels are ~20-25x dimmer in linear terms.Additional information
Cause
apply_pbr_lightingdarkens emission for clearcoat materials (pbr_functions.wgsl):Two problems compound:
The formula multiplies by the Fresnel term itself instead of by the energy the coat transmits. The glTF
KHR_materials_clearcoatspec's emission section — the same document the code comment above the line cites — defines it ascoated_emission = emission * (1 - clearcoat * clearcoat_fresnel), about0.96face-on for a full-strength coat. The current code multiplies byfresnelitself, about0.04face-on, i.e. it keeps only the light the coating reflects back inward and discards the ~96% that should be transmitted. It also ignoresclearcoatstrength entirely, so a material withclearcoat: 0.0is darkened as if fully coated.The deferred lighting pipeline defines
STANDARD_MATERIAL_CLEARCOATunconditionally (deferred/mod.rs,DeferredLightingLayout::specialize), since it cannot know per-pixel whether the G-buffer material had clearcoat. Forward rendering only compiles the block in for materials that actually use clearcoat, which is why the bug hides there: with the def gated, materials without clearcoat never execute the broken line.So under deferred, every emissive surface takes the broken path and loses ~96% of its emission face-on.
I verified the G-buffer round-trip is lossless by dumping
pbr_input.material.emissivedirectly from the deferred lighting pass — the unpacked value matches forward exactly; the loss happens only at this multiply.Suggested fix
With this change the deferred repro matches the forward render pixel-exactly on the emissive cube face,
clearcoat: 0.0materials are unaffected by the def being always-on, and full-strength clearcoat materials get the spec's mild Fresnel darkening in forward too (the current formula is wrong there as well, just masked for uncoated materials by the def gating).Workaround
Patching the shader line above in a vendored
bevy_pbr(via[patch.crates-io]) fully restores parity with forward.Related
This is a 0.19 regression introduced by #23359 (which fixed #23284, clearcoat not visible with deferred renderer). Before that PR the deferred G-buffer discarded clearcoat entirely and the deferred lighting shader did not define
STANDARD_MATERIAL_CLEARCOAT, so the broken emission multiply was never compiled into the deferred path and deferred emissives rendered at full brightness. #23359 correctly packs clearcoat into the G-buffer but definesSTANDARD_MATERIAL_CLEARCOATunconditionally in the deferred lighting shader, which put every deferred pixel onto the pre-existing broken formula. The formula bug itself predates #23359 and also affects forward rendering for materials that actually use clearcoat; the unconditional def just made it universal under deferred.