moonlit/src/game/darkness.rs

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![]
}