feat(renderer): add BRDF LUT generator and IBL resources

Implements CPU-based BRDF LUT generation using the split-sum IBL
approximation (Hammersley sampling, GGX importance sampling, Smith
geometry with IBL k=a²/2). Wraps the 256×256 Rgba8Unorm LUT in
IblResources for GPU upload via wgpu 28.0 API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:19:09 +09:00
parent 4d7ff5a122
commit ea8af38263
2 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
use crate::brdf_lut::generate_brdf_lut;
pub const BRDF_LUT_SIZE: u32 = 256;
pub struct IblResources {
pub brdf_lut_texture: wgpu::Texture,
pub brdf_lut_view: wgpu::TextureView,
pub brdf_lut_sampler: wgpu::Sampler,
}
impl IblResources {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let size = BRDF_LUT_SIZE;
// Generate CPU-side LUT data.
let lut_data = generate_brdf_lut(size);
// Convert [f32; 2] → RGBA8 pixels (R=scale*255, G=bias*255, B=0, A=255).
let mut pixels: Vec<u8> = Vec::with_capacity((size * size * 4) as usize);
for [scale, bias] in &lut_data {
pixels.push((scale.clamp(0.0, 1.0) * 255.0).round() as u8);
pixels.push((bias.clamp(0.0, 1.0) * 255.0).round() as u8);
pixels.push(0u8);
pixels.push(255u8);
}
let extent = wgpu::Extent3d {
width: size,
height: size,
depth_or_array_layers: 1,
};
// Create the texture (linear, NOT sRGB).
let brdf_lut_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("BrdfLutTexture"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &brdf_lut_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * size),
rows_per_image: Some(size),
},
extent,
);
let brdf_lut_view =
brdf_lut_texture.create_view(&wgpu::TextureViewDescriptor::default());
let brdf_lut_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("BrdfLutSampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
Self {
brdf_lut_texture,
brdf_lut_view,
brdf_lut_sampler,
}
}
/// Bind group layout for group(4):
/// binding 0 — texture_2d<f32> (filterable)
/// binding 1 — sampler (filtering)
pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("IblBindGroupLayout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
})
}
pub fn create_bind_group(
&self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("IblBindGroup"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.brdf_lut_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.brdf_lut_sampler),
},
],
})
}
}

View File

@@ -11,6 +11,8 @@ pub mod sphere;
pub mod pbr_pipeline;
pub mod shadow;
pub mod shadow_pipeline;
pub mod brdf_lut;
pub mod ibl;
pub use gpu::{GpuContext, DEPTH_FORMAT};
pub use light::{CameraUniform, LightUniform, LightData, LightsUniform, MAX_LIGHTS, LIGHT_DIRECTIONAL, LIGHT_POINT, LIGHT_SPOT};
@@ -22,3 +24,4 @@ pub use sphere::generate_sphere;
pub use pbr_pipeline::create_pbr_pipeline;
pub use shadow::{ShadowMap, ShadowUniform, ShadowPassUniform, SHADOW_MAP_SIZE, SHADOW_FORMAT};
pub use shadow_pipeline::{create_shadow_pipeline, shadow_pass_bind_group_layout};
pub use ibl::IblResources;