264 lines
9.6 KiB
Rust
264 lines
9.6 KiB
Rust
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::<PointLight2D>()
|
|
.register_type::<SpotLight2D>()
|
|
.register_type::<VisibilityBlocker>()
|
|
.register_asset_reflect::<DarknessMaterial>()
|
|
.add_plugins(Material2dPlugin::<DarknessMaterial>::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<LevelEvent>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<DarknessMaterial>>,
|
|
mut images: ResMut<Assets<Image>>,
|
|
level_query: Query<(Entity, &Handle<LdtkLevel>)>,
|
|
levels: Res<Assets<LdtkLevel>>,
|
|
) {
|
|
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<Assets<DarknessMaterial>>,
|
|
mut images: ResMut<Assets<Image>>,
|
|
rapier_context: Res<RapierContext>,
|
|
visibility_blocker_query: Query<&VisibilityBlocker>,
|
|
material_query: Query<&Handle<DarknessMaterial>>,
|
|
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<RapierContext>,
|
|
visibility_blocker_query: &Query<&VisibilityBlocker>,
|
|
light: &impl LightAabb,
|
|
) -> Vec<Vec2> {
|
|
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![]
|
|
}
|