Compare commits
8 Commits
master
...
feat/shado
| Author | SHA1 | Date |
|---|---|---|
|
|
c96625b8eb | |
|
|
499e6ded41 | |
|
|
b13ee649be | |
|
|
ca65416c73 | |
|
|
ddce5075ce | |
|
|
c1894235ba | |
|
|
69c176ed88 | |
|
|
3fbe81d388 |
13
src/game.rs
13
src/game.rs
|
|
@ -2,8 +2,6 @@ use crate::{debug, game_setup};
|
|||
use bevy::prelude::*;
|
||||
use bevy_ecs_ldtk::{LdtkWorldBundle, LevelSelection};
|
||||
|
||||
use self::darkness::SpotLight2D;
|
||||
|
||||
pub mod camera;
|
||||
pub mod darkness;
|
||||
pub mod ldtk;
|
||||
|
|
@ -26,15 +24,4 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
|||
ldtk_handle: assets.load("levels/world.ldtk"),
|
||||
..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,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use bevy::render::camera::ScalingMode;
|
|||
use bevy::{input::mouse::MouseMotion, transform::TransformSystem};
|
||||
use bevy_ecs_ldtk::prelude::*;
|
||||
|
||||
use super::darkness::{PointLight2D, SpotLight2D};
|
||||
use super::darkness::{PointLight2D, ShadowMesh, SpotLight2D};
|
||||
|
||||
pub struct GameCameraPlugin;
|
||||
|
||||
|
|
@ -95,6 +95,7 @@ fn camera_setup(mut commands: Commands) {
|
|||
ComputedVisibility::default(),
|
||||
CameraRoomRestraint,
|
||||
PointLight2D { radius: 30.0 },
|
||||
ShadowMesh::default(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -118,13 +119,32 @@ fn free_system(
|
|||
mut free_query: Query<(&mut Transform, &GameCamera, &OrthographicProjection)>,
|
||||
mut mouse_events: EventReader<MouseMotion>,
|
||||
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();
|
||||
for (mut transform, camera, projection) in free_query.iter_mut() {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ use bevy::{
|
|||
sprite::{Material2dPlugin, Mesh2dHandle},
|
||||
};
|
||||
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;
|
||||
|
||||
|
|
@ -16,7 +20,7 @@ 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 const SHADOWMAP_RESOLUTION: usize = 1024;
|
||||
|
||||
pub struct DarknessPlugin;
|
||||
|
||||
|
|
@ -24,11 +28,12 @@ impl Plugin for DarknessPlugin {
|
|||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<PointLight2D>()
|
||||
.register_type::<SpotLight2D>()
|
||||
.register_type::<ShadowMesh>()
|
||||
.register_type::<VisibilityBlocker>()
|
||||
.register_asset_reflect::<DarknessMaterial>()
|
||||
.add_plugins(Material2dPlugin::<DarknessMaterial>::default())
|
||||
.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)]
|
||||
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(
|
||||
mut commands: Commands,
|
||||
mut level_events: EventReader<LevelEvent>,
|
||||
|
|
@ -146,9 +164,9 @@ fn add_to_level(
|
|||
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(
|
||||
material: materials.add(DarknessMaterial {
|
||||
color: Color::rgba(0.0, 0.0, 0.0, 0.75),
|
||||
shadowmap_texture: Some(images.add(Image::new(
|
||||
Extent3d {
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
|
|
@ -158,7 +176,8 @@ fn add_to_level(
|
|||
vec![0; width * height * 4],
|
||||
bevy::render::render_resource::TextureFormat::R32Float,
|
||||
))),
|
||||
)),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
|
@ -171,14 +190,21 @@ fn add_to_level(
|
|||
}
|
||||
|
||||
fn prepare_lights(
|
||||
mut shadow_mesh_query: Query<&mut ShadowMesh>,
|
||||
mut materials: ResMut<Assets<DarknessMaterial>>,
|
||||
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>>,
|
||||
point_light_query: Query<(&GlobalTransform, &PointLight2D)>,
|
||||
spot_light_query: Query<(&GlobalTransform, &SpotLight2D)>,
|
||||
point_light_query: Query<(&GlobalTransform, &PointLight2D, Entity)>,
|
||||
spot_light_query: Query<(&GlobalTransform, &SpotLight2D, Entity)>,
|
||||
) {
|
||||
let point_lights: Vec<(_, _)> = point_light_query.iter().collect();
|
||||
let spot_lights: Vec<(_, _)> = spot_light_query.iter().collect();
|
||||
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,
|
||||
|
|
@ -197,27 +223,62 @@ fn prepare_lights(
|
|||
point_lights
|
||||
.iter()
|
||||
.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 {
|
||||
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))| {
|
||||
.for_each(|(i, (transform, light, entity))| {
|
||||
material.spot_lights[i] = GpuSpotLight2D {
|
||||
position: transform.translation().truncate(),
|
||||
radius: light.radius,
|
||||
|
|
@ -227,6 +288,7 @@ fn prepare_lights(
|
|||
padding2: 0,
|
||||
padding3: 0,
|
||||
};
|
||||
// TODO: Remove when shadowmapping is done
|
||||
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;
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/util.rs
17
src/util.rs
|
|
@ -124,6 +124,23 @@ pub fn move_towards_vec3(from: Vec3, to: Vec3, amount: f32) -> Vec3 {
|
|||
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 {
|
||||
let range = to - from;
|
||||
if !range.is_normal() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue