Compare commits

...

8 Commits

Author SHA1 Message Date
hheik c96625b8eb Added software shadows 2023-12-15 17:18:02 +02:00
hheik 499e6ded41 wip: Software shadows to corners only 2023-12-07 17:03:59 +02:00
hheik b13ee649be wip: Added ShadowMesh component 2023-12-07 02:21:51 +02:00
hheik ca65416c73 Added small optimisation and comments 2023-12-02 17:21:11 +02:00
hheik ddce5075ce Added keyboard movement for debugging 2023-12-02 16:41:53 +02:00
hheik c1894235ba collider point collection 2023-09-03 19:39:14 +03:00
hheik 69c176ed88 wip: added collider point collection 2023-09-02 14:49:44 +03:00
hheik 3fbe81d388 started working on shadows 2023-09-01 19:13:07 +03:00
4 changed files with 292 additions and 37 deletions

View File

@ -2,8 +2,6 @@ use crate::{debug, game_setup};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ecs_ldtk::{LdtkWorldBundle, LevelSelection}; use bevy_ecs_ldtk::{LdtkWorldBundle, LevelSelection};
use self::darkness::SpotLight2D;
pub mod camera; pub mod camera;
pub mod darkness; pub mod darkness;
pub mod ldtk; pub mod ldtk;
@ -26,15 +24,4 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
ldtk_handle: assets.load("levels/world.ldtk"), ldtk_handle: assets.load("levels/world.ldtk"),
..default() ..default()
}); });
commands.spawn((
Name::new("Spot light"),
SpatialBundle::from_transform(Transform::from_xyz(32.0, 31.0, 0.0).with_rotation(
Quat::from_euler(EulerRot::YXZ, 0.0, 0.0, f32::to_radians(-90.0)),
)),
SpotLight2D {
radius: 100.0,
angle: 1.0,
},
));
} }

View File

@ -5,7 +5,7 @@ use bevy::render::camera::ScalingMode;
use bevy::{input::mouse::MouseMotion, transform::TransformSystem}; use bevy::{input::mouse::MouseMotion, transform::TransformSystem};
use bevy_ecs_ldtk::prelude::*; use bevy_ecs_ldtk::prelude::*;
use super::darkness::{PointLight2D, SpotLight2D}; use super::darkness::{PointLight2D, ShadowMesh, SpotLight2D};
pub struct GameCameraPlugin; pub struct GameCameraPlugin;
@ -95,6 +95,7 @@ fn camera_setup(mut commands: Commands) {
ComputedVisibility::default(), ComputedVisibility::default(),
CameraRoomRestraint, CameraRoomRestraint,
PointLight2D { radius: 30.0 }, PointLight2D { radius: 30.0 },
ShadowMesh::default(),
)); ));
} }
@ -118,13 +119,32 @@ fn free_system(
mut free_query: Query<(&mut Transform, &GameCamera, &OrthographicProjection)>, mut free_query: Query<(&mut Transform, &GameCamera, &OrthographicProjection)>,
mut mouse_events: EventReader<MouseMotion>, mut mouse_events: EventReader<MouseMotion>,
mouse_input: Res<Input<MouseButton>>, mouse_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>,
time: Res<Time>,
) { ) {
let raw_mouse_motion: Vec2 = mouse_events.iter().map(|e| e.delta).sum(); let raw_mouse_motion: Vec2 = mouse_events.iter().map(|e| e.delta).sum();
for (mut transform, camera, projection) in free_query.iter_mut() { for (mut transform, camera, projection) in free_query.iter_mut() {
if camera.mode == CameraMode::Free { if camera.mode == CameraMode::Free {
let mouse_motion = raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0); let mut motion = Vec2::ZERO;
if mouse_input.pressed(MouseButton::Middle) { if mouse_input.pressed(MouseButton::Middle) {
input_movement(&mut transform, mouse_motion) motion += raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0);
}
motion.x += match (key_input.pressed(KeyCode::A), key_input.pressed(KeyCode::D)) {
(true, false) => -1.0,
(false, true) => 1.0,
(_, _) => 0.0,
} * projection.scale
* time.delta_seconds()
* 100.0;
motion.y += match (key_input.pressed(KeyCode::S), key_input.pressed(KeyCode::W)) {
(true, false) => -1.0,
(false, true) => 1.0,
(_, _) => 0.0,
} * projection.scale
* time.delta_seconds()
* 100.0;
if motion != Vec2::ZERO {
input_movement(&mut transform, motion)
} }
} }
} }

View File

@ -8,6 +8,10 @@ use bevy::{
sprite::{Material2dPlugin, Mesh2dHandle}, sprite::{Material2dPlugin, Mesh2dHandle},
}; };
use bevy_ecs_ldtk::{LdtkLevel, LevelEvent}; use bevy_ecs_ldtk::{LdtkLevel, LevelEvent};
use bevy_prototype_debug_lines::DebugLines;
use bevy_rapier2d::prelude::{Collider, QueryFilter, RapierContext};
use crate::{debug::DebugMode, util::vec2_intersection};
mod material; mod material;
@ -16,7 +20,7 @@ pub use material::*;
// Needs to be the same as in darkness.wgsl // Needs to be the same as in darkness.wgsl
pub const MAX_POINT_LIGHTS: usize = 64; pub const MAX_POINT_LIGHTS: usize = 64;
pub const MAX_SPOT_LIGHTS: usize = 64; pub const MAX_SPOT_LIGHTS: usize = 64;
pub const SHADOWMAP_RESOLUTION: usize = 256; pub const SHADOWMAP_RESOLUTION: usize = 1024;
pub struct DarknessPlugin; pub struct DarknessPlugin;
@ -24,11 +28,12 @@ impl Plugin for DarknessPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.register_type::<PointLight2D>() app.register_type::<PointLight2D>()
.register_type::<SpotLight2D>() .register_type::<SpotLight2D>()
.register_type::<ShadowMesh>()
.register_type::<VisibilityBlocker>() .register_type::<VisibilityBlocker>()
.register_asset_reflect::<DarknessMaterial>() .register_asset_reflect::<DarknessMaterial>()
.add_plugins(Material2dPlugin::<DarknessMaterial>::default()) .add_plugins(Material2dPlugin::<DarknessMaterial>::default())
.add_systems(Update, add_to_level) .add_systems(Update, add_to_level)
.add_systems(Last, prepare_lights); .add_systems(Last, (prepare_lights, prepare_shadows).chain());
} }
} }
@ -82,6 +87,19 @@ pub(crate) struct GpuSpotLight2D {
#[reflect(Component)] #[reflect(Component)]
pub struct VisibilityBlocker; pub struct VisibilityBlocker;
#[derive(Reflect, Default, Debug, Clone, Copy)]
pub struct ShadowVertex {
pub point: Vec2,
pub angle: f32,
}
#[derive(Component, Reflect, Default, Debug, Clone)]
#[reflect(Component)]
pub struct ShadowMesh {
pub light_index: usize,
pub vertices: Vec<ShadowVertex>,
}
fn add_to_level( fn add_to_level(
mut commands: Commands, mut commands: Commands,
mut level_events: EventReader<LevelEvent>, mut level_events: EventReader<LevelEvent>,
@ -146,9 +164,9 @@ fn add_to_level(
DarknessMeshBundle { DarknessMeshBundle {
transform: Transform::from_xyz(0.0, 0.0, 100.0), transform: Transform::from_xyz(0.0, 0.0, 100.0),
mesh: Mesh2dHandle(meshes.add(plane)), mesh: Mesh2dHandle(meshes.add(plane)),
material: materials.add(DarknessMaterial::new( material: materials.add(DarknessMaterial {
Color::rgba(0.0, 0.0, 0.0, 0.75), color: Color::rgba(0.0, 0.0, 0.0, 0.75),
Some(images.add(Image::new( shadowmap_texture: Some(images.add(Image::new(
Extent3d { Extent3d {
width: width as u32, width: width as u32,
height: height as u32, height: height as u32,
@ -158,7 +176,8 @@ fn add_to_level(
vec![0; width * height * 4], vec![0; width * height * 4],
bevy::render::render_resource::TextureFormat::R32Float, bevy::render::render_resource::TextureFormat::R32Float,
))), ))),
)), ..default()
}),
..default() ..default()
}, },
)); ));
@ -171,14 +190,21 @@ fn add_to_level(
} }
fn prepare_lights( fn prepare_lights(
mut shadow_mesh_query: Query<&mut ShadowMesh>,
mut materials: ResMut<Assets<DarknessMaterial>>, mut materials: ResMut<Assets<DarknessMaterial>>,
mut images: ResMut<Assets<Image>>, mut images: ResMut<Assets<Image>>,
mut debug_draw: ResMut<DebugLines>,
debug_mode: Res<DebugMode>,
rapier_context: Res<RapierContext>,
visibility_blocker_query: Query<&VisibilityBlocker>,
transform_query: Query<&GlobalTransform>,
collider_query: Query<&Collider>,
material_query: Query<&Handle<DarknessMaterial>>, material_query: Query<&Handle<DarknessMaterial>>,
point_light_query: Query<(&GlobalTransform, &PointLight2D)>, point_light_query: Query<(&GlobalTransform, &PointLight2D, Entity)>,
spot_light_query: Query<(&GlobalTransform, &SpotLight2D)>, spot_light_query: Query<(&GlobalTransform, &SpotLight2D, Entity)>,
) { ) {
let point_lights: Vec<(_, _)> = point_light_query.iter().collect(); let point_lights: Vec<(_, _, _)> = point_light_query.iter().collect();
let spot_lights: Vec<(_, _)> = spot_light_query.iter().collect(); let spot_lights: Vec<(_, _, _)> = spot_light_query.iter().collect();
for handle in &material_query { for handle in &material_query {
let material = match materials.get_mut(handle) { let material = match materials.get_mut(handle) {
Some(material) => material, Some(material) => material,
@ -197,27 +223,62 @@ fn prepare_lights(
point_lights point_lights
.iter() .iter()
.enumerate() .enumerate()
.for_each(|(i, (transform, light))| { .for_each(|(i, (transform, light, entity))| {
let rect = light.aabb();
let polygon = get_light_geometry(
&rapier_context,
&visibility_blocker_query,
&transform_query,
&collider_query,
Rect::from_corners(
rect.min + transform.translation().truncate(),
rect.max + transform.translation().truncate(),
),
);
if let Ok(mut shadow_mesh) = shadow_mesh_query.get_mut(*entity) {
shadow_mesh.light_index = i;
shadow_mesh.vertices = polygon
.iter()
.map(|point| ShadowVertex {
point: *point,
angle: f32::atan2(
point.y - transform.translation().y,
point.x - transform.translation().x,
),
})
.collect()
}
if debug_mode.enabled {
for i in 0..polygon.len() {
let p1 = polygon[i];
let p2 = polygon[(i + 1) % polygon.len()];
let t1 = i as f32 / polygon.len() as f32;
let t2 = (i + 1) as f32 / polygon.len() as f32;
let color1 = Color::rgba(1.0 - t1, t1, 0.0, 1.0);
let color2 = Color::rgba(1.0 - t2, t2, 0.0, 1.0);
debug_draw.line_gradient(
p1.extend(0.0),
p2.extend(0.0),
0.0,
color1,
color2,
);
}
}
material.point_lights[i] = GpuPointLight2D { material.point_lights[i] = GpuPointLight2D {
position: transform.translation().truncate(), position: transform.translation().truncate(),
radius: light.radius, 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; material.spot_light_count = spot_lights.len() as i32;
spot_lights spot_lights
.iter() .iter()
.enumerate() .enumerate()
.for_each(|(i, (transform, light))| { .for_each(|(i, (transform, light, entity))| {
material.spot_lights[i] = GpuSpotLight2D { material.spot_lights[i] = GpuSpotLight2D {
position: transform.translation().truncate(), position: transform.translation().truncate(),
radius: light.radius, radius: light.radius,
@ -227,6 +288,7 @@ fn prepare_lights(
padding2: 0, padding2: 0,
padding3: 0, padding3: 0,
}; };
// TODO: Remove when shadowmapping is done
for x in 0..SHADOWMAP_RESOLUTION { for x in 0..SHADOWMAP_RESOLUTION {
let offset = ((i + MAX_POINT_LIGHTS) * SHADOWMAP_RESOLUTION + x) * 4; let offset = ((i + MAX_POINT_LIGHTS) * SHADOWMAP_RESOLUTION + x) * 4;
let distance = x as f32 / SHADOWMAP_RESOLUTION as f32 * light.radius; let distance = x as f32 / SHADOWMAP_RESOLUTION as f32 * light.radius;
@ -239,3 +301,172 @@ fn prepare_lights(
}); });
} }
} }
fn prepare_shadows(
mut materials: ResMut<Assets<DarknessMaterial>>,
mut images: ResMut<Assets<Image>>,
shadow_mesh_query: Query<(&ShadowMesh, &GlobalTransform), Changed<ShadowMesh>>,
material_query: Query<&Handle<DarknessMaterial>>,
) {
// TODO: Check that the shadow mesh overlaps the darkness material
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,
};
for (shadow_mesh, global_transform) in &shadow_mesh_query {
let shadow_map = calculate_shadow_map(
global_transform.translation().truncate(),
&shadow_mesh.vertices,
);
for x in 0..SHADOWMAP_RESOLUTION {
let offset = (shadow_mesh.light_index * SHADOWMAP_RESOLUTION + x) * 4;
let distance_bytes = bytes_of(&shadow_map[x]);
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 calculate_shadow_map(
global_position: Vec2,
vertices: &Vec<ShadowVertex>,
) -> [f32; SHADOWMAP_RESOLUTION] {
let mut shadow_map = [0.0; SHADOWMAP_RESOLUTION];
let mut next_index = 0;
for x in 0..SHADOWMAP_RESOLUTION {
let angle = (x as f32 / SHADOWMAP_RESOLUTION as f32) * 2.0 * std::f32::consts::PI
- std::f32::consts::PI;
while angle >= vertices[next_index % vertices.len()].angle && next_index < vertices.len() {
next_index += 1;
}
let prev_vertex = if next_index == 0 {
vertices[vertices.len() - 1]
} else {
vertices[(next_index - 1) % vertices.len()]
};
let next_vertex = vertices[next_index % vertices.len()];
let distance = match vec2_intersection(
global_position,
global_position
+ Vec2 {
x: angle.cos(),
y: angle.sin(),
},
prev_vertex.point,
next_vertex.point,
) {
Some(point) => global_position.distance(point),
None => global_position.distance(prev_vertex.point),
};
shadow_map[x] = distance;
}
shadow_map
}
fn get_light_geometry(
rapier_context: &Res<RapierContext>,
visibility_blocker_query: &Query<&VisibilityBlocker>,
transform_query: &Query<&GlobalTransform>,
collider_query: &Query<&Collider>,
aabb: Rect,
) -> Vec<Vec2> {
let mut points = vec![
Vec2::new(aabb.min.x, aabb.min.y),
Vec2::new(aabb.max.x, aabb.min.y),
Vec2::new(aabb.max.x, aabb.max.y),
Vec2::new(aabb.min.x, aabb.max.y),
];
let collider = Collider::cuboid(aabb.half_size().x, aabb.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);
rapier_context.intersections_with_shape(aabb.center(), 0.0, &collider, filter, |coll| {
let transform = transform_query
.get(coll)
.expect("Collider should have GlobalTransform");
if let Ok(collider) = collider_query.get(coll) {
if let Some(cuboid) = collider.as_cuboid() {
let rect = Rect::from_center_half_size(
transform.translation().truncate(),
cuboid.half_extents(),
);
points.push(rect.min);
points.push(rect.max);
points.push(Vec2::new(rect.min.x, rect.max.y));
points.push(Vec2::new(rect.max.x, rect.min.y));
}
}
true
});
// FIXME: light source may not always be at bounding box center
// for example: optimal spot light bounding box
let center = aabb.center();
points.sort_unstable_by(|a, b| {
f32::atan2(a.y - center.y, a.x - center.x)
.partial_cmp(&f32::atan2(b.y - center.y, b.x - center.x))
.unwrap()
});
// Build visibility polygon
let mut polygon: Vec<_> = vec![];
for point in points.drain(..) {
/// We shoot 2 rays offset by this angle from the edge to catch what is beyond it.
const ANGLE_OFFSET: f32 = 0.0001;
/// Multiplier for hypotenuse when the other two sides are the same length. Or sqrt(2).
const HYPOTENUSE_MULT: f32 = 1.4142135623730951;
offset_cast(
aabb.center(),
(point - aabb.center()).normalize_or_zero(),
aabb.half_size().max_element() * HYPOTENUSE_MULT,
true,
filter,
ANGLE_OFFSET,
&rapier_context,
)
.iter()
.for_each(|ray| polygon.push(aabb.center() + *ray));
}
polygon
}
fn offset_cast(
ray_origin: Vec2,
ray_dir: Vec2,
max_toi: f32,
solid: bool,
filter: QueryFilter,
angle_offset: f32,
rapier_context: &Res<RapierContext>,
) -> Vec<Vec2> {
let dir1 = Vec2::new((-angle_offset).cos(), (-angle_offset).sin()).rotate(ray_dir);
let dir2 = Vec2::new(angle_offset.cos(), angle_offset.sin()).rotate(ray_dir);
let ray1 = rapier_context.cast_ray(ray_origin, dir1, max_toi, solid, filter);
let ray2 = rapier_context.cast_ray(ray_origin, dir2, max_toi, solid, filter);
let toi1 = ray1.map_or(max_toi, |(_, toi)| toi);
let toi2 = ray2.map_or(max_toi, |(_, toi)| toi);
if (toi1 - toi2).abs() > toi1 * 0.1 {
vec![dir1 * toi1, dir2 * toi2]
} else {
vec![(dir1 * toi1 + dir2 * toi2) / 2.0]
}
}

View File

@ -124,6 +124,23 @@ pub fn move_towards_vec3(from: Vec3, to: Vec3, amount: f32) -> Vec3 {
from + diff.normalize() * length.min(amount) from + diff.normalize() * length.min(amount)
} }
/// Get the intersection point (if any) of 2d lines a and b.
/// Lines are defined by 2 points on the line
pub fn vec2_intersection(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2) -> Option<Vec2> {
let a_dir = a2 - a1;
let b_dir = b2 - b1;
let determinant = a_dir.perp_dot(b_dir);
if determinant.abs() <= f32::EPSILON {
return None;
}
Some(
Vec2 {
x: a_dir.x * (b1.x * b2.y - b1.y * b2.x) - (a1.x * a2.y - a1.y * a2.x) * b_dir.x,
y: (a1.x * a2.y - a1.y * a2.x) * -b_dir.y + a_dir.y * (b1.x * b2.y - b1.y * b2.x),
} / determinant,
)
}
pub fn loop_value(from: f32, to: f32, value: f32) -> f32 { pub fn loop_value(from: f32, to: f32, value: f32) -> f32 {
let range = to - from; let range = to - from;
if !range.is_normal() { if !range.is_normal() {