From ea8af3826376372fee15ea8dd47ff2e3dc9c5755 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 21:19:09 +0900 Subject: [PATCH] feat(renderer): add BRDF LUT generator and IBL resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/voltex_renderer/src/ibl.rs | 129 ++++++++++++++++++++++++++++++ crates/voltex_renderer/src/lib.rs | 3 + 2 files changed, 132 insertions(+) create mode 100644 crates/voltex_renderer/src/ibl.rs diff --git a/crates/voltex_renderer/src/ibl.rs b/crates/voltex_renderer/src/ibl.rs new file mode 100644 index 0000000..4b42ea6 --- /dev/null +++ b/crates/voltex_renderer/src/ibl.rs @@ -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 = 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 (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), + }, + ], + }) + } +} diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index bb64f7b..a6fd64f 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -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;