use bevy::{ core::bytes_of, prelude::*, render::{ mesh::Indices, render_resource::{Extent3d, ShaderType, TextureDimension}, }, sprite::{Material2dPlugin, Mesh2dHandle}, }; use bevy_ecs_ldtk::{LdtkLevel, LevelEvent}; use bevy_rapier2d::prelude::{Collider, QueryFilter, RapierContext}; mod material; pub use material::*; // Needs to be the same as in darkness.wgsl pub const MAX_POINT_LIGHTS: usize = 64; pub const MAX_SPOT_LIGHTS: usize = 64; pub const SHADOWMAP_RESOLUTION: usize = 256; pub struct DarknessPlugin; impl Plugin for DarknessPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() .register_type::() .register_asset_reflect::() .add_plugins(Material2dPlugin::::default()) .add_systems(Update, add_to_level) .add_systems(Last, prepare_lights); } } pub trait LightAabb { fn aabb(&self) -> Rect; } #[derive(Component, Reflect, Default, Debug)] #[reflect(Component)] pub struct PointLight2D { pub radius: f32, } impl LightAabb for PointLight2D { fn aabb(&self) -> Rect { Rect::from_center_half_size(Vec2::ZERO, Vec2::splat(self.radius + 2.0)) } } #[derive(Copy, Clone, Debug, Default, Reflect, ShaderType)] pub(crate) struct GpuPointLight2D { pub(crate) position: Vec2, pub(crate) radius: f32, } #[derive(Component, Reflect, Default, Debug)] #[reflect(Component)] pub struct SpotLight2D { pub radius: f32, pub angle: f32, } impl LightAabb for SpotLight2D { fn aabb(&self) -> Rect { Rect::from_center_half_size(Vec2::ZERO, Vec2::splat(self.radius + 2.0)) } } #[derive(Copy, Clone, Debug, Default, Reflect, ShaderType)] pub(crate) struct GpuSpotLight2D { pub(crate) position: Vec2, pub(crate) radius: f32, pub(crate) rotation: f32, pub(crate) angle: f32, padding1: u32, padding2: u32, padding3: u32, } #[derive(Component, Reflect, Default, Debug, Clone, Copy)] #[reflect(Component)] pub struct VisibilityBlocker; fn add_to_level( mut commands: Commands, mut level_events: EventReader, mut meshes: ResMut>, mut materials: ResMut>, mut images: ResMut>, level_query: Query<(Entity, &Handle)>, levels: Res>, ) { for event in level_events.iter() { match event { LevelEvent::Spawned(iid) => { for (entity, ldtk_level) in level_query .iter() .map(|(entity, handle)| { ( entity, levels.get(handle).expect( format!("LDTK level '{}' should have been loaded", iid).as_str(), ), ) }) .filter(|(_, ldtk_level)| ldtk_level.level.iid == *iid) { let size = Vec2::new( ldtk_level.level.px_wid as f32, ldtk_level.level.px_hei as f32, ); let mut plane = Mesh::new(bevy::render::render_resource::PrimitiveTopology::TriangleStrip); // 2 -------- 3 // | | // | | // 0 -------- 1 let indices = vec![0, 1, 2, 3]; let positions = vec![ [0.0, 0.0, 0.0], [size.x, 0.0, 0.0], [0.0, size.y, 0.0], [size.x, size.y, 0.0], ]; let normals = vec![ [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], ]; let uvs = vec![[0.0, 1.0], [1.0, 1.0], [0.0, 0.0], [1.0, 0.0]]; plane.set_indices(Some(Indices::U32(indices))); plane.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); plane.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); plane.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); let width = SHADOWMAP_RESOLUTION; let height = MAX_SPOT_LIGHTS + MAX_POINT_LIGHTS; commands.entity(entity).with_children(|builder| { builder.spawn(( Name::new("Darkness plane"), DarknessMeshBundle { transform: Transform::from_xyz(0.0, 0.0, 100.0), mesh: Mesh2dHandle(meshes.add(plane)), material: materials.add(DarknessMaterial::new( Color::rgba(0.0, 0.0, 0.0, 0.75), Some(images.add(Image::new( Extent3d { width: width as u32, height: height as u32, depth_or_array_layers: 1, }, TextureDimension::D2, vec![0; width * height * 4], bevy::render::render_resource::TextureFormat::R32Float, ))), )), ..default() }, )); }); } } _ => (), } } } fn prepare_lights( mut materials: ResMut>, mut images: ResMut>, rapier_context: Res, visibility_blocker_query: Query<&VisibilityBlocker>, material_query: Query<&Handle>, point_light_query: Query<(&GlobalTransform, &PointLight2D)>, spot_light_query: Query<(&GlobalTransform, &SpotLight2D)>, ) { let point_lights: Vec<(_, _)> = point_light_query.iter().collect(); let spot_lights: Vec<(_, _)> = spot_light_query.iter().collect(); for handle in &material_query { let material = match materials.get_mut(handle) { Some(material) => material, None => continue, }; let shadowmap = match &material.shadowmap_texture { Some(handle) => match images.get_mut(&handle) { Some(image) => image, None => continue, }, None => continue, }; material.point_light_count = point_lights.len() as i32; point_lights .iter() .enumerate() .for_each(|(i, (transform, light))| { get_light_geometry(&rapier_context, &visibility_blocker_query, *light); material.point_lights[i] = GpuPointLight2D { position: transform.translation().truncate(), radius: light.radius, }; for x in 0..SHADOWMAP_RESOLUTION { let offset = (i * SHADOWMAP_RESOLUTION + x) * 4; let distance = x as f32 / SHADOWMAP_RESOLUTION as f32 * light.radius; let distance_bytes = bytes_of(&distance); shadowmap.data[offset + 0] = distance_bytes[0]; shadowmap.data[offset + 1] = distance_bytes[1]; shadowmap.data[offset + 2] = distance_bytes[2]; shadowmap.data[offset + 3] = distance_bytes[3]; } }); material.spot_light_count = spot_lights.len() as i32; spot_lights .iter() .enumerate() .for_each(|(i, (transform, light))| { material.spot_lights[i] = GpuSpotLight2D { position: transform.translation().truncate(), radius: light.radius, rotation: f32::atan2(transform.right().y, transform.right().x), angle: light.angle, padding1: 0, padding2: 0, padding3: 0, }; for x in 0..SHADOWMAP_RESOLUTION { let offset = ((i + MAX_POINT_LIGHTS) * SHADOWMAP_RESOLUTION + x) * 4; let distance = x as f32 / SHADOWMAP_RESOLUTION as f32 * light.radius; let distance_bytes = bytes_of(&distance); shadowmap.data[offset + 0] = distance_bytes[0]; shadowmap.data[offset + 1] = distance_bytes[1]; shadowmap.data[offset + 2] = distance_bytes[2]; shadowmap.data[offset + 3] = distance_bytes[3]; } }); } } fn get_light_geometry( rapier_context: &Res, visibility_blocker_query: &Query<&VisibilityBlocker>, light: &impl LightAabb, ) -> Vec { let rect = light.aabb(); let collider = Collider::cuboid(rect.half_size().x, rect.half_size().y); let mut filter = QueryFilter::new().exclude_sensors(); let predicate = |coll_entity| visibility_blocker_query.get(coll_entity).is_ok(); filter.predicate = Some(&predicate); let mut colls = vec![]; rapier_context.intersections_with_shape(rect.center(), 0.0, &collider, filter, |coll| { colls.push(coll); true }); vec![] }