Clustered forward rendering (#3153)

# Objective

Implement clustered-forward rendering.

## Solution

~~FIXME - in the interest of keeping the merge train moving, I'm submitting this PR now before the description is ready. I want to add in some comments into the code with references for the various bits and pieces and I want to describe some of the key decisions I made here. I'll do that as soon as I can.~~ Anyone reviewing is welcome to add review comments where you want to know more about how something or other works.

* The summary of the technique is that the view frustum is divided into a grid of sub-volumes called clusters, point lights are tested against each of the clusters to see if they would affect that volume within the scene and if so, added to a list of lights affecting that cluster. Then when shading a fragment which is a point on the surface of a mesh within the scene, the point is mapped to a cluster and only the lights affecting that clusters are used in lighting calculations. This brings huge performance and scalability benefits as most of the time lights are placed so that there are not that many that overlap each other in terms of their sphere of influence, but there may be many distinct point lights visible in the scene. Doing all the lighting calculations for all visible lights in the scene for every pixel on the screen quickly becomes a performance limitation. Clustered forward rendering allows us to make an approximate list of lights that affect each pixel, indeed each surface in the scene (as it works along the view z axis too, unlike tiled/forward+).
* WebGL2 is a platform we want to support and it does not support storage buffers. Uniform buffer bindings are limited to a maximum of 16384 bytes per binding. I used bit shifting and masking to pack the cluster light lists and various indices into a uniform buffer and the 16kB limit is very likely the first bottleneck in scaling the number of lights in a scene at the moment if the lights can affect many clusters due to their range or proximity to the camera (there are a lot of clusters close to the camera, which is an area for improvement). We could store the information in textures instead of uniform buffers to remove this bottleneck though I don’t know if there are performance implications to reading from textures instead if uniform buffers.
* Because of the uniform buffer binding size limitations we can support a maximum of 256 lights with the current size of the PointLight struct
* The z-slicing method (i.e. the mapping from view space z to a depth slice which defines the near and far planes of a cluster) is using the Doom 2016 method. I need to add comments with references to this. It’s an exponential function that simplifies well for the purposes of optimising the fragment shader. xy grid divisions are regular in screen space.
* Some optimisation work was done on the allocation of lights to clusters, which involves intersection tests, and for this number of clusters and lights the system has insignificant cost using a fairly naïve algorithm. I think for more lights / finer-grained clusters we could use a BVH, but at some point it would be just much better to use compute shaders and storage buffers.
* Something else to note is that it is absolutely infeasible to use plain cube map point light shadow mapping for many lights. It does not scale in terms of performance nor memory usage. There are some interesting methods I saw discussed in reference material that I will add a link to which render and update shadow maps piece-wise, but they also need compute shaders to work well. Basically for now you need to sacrifice point light shadows for all but a handful of point lights if you don’t want to kill performance. I set the limit to 10 but that’s just what we had from before where 10 was the maximum number of point lights before this PR.
* I added a couple of debug visualisations behind a shader def that were useful for seeing performance impact of light distribution - I should make the debug mode configurable without modifying the shader code. One mode shows the number of lights affecting each cluster by tinting toward red for few lights or green for many lights (maxes out at 16, but not sure that’s a reasonable max). The other shows which cluster the surface at a fragment belongs to by tinting it with a randomish colour. This can help to understand deeper performance issues due to screen space tiles spanning multiple clusters in depth with divergent shader execution times.

Also, there are more things that could be done as improvements, and I will document those somewhere (I'm not sure where will be the best place... in a todo alongside the code, a GitHub issue, somewhere else?) but I think it works well enough and brings significant performance and scalability benefits that it's worth integrating already now and then iterating on.
* Calculate the light’s effective range based on its intensity and physical falloff and either just use this, or take the minimum of the user-supplied range and this. This would avoid unnecessary lighting calculations for clusters that cannot be affected. This would need to take into account HDR tone mapping as in my not-fully-understanding-the-details understanding, the threshold is relative to how bright the scene is.
* Improve the z-slicing to use a larger first slice.
* More gracefully handle the cluster light list uniform buffer binding size limitations by prioritising which lights are included (some heuristic for most significant like closest to the camera, brightest, affecting the most pixels, …)
* Switch to using a texture instead of uniform buffer
* Figure out the / a better story for shadows

I will also probably add an example that demonstrates some of the issues:
* What situations exhaust the space available in the uniform buffers
  * Light range too large making lights affect many clusters and so exhausting the space for the lists of lights that affect clusters
  * Light range set to be too small producing visible artifacts where clusters the light would physically affect are not affected by the light
* Perhaps some performance issues
  * How many lights can be closely packed or affect large portions of the view before performance drops?
This commit is contained in:
Robert Swain 2021-12-09 03:08:54 +00:00
parent 7dd92e72d4
commit 2abf5cc618
16 changed files with 1095 additions and 237 deletions

View file

@ -59,7 +59,7 @@ impl Node for MainPass3dNode {
let pass_descriptor = RenderPassDescriptor {
label: Some("main_opaque_pass_3d"),
// NOTE: The opaque pass clears and initializes the color
// buffer as well as writing to it.
// buffer as well as writing to it.
color_attachments: &[target.get_color_attachment(Operations {
load: LoadOp::Clear(clear_color.0.into()),
store: true,
@ -135,8 +135,8 @@ impl Node for MainPass3dNode {
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &depth.view,
// NOTE: For the transparent pass we load the depth buffer but do not write to it.
// As the opaque and alpha mask passes run first, opaque meshes can occlude
// transparent ones.
// As the opaque and alpha mask passes run first, opaque meshes can occlude
// transparent ones.
depth_ops: Some(Operations {
load: LoadOp::Load,
store: false,

View file

@ -24,6 +24,7 @@ bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features
bevy_render2 = { path = "../bevy_render2", version = "0.5.0" }
bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" }
bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" }
bevy_window = { path = "../../crates/bevy_window", version = "0.5.0" }
# other
bitflags = "1.2"

View file

@ -60,6 +60,29 @@ impl Plugin for PbrPlugin {
.init_resource::<DirectionalLightShadowMap>()
.init_resource::<PointLightShadowMap>()
.init_resource::<AmbientLight>()
.init_resource::<VisiblePointLights>()
.add_system_to_stage(
CoreStage::PostUpdate,
// NOTE: Clusters need to have been added before update_clusters is run so
// add as an exclusive system
add_clusters
.exclusive_system()
.label(SimulationLightSystems::AddClusters),
)
.add_system_to_stage(
CoreStage::PostUpdate,
// NOTE: Must come after add_clusters!
update_clusters
.label(SimulationLightSystems::UpdateClusters)
.after(TransformSystem::TransformPropagate),
)
.add_system_to_stage(
CoreStage::PostUpdate,
assign_lights_to_clusters
.label(SimulationLightSystems::AssignLightsToClusters)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::UpdateClusters),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_directional_light_frusta
@ -70,11 +93,12 @@ impl Plugin for PbrPlugin {
CoreStage::PostUpdate,
update_point_light_frusta
.label(SimulationLightSystems::UpdatePointLightFrusta)
.after(TransformSystem::TransformPropagate),
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::AssignLightsToClusters),
)
.add_system_to_stage(
CoreStage::PostUpdate,
check_light_visibility
check_light_mesh_visibility
.label(SimulationLightSystems::CheckLightVisibility)
.after(TransformSystem::TransformPropagate)
.after(VisibilitySystems::CalculateBounds)
@ -88,6 +112,10 @@ impl Plugin for PbrPlugin {
let render_app = app.sub_app(RenderApp);
render_app
.add_system_to_stage(
RenderStage::Extract,
render::extract_clusters.label(RenderLightSystems::ExtractClusters),
)
.add_system_to_stage(
RenderStage::Extract,
render::extract_lights.label(RenderLightSystems::ExtractLights),
@ -100,6 +128,15 @@ impl Plugin for PbrPlugin {
.exclusive_system()
.label(RenderLightSystems::PrepareLights),
)
.add_system_to_stage(
RenderStage::Prepare,
// this is added as an exclusive system because it contributes new views. it must run (and have Commands applied)
// _before_ the `prepare_views()` system is run. ideally this becomes a normal system when "stageless" features come out
render::prepare_clusters
.exclusive_system()
.label(RenderLightSystems::PrepareClusters)
.after(RenderLightSystems::PrepareLights),
)
.add_system_to_stage(
RenderStage::Queue,
render::queue_shadows.label(RenderLightSystems::QueueShadows),
@ -111,6 +148,7 @@ impl Plugin for PbrPlugin {
.init_resource::<ShadowPipeline>()
.init_resource::<DrawFunctions<Shadow>>()
.init_resource::<LightMeta>()
.init_resource::<GlobalLightMeta>()
.init_resource::<SpecializedPipelines<PbrPipeline>>()
.init_resource::<SpecializedPipelines<ShadowPipeline>>();

View file

@ -1,14 +1,19 @@
use std::collections::HashSet;
use bevy_ecs::prelude::*;
use bevy_math::Mat4;
use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_render2::{
camera::{CameraProjection, OrthographicProjection},
camera::{Camera, CameraProjection, OrthographicProjection},
color::Color,
primitives::{Aabb, CubemapFrusta, Frustum, Sphere},
view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities, VisibleEntity},
view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities},
};
use bevy_transform::components::GlobalTransform;
use bevy_window::Windows;
use crate::{CubeMapFace, CubemapVisibleEntities, CUBE_MAP_FACES};
use crate::{
CubeMapFace, CubemapVisibleEntities, ViewClusterBindings, CUBE_MAP_FACES, POINT_LIGHT_NEAR_Z,
};
/// A light that emits light in all directions from a central point.
///
@ -175,11 +180,366 @@ pub struct NotShadowReceiver;
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum SimulationLightSystems {
AddClusters,
UpdateClusters,
AssignLightsToClusters,
UpdateDirectionalLightFrusta,
UpdatePointLightFrusta,
CheckLightVisibility,
}
// Clustered-forward rendering notes
// The main initial reference material used was this rather accessible article:
// http://www.aortiz.me/2018/12/21/CG.html
// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of:
// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/
// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.)
// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousas Siggraph 2016 talk about Doom 2016:
// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf
#[derive(Component, Debug)]
pub struct Clusters {
/// Tile size
pub(crate) tile_size: UVec2,
/// Number of clusters in x / y / z in the view frustum
pub(crate) axis_slices: UVec3,
aabbs: Vec<Aabb>,
pub(crate) lights: Vec<VisiblePointLights>,
}
impl Clusters {
fn new(tile_size: UVec2, screen_size: UVec2, z_slices: u32) -> Self {
let mut clusters = Self {
tile_size,
axis_slices: Default::default(),
aabbs: Default::default(),
lights: Default::default(),
};
clusters.update(tile_size, screen_size, z_slices);
clusters
}
fn from_screen_size_and_z_slices(screen_size: UVec2, z_slices: u32) -> Self {
let aspect_ratio = screen_size.x as f32 / screen_size.y as f32;
let n_tiles_y =
((ViewClusterBindings::MAX_OFFSETS as u32 / z_slices) as f32 / aspect_ratio).sqrt();
// NOTE: Round down the number of tiles in order to avoid overflowing the maximum number of
// clusters.
let n_tiles = UVec2::new(
(aspect_ratio * n_tiles_y).floor() as u32,
n_tiles_y.floor() as u32,
);
Clusters::new((screen_size + UVec2::ONE) / n_tiles, screen_size, Z_SLICES)
}
fn update(&mut self, tile_size: UVec2, screen_size: UVec2, z_slices: u32) {
self.tile_size = tile_size;
self.axis_slices = UVec3::new(
(screen_size.x + 1) / tile_size.x,
(screen_size.y + 1) / tile_size.y,
z_slices,
);
}
}
fn clip_to_view(inverse_projection: Mat4, clip: Vec4) -> Vec4 {
let view = inverse_projection * clip;
view / view.w
}
fn screen_to_view(screen_size: Vec2, inverse_projection: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 {
let tex_coord = screen / screen_size;
let clip = Vec4::new(
tex_coord.x * 2.0 - 1.0,
(1.0 - tex_coord.y) * 2.0 - 1.0,
ndc_z,
1.0,
);
clip_to_view(inverse_projection, clip)
}
// Calculate the intersection of a ray from the eye through the view space position to a z plane
fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 {
let v = p - origin;
let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v);
origin + t * v
}
fn compute_aabb_for_cluster(
z_near: f32,
z_far: f32,
tile_size: Vec2,
screen_size: Vec2,
inverse_projection: Mat4,
cluster_dimensions: UVec3,
ijk: UVec3,
) -> Aabb {
let ijk = ijk.as_vec3();
// Calculate the minimum and maximum points in screen space
let p_min = ijk.xy() * tile_size;
let p_max = p_min + tile_size;
// Convert to view space at the near plane
// NOTE: 1.0 is the near plane due to using reverse z projections
let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0);
let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0);
let z_far_over_z_near = -z_far / -z_near;
let cluster_near = -z_near * z_far_over_z_near.powf(ijk.z / cluster_dimensions.z as f32);
// NOTE: This could be simplified to:
// let cluster_far = cluster_near * z_far_over_z_near;
let cluster_far = -z_near * z_far_over_z_near.powf((ijk.z + 1.0) / cluster_dimensions.z as f32);
// Calculate the four intersection points of the min and max points with the cluster near and far planes
let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);
let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);
let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);
let cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far));
let cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far));
Aabb::from_min_max(cluster_min, cluster_max)
}
const Z_SLICES: u32 = 24;
pub fn add_clusters(
mut commands: Commands,
windows: Res<Windows>,
cameras: Query<(Entity, &Camera), Without<Clusters>>,
) {
for (entity, camera) in cameras.iter() {
let window = windows.get(camera.window).unwrap();
let clusters = Clusters::from_screen_size_and_z_slices(
UVec2::new(window.physical_width(), window.physical_height()),
Z_SLICES,
);
commands.entity(entity).insert(clusters);
}
}
pub fn update_clusters(windows: Res<Windows>, mut views: Query<(&Camera, &mut Clusters)>) {
for (camera, mut clusters) in views.iter_mut() {
let inverse_projection = camera.projection_matrix.inverse();
let window = windows.get(camera.window).unwrap();
let screen_size_u32 = UVec2::new(window.physical_width(), window.physical_height());
*clusters =
Clusters::from_screen_size_and_z_slices(screen_size_u32, clusters.axis_slices.z);
let screen_size = screen_size_u32.as_vec2();
let tile_size_u32 = clusters.tile_size;
let tile_size = tile_size_u32.as_vec2();
// Calculate view space AABBs
// NOTE: It is important that these are iterated in a specific order
// so that we can calculate the cluster index in the fragment shader!
// I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan
// along z
let mut aabbs = Vec::with_capacity(
(clusters.axis_slices.y * clusters.axis_slices.x * clusters.axis_slices.z) as usize,
);
for y in 0..clusters.axis_slices.y {
for x in 0..clusters.axis_slices.x {
for z in 0..clusters.axis_slices.z {
aabbs.push(compute_aabb_for_cluster(
camera.near,
camera.far,
tile_size,
screen_size,
inverse_projection,
clusters.axis_slices,
UVec3::new(x, y, z),
));
}
}
}
clusters.aabbs = aabbs;
}
}
#[derive(Clone, Component, Debug, Default)]
pub struct VisiblePointLights {
pub entities: Vec<Entity>,
}
impl VisiblePointLights {
pub fn from_light_count(count: usize) -> Self {
Self {
entities: Vec::with_capacity(count),
}
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
self.entities.iter()
}
pub fn len(&self) -> usize {
self.entities.len()
}
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
}
fn view_z_to_z_slice(cluster_factors: Vec2, view_z: f32) -> u32 {
// NOTE: had to use -view_z to make it positive else log(negative) is nan
((-view_z).ln() * cluster_factors.x - cluster_factors.y).floor() as u32
}
fn ndc_position_to_cluster(
cluster_dimensions: UVec3,
cluster_factors: Vec2,
ndc_p: Vec3,
view_z: f32,
) -> UVec3 {
let cluster_dimensions_f32 = cluster_dimensions.as_vec3();
let frag_coord =
(ndc_p.xy() * Vec2::new(0.5, -0.5) + Vec2::splat(0.5)).clamp(Vec2::ZERO, Vec2::ONE);
let xy = (frag_coord * cluster_dimensions_f32.xy()).floor();
let z_slice = view_z_to_z_slice(cluster_factors, view_z);
xy.as_uvec2()
.extend(z_slice)
.clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)
}
fn cluster_to_index(cluster_dimensions: UVec3, cluster: UVec3) -> usize {
((cluster.y * cluster_dimensions.x + cluster.x) * cluster_dimensions.z + cluster.z) as usize
}
// NOTE: Run this before update_point_light_frusta!
pub fn assign_lights_to_clusters(
mut commands: Commands,
mut global_lights: ResMut<VisiblePointLights>,
mut views: Query<(Entity, &GlobalTransform, &Camera, &Frustum, &mut Clusters)>,
lights: Query<(Entity, &GlobalTransform, &PointLight)>,
) {
let light_count = lights.iter().count();
let mut global_lights_set = HashSet::with_capacity(light_count);
for (view_entity, view_transform, camera, frustum, mut clusters) in views.iter_mut() {
let view_transform = view_transform.compute_matrix();
let inverse_view_transform = view_transform.inverse();
let cluster_count = clusters.aabbs.len();
let z_slices_of_ln_zfar_over_znear =
clusters.axis_slices.z as f32 / (camera.far / camera.near).ln();
let cluster_factors = Vec2::new(
z_slices_of_ln_zfar_over_znear,
camera.near.ln() * z_slices_of_ln_zfar_over_znear,
);
let mut clusters_lights =
vec![VisiblePointLights::from_light_count(light_count); cluster_count];
let mut visible_lights_set = HashSet::with_capacity(light_count);
for (light_entity, light_transform, light) in lights.iter() {
let light_sphere = Sphere {
center: light_transform.translation,
radius: light.range,
};
// Check if the light is within the view frustum
if !frustum.intersects_sphere(&light_sphere) {
continue;
}
// Calculate an AABB for the light in view space, find the corresponding clusters for the min and max
// points of the AABB, then iterate over just those clusters for this light
let light_aabb_view = Aabb {
center: (inverse_view_transform * light_sphere.center.extend(1.0)).xyz(),
half_extents: Vec3::splat(light_sphere.radius),
};
let (light_aabb_view_min, light_aabb_view_max) =
(light_aabb_view.min(), light_aabb_view.max());
// Is there a cheaper way to do this? The problem is that because of perspective
// the point at max z but min xy may be less xy in screenspace, and similar. As
// such, projecting the min and max xy at both the closer and further z and taking
// the min and max of those projected points addresses this.
let (
light_aabb_view_xymin_near,
light_aabb_view_xymin_far,
light_aabb_view_xymax_near,
light_aabb_view_xymax_far,
) = (
light_aabb_view_min,
light_aabb_view_min.xy().extend(light_aabb_view_max.z),
light_aabb_view_max.xy().extend(light_aabb_view_min.z),
light_aabb_view_max,
);
let (
light_aabb_clip_xymin_near,
light_aabb_clip_xymin_far,
light_aabb_clip_xymax_near,
light_aabb_clip_xymax_far,
) = (
camera.projection_matrix * light_aabb_view_xymin_near.extend(1.0),
camera.projection_matrix * light_aabb_view_xymin_far.extend(1.0),
camera.projection_matrix * light_aabb_view_xymax_near.extend(1.0),
camera.projection_matrix * light_aabb_view_xymax_far.extend(1.0),
);
let (
light_aabb_ndc_xymin_near,
light_aabb_ndc_xymin_far,
light_aabb_ndc_xymax_near,
light_aabb_ndc_xymax_far,
) = (
light_aabb_clip_xymin_near.xyz() / light_aabb_clip_xymin_near.w,
light_aabb_clip_xymin_far.xyz() / light_aabb_clip_xymin_far.w,
light_aabb_clip_xymax_near.xyz() / light_aabb_clip_xymax_near.w,
light_aabb_clip_xymax_far.xyz() / light_aabb_clip_xymax_far.w,
);
let (light_aabb_ndc_min, light_aabb_ndc_max) = (
light_aabb_ndc_xymin_near
.min(light_aabb_ndc_xymin_far)
.min(light_aabb_ndc_xymax_near)
.min(light_aabb_ndc_xymax_far),
light_aabb_ndc_xymin_near
.max(light_aabb_ndc_xymin_far)
.max(light_aabb_ndc_xymax_near)
.max(light_aabb_ndc_xymax_far),
);
let min_cluster = ndc_position_to_cluster(
clusters.axis_slices,
cluster_factors,
light_aabb_ndc_min,
light_aabb_view_min.z,
);
let max_cluster = ndc_position_to_cluster(
clusters.axis_slices,
cluster_factors,
light_aabb_ndc_max,
light_aabb_view_max.z,
);
let (min_cluster, max_cluster) =
(min_cluster.min(max_cluster), min_cluster.max(max_cluster));
for y in min_cluster.y..=max_cluster.y {
for x in min_cluster.x..=max_cluster.x {
for z in min_cluster.z..=max_cluster.z {
let cluster_index =
cluster_to_index(clusters.axis_slices, UVec3::new(x, y, z));
let cluster_aabb = &clusters.aabbs[cluster_index];
if light_sphere.intersects_obb(cluster_aabb, &view_transform) {
global_lights_set.insert(light_entity);
visible_lights_set.insert(light_entity);
clusters_lights[cluster_index].entities.push(light_entity);
}
}
}
}
}
for cluster_lights in clusters_lights.iter_mut() {
cluster_lights.entities.shrink_to_fit();
}
clusters.lights = clusters_lights;
commands.entity(view_entity).insert(VisiblePointLights {
entities: visible_lights_set.into_iter().collect(),
});
}
global_lights.entities = global_lights_set.into_iter().collect();
}
pub fn update_directional_light_frusta(
mut views: Query<(&GlobalTransform, &DirectionalLight, &mut Frustum)>,
) {
@ -202,20 +562,30 @@ pub fn update_directional_light_frusta(
}
}
// NOTE: Run this after assign_lights_to_clusters!
pub fn update_point_light_frusta(
mut views: Query<(&GlobalTransform, &PointLight, &mut CubemapFrusta)>,
global_lights: Res<VisiblePointLights>,
mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>,
) {
let projection = Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1);
let projection =
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z);
let view_rotations = CUBE_MAP_FACES
.iter()
.map(|CubeMapFace { target, up }| GlobalTransform::identity().looking_at(*target, *up))
.collect::<Vec<_>>();
for (transform, point_light, mut cubemap_frusta) in views.iter_mut() {
let global_lights_set = global_lights
.entities
.iter()
.copied()
.collect::<HashSet<_>>();
for (entity, transform, point_light, mut cubemap_frusta) in views.iter_mut() {
// The frusta are used for culling meshes to the light for shadow mapping
// so if shadow mapping is disabled for this light, then the frusta are
// not needed.
if !point_light.shadows_enabled {
// Also, if the light is not relevant for any cluster, it will not be in the
// global lights set and so there is no need to update its frusta.
if !point_light.shadows_enabled || !global_lights_set.contains(&entity) {
continue;
}
@ -239,7 +609,10 @@ pub fn update_point_light_frusta(
}
}
pub fn check_light_visibility(
pub fn check_light_mesh_visibility(
// NOTE: VisiblePointLights is an alias for VisibleEntities so the Without<DirectionalLight>
// is needed to avoid an unnecessary QuerySet
visible_point_lights: Query<&VisiblePointLights, Without<DirectionalLight>>,
mut point_lights: Query<(
&PointLight,
&GlobalTransform,
@ -304,7 +677,7 @@ pub fn check_light_visibility(
}
computed_visibility.is_visible = true;
visible_entities.entities.push(VisibleEntity { entity });
visible_entities.entities.push(entity);
}
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
@ -312,67 +685,76 @@ pub fn check_light_visibility(
}
// Point lights
for (point_light, transform, cubemap_frusta, mut cubemap_visible_entities, maybe_view_mask) in
point_lights.iter_mut()
{
for visible_entities in cubemap_visible_entities.iter_mut() {
visible_entities.entities.clear();
}
for visible_lights in visible_point_lights.iter() {
for light_entity in visible_lights.entities.iter().copied() {
if let Ok((
point_light,
transform,
cubemap_frusta,
mut cubemap_visible_entities,
maybe_view_mask,
)) = point_lights.get_mut(light_entity)
{
for visible_entities in cubemap_visible_entities.iter_mut() {
visible_entities.entities.clear();
}
// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !point_light.shadows_enabled {
continue;
}
let view_mask = maybe_view_mask.copied().unwrap_or_default();
let light_sphere = Sphere {
center: transform.translation,
radius: point_light.range,
};
for (
entity,
visibility,
mut computed_visibility,
maybe_entity_mask,
maybe_aabb,
maybe_transform,
) in visible_entity_query.iter_mut()
{
if !visibility.is_visible {
continue;
}
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
if !view_mask.intersects(&entity_mask) {
continue;
}
// If we have an aabb and transform, do frustum culling
if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
let model_to_world = transform.compute_matrix();
// Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
if !light_sphere.intersects_obb(aabb, &model_to_world) {
// NOTE: If shadow mapping is disabled for the light then it must have no visible entities
if !point_light.shadows_enabled {
continue;
}
for (frustum, visible_entities) in cubemap_frusta
.iter()
.zip(cubemap_visible_entities.iter_mut())
let view_mask = maybe_view_mask.copied().unwrap_or_default();
let light_sphere = Sphere {
center: transform.translation,
radius: point_light.range,
};
for (
entity,
visibility,
mut computed_visibility,
maybe_entity_mask,
maybe_aabb,
maybe_transform,
) in visible_entity_query.iter_mut()
{
if frustum.intersects_obb(aabb, &model_to_world) {
if !visibility.is_visible {
continue;
}
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
if !view_mask.intersects(&entity_mask) {
continue;
}
// If we have an aabb and transform, do frustum culling
if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) {
let model_to_world = transform.compute_matrix();
// Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light
if !light_sphere.intersects_obb(aabb, &model_to_world) {
continue;
}
for (frustum, visible_entities) in cubemap_frusta
.iter()
.zip(cubemap_visible_entities.iter_mut())
{
if frustum.intersects_obb(aabb, &model_to_world) {
computed_visibility.is_visible = true;
visible_entities.entities.push(entity);
}
}
} else {
computed_visibility.is_visible = true;
visible_entities.entities.push(VisibleEntity { entity });
for visible_entities in cubemap_visible_entities.iter_mut() {
visible_entities.entities.push(entity)
}
}
}
} else {
computed_visibility.is_visible = true;
for visible_entities in cubemap_visible_entities.iter_mut() {
visible_entities.entities.push(VisibleEntity { entity })
}
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
// to prevent holding unneeded memory
}
}
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
// to prevent holding unneeded memory
}
}

View file

@ -1,7 +1,7 @@
use crate::{
AmbientLight, CubemapVisibleEntities, DirectionalLight, DirectionalLightShadowMap, DrawMesh,
MeshPipeline, NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup,
SHADOW_SHADER_HANDLE,
AmbientLight, Clusters, CubemapVisibleEntities, DirectionalLight, DirectionalLightShadowMap,
DrawMesh, MeshPipeline, NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup,
VisiblePointLights, SHADOW_SHADER_HANDLE,
};
use bevy_asset::Handle;
use bevy_core::FloatOrd;
@ -10,9 +10,9 @@ use bevy_ecs::{
prelude::*,
system::{lifetimeless::*, SystemParamItem},
};
use bevy_math::{const_vec3, Mat4, Vec3, Vec4};
use bevy_math::{const_vec3, Mat4, UVec3, UVec4, Vec3, Vec4, Vec4Swizzles};
use bevy_render2::{
camera::CameraProjection,
camera::{Camera, CameraProjection},
color::Color,
mesh::Mesh,
render_asset::RenderAssets,
@ -25,17 +25,18 @@ use bevy_render2::{
render_resource::*,
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::*,
view::{
ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities, VisibleEntity,
},
view::{ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities},
};
use bevy_transform::components::GlobalTransform;
use bevy_utils::{tracing::warn, HashMap};
use crevice::std140::AsStd140;
use std::num::NonZeroU32;
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum RenderLightSystems {
ExtractClusters,
ExtractLights,
PrepareClusters,
PrepareLights,
QueueShadows,
}
@ -69,6 +70,8 @@ pub struct ExtractedDirectionalLight {
shadows_enabled: bool,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
near: f32,
far: f32,
}
pub type ExtractedDirectionalLightShadowMap = DirectionalLightShadowMap;
@ -76,18 +79,20 @@ pub type ExtractedDirectionalLightShadowMap = DirectionalLightShadowMap;
#[repr(C)]
#[derive(Copy, Clone, AsStd140, Default, Debug)]
pub struct GpuPointLight {
projection: Mat4,
color: Vec4,
position: Vec3,
inverse_square_range: f32,
radius: f32,
near: f32,
far: f32,
// The lower-right 2x2 values of the projection matrix 22 23 32 33
projection_lr: Vec4,
color_inverse_square_range: Vec4,
position_radius: Vec4,
flags: u32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
}
#[derive(AsStd140)]
pub struct GpuPointLights {
data: [GpuPointLight; MAX_POINT_LIGHTS],
}
// NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.frag!
bitflags::bitflags! {
#[repr(transparent)]
@ -123,17 +128,23 @@ bitflags::bitflags! {
#[derive(Copy, Clone, Debug, AsStd140)]
pub struct GpuLights {
// TODO: this comes first to work around a WGSL alignment issue. We need to solve this issue before releasing the renderer rework
point_lights: [GpuPointLight; MAX_POINT_LIGHTS],
directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS],
ambient_color: Vec4,
n_point_lights: u32,
cluster_dimensions: UVec4,
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
// z is cluster_dimensions.z / log(far / near)
// w is cluster_dimensions.z * log(near) / log(far / near)
cluster_factors: Vec4,
n_directional_lights: u32,
}
// NOTE: this must be kept in sync with the same constants in pbr.frag
pub const MAX_POINT_LIGHTS: usize = 10;
pub const MAX_POINT_LIGHTS: usize = 256;
// FIXME: How should we handle shadows for clustered forward? Limiting to maximum 10
// point light shadow maps for now
pub const MAX_POINT_LIGHT_SHADOW_MAPS: usize = 10;
pub const MAX_DIRECTIONAL_LIGHTS: usize = 1;
pub const POINT_SHADOW_LAYERS: u32 = (6 * MAX_POINT_LIGHTS) as u32;
pub const POINT_SHADOW_LAYERS: u32 = (6 * MAX_POINT_LIGHT_SHADOW_MAPS) as u32;
pub const DIRECTIONAL_SHADOW_LAYERS: u32 = MAX_DIRECTIONAL_LIGHTS as u32;
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
@ -308,17 +319,38 @@ impl SpecializedPipeline for ShadowPipeline {
}
}
#[derive(Component)]
pub struct ExtractedClusterConfig {
/// Number of clusters in x / y / z in the view frustum
axis_slices: UVec3,
}
#[derive(Component)]
pub struct ExtractedClustersPointLights {
data: Vec<VisiblePointLights>,
}
pub fn extract_clusters(mut commands: Commands, views: Query<(Entity, &Clusters), With<Camera>>) {
for (entity, clusters) in views.iter() {
commands.get_or_spawn(entity).insert_bundle((
ExtractedClustersPointLights {
data: clusters.lights.clone(),
},
ExtractedClusterConfig {
axis_slices: clusters.axis_slices,
},
));
}
}
pub fn extract_lights(
mut commands: Commands,
ambient_light: Res<AmbientLight>,
point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
mut point_lights: Query<(
Entity,
&PointLight,
&mut CubemapVisibleEntities,
&GlobalTransform,
)>,
global_point_lights: Res<VisiblePointLights>,
// visible_point_lights: Query<&VisiblePointLights>,
mut point_lights: Query<(&PointLight, &mut CubemapVisibleEntities, &GlobalTransform)>,
mut directional_lights: Query<(
Entity,
&DirectionalLight,
@ -342,28 +374,34 @@ pub fn extract_lights(
// NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to:
// https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/
let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32;
for (entity, point_light, cubemap_visible_entities, transform) in point_lights.iter_mut() {
let render_cubemap_visible_entities = std::mem::take(cubemap_visible_entities.into_inner());
commands.get_or_spawn(entity).insert_bundle((
ExtractedPointLight {
color: point_light.color,
// NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
// for details.
intensity: point_light.intensity / (4.0 * std::f32::consts::PI),
range: point_light.range,
radius: point_light.radius,
transform: *transform,
shadows_enabled: point_light.shadows_enabled,
shadow_depth_bias: point_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: point_light.shadow_normal_bias
* point_light_texel_size
* std::f32::consts::SQRT_2,
},
render_cubemap_visible_entities,
));
for entity in global_point_lights.iter().copied() {
if let Ok((point_light, cubemap_visible_entities, transform)) = point_lights.get_mut(entity)
{
let render_cubemap_visible_entities =
std::mem::take(cubemap_visible_entities.into_inner());
commands.get_or_spawn(entity).insert_bundle((
ExtractedPointLight {
color: point_light.color,
// NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian
// for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower
// for details.
intensity: point_light.intensity / (4.0 * std::f32::consts::PI),
range: point_light.range,
radius: point_light.radius,
transform: *transform,
shadows_enabled: point_light.shadows_enabled,
shadow_depth_bias: point_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: point_light.shadow_normal_bias
* point_light_texel_size
* std::f32::consts::SQRT_2,
},
render_cubemap_visible_entities,
));
}
}
for (entity, directional_light, visible_entities, transform) in directional_lights.iter_mut() {
// Calulate the directional light shadow map texel size using the largest x,y dimension of
// the orthographic projection divided by the shadow map resolution
@ -390,12 +428,16 @@ pub fn extract_lights(
shadow_normal_bias: directional_light.shadow_normal_bias
* directional_light_texel_size
* std::f32::consts::SQRT_2,
near: directional_light.shadow_projection.near,
far: directional_light.shadow_projection.far,
},
render_visible_entities,
));
}
}
pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32;
// Can't do `Vec3::Y * -1.0` because mul isn't const
const NEGATIVE_X: Vec3 = const_vec3!([-1.0, 0.0, 0.0]);
const NEGATIVE_Y: Vec3 = const_vec3!([0.0, -1.0, 0.0]);
@ -476,6 +518,12 @@ pub struct ViewLightsUniformOffset {
pub offset: u32,
}
#[derive(Default)]
pub struct GlobalLightMeta {
pub gpu_point_lights: UniformVec<GpuPointLights>,
pub entity_to_index: HashMap<Entity, usize>,
}
#[derive(Default)]
pub struct LightMeta {
pub view_gpu_lights: DynamicUniformVec<GpuLights>,
@ -499,8 +547,12 @@ pub fn prepare_lights(
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut global_light_meta: ResMut<GlobalLightMeta>,
mut light_meta: ResMut<LightMeta>,
views: Query<Entity, With<RenderPhase<Transparent3d>>>,
views: Query<
(Entity, &ExtractedView, &ExtractedClusterConfig),
With<RenderPhase<Transparent3d>>,
>,
ambient_light: Res<ExtractedAmbientLight>,
point_light_shadow_map: Res<ExtractedPointLightShadowMap>,
directional_light_shadow_map: Res<ExtractedDirectionalLightShadowMap>,
@ -511,14 +563,53 @@ pub fn prepare_lights(
// Pre-calculate for PointLights
let cube_face_projection =
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1);
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z);
let cube_face_rotations = CUBE_MAP_FACES
.iter()
.map(|CubeMapFace { target, up }| GlobalTransform::identity().looking_at(*target, *up))
.collect::<Vec<_>>();
global_light_meta.gpu_point_lights.clear();
global_light_meta.entity_to_index.clear();
let n_point_lights = point_lights.iter().count();
if global_light_meta.entity_to_index.capacity() < n_point_lights {
global_light_meta.entity_to_index.reserve(n_point_lights);
}
let mut gpu_point_lights = [GpuPointLight::default(); MAX_POINT_LIGHTS];
for (index, (entity, light)) in point_lights.iter().enumerate() {
let mut flags = PointLightFlags::NONE;
if light.shadows_enabled {
flags |= PointLightFlags::SHADOWS_ENABLED;
}
gpu_point_lights[index] = GpuPointLight {
projection_lr: Vec4::new(
cube_face_projection.z_axis.z,
cube_face_projection.z_axis.w,
cube_face_projection.w_axis.z,
cube_face_projection.w_axis.w,
),
// premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3]
color_inverse_square_range: (Vec4::from_slice(&light.color.as_linear_rgba_f32())
* light.intensity)
.xyz()
.extend(1.0 / (light.range * light.range)),
position_radius: light.transform.translation.extend(light.radius),
flags: flags.bits,
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
};
global_light_meta.entity_to_index.insert(entity, index);
}
global_light_meta.gpu_point_lights.push(GpuPointLights {
data: gpu_point_lights,
});
global_light_meta
.gpu_point_lights
.write_buffer(&render_device, &render_queue);
// set up light data for each view
for entity in views.iter() {
for (entity, extracted_view, clusters) in views.iter() {
let point_light_depth_texture = texture_cache.get(
&render_device,
TextureDescriptor {
@ -553,23 +644,37 @@ pub fn prepare_lights(
);
let mut view_lights = Vec::new();
let z_times_ln_far_over_near =
clusters.axis_slices.z as f32 / (extracted_view.far / extracted_view.near).ln();
let mut gpu_lights = GpuLights {
directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS],
ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32())
* ambient_light.brightness,
n_point_lights: point_lights.iter().len() as u32,
cluster_factors: Vec4::new(
clusters.axis_slices.x as f32 / extracted_view.width as f32,
clusters.axis_slices.y as f32 / extracted_view.height as f32,
z_times_ln_far_over_near,
extracted_view.near.ln() * z_times_ln_far_over_near,
),
cluster_dimensions: clusters.axis_slices.extend(0),
n_directional_lights: directional_lights.iter().len() as u32,
point_lights: [GpuPointLight::default(); MAX_POINT_LIGHTS],
directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS],
};
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
for (light_index, (light_entity, light)) in point_lights.iter().enumerate() {
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
// and ignore rotation because we want the shadow map projections to align with the axes
let view_translation = GlobalTransform::from_translation(light.transform.translation);
let mut point_light_count = 0;
for (light_entity, light) in point_lights.iter() {
if point_light_count < MAX_POINT_LIGHT_SHADOW_MAPS && light.shadows_enabled {
point_light_count += 1;
let light_index = *global_light_meta
.entity_to_index
.get(&light_entity)
.unwrap();
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
// and ignore rotation because we want the shadow map projections to align with the axes
let view_translation =
GlobalTransform::from_translation(light.transform.translation);
if light.shadows_enabled {
for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() {
let depth_texture_view =
point_light_depth_texture
@ -601,6 +706,8 @@ pub fn prepare_lights(
height: point_light_shadow_map.size as u32,
transform: view_translation * *view_rotation,
projection: cube_face_projection,
near: POINT_LIGHT_NEAR_Z,
far: light.range,
},
RenderPhase::<Shadow>::default(),
LightEntity::Point {
@ -612,26 +719,6 @@ pub fn prepare_lights(
view_lights.push(view_light_entity);
}
}
let mut flags = PointLightFlags::NONE;
if light.shadows_enabled {
flags |= PointLightFlags::SHADOWS_ENABLED;
}
gpu_lights.point_lights[light_index] = GpuPointLight {
projection: cube_face_projection,
// premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3]
color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * light.intensity,
radius: light.radius,
position: light.transform.translation,
inverse_square_range: 1.0 / (light.range * light.range),
near: 0.1,
far: light.range,
flags: flags.bits,
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
};
}
for (i, (light_entity, light)) in directional_lights
@ -656,7 +743,7 @@ pub fn prepare_lights(
let intensity = light.illuminance * exposure;
// NOTE: A directional light seems to have to have an eye position on the line along the direction of the light
// through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this.
// through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this.
let view = Mat4::look_at_rh(Vec3::ZERO, light.direction, Vec3::Y);
// NOTE: This orthographic projection defines the volume within which shadows from a directional light can be cast
let projection = light.projection;
@ -705,6 +792,8 @@ pub fn prepare_lights(
height: directional_light_shadow_map.size as u32,
transform: GlobalTransform::from_matrix(view.inverse()),
projection,
near: light.near,
far: light.far,
},
RenderPhase::<Shadow>::default(),
LightEntity::Directional { light_entity },
@ -760,6 +849,142 @@ pub fn prepare_lights(
.write_buffer(&render_device, &render_queue);
}
const CLUSTER_OFFSET_MASK: u32 = (1 << 24) - 1;
const CLUSTER_COUNT_SIZE: u32 = 8;
const CLUSTER_COUNT_MASK: u32 = (1 << 8) - 1;
const POINT_LIGHT_INDEX_MASK: u32 = (1 << 8) - 1;
// NOTE: With uniform buffer max binding size as 16384 bytes
// that means we can fit say 128 point lights in one uniform
// buffer, which means the count can be at most 128 so it
// needs 7 bits, use 8 for convenience.
// The array of indices can also use u8 and that means the
// offset in to the array of indices needs to be able to address
// 16384 values. lod2(16384) = 21 bits.
// This means we can pack the offset into the upper 24 bits of a u32
// and the count into the lower 8 bits.
// FIXME: Probably there are endianness concerns here????!!!!!
fn pack_offset_and_count(offset: usize, count: usize) -> u32 {
((offset as u32 & CLUSTER_OFFSET_MASK) << CLUSTER_COUNT_SIZE)
| (count as u32 & CLUSTER_COUNT_MASK)
}
#[derive(Component, Default)]
pub struct ViewClusterBindings {
n_indices: usize,
// NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment
pub cluster_light_index_lists: UniformVec<[UVec4; Self::MAX_UNIFORM_ITEMS]>,
n_offsets: usize,
// NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment
pub cluster_offsets_and_counts: UniformVec<[UVec4; Self::MAX_UNIFORM_ITEMS]>,
}
impl ViewClusterBindings {
pub const MAX_OFFSETS: usize = 16384 / 4;
const MAX_UNIFORM_ITEMS: usize = Self::MAX_OFFSETS / 4;
const MAX_INDICES: usize = 16384;
pub fn reserve_and_clear(&mut self) {
self.cluster_light_index_lists.clear();
self.cluster_light_index_lists
.push([UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]);
self.cluster_offsets_and_counts.clear();
self.cluster_offsets_and_counts
.push([UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]);
}
pub fn push_offset_and_count(&mut self, offset: usize, count: usize) {
let array_index = self.n_offsets >> 2; // >> 2 is equivalent to / 4
if array_index >= Self::MAX_UNIFORM_ITEMS {
warn!("cluster offset and count out of bounds!");
return;
}
let component = self.n_offsets & ((1 << 2) - 1);
let packed = pack_offset_and_count(offset, count);
self.cluster_offsets_and_counts.get_mut(0)[array_index][component] = packed;
self.n_offsets += 1;
}
pub fn n_indices(&self) -> usize {
self.n_indices
}
pub fn push_index(&mut self, index: usize) {
let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16
let component = (self.n_indices >> 2) & ((1 << 2) - 1);
let sub_index = self.n_indices & ((1 << 2) - 1);
let index = index as u32 & POINT_LIGHT_INDEX_MASK;
self.cluster_light_index_lists.get_mut(0)[array_index][component] |=
index << (8 * sub_index);
self.n_indices += 1;
}
}
pub fn prepare_clusters(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
global_light_meta: Res<GlobalLightMeta>,
views: Query<
(
Entity,
&ExtractedClusterConfig,
&ExtractedClustersPointLights,
),
With<RenderPhase<Transparent3d>>,
>,
) {
for (entity, cluster_config, extracted_clusters) in views.iter() {
let mut view_clusters_bindings = ViewClusterBindings::default();
view_clusters_bindings.reserve_and_clear();
let mut indices_full = false;
let mut cluster_index = 0;
for _y in 0..cluster_config.axis_slices.y {
for _x in 0..cluster_config.axis_slices.x {
for _z in 0..cluster_config.axis_slices.z {
let offset = view_clusters_bindings.n_indices();
let cluster_lights = &extracted_clusters.data[cluster_index];
let count = cluster_lights.len();
view_clusters_bindings.push_offset_and_count(offset, count);
if !indices_full {
for entity in cluster_lights.iter() {
if let Some(light_index) = global_light_meta.entity_to_index.get(entity)
{
if view_clusters_bindings.n_indices()
>= ViewClusterBindings::MAX_INDICES
{
warn!("Cluster light index lists is full! The PointLights in the view are affecting too many clusters.");
indices_full = true;
break;
}
view_clusters_bindings.push_index(*light_index);
}
}
}
cluster_index += 1;
}
}
}
view_clusters_bindings
.cluster_light_index_lists
.write_buffer(&render_device, &render_queue);
view_clusters_bindings
.cluster_offsets_and_counts
.write_buffer(&render_device, &render_queue);
commands.get_or_spawn(entity).insert(view_clusters_bindings);
}
}
pub fn queue_shadow_view_bind_group(
render_device: Res<RenderDevice>,
shadow_pipeline: Res<ShadowPipeline>,
@ -813,10 +1038,10 @@ pub fn queue_shadows(
.get(*face_index),
};
// NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued
for VisibleEntity { entity, .. } in visible_entities.iter() {
// so no meshes will be queued
for entity in visible_entities.iter().copied() {
let mut key = ShadowPipelineKey::empty();
if let Ok(mesh_handle) = casting_meshes.get(*entity) {
if let Ok(mesh_handle) = casting_meshes.get(entity) {
if let Some(mesh) = render_meshes.get(mesh_handle) {
if mesh.has_tangents {
key |= ShadowPipelineKey::VERTEX_TANGENTS;
@ -828,7 +1053,7 @@ pub fn queue_shadows(
shadow_phase.add(Shadow {
draw_function: draw_shadow_mesh,
pipeline: pipeline_id,
entity: *entity,
entity,
distance: 0.0, // TODO: sort back-to-front
});
}

View file

@ -1,6 +1,6 @@
use crate::{
GpuLights, LightMeta, NotShadowCaster, NotShadowReceiver, ShadowPipeline,
ViewLightsUniformOffset, ViewShadowBindings,
GlobalLightMeta, GpuLights, LightMeta, NotShadowCaster, NotShadowReceiver, ShadowPipeline,
ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings,
};
use bevy_app::Plugin;
use bevy_asset::{Assets, Handle, HandleUntyped};
@ -237,6 +237,47 @@ impl FromWorld for MeshPipeline {
},
count: None,
},
// PointLights
BindGroupLayoutEntry {
binding: 6,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
// NOTE: Static size for uniform buffers. GpuPointLight has a padded
// size of 128 bytes, so 16384 / 128 = 128 point lights max
min_binding_size: BufferSize::new(16384),
},
count: None,
},
// ClusteredLightIndexLists
BindGroupLayoutEntry {
binding: 7,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
// NOTE: With 128 point lights max, indices need 7 bits. Use u8 for
// convenience.
min_binding_size: BufferSize::new(16384),
},
count: None,
},
// ClusterOffsetsAndCounts
BindGroupLayoutEntry {
binding: 8,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
// NOTE: The offset needs to address 16384 indices, which needs 21 bits.
// The count can be at most all 128 lights so 7 bits.
// Pack the offset into the upper 24 bits and the count into the
// lower 8 bits for convenience.
min_binding_size: BufferSize::new(16384),
},
count: None,
},
],
label: Some("mesh_view_layout"),
});
@ -514,20 +555,23 @@ pub struct MeshViewBindGroup {
pub value: BindGroup,
}
#[allow(clippy::too_many_arguments)]
pub fn queue_mesh_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
mesh_pipeline: Res<MeshPipeline>,
shadow_pipeline: Res<ShadowPipeline>,
light_meta: Res<LightMeta>,
global_light_meta: Res<GlobalLightMeta>,
view_uniforms: Res<ViewUniforms>,
mut views: Query<(Entity, &ViewShadowBindings)>,
mut views: Query<(Entity, &ViewShadowBindings, &ViewClusterBindings)>,
) {
if let (Some(view_binding), Some(light_binding)) = (
if let (Some(view_binding), Some(light_binding), Some(point_light_binding)) = (
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
global_light_meta.gpu_point_lights.binding(),
) {
for (entity, view_shadow_bindings) in views.iter_mut() {
for (entity, view_shadow_bindings, view_cluster_bindings) in views.iter_mut() {
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
BindGroupEntry {
@ -560,6 +604,24 @@ pub fn queue_mesh_view_bind_groups(
&shadow_pipeline.directional_light_sampler,
),
},
BindGroupEntry {
binding: 6,
resource: point_light_binding.clone(),
},
BindGroupEntry {
binding: 7,
resource: view_cluster_bindings
.cluster_light_index_lists
.binding()
.unwrap(),
},
BindGroupEntry {
binding: 8,
resource: view_cluster_bindings
.cluster_offsets_and_counts
.binding()
.unwrap(),
},
],
label: Some("mesh_view_bind_group"),
layout: &mesh_pipeline.view_layout,

View file

@ -1,18 +1,20 @@
[[block]]
struct View {
view_proj: mat4x4<f32>;
inverse_view: mat4x4<f32>;
projection: mat4x4<f32>;
world_position: vec3<f32>;
near: f32;
far: f32;
width: f32;
height: f32;
};
struct PointLight {
projection: mat4x4<f32>;
color: vec4<f32>;
position: vec3<f32>;
inverse_square_range: f32;
radius: f32;
near: f32;
far: f32;
// NOTE: [2][2] [2][3] [3][2] [3][3]
projection_lr: vec4<f32>;
color_inverse_square_range: vec4<f32>;
position_radius: vec4<f32>;
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
shadow_depth_bias: f32;
@ -36,14 +38,35 @@ let DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
[[block]]
struct Lights {
// NOTE: this array size must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs
// TODO: this can be removed if we move to storage buffers for light arrays
point_lights: array<PointLight, 10>;
directional_lights: array<DirectionalLight, 1>;
directional_lights: array<DirectionalLight, 1u>;
ambient_color: vec4<f32>;
n_point_lights: u32;
// x/y/z dimensions
cluster_dimensions: vec4<u32>;
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
// z is cluster_dimensions.z / log(far / near)
// w is cluster_dimensions.z * log(near) / log(far / near)
cluster_factors: vec4<f32>;
n_directional_lights: u32;
};
[[block]]
struct PointLights {
data: array<PointLight, 256u>;
};
[[block]]
struct ClusterLightIndexLists {
// each u32 contains 4 u8 indices into the PointLights array
data: array<vec4<u32>, 1024u>;
};
[[block]]
struct ClusterOffsetsAndCounts {
// each u32 contains a 24-bit index into ClusterLightIndexLists in the high 24 bits
// and an 8-bit count of the number of lights in the low 8 bits
data: array<vec4<u32>, 1024u>;
};
[[group(0), binding(0)]]
var<uniform> view: View;
[[group(0), binding(1)]]
@ -56,3 +79,9 @@ var point_shadow_textures_sampler: sampler_comparison;
var directional_shadow_textures: texture_depth_2d_array;
[[group(0), binding(5)]]
var directional_shadow_textures_sampler: sampler_comparison;
[[group(0), binding(6)]]
var<uniform> point_lights: PointLights;
[[group(0), binding(7)]]
var<uniform> cluster_light_index_lists: ClusterLightIndexLists;
[[group(0), binding(8)]]
var<uniform> cluster_offsets_and_counts: ClusterOffsetsAndCounts;

View file

@ -249,7 +249,7 @@ pub fn queue_meshes(
for visible_entity in &visible_entities.entities {
if let Ok((material_handle, mesh_handle, mesh_uniform)) =
standard_material_meshes.get(visible_entity.entity)
standard_material_meshes.get(*visible_entity)
{
if let Some(material) = render_materials.get(material_handle) {
let mut pbr_key = PbrPipelineKey {
@ -275,37 +275,37 @@ pub fn queue_meshes(
match material.alpha_mode {
AlphaMode::Opaque => {
opaque_phase.add(Opaque3d {
entity: visible_entity.entity,
entity: *visible_entity,
draw_function: draw_opaque_pbr,
pipeline: pipeline_id,
// NOTE: Front-to-back ordering for opaque with ascending sort means near should have the
// lowest sort key and getting further away should increase. As we have
// -z in front of the camera, values in view space decrease away from the
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
// lowest sort key and getting further away should increase. As we have
// -z in front of the camera, values in view space decrease away from the
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
distance: -mesh_z,
});
}
AlphaMode::Mask(_) => {
alpha_mask_phase.add(AlphaMask3d {
entity: visible_entity.entity,
entity: *visible_entity,
draw_function: draw_alpha_mask_pbr,
pipeline: pipeline_id,
// NOTE: Front-to-back ordering for alpha mask with ascending sort means near should have the
// lowest sort key and getting further away should increase. As we have
// -z in front of the camera, values in view space decrease away from the
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
// lowest sort key and getting further away should increase. As we have
// -z in front of the camera, values in view space decrease away from the
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
distance: -mesh_z,
});
}
AlphaMode::Blend => {
transparent_phase.add(Transparent3d {
entity: visible_entity.entity,
entity: *visible_entity,
draw_function: draw_transparent_pbr,
pipeline: pipeline_id,
// NOTE: Back-to-front ordering for transparent with ascending sort means far should have the
// lowest sort key and getting closer should increase. As we have
// -z in front of the camera, the largest distance is -far with values increasing toward the
// camera. As such we can just use mesh_z as the distance
// lowest sort key and getting closer should increase. As we have
// -z in front of the camera, the largest distance is -far with values increasing toward the
// camera. As such we can just use mesh_z as the distance
distance: mesh_z,
});
}

View file

@ -239,23 +239,57 @@ fn reinhard_extended_luminance(color: vec3<f32>, max_white_l: f32) -> vec3<f32>
return change_luminance(color, l_new);
}
fn view_z_to_z_slice(view_z: f32) -> u32 {
// NOTE: had to use -view_z to make it positive else log(negative) is nan
return u32(floor(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w));
}
fn fragment_cluster_index(frag_coord: vec2<f32>, view_z: f32) -> u32 {
let xy = vec2<u32>(floor(frag_coord * lights.cluster_factors.xy));
let z_slice = view_z_to_z_slice(view_z);
return (xy.y * lights.cluster_dimensions.x + xy.x) * lights.cluster_dimensions.z + z_slice;
}
struct ClusterOffsetAndCount {
offset: u32;
count: u32;
};
fn unpack_offset_and_count(cluster_index: u32) -> ClusterOffsetAndCount {
let offset_and_count = cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)];
var output: ClusterOffsetAndCount;
// The offset is stored in the upper 24 bits
output.offset = (offset_and_count >> 8u) & ((1u << 24u) - 1u);
// The count is stored in the lower 8 bits
output.count = offset_and_count & ((1u << 8u) - 1u);
return output;
}
fn get_light_id(index: u32) -> u32 {
// The index is correct but in cluster_light_index_lists we pack 4 u8s into a u32
// This means the index into cluster_light_index_lists is index / 4
let indices = cluster_light_index_lists.data[index >> 4u][(index >> 2u) & ((1u << 2u) - 1u)];
// And index % 4 gives the sub-index of the u8 within the u32 so we shift by 8 * sub-index
return (indices >> (8u * (index & ((1u << 2u) - 1u)))) & ((1u << 8u) - 1u);
}
fn point_light(
world_position: vec3<f32>, light: PointLight, roughness: f32, NdotV: f32, N: vec3<f32>, V: vec3<f32>,
R: vec3<f32>, F0: vec3<f32>, diffuseColor: vec3<f32>
) -> vec3<f32> {
let light_to_frag = light.position.xyz - world_position.xyz;
let light_to_frag = light.position_radius.xyz - world_position.xyz;
let distance_square = dot(light_to_frag, light_to_frag);
let rangeAttenuation =
getDistanceAttenuation(distance_square, light.inverse_square_range);
getDistanceAttenuation(distance_square, light.color_inverse_square_range.w);
// Specular.
// Representative Point Area Lights.
// see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16
let a = roughness;
let centerToRay = dot(light_to_frag, R) * R - light_to_frag;
let closestPoint = light_to_frag + centerToRay * saturate(light.radius * inverseSqrt(dot(centerToRay, centerToRay)));
let closestPoint = light_to_frag + centerToRay * saturate(light.position_radius.w * inverseSqrt(dot(centerToRay, centerToRay)));
let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint));
let normalizationFactor = a / saturate(a + (light.radius * 0.5 * LspecLengthInverse));
let normalizationFactor = a / saturate(a + (light.position_radius.w * 0.5 * LspecLengthInverse));
let specularIntensity = normalizationFactor * normalizationFactor;
var L: vec3<f32> = closestPoint * LspecLengthInverse; // normalize() equivalent?
@ -291,7 +325,7 @@ fn point_light(
// TODO compensate for energy loss https://google.github.io/filament/Filament.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance
return ((diffuse + specular_light) * light.color.rgb) * (rangeAttenuation * NoL);
return ((diffuse + specular_light) * light.color_inverse_square_range.rgb) * (rangeAttenuation * NoL);
}
fn directional_light(light: DirectionalLight, roughness: f32, NdotV: f32, normal: vec3<f32>, view: vec3<f32>, R: vec3<f32>, F0: vec3<f32>, diffuseColor: vec3<f32>) -> vec3<f32> {
@ -309,12 +343,12 @@ fn directional_light(light: DirectionalLight, roughness: f32, NdotV: f32, normal
return (specular_light + diffuse) * light.color.rgb * NoL;
}
fn fetch_point_shadow(light_id: i32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
let light = lights.point_lights[light_id];
fn fetch_point_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
let light = point_lights.data[light_id];
// because the shadow maps align with the axes and the frustum planes are at 45 degrees
// we can get the worldspace depth by taking the largest absolute axis
let surface_to_light = light.position.xyz - frag_position.xyz;
let surface_to_light = light.position_radius.xyz - frag_position.xyz;
let surface_to_light_abs = abs(surface_to_light);
let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z));
@ -326,38 +360,27 @@ fn fetch_point_shadow(light_id: i32, frag_position: vec4<f32>, surface_normal: v
let offset_position = frag_position.xyz + normal_offset + depth_offset;
// similar largest-absolute-axis trick as above, but now with the offset fragment position
let frag_ls = light.position.xyz - offset_position.xyz;
let frag_ls = light.position_radius.xyz - offset_position.xyz;
let abs_position_ls = abs(frag_ls);
let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z));
// NOTE: These simplifications come from multiplying:
// projection * vec4(0, 0, -major_axis_magnitude, 1.0)
// and keeping only the terms that have any impact on the depth.
// projection * vec4(0, 0, -major_axis_magnitude, 1.0)
// and keeping only the terms that have any impact on the depth.
// Projection-agnostic approach:
let z = -major_axis_magnitude * light.projection[2][2] + light.projection[3][2];
let w = -major_axis_magnitude * light.projection[2][3] + light.projection[3][3];
// For perspective_rh:
// let proj_r = light.far / (light.near - light.far);
// let z = -major_axis_magnitude * proj_r + light.near * proj_r;
// let w = major_axis_magnitude;
// For perspective_infinite_reverse_rh:
// let z = light.near;
// let w = major_axis_magnitude;
let depth = z / w;
let zw = -major_axis_magnitude * light.projection_lr.xy + light.projection_lr.zw;
let depth = zw.x / zw.y;
// do the lookup, using HW PCF and comparison
// NOTE: Due to the non-uniform control flow above, we must use the Level variant of
// textureSampleCompare to avoid undefined behaviour due to some of the fragments in
// a quad (2x2 fragments) being processed not being sampled, and this messing with
// mip-mapping functionality. The shadow maps have no mipmaps so Level just samples
// from LOD 0.
// textureSampleCompare to avoid undefined behaviour due to some of the fragments in
// a quad (2x2 fragments) being processed not being sampled, and this messing with
// mip-mapping functionality. The shadow maps have no mipmaps so Level just samples
// from LOD 0.
return textureSampleCompareLevel(point_shadow_textures, point_shadow_textures_sampler, frag_ls, i32(light_id), depth);
}
fn fetch_directional_shadow(light_id: i32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
let light = lights.directional_lights[light_id];
// The normal bias is scaled to the texel size.
@ -384,12 +407,25 @@ fn fetch_directional_shadow(light_id: i32, frag_position: vec4<f32>, surface_nor
let depth = offset_position_ndc.z;
// do the lookup, using HW PCF and comparison
// NOTE: Due to non-uniform control flow above, we must use the level variant of the texture
// sampler to avoid use of implicit derivatives causing possible undefined behavior.
// sampler to avoid use of implicit derivatives causing possible undefined behavior.
return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler, light_local, i32(light_id), depth);
}
fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3<f32> {
let rgb = clamp(
abs(
((hue * 6.0 + vec3<f32>(0.0, 4.0, 2.0)) % 6.0) - 3.0
) - 1.0,
vec3<f32>(0.0),
vec3<f32>(1.0)
);
return value * mix( vec3<f32>(1.0), rgb, vec3<f32>(saturation));
}
struct FragmentInput {
[[builtin(front_facing)]] is_front: bool;
[[builtin(position)]] frag_coord: vec4<f32>;
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
@ -495,28 +531,34 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
// accumulate color
var light_accum: vec3<f32> = vec3<f32>(0.0);
let n_point_lights = i32(lights.n_point_lights);
let n_directional_lights = i32(lights.n_directional_lights);
for (var i: i32 = 0; i < n_point_lights; i = i + 1) {
let light = lights.point_lights[i];
var shadow: f32;
let view_z = dot(vec4<f32>(
view.inverse_view[0].z,
view.inverse_view[1].z,
view.inverse_view[2].z,
view.inverse_view[3].z
), in.world_position);
let cluster_index = fragment_cluster_index(in.frag_coord.xy, view_z);
let offset_and_count = unpack_offset_and_count(cluster_index);
for (var i: u32 = offset_and_count.offset; i < offset_and_count.offset + offset_and_count.count; i = i + 1u) {
let light_id = get_light_id(i);
let light = point_lights.data[light_id];
var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_point_shadow(i, in.world_position, in.world_normal);
} else {
shadow = 1.0;
shadow = fetch_point_shadow(light_id, in.world_position, in.world_normal);
}
let light_contrib = point_light(in.world_position.xyz, light, roughness, NdotV, N, V, R, F0, diffuse_color);
light_accum = light_accum + light_contrib * shadow;
}
for (var i: i32 = 0; i < n_directional_lights; i = i + 1) {
let n_directional_lights = lights.n_directional_lights;
for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) {
let light = lights.directional_lights[i];
var shadow: f32;
var shadow: f32 = 1.0;
if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
&& (light.flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) {
shadow = fetch_directional_shadow(i, in.world_position, in.world_normal);
} else {
shadow = 1.0;
}
let light_contrib = directional_light(light, roughness, NdotV, N, V, R, F0, diffuse_color);
light_accum = light_accum + light_contrib * shadow;
@ -531,6 +573,32 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
emissive.rgb * output_color.a,
output_color.a);
#ifdef CLUSTERED_FORWARD_DEBUG
// Cluster allocation debug (using 'over' alpha blending)
let cluster_debug_mode = 1;
let cluster_overlay_alpha = 1.0;
if (cluster_debug_mode == 0) {
// NOTE: This debug mode visualises the z-slices
var z_slice: u32 = view_z_to_z_slice(view_z);
// A hack to make the colors alternate a bit more
if ((z_slice & 1u) == 1u) {
z_slice = z_slice + lights.cluster_dimensions.z / 2u;
}
let slice_color = hsv2rgb(f32(z_slice) / f32(lights.cluster_dimensions.z + 1u), 1.0, 0.5);
output_color = vec4<f32>(
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color,
output_color.a
);
} elseif (cluster_debug_mode == 1) {
// NOTE: This debug mode visualises the number of lights within the cluster that contains
// the fragment. It shows a sort of lighting complexity measure.
output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r
+ cluster_overlay_alpha * smoothStep(0.0, 16.0, f32(offset_and_count.count));
output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g
+ cluster_overlay_alpha * (1.0 - smoothStep(0.0, 16.0, f32(offset_and_count.count)));
}
#endif
// tone_mapping
output_color = vec4<f32>(reinhard_luminance(output_color.rgb), output_color.a);
// Gamma correction.

View file

@ -42,6 +42,8 @@ impl PerspectiveCameraBundle {
PerspectiveCameraBundle {
camera: Camera {
name: Some(name.to_string()),
near: perspective_projection.near,
far: perspective_projection.far,
..Default::default()
},
perspective_projection,
@ -94,6 +96,8 @@ impl OrthographicCameraBundle {
OrthographicCameraBundle {
camera: Camera {
name: Some(CameraPlugin::CAMERA_2D.to_string()),
near: orthographic_projection.near,
far: orthographic_projection.far,
..Default::default()
},
orthographic_projection,
@ -120,6 +124,8 @@ impl OrthographicCameraBundle {
OrthographicCameraBundle {
camera: Camera {
name: Some(CameraPlugin::CAMERA_3D.to_string()),
near: orthographic_projection.near,
far: orthographic_projection.far,
..Default::default()
},
orthographic_projection,
@ -142,6 +148,8 @@ impl OrthographicCameraBundle {
OrthographicCameraBundle {
camera: Camera {
name: Some(name.to_string()),
near: orthographic_projection.near,
far: orthographic_projection.far,
..Default::default()
},
orthographic_projection,

View file

@ -23,6 +23,8 @@ pub struct Camera {
pub window: WindowId,
#[reflect(ignore)]
pub depth_calculation: DepthCalculation,
pub near: f32,
pub far: f32,
}
#[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)]

View file

@ -88,6 +88,8 @@ fn extract_cameras(
transform: *transform,
width: window.physical_width().max(1),
height: window.physical_height().max(1),
near: camera.near,
far: camera.far,
},
visible_entities.clone(),
));

View file

@ -32,6 +32,23 @@ impl Aabb {
.abs()
.dot(half_extents)
}
pub fn min(&self) -> Vec3 {
self.center - self.half_extents
}
pub fn max(&self) -> Vec3 {
self.center + self.half_extents
}
}
impl From<Sphere> for Aabb {
fn from(sphere: Sphere) -> Self {
Self {
center: sphere.center,
half_extents: Vec3::splat(sphere.radius),
}
}
}
#[derive(Debug, Default)]
@ -41,12 +58,12 @@ pub struct Sphere {
}
impl Sphere {
pub fn intersects_obb(&self, aabb: &Aabb, model_to_world: &Mat4) -> bool {
let aabb_center_world = *model_to_world * aabb.center.extend(1.0);
pub fn intersects_obb(&self, aabb: &Aabb, local_to_world: &Mat4) -> bool {
let aabb_center_world = *local_to_world * aabb.center.extend(1.0);
let axes = [
Vec3A::from(model_to_world.x_axis),
Vec3A::from(model_to_world.y_axis),
Vec3A::from(model_to_world.z_axis),
Vec3A::from(local_to_world.x_axis),
Vec3A::from(local_to_world.y_axis),
Vec3A::from(local_to_world.z_axis),
];
let v = Vec3A::from(aabb_center_world) - Vec3A::from(self.center);
let d = v.length();

View file

@ -63,6 +63,10 @@ impl<T: AsStd140> UniformVec<T> {
index
}
pub fn get_mut(&mut self, index: usize) -> &mut T {
&mut self.values[index]
}
pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) -> bool {
if capacity > self.capacity {
self.capacity = capacity;
@ -96,6 +100,10 @@ impl<T: AsStd140> UniformVec<T> {
pub fn clear(&mut self) {
self.values.clear();
}
pub fn values(&self) -> &[T] {
&self.values
}
}
pub struct DynamicUniformVec<T: AsStd140> {

View file

@ -64,13 +64,20 @@ pub struct ExtractedView {
pub transform: GlobalTransform,
pub width: u32,
pub height: u32,
pub near: f32,
pub far: f32,
}
#[derive(Clone, AsStd140)]
pub struct ViewUniform {
view_proj: Mat4,
inverse_view: Mat4,
projection: Mat4,
world_position: Vec3,
near: f32,
far: f32,
width: f32,
height: f32,
}
#[derive(Default)]
@ -123,11 +130,17 @@ fn prepare_view_uniforms(
view_uniforms.uniforms.clear();
for (entity, camera) in views.iter() {
let projection = camera.projection;
let inverse_view = camera.transform.compute_matrix().inverse();
let view_uniforms = ViewUniformOffset {
offset: view_uniforms.uniforms.push(ViewUniform {
view_proj: projection * camera.transform.compute_matrix().inverse(),
view_proj: projection * inverse_view,
inverse_view,
projection,
world_position: camera.transform.translation,
near: camera.near,
far: camera.far,
width: camera.width as f32,
height: camera.height as f32,
}),
};

View file

@ -40,22 +40,25 @@ impl Default for ComputedVisibility {
}
}
#[derive(Clone, Debug)]
pub struct VisibleEntity {
pub entity: Entity,
}
#[derive(Component, Clone, Default, Debug, Reflect)]
#[derive(Clone, Component, Default, Debug, Reflect)]
#[reflect(Component)]
pub struct VisibleEntities {
#[reflect(ignore)]
pub entities: Vec<VisibleEntity>,
pub entities: Vec<Entity>,
}
impl VisibleEntities {
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &VisibleEntity> {
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
self.entities.iter()
}
pub fn len(&self) -> usize {
self.entities.len()
}
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
@ -178,7 +181,7 @@ pub fn check_visibility(
}
computed_visibility.is_visible = true;
visible_entities.entities.push(VisibleEntity { entity });
visible_entities.entities.push(entity);
}
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize