render: fix spot light culling

This commit is contained in:
SeanOMik 2024-03-22 10:46:52 -04:00
parent e2844a11a6
commit 0f11fe2e6d
Signed by: SeanOMik
GPG Key ID: FEC9E2FC15235964
4 changed files with 114 additions and 87 deletions

View File

@ -182,6 +182,7 @@ async fn main() {
color: Vec3::new(1.0, 0.0, 0.0),
intensity: 1.0,
range: 1.5,
//cutoff: math::Angle::Degrees(45.0),
..Default::default()
},
Transform::new(

View File

@ -44,6 +44,7 @@ pub struct CameraUniform {
pub inverse_projection: glam::Mat4,
/// The view projection matrix
pub view_projection: glam::Mat4,
pub projection: glam::Mat4,
/// The position of the camera
pub position: glam::Vec3,
pub tile_debug: u32,
@ -57,6 +58,7 @@ impl Default for CameraUniform {
view: glam::Mat4::IDENTITY,
inverse_projection: glam::Mat4::IDENTITY,
view_projection: glam::Mat4::IDENTITY,
projection: glam::Mat4::IDENTITY,
position: Default::default(),
tile_debug: 0,
//_padding: 0,
@ -65,11 +67,12 @@ impl Default for CameraUniform {
}
impl CameraUniform {
pub fn new(view: glam::Mat4, inverse_projection: glam::Mat4, view_projection: glam::Mat4, position: glam::Vec3) -> Self {
pub fn new(view: glam::Mat4, inverse_projection: glam::Mat4, view_projection: glam::Mat4, projection: glam::Mat4, position: glam::Vec3) -> Self {
Self {
view,
inverse_projection,
view_projection,
projection,
position,
tile_debug: 0
}
@ -129,6 +132,7 @@ impl RenderCamera {
view,
inverse_projection: proj.inverse(),
view_projection: self.view_proj,
projection: proj,
position,
tile_debug: camera.debug as u32,
}
@ -151,6 +155,7 @@ impl RenderCamera {
view,
inverse_projection: proj.inverse(),
view_projection: self.view_proj,
projection: proj,
position,
tile_debug: camera.debug as u32,
}

View File

@ -6,6 +6,8 @@ const LIGHT_TY_DIRECTIONAL = 0u;
const LIGHT_TY_POINT = 1u;
const LIGHT_TY_SPOT = 2u;
const ALPHA_CUTOFF = 0.1;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) tex_coords: vec2<f32>,
@ -23,6 +25,7 @@ struct CameraUniform {
view: mat4x4<f32>,
inverse_projection: mat4x4<f32>,
view_projection: mat4x4<f32>,
projection: mat4x4<f32>,
position: vec3<f32>,
tile_debug: u32,
};
@ -132,13 +135,17 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return debug_grid(in);
}
let tile_index = vec2<u32>(floor(in.clip_position.xy / 16.0));
let tile: vec2<u32> = textureLoad(t_light_grid, tile_index).xy;
let object_color: vec4<f32> = textureSample(t_diffuse, s_diffuse, in.tex_coords);
let specular_color: vec3<f32> = textureSample(t_specular, s_specular, in.tex_coords).xyz;
var light_res = vec3<f32>(0.0);
if (object_color.a < ALPHA_CUTOFF) {
discard;
}
let tile_index = vec2<u32>(floor(in.clip_position.xy / 16.0));
let tile: vec2<u32> = textureLoad(t_light_grid, tile_index).xy;
let light_offset = tile.x;
let light_count = tile.y;
@ -269,20 +276,19 @@ fn blinn_phong_spot_light(world_pos: vec3<f32>, world_norm: vec3<f32>, spot_ligh
//// end of specular ////
//// spot light soft edges ////
let theta = dot(light_dir, normalize(-spot_light.direction));
let epsilon = spot_light.spot_cutoff - spot_light.spot_outer_cutoff;
let intensity = clamp((theta - spot_light.spot_outer_cutoff) / epsilon, 0.0, 1.0);
//diffuse_color *= intensity;
//specular_color *= intensity;
let min_cos = cos(spot_light.spot_cutoff);
let max_cos = lerp(min_cos, 1.0, 0.5);
let cos_angle = dot(spot_light.direction, -light_dir);
let cone = smoothstep(min_cos, max_cos, cos_angle);
//// end of spot light soft edges ////
//// spot light attenuation ////
let distance = length(light_pos - world_pos);
let attenuation = calc_attenuation(spot_light, distance);
ambient_color *= attenuation * intensity;
diffuse_color *= attenuation * intensity;
specular_color *= attenuation * intensity;
ambient_color *= attenuation * spot_light.intensity * cone;
diffuse_color *= attenuation * spot_light.intensity * cone;
specular_color *= attenuation * spot_light.intensity * cone;
//// end of spot light attenuation ////
@ -292,3 +298,7 @@ fn blinn_phong_spot_light(world_pos: vec3<f32>, world_norm: vec3<f32>, spot_ligh
fn calc_attenuation(light: Light, distance: f32) -> f32 {
return 1.0 - smoothstep(light.range * light.smoothness, light.range, distance);
}
fn lerp(start: f32, end: f32, alpha: f32) -> f32 {
return (start + (end - start) * alpha);
}

View File

@ -5,20 +5,25 @@ const LIGHT_TY_DIRECTIONAL = 0u;
const LIGHT_TY_POINT = 1u;
const LIGHT_TY_SPOT = 2u;
type vec2f = vec2<f32>;
type vec3f = vec3<f32>;
type vec4f = vec4<f32>;
struct CameraUniform {
view: mat4x4<f32>,
inverse_projection: mat4x4<f32>,
//projection: mat4x4<f32>,
view_projection: mat4x4<f32>,
position: vec3<f32>,
projection: mat4x4<f32>,
position: vec3f,
tile_debug: u32,
};
struct Light {
position: vec3<f32>,
position: vec3f,
light_ty: u32,
direction: vec3<f32>,
direction: vec3f,
enabled: u32,
color: vec3<f32>,
color: vec3f,
range: f32,
intensity: f32,
@ -34,16 +39,21 @@ struct Lights {
};
struct Cone {
tip: vec3<f32>,
tip: vec3f,
height: f32,
direction: vec3<f32>,
direction: vec3f,
radius: f32,
}
struct Plane {
normal: vec3f,
origin_distance: f32,
}
var<workgroup> wg_min_depth: atomic<u32>;
var<workgroup> wg_max_depth: atomic<u32>;
var<workgroup> wg_light_index_start: atomic<u32>;
var<workgroup> wg_frustum_planes: array<vec4<f32>, 6>;
var<workgroup> wg_frustum_planes: array<Plane, 6>;
// index list of visible light sources for this tile
var<workgroup> wg_visible_light_indices: array<u32, MAX_TILE_VISIBLE_LIGHTS>;
@ -107,41 +117,50 @@ fn cs_main(
// Create the frustum planes that will be used for this time
if (local_invocation_index == 0u) {
// Compute the 4 corner points on the far clipping plane to use as the frustum vertices.
var screen_space: array<vec4<f32>, 4>;
// this algorithm is adapted from Google's filament:
// https://github.com/google/filament/blob/3644e7f80827f1cd2caef4a21e410a2243eb6e84/filament/src/Froxelizer.cpp#L402C57-L402C73
let tile_width_clip_space = f32(2u * BLOCK_SIZE) / f32(u_screen_size.x);
let tile_height_clip_space = f32(2u * BLOCK_SIZE) / f32(u_screen_size.y);
// top left point
var temp: vec2<u32> = workgroup_id.xy * BLOCK_SIZE;
screen_space[0] = vec4<f32>(f32(temp.x), f32(temp.y), -1.0, 1.0);
let tr_projection = transpose(u_camera.projection);
// top right point
var temp2 = vec2<f32>(f32(workgroup_id.x) + 1.0, f32(workgroup_id.y)) * f32(BLOCK_SIZE);
screen_space[1] = vec4<f32>(temp2.x, temp2.y, -1.0, 1.0);
var planes: array<vec4f, 4>;
// bottom left point
temp2 = vec2<f32>(f32(workgroup_id.x), f32(workgroup_id.y) + 1.0) * f32(BLOCK_SIZE);
screen_space[2] = vec4<f32>(temp2.x, temp2.y, -1.0, 1.0);
// bottom right point
temp2 = vec2<f32>(f32(workgroup_id.x) + 1.0, f32(workgroup_id.y) + 1.0) * f32(BLOCK_SIZE);
screen_space[3] = vec4<f32>(temp2.x, temp2.y, -1.0, 1.0);
// convert screenspace to view space
var view_space: array<vec3<f32>, 4>;
for (var i = 0u; i < 4u; i++) {
view_space[i] = screen_to_view(screen_space[i]).xyz;
// left plane
{
let x = (f32(workgroup_id.x) * tile_width_clip_space) - 1.0;
let p = tr_projection * vec4f(-1.0, 0.0, 0.0, x);
planes[0] = -vec4f(normalize(p.xyz), 0.0);
}
// View space eye is always at the origin
let eye_pos = vec3<f32>(0.0, 0.0, 0.0);
// right plane
{
let x = (f32(workgroup_id.x + 1u) * tile_width_clip_space) - 1.0;
let p = tr_projection * vec4f(-1.0, 0.0, 0.0, x);
planes[1] = vec4f(normalize(p.xyz), 0.0);
}
wg_frustum_planes[0] = compute_plane(eye_pos, view_space[2], view_space[0]); // left plane
wg_frustum_planes[1] = compute_plane(eye_pos, view_space[1], view_space[3]); // right plane
wg_frustum_planes[2] = compute_plane(eye_pos, view_space[0], view_space[1]); // top plane
wg_frustum_planes[3] = compute_plane(eye_pos, view_space[3], view_space[2]); // bottom plane
// top plane
{
let y = (f32(workgroup_id.y) * tile_height_clip_space) - 1.0;
let p = tr_projection * vec4f(0.0, 1.0, 0.0, y);
planes[2] = -vec4f(normalize(p.xyz), 0.0);
}
wg_frustum_planes[4] = vec4<f32>(0.0, 0.0, -1.0, -min_depth);
wg_frustum_planes[5] = vec4<f32>(0.0, 0.0, 1.0, -max_depth);
// bottom plane
{
let y = (f32(workgroup_id.y + 1u) * tile_height_clip_space) - 1.0;
let p = tr_projection * vec4f(0.0, 1.0, 0.0, y);
planes[3] = vec4f(normalize(p.xyz), 0.0);
}
wg_frustum_planes[0] = Plane(planes[0].xyz, planes[0].w);
wg_frustum_planes[1] = Plane(planes[1].xyz, planes[1].w);
wg_frustum_planes[2] = Plane(planes[2].xyz, planes[2].w);
wg_frustum_planes[3] = Plane(planes[3].xyz, planes[3].w);
wg_frustum_planes[4] = Plane(vec3f(0.0, 0.0, -1.0), -min_depth);
wg_frustum_planes[5] = Plane(vec3f(0.0, 0.0, 1.0), -max_depth);
}
workgroupBarrier();
@ -157,29 +176,27 @@ fn cs_main(
let light = u_lights.data[light_index];
if (light.enabled == 1u) {
let position_vec4 = u_camera.view * vec4<f32>(light.position, 1.0);
let position = position_vec4.xyz;
let radius = light.range;
let position_vs = (u_camera.view * vec4f(light.position, 1.0)).xyz;
if (light.light_ty == LIGHT_TY_DIRECTIONAL) {
add_light(light_index);
} else if (light.light_ty == LIGHT_TY_POINT
&& sphere_inside_frustrum(wg_frustum_planes, position, radius)) {
&& sphere_inside_frustrum(wg_frustum_planes, position_vs, light.range)) {
// TODO: add the light to the transparent geometry list
if (!sphere_inside_plane(position, radius, wg_frustum_planes[4])) {
if (!sphere_inside_plane(position_vs, light.range, wg_frustum_planes[4])) {
add_light(light_index);
}
} else if (light.light_ty == LIGHT_TY_SPOT) {
let dir_vs = u_camera.view * vec4<f32>(light.direction, 1.0);
let dir_vs = (u_camera.view * vec4f(light.direction, 1.0)).xyz;
let cone_radius = tan(light.spot_cutoff) * light.range;
let cone = Cone(position, radius, dir_vs.xyz, cone_radius);
let cone = Cone(position_vs, light.range, dir_vs, cone_radius);
if (cone_inside_frustum(cone, wg_frustum_planes)) {
// TODO: add the light to the transparent geometry list
add_light(light_index);
if (!cone_inside_plane(cone, wg_frustum_planes[4])) {
add_light(light_index);
}
}
}
@ -220,7 +237,7 @@ fn add_light(light_index: u32) -> bool {
return false;
}
fn sphere_inside_frustrum(frustum: array<vec4<f32>, 6>, sphere_origin: vec3<f32>, radius: f32) -> bool {
fn sphere_inside_frustrum(frustum: array<Plane, 6>, sphere_origin: vec3f, radius: f32) -> bool {
// to be able to index this array with a non-const value,
// it must be defined as a var
var frustum_v = frustum;
@ -239,11 +256,11 @@ fn sphere_inside_frustrum(frustum: array<vec4<f32>, 6>, sphere_origin: vec3<f32>
///
/// Source: Real-time collision detection, Christer Ericson (2005)
/// (https://www.3dgep.com/forward-plus/#light-culling-compute-shader)
fn sphere_inside_plane(sphere_origin: vec3<f32>, radius: f32, plane: vec4<f32>) -> bool {
return dot(plane.xyz, sphere_origin) - plane.w < -radius;
fn sphere_inside_plane(sphere_origin: vec3f, radius: f32, plane: Plane) -> bool {
return dot(plane.normal, sphere_origin) - plane.origin_distance < -radius;
}
fn clip_to_view(clip: vec4<f32>) -> vec4<f32> {
fn clip_to_view(clip: vec4f) -> vec4f {
// view space position
var view = u_camera.inverse_projection * clip;
@ -251,12 +268,12 @@ fn clip_to_view(clip: vec4<f32>) -> vec4<f32> {
return view / view.w;
}
fn screen_to_view(screen: vec4<f32>) -> vec4<f32> {
fn screen_to_view(screen: vec4f) -> vec4f {
// convert to normalized texture coordinates
let tex_coord = screen.xy / vec2<f32>(u_screen_size);
// convert to clip space
let clip = vec4<f32>( vec2<f32>(tex_coord.x, 1.0 - tex_coord.y) * 2.0 - 1.0, screen.z, screen.w);
let clip = vec4f( vec2<f32>(tex_coord.x, 1.0 - tex_coord.y) * 2.0 - 1.0, screen.z, screen.w);
return clip_to_view(clip);
}
@ -264,52 +281,46 @@ fn screen_to_view(screen: vec4<f32>) -> vec4<f32> {
/// Compute a plane from 3 noncollinear points that form a triangle.
/// This equation assumes a right-handed (counter-clockwise winding order)
/// coordinate system to determine the direction of the plane normal.
fn compute_plane(p0: vec3<f32>, p1: vec3<f32>, p2: vec3<f32>) -> vec4<f32> {
fn compute_plane(p0: vec3f, p1: vec3f, p2: vec3f) -> Plane {
let v0 = p1 - p0;
let v2 = p2 - p0;
var plane = vec4<f32>(normalize(cross(v0, v2)), 0.0);
let normal = vec4f(normalize(cross(v0, v2)), 0.0);
// find the distance to the origin
plane.w = dot(plane.xyz, p0);
let distance = dot(normal.xyz, p0);
return plane;
return Plane(normal.xyz, distance);
}
fn point_inside_plane(point: vec3<f32>, plane: vec4<f32>) -> bool {
return dot(plane.xyz, point) - plane.w < 0.0;
fn point_inside_plane(point: vec3f, plane: Plane) -> bool {
return dot(plane.normal, point) + plane.origin_distance < 0.0;
}
fn point_intersect_plane(point: vec3f, plane: Plane) -> f32 {
return dot(plane.normal, point) + plane.origin_distance;
}
/// Check to see if a cone if fully behind (inside the negative halfspace of) a plane.
///
/// Source: Real-time collision detection, Christer Ericson (2005)
/// (https://www.3dgep.com/forward-plus/#light-culling-compute-shader)
fn cone_inside_plane(cone: Cone, plane: vec4<f32>) -> bool {
// Compute the farthest point on the end of the cone to the positive space of the plane.
let m = cross(cross(plane.xyz, cone.direction), cone.direction);
let farthest = cone.tip + cone.direction * cone.height - m * cone.radius;
fn cone_inside_plane(cone: Cone, plane: Plane) -> bool {
let dir = cone.direction;
let furthest_direction = cross(cross(plane.normal, dir), dir);
let furthest = cone.tip + dir * cone.height - furthest_direction * cone.radius;
// The cone is in the negative halfspace of the plane if the tip of the cone,
// and the farthest point on the end of the cone are inside the negative halfspace
// of the plane.
return point_inside_plane(cone.tip, plane) && point_inside_plane(farthest, plane);
return point_inside_plane(cone.tip, plane) && point_inside_plane(furthest, plane);
}
fn cone_inside_frustum(cone: Cone, frustum: array<vec4<f32>, 6>) -> bool {
//let near_plane = frustum[4];
//let far_plane = frustum[5];
// check near and far clipping planes first
//if (cone_inside_plane(cone, near_plane) || cone_inside_plane(cone, far_plane)) {
// return false;
//}
// to be able to index this array with a non-const value,
// it must be defined as a var
var frustum_v = frustum;
fn cone_inside_frustum(cone: Cone, frustum: array<Plane, 6>) -> bool {
var frustum = frustum;
for (var i = 0u; i < 4u; i++) {
if (cone_inside_plane(cone, frustum_v[i])) {
// TODO: better cone checking
if (sphere_inside_plane(cone.tip, cone.radius, frustum[i])) {
return false;
}
}