Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added crates/bevy_pbr/src/environment_map/f_ab.ktx2
Binary file not shown.
29 changes: 29 additions & 0 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ pub struct LtcLuts {
pub ltc_2: Handle<Image>,
}

/// The split-sum approximation LUT (`F_AB`) indexed by (`NdotV`, `perceptual_roughness`).
#[derive(Resource, Clone)]
pub struct FabLut {
pub texture: Handle<Image>,
}

impl Plugin for PbrPlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "render/pbr_types.wgsl");
Expand Down Expand Up @@ -330,6 +336,29 @@ impl Plugin for PbrPlugin {
}
}

let has_fab_lut = app
.get_sub_app(RenderApp)
.is_some_and(|render_app| render_app.world().is_resource_added::<FabLut>());

if !has_fab_lut {
let mut images = app.world_mut().resource_mut::<Assets<Image>>();
let texture = images.add(
Image::from_buffer(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to gate this behind the ktx2 feature?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should feature gate this in general. Including these bytes here will increase the size of the binary so it should be opt in.

I explored this BRDF lut approach earlier for the PR from a month ago (clamp FAB)

https://github.com/mate-h/bevy/tree/m/brdf-lut

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go with @mate-h's suggestion to feature-gate the LUT so ktx2 can just be enabled when we need it.
The LTC LUTs have the same problem, I'll open an issue.

include_bytes!("environment_map/f_ab.ktx2"),
ImageType::Extension("ktx2"),
CompressedImageFormats::NONE,
false,
ImageSampler::linear(),
RenderAssetUsages::RENDER_WORLD,
)
.expect("Failed to decode embedded F_AB LUT"),
);

if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.world_mut().insert_resource(FabLut { texture });
}
}

let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
Expand Down
28 changes: 26 additions & 2 deletions crates/bevy_pbr/src/render/mesh_view_bindings.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::LtcLuts;
use crate::{FabLut, LtcLuts};
use alloc::sync::Arc;
use bevy_core_pipeline::{
oit::{resolve::is_oit_supported, OitBuffers, OrderIndependentTransparencySettings},
Expand Down Expand Up @@ -450,6 +450,14 @@ pub fn layout_entries(
),
(39, sampler(SamplerBindingType::Filtering)),
));
// F_AB
entries = entries.extend_with_indices((
(
40,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope we're not close to hitting binding limits on this 😬 if we are might consider reusing a linear sampler

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are, hitting max_sampled_textures_per_shader_stage on webgl -_-
I think I'll just make LTC reuse the same sampler for both of its LUTs to free up a slot.

texture_2d(TextureSampleType::Float { filterable: true }),
),
(41, sampler(SamplerBindingType::Filtering)),
));
let mut binding_array_entries = DynamicBindGroupLayoutEntries::new(ShaderStages::FRAGMENT);
binding_array_entries = binding_array_entries.extend_with_indices((
(0, environment_map_entries[0]),
Expand Down Expand Up @@ -648,13 +656,22 @@ pub fn prepare_mesh_view_bind_groups(
Res<ContactShadowsBuffer>,
),
oit_buffers: Res<OitBuffers>,
(decals_buffer, render_decals, atmosphere_buffer, atmosphere_sampler, blue_noise, ltc_luts): (
(
decals_buffer,
render_decals,
atmosphere_buffer,
atmosphere_sampler,
blue_noise,
ltc_luts,
fab_lut,
): (
Res<DecalsBuffer>,
Res<RenderClusteredDecals>,
Option<Res<AtmosphereBuffer>>,
Option<Res<AtmosphereSampler>>,
Res<Bluenoise>,
Res<LtcLuts>,
Res<FabLut>,
),
) {
if let (
Expand Down Expand Up @@ -842,6 +859,13 @@ pub fn prepare_mesh_view_bind_groups(
(39, ltc2_sampler),
));

// F_AB
let (fab_view, fab_sampler) = images
.get(&fab_lut.texture)
.map(|img| (&img.texture_view, &img.sampler))
.unwrap_or((&fallback_image.d2.texture_view, &fallback_image.d2.sampler));
entries = entries.extend_with_indices(((40, fab_view), (41, fab_sampler)));

let mut entries_binding_array = DynamicBindGroupEntries::new();

let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get(
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_pbr/src/render/mesh_view_bindings.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u;
@group(0) @binding(37) var ltc_lut1_sampler: sampler;
@group(0) @binding(38) var ltc_lut2: texture_2d<f32>;
@group(0) @binding(39) var ltc_lut2_sampler: sampler;
@group(0) @binding(40) var f_ab_lut: texture_2d<f32>;
@group(0) @binding(41) var f_ab_lut_sampler: sampler;

#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
@group(1) @binding(0) var diffuse_environment_maps: binding_array<texture_cube<f32>, 8u>;
Expand Down
10 changes: 1 addition & 9 deletions crates/bevy_pbr/src/render/pbr_lighting.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -526,16 +526,8 @@ fn Fd_Burley(
}

// Scale/bias approximation
// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile
// TODO: Use a LUT (more accurate)
fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2<f32> {
let c0 = vec4<f32>(-1.0, -0.0275, -0.572, 0.022);
let c1 = vec4<f32>(1.0, 0.0425, 1.04, -0.04);
let r = perceptual_roughness * c0 + c1;
let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
// Keep F_ab positive to avoid divide-by-zero in downstream BRDF terms.
let f_ab_epsilon = 0.00005;
return max(vec2<f32>(-1.04, 1.04) * a004 + r.zw, vec2<f32>(f_ab_epsilon));
return textureSampleLevel(view_bindings::f_ab_lut, view_bindings::f_ab_lut_sampler, vec2<f32>(NdotV, perceptual_roughness), 0.0).rg;
Copy link
Copy Markdown
Contributor

@mate-h mate-h Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't completely replace the polynomial approximation with the lut lets keep both. Add a shader def and enable the LUT with a feature flag. We should discuss whether this should be opt in or on by default.

}

fn EnvBRDFApprox(F0: vec3<f32>, F_ab: vec2<f32>) -> vec3<f32> {
Expand Down
15 changes: 14 additions & 1 deletion crates/bevy_solari/src/scene/binder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bevy_ecs::{
};
use bevy_math::{ops::cos, Mat4, Vec3};
use bevy_pbr::{
ExtractedDirectionalLight, MeshMaterial3d, PreviousGlobalTransform, StandardMaterial,
ExtractedDirectionalLight, FabLut, MeshMaterial3d, PreviousGlobalTransform, StandardMaterial,
};
use bevy_platform::{collections::HashMap, hash::FixedHasher};
use bevy_render::{
Expand Down Expand Up @@ -48,6 +48,7 @@ pub fn prepare_raytracing_scene_bindings(
material_assets: Res<StandardMaterialAssets>,
texture_assets: Res<RenderAssets<GpuImage>>,
fallback_texture: Res<FallbackImage>,
fab_lut: Res<FabLut>,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
render_queue: Res<RenderQueue>,
Expand Down Expand Up @@ -266,6 +267,14 @@ pub fn prepare_raytracing_scene_bindings(
command_encoder.build_acceleration_structures(&[], [&tlas]);
render_queue.submit([command_encoder.finish()]);

let (fab_view, fab_sampler) = texture_assets
.get(&fab_lut.texture)
.map(|img| (&img.texture_view, &img.sampler))
.unwrap_or((
&fallback_texture.d2.texture_view,
&fallback_texture.d2.sampler,
));

raytracing_scene_bindings.bind_group = Some(render_device.create_bind_group(
"raytracing_scene_bind_group",
&pipeline_cache.get_bind_group_layout(&raytracing_scene_bindings.bind_group_layout),
Expand All @@ -283,6 +292,8 @@ pub fn prepare_raytracing_scene_bindings(
light_sources.binding().unwrap(),
directional_lights.binding().unwrap(),
previous_frame_light_id_translations.binding().unwrap(),
fab_view,
fab_sampler,
)),
));
}
Expand Down Expand Up @@ -310,6 +321,8 @@ impl RaytracingSceneBindings {
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
),
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_solari/src/scene/brdf.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ enable wgpu_ray_query;
#define_import_path bevy_solari::brdf

#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance
#import bevy_pbr::lighting::{F_AB, D_GGX, V_SmithGGXCorrelated, specular_multiscatter}
#import bevy_pbr::lighting::{D_GGX, V_SmithGGXCorrelated, specular_multiscatter}
#import bevy_pbr::pbr_functions::{calculate_tbn_mikktspace, calculate_diffuse_color, calculate_F0}
#import bevy_pbr::utils::{rand_f, sample_cosine_hemisphere}
#import bevy_render::maths::PI
#import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf, ggx_vndf_sample_invalid}
#import bevy_solari::scene_bindings::{ResolvedMaterial, MIRROR_ROUGHNESS_THRESHOLD}
#import bevy_solari::scene_bindings::{ResolvedMaterial, MIRROR_ROUGHNESS_THRESHOLD, F_AB}

struct EvaluateAndSampleBrdfResult {
wi: vec3<f32>,
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ const LIGHT_NOT_PRESENT_THIS_FRAME = 0xFFFFFFFFu;
@group(0) @binding(10) var<storage> light_sources: array<LightSource>;
@group(0) @binding(11) var<storage> directional_lights: array<DirectionalLight>;
@group(0) @binding(12) var<storage> previous_frame_light_id_translations: array<u32>;
@group(0) @binding(13) var f_ab_lut: texture_2d<f32>;
@group(0) @binding(14) var f_ab_lut_sampler: sampler;

const RAY_T_MIN = 0.001f;
const RAY_T_MAX = 100000.0f;
Expand All @@ -105,6 +107,11 @@ fn sample_texture(id: u32, uv: vec2<f32>) -> vec3<f32> {
return textureSampleLevel(textures[id], samplers[id], uv, 0.0).rgb; // TODO: Mipmap
}

// Scale/bias approximation
fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2<f32> {
return textureSampleLevel(f_ab_lut, f_ab_lut_sampler, vec2<f32>(NdotV, perceptual_roughness), 0.0).rg;
}

struct ResolvedMaterial {
base_color: vec3<f32>,
emissive: vec3<f32>,
Expand Down