feat(renderer): add multi-light system with LightsUniform and updated PBR shader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ pub mod sphere;
|
|||||||
pub mod pbr_pipeline;
|
pub mod pbr_pipeline;
|
||||||
|
|
||||||
pub use gpu::{GpuContext, DEPTH_FORMAT};
|
pub use gpu::{GpuContext, DEPTH_FORMAT};
|
||||||
pub use light::{CameraUniform, LightUniform};
|
pub use light::{CameraUniform, LightUniform, LightData, LightsUniform, MAX_LIGHTS, LIGHT_DIRECTIONAL, LIGHT_POINT, LIGHT_SPOT};
|
||||||
pub use mesh::Mesh;
|
pub use mesh::Mesh;
|
||||||
pub use camera::{Camera, FpsController};
|
pub use camera::{Camera, FpsController};
|
||||||
pub use texture::GpuTexture;
|
pub use texture::GpuTexture;
|
||||||
|
|||||||
@@ -39,3 +39,157 @@ impl LightUniform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-light support
|
||||||
|
pub const MAX_LIGHTS: usize = 16;
|
||||||
|
pub const LIGHT_DIRECTIONAL: u32 = 0;
|
||||||
|
pub const LIGHT_POINT: u32 = 1;
|
||||||
|
pub const LIGHT_SPOT: u32 = 2;
|
||||||
|
|
||||||
|
/// Per-light data. Must be exactly 64 bytes (4 × vec4) for WGSL array alignment.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
|
pub struct LightData {
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub light_type: u32, // 16 bytes
|
||||||
|
pub direction: [f32; 3],
|
||||||
|
pub range: f32, // 32 bytes
|
||||||
|
pub color: [f32; 3],
|
||||||
|
pub intensity: f32, // 48 bytes
|
||||||
|
pub inner_cone: f32,
|
||||||
|
pub outer_cone: f32,
|
||||||
|
pub _padding: [f32; 2], // 64 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LightData {
|
||||||
|
pub fn directional(direction: [f32; 3], color: [f32; 3], intensity: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
position: [0.0; 3],
|
||||||
|
light_type: LIGHT_DIRECTIONAL,
|
||||||
|
direction,
|
||||||
|
range: 0.0,
|
||||||
|
color,
|
||||||
|
intensity,
|
||||||
|
inner_cone: 0.0,
|
||||||
|
outer_cone: 0.0,
|
||||||
|
_padding: [0.0; 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn point(position: [f32; 3], color: [f32; 3], intensity: f32, range: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
light_type: LIGHT_POINT,
|
||||||
|
direction: [0.0; 3],
|
||||||
|
range,
|
||||||
|
color,
|
||||||
|
intensity,
|
||||||
|
inner_cone: 0.0,
|
||||||
|
outer_cone: 0.0,
|
||||||
|
_padding: [0.0; 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spot(
|
||||||
|
position: [f32; 3],
|
||||||
|
direction: [f32; 3],
|
||||||
|
color: [f32; 3],
|
||||||
|
intensity: f32,
|
||||||
|
range: f32,
|
||||||
|
inner_angle_deg: f32,
|
||||||
|
outer_angle_deg: f32,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
light_type: LIGHT_SPOT,
|
||||||
|
direction,
|
||||||
|
range,
|
||||||
|
color,
|
||||||
|
intensity,
|
||||||
|
inner_cone: inner_angle_deg.to_radians().cos(),
|
||||||
|
outer_cone: outer_angle_deg.to_radians().cos(),
|
||||||
|
_padding: [0.0; 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uniform buffer holding up to MAX_LIGHTS lights plus ambient color.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
|
pub struct LightsUniform {
|
||||||
|
pub lights: [LightData; MAX_LIGHTS],
|
||||||
|
pub count: u32,
|
||||||
|
pub ambient_color: [f32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LightsUniform {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: [LightData::zeroed(); MAX_LIGHTS],
|
||||||
|
count: 0,
|
||||||
|
ambient_color: [0.03, 0.03, 0.03],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_light(&mut self, light: LightData) {
|
||||||
|
if (self.count as usize) < MAX_LIGHTS {
|
||||||
|
self.lights[self.count as usize] = light;
|
||||||
|
self.count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_light_data_size() {
|
||||||
|
assert_eq!(mem::size_of::<LightData>() % 16, 0,
|
||||||
|
"LightData must be a multiple of 16 bytes for WGSL array alignment");
|
||||||
|
assert_eq!(mem::size_of::<LightData>(), 64,
|
||||||
|
"LightData must be exactly 64 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lights_uniform_add() {
|
||||||
|
let mut u = LightsUniform::new();
|
||||||
|
u.add_light(LightData::directional([0.0, -1.0, 0.0], [1.0, 1.0, 1.0], 1.0));
|
||||||
|
u.add_light(LightData::point([0.0, 5.0, 0.0], [1.0, 0.0, 0.0], 2.0, 10.0));
|
||||||
|
assert_eq!(u.count, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lights_uniform_max() {
|
||||||
|
let mut u = LightsUniform::new();
|
||||||
|
for _ in 0..20 {
|
||||||
|
u.add_light(LightData::directional([0.0, -1.0, 0.0], [1.0, 1.0, 1.0], 1.0));
|
||||||
|
}
|
||||||
|
assert_eq!(u.count, MAX_LIGHTS as u32,
|
||||||
|
"count must be capped at MAX_LIGHTS (16)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spot_light_cone() {
|
||||||
|
let light = LightData::spot(
|
||||||
|
[0.0, 10.0, 0.0],
|
||||||
|
[0.0, -1.0, 0.0],
|
||||||
|
[1.0, 1.0, 1.0],
|
||||||
|
3.0,
|
||||||
|
20.0,
|
||||||
|
15.0,
|
||||||
|
30.0,
|
||||||
|
);
|
||||||
|
let expected_inner = 15.0_f32.to_radians().cos();
|
||||||
|
let expected_outer = 30.0_f32.to_radians().cos();
|
||||||
|
assert!((light.inner_cone - expected_inner).abs() < 1e-6,
|
||||||
|
"inner_cone should be cos(15°)");
|
||||||
|
assert!((light.outer_cone - expected_outer).abs() < 1e-6,
|
||||||
|
"outer_cone should be cos(30°)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,22 @@ struct CameraUniform {
|
|||||||
camera_pos: vec3<f32>,
|
camera_pos: vec3<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LightUniform {
|
struct LightData {
|
||||||
|
position: vec3<f32>,
|
||||||
|
light_type: u32,
|
||||||
direction: vec3<f32>,
|
direction: vec3<f32>,
|
||||||
|
range: f32,
|
||||||
color: vec3<f32>,
|
color: vec3<f32>,
|
||||||
ambient_strength: f32,
|
intensity: f32,
|
||||||
|
inner_cone: f32,
|
||||||
|
outer_cone: f32,
|
||||||
|
_padding: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LightsUniform {
|
||||||
|
lights: array<LightData, 16>,
|
||||||
|
count: u32,
|
||||||
|
ambient_color: vec3<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MaterialUniform {
|
struct MaterialUniform {
|
||||||
@@ -18,7 +30,7 @@ struct MaterialUniform {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> camera: CameraUniform;
|
@group(0) @binding(0) var<uniform> camera: CameraUniform;
|
||||||
@group(0) @binding(1) var<uniform> light: LightUniform;
|
@group(0) @binding(1) var<uniform> lights_uniform: LightsUniform;
|
||||||
|
|
||||||
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
|
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
|
||||||
@group(1) @binding(1) var s_diffuse: sampler;
|
@group(1) @binding(1) var s_diffuse: sampler;
|
||||||
@@ -81,6 +93,78 @@ fn fresnel_schlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
|
|||||||
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
|
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Point light distance attenuation: inverse-square with smooth falloff at range boundary
|
||||||
|
fn attenuation_point(distance: f32, range: f32) -> f32 {
|
||||||
|
let d_over_r = distance / range;
|
||||||
|
let d_over_r4 = d_over_r * d_over_r * d_over_r * d_over_r;
|
||||||
|
let falloff = clamp(1.0 - d_over_r4, 0.0, 1.0);
|
||||||
|
return (falloff * falloff) / (distance * distance + 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spot light angular attenuation
|
||||||
|
fn attenuation_spot(light: LightData, L: vec3<f32>) -> f32 {
|
||||||
|
let spot_dir = normalize(light.direction);
|
||||||
|
let theta = dot(spot_dir, -L);
|
||||||
|
return clamp(
|
||||||
|
(theta - light.outer_cone) / (light.inner_cone - light.outer_cone + 0.0001),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cook-Torrance BRDF contribution for one light
|
||||||
|
fn compute_light_contribution(
|
||||||
|
light: LightData,
|
||||||
|
N: vec3<f32>,
|
||||||
|
V: vec3<f32>,
|
||||||
|
world_pos: vec3<f32>,
|
||||||
|
F0: vec3<f32>,
|
||||||
|
albedo: vec3<f32>,
|
||||||
|
metallic: f32,
|
||||||
|
roughness: f32,
|
||||||
|
) -> vec3<f32> {
|
||||||
|
var L: vec3<f32>;
|
||||||
|
var radiance: vec3<f32>;
|
||||||
|
|
||||||
|
if light.light_type == 0u {
|
||||||
|
// Directional
|
||||||
|
L = normalize(-light.direction);
|
||||||
|
radiance = light.color * light.intensity;
|
||||||
|
} else if light.light_type == 1u {
|
||||||
|
// Point
|
||||||
|
let to_light = light.position - world_pos;
|
||||||
|
let dist = length(to_light);
|
||||||
|
L = normalize(to_light);
|
||||||
|
let att = attenuation_point(dist, light.range);
|
||||||
|
radiance = light.color * light.intensity * att;
|
||||||
|
} else {
|
||||||
|
// Spot
|
||||||
|
let to_light = light.position - world_pos;
|
||||||
|
let dist = length(to_light);
|
||||||
|
L = normalize(to_light);
|
||||||
|
let att_dist = attenuation_point(dist, light.range);
|
||||||
|
let att_ang = attenuation_spot(light, L);
|
||||||
|
radiance = light.color * light.intensity * att_dist * att_ang;
|
||||||
|
}
|
||||||
|
|
||||||
|
let H = normalize(V + L);
|
||||||
|
|
||||||
|
let NDF = distribution_ggx(N, H, roughness);
|
||||||
|
let G = geometry_smith(N, V, L, roughness);
|
||||||
|
let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
|
||||||
|
|
||||||
|
let ks = F;
|
||||||
|
let kd = (vec3<f32>(1.0) - ks) * (1.0 - metallic);
|
||||||
|
|
||||||
|
let numerator = NDF * G * F;
|
||||||
|
let NdotL = max(dot(N, L), 0.0);
|
||||||
|
let NdotV = max(dot(N, V), 0.0);
|
||||||
|
let denominator = 4.0 * NdotV * NdotL + 0.0001;
|
||||||
|
let specular = numerator / denominator;
|
||||||
|
|
||||||
|
return (kd * albedo / 3.14159265358979 + specular) * radiance * NdotL;
|
||||||
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let tex_color = textureSample(t_diffuse, s_diffuse, in.uv);
|
let tex_color = textureSample(t_diffuse, s_diffuse, in.uv);
|
||||||
@@ -95,29 +179,18 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
// F0: base reflectivity; 0.04 for dielectrics, albedo for metals
|
// F0: base reflectivity; 0.04 for dielectrics, albedo for metals
|
||||||
let F0 = mix(vec3<f32>(0.04, 0.04, 0.04), albedo, metallic);
|
let F0 = mix(vec3<f32>(0.04, 0.04, 0.04), albedo, metallic);
|
||||||
|
|
||||||
// Single directional light
|
// Accumulate contribution from all active lights
|
||||||
let L = normalize(-light.direction);
|
var Lo = vec3<f32>(0.0);
|
||||||
let H = normalize(V + L);
|
let light_count = min(lights_uniform.count, 16u);
|
||||||
let radiance = light.color;
|
for (var i = 0u; i < light_count; i++) {
|
||||||
|
Lo += compute_light_contribution(
|
||||||
// Cook-Torrance BRDF components
|
lights_uniform.lights[i],
|
||||||
let NDF = distribution_ggx(N, H, roughness);
|
N, V, in.world_pos, F0, albedo, metallic, roughness,
|
||||||
let G = geometry_smith(N, V, L, roughness);
|
);
|
||||||
let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
|
}
|
||||||
|
|
||||||
let ks = F;
|
|
||||||
let kd = (vec3<f32>(1.0) - ks) * (1.0 - metallic);
|
|
||||||
|
|
||||||
let numerator = NDF * G * F;
|
|
||||||
let NdotL = max(dot(N, L), 0.0);
|
|
||||||
let NdotV = max(dot(N, V), 0.0);
|
|
||||||
let denominator = 4.0 * NdotV * NdotL + 0.0001;
|
|
||||||
let specular = numerator / denominator;
|
|
||||||
|
|
||||||
let Lo = (kd * albedo / 3.14159265358979 + specular) * radiance * NdotL;
|
|
||||||
|
|
||||||
// Ambient term
|
// Ambient term
|
||||||
let ambient = light.ambient_strength * light.color * albedo * ao;
|
let ambient = lights_uniform.ambient_color * albedo * ao;
|
||||||
|
|
||||||
var color = ambient + Lo;
|
var color = ambient + Lo;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user