1use glam::{Mat4, Vec2, Vec3};
90
91pub const MAX_FALLOFF_STOPS: usize = 8;
93
94pub const MAX_LIGHT_LAYERS: u32 = 8;
99
100pub const MAX_OCCLUDER_POINTS: usize = 256;
102
103pub const MAX_MASK_TAPS: usize = 4;
107
108const _: () = assert!(MAX_MASK_TAPS >= 4, "Pcf4 needs one mask channel per tap");
109
110pub const SELF_INTERSECT_CHECK_POINTS: usize = 64;
113
114#[derive(Clone, Copy, Debug, Default, PartialEq)]
116pub struct LightStop {
117 pub at: f32,
119 pub color: [f32; 4],
121}
122
123#[derive(Clone, Debug, PartialEq)]
125pub enum LightFalloff {
126 Stops(Vec<LightStop>),
132 Texture {
139 slot: u32,
140 half_extent: Vec2,
141 offset: Vec2,
142 },
143}
144
145impl LightFalloff {
146 pub fn stops(stops: impl Into<Vec<LightStop>>) -> Self {
149 Self::Stops(stops.into())
150 }
151
152 pub fn half_extent(&self, radius: f32) -> Vec2 {
155 match self {
156 Self::Stops(_) => Vec2::splat(radius),
157 Self::Texture { half_extent, .. } => *half_extent,
158 }
159 }
160}
161
162impl Default for LightFalloff {
163 fn default() -> Self {
164 Self::Stops(vec![
165 LightStop {
166 at: 0.0,
167 color: [1.0, 1.0, 1.0, 1.0],
168 },
169 LightStop {
170 at: 0.5,
171 color: [0.5, 0.5, 0.5, 1.0],
172 },
173 LightStop {
174 at: 1.0,
175 color: [0.0, 0.0, 0.0, 1.0],
176 },
177 ])
178 }
179}
180
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
183pub enum LightBlend {
184 #[default]
186 Add,
187 Sub,
189 Mix,
191}
192
193#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
214pub enum ShadowFilter {
215 #[default]
217 None,
218 Pcf4,
220}
221
222impl ShadowFilter {
223 pub fn tap_count(self) -> u32 {
225 match self {
226 Self::None => 1,
227 Self::Pcf4 => 4,
228 }
229 }
230
231 pub fn tap_weight(self) -> f32 {
234 1.0 / self.tap_count() as f32
235 }
236}
237
238#[derive(Clone, Debug, PartialEq)]
245pub struct Light2d {
246 pub position: Vec2,
248 pub radius: f32,
253 pub color: [f32; 3],
255 pub energy: f32,
257 pub falloff: LightFalloff,
259 pub blend: LightBlend,
261 pub shadows: bool,
263 pub shadow_filter: ShadowFilter,
265 pub shadow_softness: f32,
268 pub light_mask: u32,
270}
271
272impl Default for Light2d {
273 fn default() -> Self {
274 Self {
275 position: Vec2::ZERO,
276 radius: 128.0,
277 color: [1.0, 1.0, 1.0],
278 energy: 1.0,
279 falloff: LightFalloff::default(),
280 blend: LightBlend::Add,
281 shadows: false,
282 shadow_filter: ShadowFilter::None,
283 shadow_softness: 4.0,
284 light_mask: 1,
285 }
286 }
287}
288
289impl Light2d {
290 pub fn point(position: Vec2, radius: f32) -> Self {
292 Self {
293 position,
294 radius,
295 ..Default::default()
296 }
297 }
298
299 pub fn falloff_at(&self, t: f32) -> Option<[f32; 4]> {
302 match &self.falloff {
303 LightFalloff::Stops(stops) => Some(stops_at(stops, t)),
304 LightFalloff::Texture { .. } => None,
305 }
306 }
307
308 pub fn texture_uv(&self, world: Vec2) -> Vec2 {
312 match &self.falloff {
313 LightFalloff::Texture {
314 half_extent,
315 offset,
316 ..
317 } => texture_uv(self.position, *offset, *half_extent, world),
318 LightFalloff::Stops(_) => Vec2::splat(0.5),
319 }
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq)]
332pub struct AmbientLight {
333 pub color: [f32; 3],
335 pub energy: f32,
337 pub light_mask: u32,
340}
341
342impl Default for AmbientLight {
343 fn default() -> Self {
344 Self {
345 color: [1.0, 1.0, 1.0],
346 energy: 0.0,
347 light_mask: 1,
348 }
349 }
350}
351
352#[derive(Clone, Debug, PartialEq)]
365pub struct Occluder2d {
366 pub points: Vec<Vec2>,
368 pub closed: bool,
371 pub light_mask: u32,
373}
374
375impl Default for Occluder2d {
376 fn default() -> Self {
377 Self {
378 points: Vec::new(),
379 closed: true,
380 light_mask: 1,
381 }
382 }
383}
384
385impl Occluder2d {
386 pub fn rect(min: Vec2, max: Vec2) -> Self {
388 let min = min.min(max);
389 let max = min.max(max);
390 Self {
391 points: vec![
392 min,
393 Vec2::new(max.x, min.y),
394 max,
395 Vec2::new(min.x, max.y),
396 ],
397 closed: true,
398 light_mask: 1,
399 }
400 }
401
402 pub fn cell(cell: [i32; 2], cell_size: f32) -> Self {
404 let min = Vec2::new(cell[0] as f32 * cell_size, cell[1] as f32 * cell_size);
405 Self::rect(min, min + Vec2::splat(cell_size))
406 }
407
408 pub fn polygon(points: impl Into<Vec<Vec2>>) -> Self {
410 Self {
411 points: points.into(),
412 closed: true,
413 light_mask: 1,
414 }
415 }
416
417 pub fn contains(&self, p: Vec2) -> bool {
420 if !self.closed || self.points.len() < 3 {
421 return false;
422 }
423 let mut inside = false;
424 let n = self.points.len();
425 for i in 0..n {
426 let a = self.points[i];
427 let b = self.points[(i + 1) % n];
428 if (a.y > p.y) != (b.y > p.y) {
429 let t = (p.y - a.y) / (b.y - a.y);
430 if p.x < a.x + t * (b.x - a.x) {
431 inside = !inside;
432 }
433 }
434 }
435 inside
436 }
437
438 pub fn rect_masked(min: Vec2, max: Vec2, light_mask: u32) -> Self {
440 Self {
441 light_mask,
442 ..Self::rect(min, max)
443 }
444 }
445
446 pub fn aabb(&self) -> [f32; 4] {
448 aabb_of(&self.points)
449 }
450
451 pub fn validate(&self) -> Result<(), OccluderRejectReason> {
453 ValidatedOccluder::validate(self).map(|_| ())
454 }
455}
456
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459pub enum OccluderRejectReason {
460 TooFewPoints,
462 TooManyPoints,
464 NonFinitePoint,
466 DegenerateArea,
468 SelfIntersecting,
470 ZeroMask,
472}
473
474#[derive(Clone, Debug, PartialEq)]
476pub struct ValidatedOccluder {
477 points: Vec<Vec2>,
478 closed: bool,
479 light_mask: u32,
480 aabb: [f32; 4],
481}
482
483impl ValidatedOccluder {
484 pub fn validate(occluder: &Occluder2d) -> Result<Self, OccluderRejectReason> {
486 let n = occluder.points.len();
487 if n > MAX_OCCLUDER_POINTS {
488 return Err(OccluderRejectReason::TooManyPoints);
489 }
490 let min_points = if occluder.closed { 3 } else { 2 };
491 if n < min_points {
492 return Err(OccluderRejectReason::TooFewPoints);
493 }
494 if occluder.points.iter().any(|p| !p.is_finite()) {
495 return Err(OccluderRejectReason::NonFinitePoint);
496 }
497 if occluder.light_mask == 0 {
498 return Err(OccluderRejectReason::ZeroMask);
500 }
501 let aabb = aabb_of(&occluder.points);
502 if occluder.closed {
503 if n <= SELF_INTERSECT_CHECK_POINTS && self_intersects(&occluder.points) {
509 return Err(OccluderRejectReason::SelfIntersecting);
510 }
511 let width = aabb[2] - aabb[0];
512 let height = aabb[3] - aabb[1];
513 let scale = width.max(height);
514 let area = signed_area(&occluder.points).abs();
515 if !(area > scale * scale * 1e-6) {
516 return Err(OccluderRejectReason::DegenerateArea);
517 }
518 }
519 Ok(Self {
520 points: occluder.points.clone(),
521 closed: occluder.closed,
522 light_mask: occluder.light_mask,
523 aabb,
524 })
525 }
526
527 pub fn points(&self) -> &[Vec2] {
529 &self.points
530 }
531
532 pub fn closed(&self) -> bool {
534 self.closed
535 }
536
537 pub fn light_mask(&self) -> u32 {
539 self.light_mask
540 }
541
542 pub fn aabb(&self) -> [f32; 4] {
544 self.aabb
545 }
546}
547
548#[derive(Clone, Copy, Debug, PartialEq)]
551pub struct ShadowWedge {
552 pub quad: [Vec2; 4],
554 pub weight: f32,
556 pub tap: u32,
559}
560
561impl ShadowWedge {
562 pub fn mask_weights(&self) -> [f32; MAX_MASK_TAPS] {
570 let mut out = [0.0; MAX_MASK_TAPS];
571 if let Some(slot) = out.get_mut(self.tap as usize) {
572 *slot = self.weight;
573 }
574 out
575 }
576}
577
578#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
580pub struct ShadowCasters {
581 pub used: usize,
583 pub dropped: usize,
585}
586
587#[derive(Clone, Copy, Debug, PartialEq)]
589pub struct LightBudget {
590 pub max_lights: usize,
592 pub max_layers: u32,
595 pub max_shadow_casters: usize,
597 pub max_shadow_vertices: usize,
600}
601
602impl Default for LightBudget {
603 fn default() -> Self {
604 Self {
605 max_lights: 16,
606 max_layers: MAX_LIGHT_LAYERS,
607 max_shadow_casters: 64,
608 max_shadow_vertices: 12_288,
609 }
610 }
611}
612
613#[derive(Clone, Debug, PartialEq)]
615pub struct PlannedLight {
616 pub source: usize,
618 pub layers: u32,
620 pub center: Vec2,
622 pub half_extent: Vec2,
624 pub extent: f32,
626 pub color: [f32; 3],
628 pub energy: f32,
630 pub blend: LightBlend,
632 pub stops: [LightStop; MAX_FALLOFF_STOPS],
634 pub stop_count: u32,
636 pub texture_slot: u32,
638 pub texture_offset: Vec2,
640 pub texture_half_extent: Vec2,
642 pub shadowed: bool,
644 pub shadow_filter: ShadowFilter,
646 pub wedges: Vec<ShadowWedge>,
649}
650
651impl PlannedLight {
652 pub fn wedge_vertex_count(&self) -> usize {
654 self.wedges.len() * 6
655 }
656
657 pub fn layer_indices(&self) -> impl Iterator<Item = u32> + '_ {
659 (0..MAX_LIGHT_LAYERS).filter(move |bit| self.layers & (1 << bit) != 0)
660 }
661}
662
663#[derive(Clone, Copy, Debug, PartialEq)]
665pub enum LightIssueKind {
666 ZeroMask,
668 MaskBeyondLayerRange {
670 dropped: u32,
672 },
673 NonFinitePosition,
675 NonFiniteValue,
677 NonFiniteAmbient,
680 ZeroExtent,
683 StopsEmpty,
685 TooManyStops {
687 count: usize,
689 max: usize,
691 },
692 StopNotFinite,
694 StopOutOfRange {
696 at: f32,
698 },
699 StopsNotAscending {
701 at: f32,
703 previous: f32,
705 },
706 OverLightBudget {
708 max: usize,
710 },
711 ShadowCastersExhausted {
713 used: usize,
715 dropped: usize,
717 },
718 ShadowVertexBudget {
721 needed: usize,
723 max: usize,
725 },
726}
727
728#[derive(Clone, Copy, Debug, PartialEq)]
730pub struct LightIssue {
731 pub source: usize,
734 pub kind: LightIssueKind,
736}
737
738pub const AMBIENT_SOURCE: usize = usize::MAX;
741
742#[derive(Clone, Copy, Debug, PartialEq, Eq)]
744pub struct OccluderReject {
745 pub index: usize,
747 pub reason: OccluderRejectReason,
749}
750
751#[derive(Clone, Debug, PartialEq)]
753pub struct LightingPlan {
754 pub world_rect: [f32; 4],
757 pub layers: u32,
762 pub layer_clamp: u32,
764 pub lights: Vec<PlannedLight>,
766 pub ambient: AmbientLight,
768 pub issues: Vec<LightIssue>,
770 pub occluder_rejects: Vec<OccluderReject>,
772 pub culled: usize,
775}
776
777impl LightingPlan {
778 pub fn wedge_vertices(&self) -> usize {
780 self.lights.iter().map(PlannedLight::wedge_vertex_count).sum()
781 }
782
783 pub fn ambient_rgb(&self, layer: u32) -> [f32; 3] {
787 if layer + 1 >= self.layers || self.ambient.light_mask & (1u32 << layer) == 0 {
788 return [0.0; 3];
789 }
790 [
791 self.ambient.color[0] * self.ambient.energy,
792 self.ambient.color[1] * self.ambient.energy,
793 self.ambient.color[2] * self.ambient.energy,
794 ]
795 }
796}
797
798pub fn stops_at(stops: &[LightStop], t: f32) -> [f32; 4] {
801 let Some(first) = stops.first() else {
802 return [0.0; 4];
803 };
804 let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 1.0 };
805 let mut ramp = first.color;
806 let mut previous = 0.0f32;
807 for stop in &stops[1..] {
808 let span = (stop.at - previous).max(1e-6);
811 let w = ((t - previous) / span).clamp(0.0, 1.0);
812 ramp = [
813 ramp[0] + (stop.color[0] - ramp[0]) * w,
814 ramp[1] + (stop.color[1] - ramp[1]) * w,
815 ramp[2] + (stop.color[2] - ramp[2]) * w,
816 ramp[3] + (stop.color[3] - ramp[3]) * w,
817 ];
818 previous = stop.at;
819 }
820 ramp
821}
822
823pub fn texture_uv(center: Vec2, offset: Vec2, half_extent: Vec2, world: Vec2) -> Vec2 {
826 let half = Vec2::new(
827 if half_extent.x.abs() > 1e-6 {
828 half_extent.x
829 } else {
830 1.0
831 },
832 if half_extent.y.abs() > 1e-6 {
833 half_extent.y
834 } else {
835 1.0
836 },
837 );
838 (world - center - offset) / (half * 2.0) + Vec2::splat(0.5)
839}
840
841pub fn item_layer(light_mask: u32) -> u32 {
844 light_mask.trailing_zeros().min(MAX_LIGHT_LAYERS)
845}
846
847pub fn visible_world_rect(view_proj: Mat4) -> Option<[f32; 4]> {
850 if !view_proj.is_finite() {
851 return None;
852 }
853 let inverse = view_proj.inverse();
854 if !inverse.is_finite() || inverse.determinant() == 0.0 {
855 return None;
856 }
857 let mut min = Vec2::splat(f32::INFINITY);
858 let mut max = Vec2::splat(f32::NEG_INFINITY);
859 for corner in [[-1.0f32, -1.0f32], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]] {
860 let p = inverse.project_point3(Vec3::new(corner[0], corner[1], 0.0));
861 if !p.is_finite() {
862 return None;
863 }
864 let p = p.truncate();
865 min = min.min(p);
866 max = max.max(p);
867 }
868 let size = max - min;
869 if !(size.x > 1e-3 && size.y > 1e-3) {
870 return None;
871 }
872 Some([min.x, min.y, size.x, size.y])
873}
874
875#[allow(clippy::too_many_arguments)]
891pub fn shadow_wedges(
892 center: Vec2,
893 extent: f32,
894 filter: ShadowFilter,
895 softness: f32,
896 light_mask: u32,
897 light_aabb: [f32; 4],
898 occluders: &[ValidatedOccluder],
899 max_casters: usize,
900 out: &mut Vec<ShadowWedge>,
901) -> ShadowCasters {
902 let mut casters = ShadowCasters::default();
903 if !(extent.is_finite() && extent > 0.0) {
904 return casters;
905 }
906 let softness = if softness.is_finite() {
907 softness.max(0.0)
908 } else {
909 0.0
910 };
911 let taps = filter.tap_count();
912 let weight = filter.tap_weight();
913 debug_assert!(
914 taps as usize <= MAX_MASK_TAPS,
915 "a filter with more taps than mask channels would lose its penumbra"
916 );
917 let reach = extent * 1.5 + softness * 2.0;
918 for occluder in occluders {
919 if occluder.light_mask & light_mask == 0 || !aabb_overlap(occluder.aabb, light_aabb) {
920 continue;
921 }
922 if casters.used >= max_casters {
923 casters.dropped += 1;
924 continue;
925 }
926 casters.used += 1;
927 let points = &occluder.points;
928 let n = points.len();
929 let edges = if occluder.closed { n } else { n - 1 };
930 for edge in 0..edges {
931 let a = points[edge];
932 let b = points[(edge + 1) % n];
933 let mid = (a + b) * 0.5;
934 let delta = if taps > 1 && softness > 0.0 {
935 (softness / (mid - center).length().max(1e-3)).min(0.5)
936 } else {
937 0.0
938 };
939 for tap in 0..taps {
940 let offset = if taps == 1 {
941 0.0
942 } else {
943 TAP_OFFSETS[tap as usize % TAP_OFFSETS.len()]
944 };
945 let angle = offset * delta;
946 let ar = rotate_about(center, a, angle);
947 let br = rotate_about(center, b, angle);
948 let af = extrude(center, ar, reach);
949 let bf = extrude(center, br, reach);
950 out.push(ShadowWedge {
951 quad: [ar, br, bf, af],
952 weight,
953 tap,
954 });
955 }
956 }
957 }
958 casters
959}
960
961const TAP_OFFSETS: [f32; 4] = [-0.75, -0.25, 0.25, 0.75];
964
965pub fn plan_lighting(
972 lights: &[Light2d],
973 occluders: &[Occluder2d],
974 ambient: &AmbientLight,
975 world_rect: [f32; 4],
976 budget: &LightBudget,
977) -> LightingPlan {
978 let mut plan = LightingPlan {
979 world_rect,
980 layers: 2,
981 layer_clamp: 1,
982 lights: Vec::new(),
983 ambient: *ambient,
984 issues: Vec::new(),
985 occluder_rejects: Vec::new(),
986 culled: 0,
987 };
988 if !ambient.color.iter().all(|c| c.is_finite()) || !ambient.energy.is_finite() {
989 plan.ambient = AmbientLight {
992 color: [0.0; 3],
993 ..*ambient
994 };
995 plan.issues.push(LightIssue {
996 source: AMBIENT_SOURCE,
997 kind: LightIssueKind::NonFiniteAmbient,
998 });
999 }
1000 let mut validated: Vec<ValidatedOccluder> = Vec::with_capacity(occluders.len());
1001 for (index, occluder) in occluders.iter().enumerate() {
1002 match ValidatedOccluder::validate(occluder) {
1003 Ok(valid) => validated.push(valid),
1004 Err(reason) => plan.occluder_rejects.push(OccluderReject { index, reason }),
1005 }
1006 }
1007
1008 let max_layers = budget.max_layers.clamp(1, MAX_LIGHT_LAYERS);
1009 let layer_bits = if max_layers >= 32 {
1010 u32::MAX
1011 } else {
1012 (1u32 << max_layers) - 1
1013 };
1014 let mut used_layers = if ambient.light_mask & layer_bits != 0 {
1015 (32 - (ambient.light_mask & layer_bits).leading_zeros()).max(1)
1016 } else {
1017 0
1018 };
1019 let view_bounds = [
1020 world_rect[0],
1021 world_rect[1],
1022 world_rect[0] + world_rect[2],
1023 world_rect[1] + world_rect[3],
1024 ];
1025 let mut wedge_vertices = 0usize;
1026
1027 for (source, light) in lights.iter().enumerate() {
1028 let issue = |plan: &mut LightingPlan, kind: LightIssueKind| {
1029 plan.issues.push(LightIssue { source, kind });
1030 };
1031 if plan.lights.len() >= budget.max_lights {
1032 issue(
1033 &mut plan,
1034 LightIssueKind::OverLightBudget {
1035 max: budget.max_lights,
1036 },
1037 );
1038 continue;
1039 }
1040 if light.light_mask == 0 {
1041 issue(&mut plan, LightIssueKind::ZeroMask);
1042 continue;
1043 }
1044 if !light.position.is_finite() {
1045 issue(&mut plan, LightIssueKind::NonFinitePosition);
1046 continue;
1047 }
1048 if !light.color.iter().all(|c| c.is_finite()) || !light.energy.is_finite() {
1049 issue(&mut plan, LightIssueKind::NonFiniteValue);
1050 continue;
1051 }
1052 let dropped = light.light_mask & !layer_bits;
1053 if dropped != 0 {
1054 issue(&mut plan, LightIssueKind::MaskBeyondLayerRange { dropped });
1055 }
1056 let layers = light.light_mask & layer_bits;
1057 if layers == 0 {
1058 issue(&mut plan, LightIssueKind::ZeroMask);
1059 continue;
1060 }
1061 let mut stops = [LightStop::default(); MAX_FALLOFF_STOPS];
1062 let mut stop_count = 0u32;
1063 let mut texture_slot = 0u32;
1064 let mut texture_offset = Vec2::ZERO;
1065 let mut texture_half_extent = Vec2::ONE;
1066 match &light.falloff {
1067 LightFalloff::Stops(list) => {
1068 if list.is_empty() {
1069 issue(&mut plan, LightIssueKind::StopsEmpty);
1070 continue;
1071 }
1072 if list.len() > MAX_FALLOFF_STOPS {
1073 issue(
1074 &mut plan,
1075 LightIssueKind::TooManyStops {
1076 count: list.len(),
1077 max: MAX_FALLOFF_STOPS,
1078 },
1079 );
1080 continue;
1081 }
1082 let mut ok = true;
1083 for (i, stop) in list.iter().enumerate() {
1084 if !stop.at.is_finite() || stop.color.iter().any(|c| !c.is_finite()) {
1085 issue(&mut plan, LightIssueKind::StopNotFinite);
1086 ok = false;
1087 break;
1088 }
1089 if !(0.0..=1.0).contains(&stop.at) {
1090 issue(&mut plan, LightIssueKind::StopOutOfRange { at: stop.at });
1091 ok = false;
1092 break;
1093 }
1094 if i > 0 && stop.at <= list[i - 1].at {
1095 issue(
1096 &mut plan,
1097 LightIssueKind::StopsNotAscending {
1098 at: stop.at,
1099 previous: list[i - 1].at,
1100 },
1101 );
1102 ok = false;
1103 break;
1104 }
1105 stops[i] = *stop;
1106 }
1107 if !ok {
1108 continue;
1109 }
1110 stop_count = list.len() as u32;
1111 }
1112 LightFalloff::Texture {
1113 slot,
1114 half_extent,
1115 offset,
1116 } => {
1117 texture_slot = *slot;
1118 texture_offset = *offset;
1119 texture_half_extent = *half_extent;
1120 }
1121 }
1122 let half_extent = light.falloff.half_extent(light.radius);
1123 if !half_extent.is_finite()
1124 || !(half_extent.x > 0.0 && half_extent.y > 0.0)
1125 || !light.radius.is_finite()
1126 {
1127 issue(&mut plan, LightIssueKind::ZeroExtent);
1128 continue;
1129 }
1130 if !texture_offset.is_finite() {
1131 issue(&mut plan, LightIssueKind::NonFiniteValue);
1132 continue;
1133 }
1134 let extent = half_extent.length();
1135 let aabb = [
1136 light.position.x - half_extent.x,
1137 light.position.y - half_extent.y,
1138 light.position.x + half_extent.x,
1139 light.position.y + half_extent.y,
1140 ];
1141 if !aabb_overlap(aabb, view_bounds) {
1142 plan.culled += 1;
1143 continue;
1144 }
1145
1146 let mut wedges = Vec::new();
1147 let mut shadowed = false;
1148 if light.shadows && light.shadow_filter.tap_count() > 0 {
1149 let casters = shadow_wedges(
1150 light.position,
1151 extent,
1152 light.shadow_filter,
1153 light.shadow_softness,
1154 light.light_mask,
1155 aabb,
1156 &validated,
1157 budget.max_shadow_casters,
1158 &mut wedges,
1159 );
1160 if casters.dropped > 0 {
1161 issue(
1162 &mut plan,
1163 LightIssueKind::ShadowCastersExhausted {
1164 used: casters.used,
1165 dropped: casters.dropped,
1166 },
1167 );
1168 }
1169 let needed = wedges.len() * 6;
1170 if needed > 0 && wedge_vertices + needed > budget.max_shadow_vertices {
1171 wedges.clear();
1172 issue(
1173 &mut plan,
1174 LightIssueKind::ShadowVertexBudget {
1175 needed,
1176 max: budget.max_shadow_vertices,
1177 },
1178 );
1179 } else {
1180 wedge_vertices += needed;
1181 shadowed = !wedges.is_empty();
1182 }
1183 }
1184
1185 used_layers = used_layers.max(32 - layers.leading_zeros());
1186 plan.lights.push(PlannedLight {
1187 source,
1188 layers,
1189 center: light.position,
1190 half_extent,
1191 extent,
1192 color: light.color,
1193 energy: light.energy,
1194 blend: light.blend,
1195 stops,
1196 stop_count,
1197 texture_slot,
1198 texture_offset,
1199 texture_half_extent,
1200 shadowed,
1201 shadow_filter: light.shadow_filter,
1202 wedges,
1203 });
1204 }
1205
1206 plan.layers = (used_layers + 1).max(2);
1207 plan.layer_clamp = plan.layers - 1;
1208 plan
1209}
1210
1211fn aabb_of(points: &[Vec2]) -> [f32; 4] {
1212 let mut aabb = [
1213 f32::INFINITY,
1214 f32::INFINITY,
1215 f32::NEG_INFINITY,
1216 f32::NEG_INFINITY,
1217 ];
1218 for p in points {
1219 aabb[0] = aabb[0].min(p.x);
1220 aabb[1] = aabb[1].min(p.y);
1221 aabb[2] = aabb[2].max(p.x);
1222 aabb[3] = aabb[3].max(p.y);
1223 }
1224 aabb
1225}
1226
1227fn aabb_overlap(a_min_max: [f32; 4], b_min_max: [f32; 4]) -> bool {
1232 a_min_max[0] <= b_min_max[2]
1233 && a_min_max[2] >= b_min_max[0]
1234 && a_min_max[1] <= b_min_max[3]
1235 && a_min_max[3] >= b_min_max[1]
1236}
1237
1238fn signed_area(points: &[Vec2]) -> f32 {
1239 let n = points.len();
1240 let mut area = 0.0;
1241 for i in 0..n {
1242 let a = points[i];
1243 let b = points[(i + 1) % n];
1244 area += a.x * b.y - b.x * a.y;
1245 }
1246 area * 0.5
1247}
1248
1249fn self_intersects(points: &[Vec2]) -> bool {
1252 let n = points.len();
1253 for i in 0..n {
1254 let a0 = points[i];
1255 let a1 = points[(i + 1) % n];
1256 for j in (i + 1)..n {
1257 if j == i || (j + 1) % n == i || j == (i + 1) % n {
1258 continue;
1259 }
1260 let b0 = points[j];
1261 let b1 = points[(j + 1) % n];
1262 if segments_cross(a0, a1, b0, b1) {
1263 return true;
1264 }
1265 }
1266 }
1267 false
1268}
1269
1270fn segments_cross(a0: Vec2, a1: Vec2, b0: Vec2, b1: Vec2) -> bool {
1271 let d1 = cross(b1 - b0, a0 - b0);
1272 let d2 = cross(b1 - b0, a1 - b0);
1273 let d3 = cross(a1 - a0, b0 - a0);
1274 let d4 = cross(a1 - a0, b1 - a0);
1275 d1 * d2 < 0.0 && d3 * d4 < 0.0
1276}
1277
1278fn cross(a: Vec2, b: Vec2) -> f32 {
1279 a.x * b.y - a.y * b.x
1280}
1281
1282fn rotate_about(center: Vec2, p: Vec2, angle: f32) -> Vec2 {
1283 if angle == 0.0 {
1284 return p;
1285 }
1286 let (s, c) = angle.sin_cos();
1287 let d = p - center;
1288 center + Vec2::new(d.x * c - d.y * s, d.x * s + d.y * c)
1289}
1290
1291fn extrude(c: Vec2, p: Vec2, reach: f32) -> Vec2 {
1295 let d = p - c;
1296 let len = d.length();
1297 if len > 1e-6 {
1298 c + d / len * reach.max(len * 1.05)
1299 } else {
1300 p
1301 }
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307
1308 fn stops3() -> Vec<LightStop> {
1309 vec![
1310 LightStop {
1311 at: 0.0,
1312 color: [1.0, 1.0, 1.0, 1.0],
1313 },
1314 LightStop {
1315 at: 0.5,
1316 color: [0.5, 0.5, 0.5, 1.0],
1317 },
1318 LightStop {
1319 at: 1.0,
1320 color: [0.0, 0.0, 0.0, 1.0],
1321 },
1322 ]
1323 }
1324
1325 #[test]
1326 fn stops_ramp_interpolates_at_known_distances() {
1327 let ramp = stops3();
1328 assert_eq!(stops_at(&ramp, 0.0), [1.0, 1.0, 1.0, 1.0]);
1329 assert_eq!(stops_at(&ramp, 0.5), [0.5, 0.5, 0.5, 1.0]);
1330 assert_eq!(stops_at(&ramp, 1.0), [0.0, 0.0, 0.0, 1.0]);
1331 let mid = stops_at(&ramp, 0.25);
1333 assert!((mid[0] - 0.75).abs() < 1e-6, "got {mid:?}");
1334 assert_eq!(stops_at(&ramp, -1.0), [1.0, 1.0, 1.0, 1.0]);
1336 assert_eq!(stops_at(&ramp, 4.0), [0.0, 0.0, 0.0, 1.0]);
1337 assert_eq!(stops_at(&ramp, f32::NAN), [0.0, 0.0, 0.0, 1.0]);
1338 let flat = [LightStop {
1340 at: 0.3,
1341 color: [0.25, 0.25, 0.25, 0.5],
1342 }];
1343 assert_eq!(stops_at(&flat, 0.0), [0.25, 0.25, 0.25, 0.5]);
1344 assert_eq!(stops_at(&flat, 1.0), [0.25, 0.25, 0.25, 0.5]);
1345 assert_eq!(stops_at(&[], 0.5), [0.0; 4]);
1347 }
1348
1349 #[test]
1350 fn radius_ramp_matches_stops_at_known_distances() {
1351 let light = Light2d {
1352 position: Vec2::ZERO,
1353 radius: 100.0,
1354 falloff: LightFalloff::stops(stops3()),
1355 ..Default::default()
1356 };
1357 let at = |d: f32| light.falloff_at(d / light.radius).expect("stops ramp");
1358 assert_eq!(at(0.0), [1.0, 1.0, 1.0, 1.0]);
1359 assert_eq!(at(50.0), [0.5, 0.5, 0.5, 1.0]);
1360 assert_eq!(at(100.0), [0.0, 0.0, 0.0, 1.0]);
1361 assert!((at(25.0)[0] - 0.75).abs() < 1e-6);
1362 assert_eq!(at(250.0), [0.0, 0.0, 0.0, 1.0]);
1364 }
1365
1366 #[test]
1367 fn texture_falloff_maps_world_to_local_uv() {
1368 let light = Light2d {
1369 position: Vec2::new(100.0, 100.0),
1370 radius: 64.0,
1371 falloff: LightFalloff::Texture {
1372 slot: 0,
1373 half_extent: Vec2::new(64.0, 32.0),
1374 offset: Vec2::new(8.0, -4.0),
1375 },
1376 ..Default::default()
1377 };
1378 assert_eq!(light.falloff_at(0.5), None, "texture ramps stay on the GPU");
1379 let uv = |x: f32, y: f32| light.texture_uv(Vec2::new(x, y));
1380 let center = Vec2::new(108.0, 96.0);
1381 assert!((uv(center.x, center.y) - Vec2::splat(0.5)).length() < 1e-6);
1382 assert!((uv(center.x - 64.0, center.y) - Vec2::new(0.0, 0.5)).length() < 1e-6);
1383 assert!((uv(center.x, center.y + 32.0) - Vec2::new(0.5, 1.0)).length() < 1e-6);
1384 assert!(uv(center.x + 100.0, center.y).x > 1.0);
1386 let degenerate = Light2d {
1388 falloff: LightFalloff::Texture {
1389 slot: 0,
1390 half_extent: Vec2::ZERO,
1391 offset: Vec2::ZERO,
1392 },
1393 ..Default::default()
1394 };
1395 assert!(degenerate.texture_uv(Vec2::new(3.0, 4.0)).is_finite());
1396 }
1397
1398 #[test]
1399 fn item_layers_follow_the_lowest_shared_bit() {
1400 assert_eq!(item_layer(1), 0);
1401 assert_eq!(item_layer(0b010), 1);
1402 assert_eq!(item_layer(0b1001), 0);
1403 assert_eq!(item_layer(0b1000_0000), 7);
1404 assert_eq!(item_layer(0), MAX_LIGHT_LAYERS);
1406 assert_eq!(item_layer(1 << 9), MAX_LIGHT_LAYERS);
1407 }
1408
1409 #[test]
1410 fn mask_filtering_selects_lit_layers() {
1411 let dim = AmbientLight {
1412 color: [1.0, 1.0, 1.0],
1413 energy: 0.25,
1414 light_mask: 0b101,
1415 };
1416 let lights = vec![
1417 Light2d {
1418 position: Vec2::new(0.0, 0.0),
1419 light_mask: 0b011,
1420 ..Default::default()
1421 },
1422 Light2d {
1423 position: Vec2::new(0.0, 0.0),
1424 light_mask: 0b100,
1425 ..Default::default()
1426 },
1427 ];
1428 let plan = plan_lighting(
1429 &lights,
1430 &[],
1431 &dim,
1432 [-100.0, -100.0, 200.0, 200.0],
1433 &LightBudget::default(),
1434 );
1435 assert_eq!(plan.lights.len(), 2);
1436 assert_eq!(plan.lights[0].layer_indices().collect::<Vec<_>>(), [0, 1]);
1437 assert_eq!(plan.lights[1].layer_indices().collect::<Vec<_>>(), [2]);
1438 assert_eq!((plan.layers, plan.layer_clamp), (4, 3));
1440 assert_eq!(item_layer(0b010), 1);
1442 assert_eq!(plan.lights[0].layers & (1 << item_layer(0b010)), 1 << 1);
1443 assert_eq!(plan.lights[1].layers & (1 << item_layer(0b010)), 0);
1444 assert_eq!(plan.lights[1].layers & (1 << item_layer(0b100)), 1 << 2);
1445 assert_eq!(plan.ambient_rgb(0), [0.25, 0.25, 0.25]);
1447 assert_eq!(plan.ambient_rgb(1), [0.0, 0.0, 0.0]);
1448 assert_eq!(plan.ambient_rgb(2), [0.25, 0.25, 0.25]);
1449 assert_eq!(plan.ambient_rgb(3), [0.0, 0.0, 0.0]);
1450 assert_eq!(plan.ambient_rgb(9), [0.0, 0.0, 0.0]);
1451 }
1452
1453 #[test]
1454 fn zero_mask_lights_and_out_of_range_bits_are_reported() {
1455 let lights = vec![
1456 Light2d {
1457 light_mask: 0,
1458 ..Default::default()
1459 },
1460 Light2d {
1461 light_mask: 0x100,
1462 ..Default::default()
1463 },
1464 Light2d {
1465 light_mask: 0x101,
1466 ..Default::default()
1467 },
1468 ]; let plan = plan_lighting(
1469 &lights,
1470 &[],
1471 &AmbientLight::default(),
1472 [-10.0, -10.0, 20.0, 20.0],
1473 &LightBudget::default(),
1474 );
1475 assert_eq!(plan.lights.len(), 1);
1476 assert_eq!(plan.lights[0].source, 2);
1477 assert_eq!(plan.lights[0].layers, 1);
1478 assert_eq!(
1479 plan.issues,
1480 [
1481 LightIssue {
1482 source: 0,
1483 kind: LightIssueKind::ZeroMask
1484 },
1485 LightIssue {
1486 source: 1,
1487 kind: LightIssueKind::MaskBeyondLayerRange { dropped: 0x100 }
1488 },
1489 LightIssue {
1490 source: 1,
1491 kind: LightIssueKind::ZeroMask
1492 },
1493 LightIssue {
1494 source: 2,
1495 kind: LightIssueKind::MaskBeyondLayerRange { dropped: 0x100 }
1496 },
1497 ]
1498 );
1499 }
1500
1501 #[test]
1502 fn degenerate_lights_are_dropped_with_typed_issues() {
1503 let budget = LightBudget::default();
1504 let world = [-1e4, -1e4, 2e4, 2e4];
1505 let ambient = AmbientLight::default();
1506 let cases: Vec<(Light2d, LightIssueKind)> = vec![
1507 (
1508 Light2d {
1509 radius: 0.0,
1510 ..Default::default()
1511 },
1512 LightIssueKind::ZeroExtent,
1513 ),
1514 (
1515 Light2d {
1516 radius: f32::NAN,
1517 ..Default::default()
1518 },
1519 LightIssueKind::ZeroExtent,
1520 ),
1521 (
1522 Light2d {
1523 position: Vec2::new(f32::INFINITY, 0.0),
1524 ..Default::default()
1525 },
1526 LightIssueKind::NonFinitePosition,
1527 ),
1528 (
1529 Light2d {
1530 color: [f32::NAN, 1.0, 1.0],
1531 ..Default::default()
1532 },
1533 LightIssueKind::NonFiniteValue,
1534 ),
1535 (
1536 Light2d {
1537 energy: f32::INFINITY,
1538 ..Default::default()
1539 },
1540 LightIssueKind::NonFiniteValue,
1541 ),
1542 (
1543 Light2d {
1544 falloff: LightFalloff::Texture {
1545 slot: 0,
1546 half_extent: Vec2::splat(4.0),
1547 offset: Vec2::new(f32::NAN, 0.0),
1548 },
1549 ..Default::default()
1550 },
1551 LightIssueKind::NonFiniteValue,
1552 ),
1553 (
1554 Light2d {
1555 falloff: LightFalloff::stops(Vec::new()),
1556 ..Default::default()
1557 },
1558 LightIssueKind::StopsEmpty,
1559 ),
1560 (
1561 Light2d {
1562 falloff: LightFalloff::stops(vec![LightStop::default(); 9]),
1563 ..Default::default()
1564 },
1565 LightIssueKind::TooManyStops { count: 9, max: 8 },
1566 ),
1567 (
1568 Light2d {
1569 falloff: LightFalloff::stops(vec![
1570 LightStop {
1571 at: 0.0,
1572 color: [1.0; 4],
1573 },
1574 LightStop {
1575 at: 0.2,
1576 color: [1.0; 4],
1577 },
1578 LightStop {
1579 at: 0.1,
1580 color: [1.0; 4],
1581 },
1582 ]),
1583 ..Default::default()
1584 },
1585 LightIssueKind::StopsNotAscending {
1586 at: 0.1,
1587 previous: 0.2,
1588 },
1589 ),
1590 (
1591 Light2d {
1592 falloff: LightFalloff::stops(vec![
1593 LightStop {
1594 at: 0.0,
1595 color: [1.0; 4],
1596 },
1597 LightStop {
1598 at: 1.5,
1599 color: [1.0; 4],
1600 },
1601 ]),
1602 ..Default::default()
1603 },
1604 LightIssueKind::StopOutOfRange { at: 1.5 },
1605 ),
1606 ];
1607 for (light, expected) in cases {
1608 let plan = plan_lighting(&[light], &[], &ambient, world, &budget);
1609 assert!(plan.lights.is_empty(), "{expected:?} must not be shaded");
1610 assert_eq!(plan.issues.len(), 1, "{expected:?}");
1611 assert_eq!(plan.issues[0].kind, expected);
1612 }
1613 }
1614
1615 #[test]
1616 fn non_finite_ambient_falls_back_to_black_and_reports_it() {
1617 let dim = AmbientLight {
1618 color: [f32::NAN, 1.0, 1.0],
1619 energy: 1.0,
1620 light_mask: 1,
1621 };
1622 let plan = plan_lighting(
1623 &[Light2d::point(Vec2::ZERO, 8.0)],
1624 &[],
1625 &dim,
1626 [-10.0, -10.0, 20.0, 20.0],
1627 &LightBudget::default(),
1628 );
1629 assert_eq!(plan.ambient_rgb(0), [0.0, 0.0, 0.0], "a NaN never clears");
1630 assert_eq!(
1631 plan.issues,
1632 [LightIssue {
1633 source: AMBIENT_SOURCE,
1634 kind: LightIssueKind::NonFiniteAmbient
1635 }]
1636 );
1637 assert_eq!(plan.lights.len(), 1, "the light itself is unaffected");
1638 }
1639
1640 #[test]
1641 fn occluder_validation_rejects_degenerate_shapes() {
1642 assert_eq!(
1643 Occluder2d::polygon(Vec::new()).validate(),
1644 Err(OccluderRejectReason::TooFewPoints)
1645 );
1646 assert_eq!(
1647 Occluder2d::polygon(vec![Vec2::ZERO, Vec2::X]).validate(),
1648 Err(OccluderRejectReason::TooFewPoints)
1649 );
1650 assert_eq!(
1651 Occluder2d::polygon(vec![Vec2::ZERO, Vec2::X, Vec2::new(f32::NAN, 0.0)]).validate(),
1652 Err(OccluderRejectReason::NonFinitePoint)
1653 );
1654 assert_eq!(
1656 Occluder2d::polygon(vec![Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(2.0, 0.0)]).validate(),
1657 Err(OccluderRejectReason::DegenerateArea)
1658 );
1659 assert_eq!(
1660 Occluder2d::polygon(vec![Vec2::ZERO, Vec2::ZERO, Vec2::ZERO]).validate(),
1661 Err(OccluderRejectReason::DegenerateArea)
1662 );
1663 assert_eq!(
1665 Occluder2d::polygon(vec![
1666 Vec2::ZERO,
1667 Vec2::new(2.0, 0.0),
1668 Vec2::new(0.0, 2.0),
1669 Vec2::new(2.0, 2.0),
1670 ])
1671 .validate(),
1672 Err(OccluderRejectReason::SelfIntersecting)
1673 );
1674 assert_eq!(
1675 Occluder2d::polygon(vec![Vec2::ZERO; MAX_OCCLUDER_POINTS + 1]).validate(),
1676 Err(OccluderRejectReason::TooManyPoints)
1677 );
1678 assert_eq!(
1680 Occluder2d::rect_masked(Vec2::ZERO, Vec2::splat(4.0), 0).validate(),
1681 Err(OccluderRejectReason::ZeroMask)
1682 );
1683 let valid = ValidatedOccluder::validate(&Occluder2d::rect_masked(
1685 Vec2::new(-1.0, -2.0),
1686 Vec2::new(3.0, 4.0),
1687 0b101,
1688 ))
1689 .expect("rect is valid");
1690 assert_eq!(valid.light_mask(), 0b101);
1691 assert_eq!(valid.aabb(), [-1.0, -2.0, 3.0, 4.0]);
1692 assert!(valid.closed());
1693 let open = ValidatedOccluder::validate(&Occluder2d {
1694 points: vec![Vec2::ZERO, Vec2::new(10.0, 0.0)],
1695 closed: false,
1696 light_mask: 1,
1697 });
1698 assert!(open.is_ok(), "an open polyline is a thin wall, not a reject");
1699 let pinched = Occluder2d::polygon(vec![
1700 Vec2::ZERO,
1701 Vec2::new(4.0, 0.0),
1702 Vec2::new(2.0, 2.0),
1703 Vec2::new(4.0, 4.0),
1704 Vec2::new(0.0, 4.0),
1705 Vec2::new(2.0, 2.0),
1706 ]);
1707 assert!(
1708 pinched.validate().is_ok(),
1709 "touching at a point is not a self-crossing"
1710 );
1711 let plan = plan_lighting(
1713 &[],
1714 &[
1715 Occluder2d::polygon(Vec::new()),
1716 Occluder2d::rect(Vec2::ZERO, Vec2::splat(1.0)),
1717 ],
1718 &AmbientLight::default(),
1719 [-10.0, -10.0, 20.0, 20.0],
1720 &LightBudget::default(),
1721 );
1722 assert_eq!(
1723 plan.occluder_rejects,
1724 [OccluderReject {
1725 index: 0,
1726 reason: OccluderRejectReason::TooFewPoints
1727 }]
1728 );
1729 }
1730
1731 #[test]
1732 fn over_budget_lights_drop_in_input_order() {
1733 let lights: Vec<Light2d> = (0..5)
1734 .map(|i| Light2d {
1735 position: Vec2::new(i as f32 * 4.0, 0.0),
1736 radius: 8.0,
1737 ..Default::default()
1738 })
1739 .collect();
1740 let budget = LightBudget {
1741 max_lights: 2,
1742 ..Default::default()
1743 };
1744 let world = [-100.0, -100.0, 200.0, 200.0];
1745 let first = plan_lighting(
1746 &lights,
1747 &[],
1748 &AmbientLight::default(),
1749 world,
1750 &budget,
1751 );
1752 let again = plan_lighting(
1753 &lights,
1754 &[],
1755 &AmbientLight::default(),
1756 world,
1757 &budget,
1758 );
1759 assert_eq!(first, again, "the plan is deterministic");
1760 assert_eq!(
1761 first
1762 .lights
1763 .iter()
1764 .map(|light| light.source)
1765 .collect::<Vec<_>>(),
1766 [0, 1]
1767 );
1768 assert_eq!(
1769 first
1770 .issues
1771 .iter()
1772 .map(|issue| (issue.source, issue.kind))
1773 .collect::<Vec<_>>(),
1774 [
1775 (2, LightIssueKind::OverLightBudget { max: 2 }),
1776 (3, LightIssueKind::OverLightBudget { max: 2 }),
1777 (4, LightIssueKind::OverLightBudget { max: 2 }),
1778 ]
1779 );
1780 let offscreen = LightBudget::default();
1782 let plan = plan_lighting(
1783 &[Light2d::point(Vec2::new(10_000.0, 10_000.0), 4.0)],
1784 &[],
1785 &AmbientLight::default(),
1786 [-10.0, -10.0, 20.0, 20.0],
1787 &offscreen,
1788 );
1789 assert!(plan.lights.is_empty() && plan.issues.is_empty() && plan.culled == 1);
1790 }
1791
1792 #[test]
1793 fn light_on_an_occluder_vertex_keeps_finite_wedges() {
1794 let light = Light2d {
1797 position: Vec2::new(10.0, 5.0),
1798 radius: 10.0,
1799 shadows: true,
1800 ..Default::default()
1801 };
1802 let occluder = Occluder2d::rect(Vec2::new(10.0, -5.0), Vec2::new(20.0, 5.0));
1803 let valid = ValidatedOccluder::validate(&occluder).expect("rect is valid");
1804 let mut wedges = Vec::new();
1805 let casters = shadow_wedges(
1806 light.position,
1807 light.radius,
1808 ShadowFilter::None,
1809 light.shadow_softness,
1810 light.light_mask,
1811 occluder.aabb(),
1812 std::slice::from_ref(&valid),
1813 64,
1814 &mut wedges,
1815 );
1816 assert_eq!((casters.used, casters.dropped), (1, 0));
1817 assert_eq!(wedges.len(), 4, "one wedge per edge");
1818 for wedge in &wedges {
1819 assert!(
1820 wedge.quad.iter().all(|p| p.is_finite()),
1821 "no NaN from a light exactly on a vertex: {:?}",
1822 wedge.quad
1823 );
1824 }
1825 let corner = light.position;
1828 let touching = wedges
1829 .iter()
1830 .filter(|w| w.quad.iter().filter(|p| **p == corner).count() == 2)
1831 .count();
1832 assert_eq!(touching, 2, "the two edges at the vertex stay finite");
1833 assert!(wedges.iter().any(|w| w.quad[1].x > 20.0 || w.quad[2].x > 20.0));
1836 let mut soft = Vec::new();
1838 shadow_wedges(
1839 light.position,
1840 light.radius,
1841 ShadowFilter::Pcf4,
1842 4.0,
1843 light.light_mask,
1844 occluder.aabb(),
1845 std::slice::from_ref(&valid),
1846 64,
1847 &mut soft,
1848 );
1849 assert_eq!(soft.len(), 16, "four taps per edge");
1850 for (index, wedge) in soft.iter().enumerate() {
1851 assert!(wedge.quad.iter().all(|p| p.is_finite()));
1852 assert!((wedge.weight - 0.25).abs() < 1e-6);
1853 assert_eq!(wedge.tap, index as u32 % 4);
1856 let mut want = [0.0; MAX_MASK_TAPS];
1857 want[wedge.tap as usize] = 0.25;
1858 assert_eq!(wedge.mask_weights(), want);
1859 }
1860 }
1861
1862 #[test]
1863 fn wedges_cover_the_shadow_behind_a_wall_and_stop_at_the_light() {
1864 let light = Light2d {
1867 position: Vec2::new(-20.0, 0.0),
1868 radius: 100.0,
1869 shadows: true,
1870 ..Default::default()
1871 };
1872 let wall = Occluder2d::rect(Vec2::new(0.0, -10.0), Vec2::new(10.0, 10.0));
1873 let valid = ValidatedOccluder::validate(&wall).expect("rect is valid");
1874 let mut wedges = Vec::new();
1875 shadow_wedges(
1876 light.position,
1877 light.radius,
1878 ShadowFilter::None,
1879 0.0,
1880 light.light_mask,
1881 wall.aabb(),
1882 std::slice::from_ref(&valid),
1883 64,
1884 &mut wedges,
1885 );
1886 let covered = |p: Vec2| {
1887 wedges
1888 .iter()
1889 .any(|w| point_in_quad(p, w.quad[0], w.quad[1], w.quad[2], w.quad[3]))
1890 };
1891 assert!(covered(Vec2::new(20.0, 0.0)), "behind the wall");
1892 assert!(covered(Vec2::new(5.0, 0.0)), "inside the wall is solid");
1893 assert!(!covered(Vec2::new(-1.0, 0.0)), "in front of the wall stays lit");
1894 let mut masked = Vec::new();
1896 let other = ValidatedOccluder::validate(&Occluder2d::rect_masked(
1897 Vec2::new(0.0, -10.0),
1898 Vec2::new(10.0, 10.0),
1899 0b010,
1900 ))
1901 .expect("rect is valid");
1902 let casters = shadow_wedges(
1903 light.position,
1904 light.radius,
1905 ShadowFilter::None,
1906 0.0,
1907 light.light_mask,
1908 wall.aabb(),
1909 std::slice::from_ref(&other),
1910 64,
1911 &mut masked,
1912 );
1913 assert_eq!((casters.used, masked.len()), (0, 0));
1914 let mut drops = Vec::new();
1916 let many: Vec<ValidatedOccluder> = (0..4)
1917 .map(|i| {
1918 ValidatedOccluder::validate(&Occluder2d::rect(
1919 Vec2::new(i as f32 - 1.0, -1.0),
1920 Vec2::new(i as f32, 1.0),
1921 ))
1922 .expect("valid")
1923 })
1924 .collect();
1925 let casters = shadow_wedges(
1926 light.position,
1927 light.radius,
1928 ShadowFilter::None,
1929 0.0,
1930 light.light_mask,
1931 [-4.0, -4.0, 8.0, 8.0],
1932 &many,
1933 2,
1934 &mut drops,
1935 );
1936 assert_eq!((casters.used, casters.dropped), (2, 2));
1937 assert_eq!(drops.len(), 2 * 4);
1938 }
1939
1940 #[test]
1941 fn shadow_vertex_budget_unshadows_loudly() {
1942 let mut lights = Vec::new();
1943 for i in 0..2 {
1944 let mut light = Light2d::point(Vec2::new(i as f32 * 4.0, 0.0), 20.0);
1945 light.shadows = true;
1946 lights.push(light);
1947 }
1948 let budget = LightBudget {
1949 max_shadow_vertices: 32,
1950 ..Default::default()
1951 };
1952 let plan = plan_lighting(
1953 &lights,
1954 &[Occluder2d::rect(Vec2::new(1.0, -2.0), Vec2::new(2.0, 2.0))],
1955 &AmbientLight::default(),
1956 [-100.0, -100.0, 200.0, 200.0],
1957 &budget,
1958 );
1959 assert_eq!(plan.lights.len(), 2);
1960 assert!(plan.lights[0].shadowed);
1962 assert!(!plan.lights[1].shadowed);
1963 assert_eq!(
1964 plan.issues,
1965 [LightIssue {
1966 source: 1,
1967 kind: LightIssueKind::ShadowVertexBudget {
1968 needed: 24,
1969 max: 32
1970 }
1971 }]
1972 );
1973 }
1974
1975 #[test]
1979 fn aabb_overlap_reads_min_max_corners() {
1980 assert!(!aabb_overlap([70.0, 70.0, 80.0, 80.0], [50.0, 50.0, 60.0, 60.0]));
1981 assert!(aabb_overlap([0.0, 0.0, 10.0, 10.0], [-100.0, -100.0, 50.0, 50.0]));
1982 assert!(aabb_overlap([0.0, 0.0, 10.0, 10.0], [5.0, 5.0, 15.0, 15.0]));
1983 assert!(
1984 aabb_overlap([0.0, 0.0, 10.0, 10.0], [10.0, 10.0, 20.0, 20.0]),
1985 "touching corners count as overlap"
1986 );
1987 assert!(!aabb_overlap([0.0, 0.0, 10.0, 10.0], [11.0, 0.0, 20.0, 10.0]));
1988 assert!(!aabb_overlap([0.0, 0.0, 10.0, 10.0], [0.0, 11.0, 10.0, 20.0]));
1989 }
1990
1991 #[test]
1996 fn casters_and_lights_are_bounds_tested_against_corners() {
1997 let light = Light2d {
1998 position: Vec2::ZERO,
1999 radius: 24.0,
2000 shadows: true,
2001 shadow_filter: ShadowFilter::None,
2002 ..Default::default()
2003 };
2004 let occluder = Occluder2d::rect(Vec2::new(8.0, -4.0), Vec2::new(16.0, 4.0));
2005 let plan = plan_lighting(
2006 std::slice::from_ref(&light),
2007 std::slice::from_ref(&occluder),
2008 &AmbientLight::default(),
2009 [-64.0, -64.0, 128.0, 128.0],
2010 &LightBudget::default(),
2011 );
2012 assert_eq!(plan.culled, 0);
2013 assert_eq!(plan.lights.len(), 1);
2014 let planned = &plan.lights[0];
2015 assert!(planned.shadowed, "the caster past the light was skipped");
2016 assert_eq!(planned.wedges.len(), 4, "one wedge per rect edge, not none");
2017 let behind = Vec2::new(30.0, 0.0);
2020 assert!(
2021 planned.wedges.iter().any(|w| point_in_quad(
2022 behind,
2023 w.quad[0],
2024 w.quad[1],
2025 w.quad[2],
2026 w.quad[3]
2027 )),
2028 "no wedge covers the point behind the caster"
2029 );
2030
2031 let panned = plan_lighting(
2034 &[Light2d::point(Vec2::new(-350.0, -275.0), 10.0)],
2035 &[],
2036 &AmbientLight::default(),
2037 [-400.0, -300.0, 100.0, 50.0],
2038 &LightBudget::default(),
2039 );
2040 assert_eq!(panned.culled, 0, "a light inside the view was culled");
2041 assert_eq!(panned.lights.len(), 1);
2042 }
2043
2044 #[test]
2045 fn visible_world_rect_inverts_a_y_down_camera() {
2046 let camera = crate::screen_camera([800.0, 600.0]);
2047 let rect = visible_world_rect(camera).expect("an ortho camera has a rect");
2048 assert!((rect[0]).abs() < 1e-3 && (rect[1]).abs() < 1e-3);
2049 assert!((rect[2] - 800.0).abs() < 1e-3 && (rect[3] - 600.0).abs() < 1e-3);
2050 assert_eq!(visible_world_rect(Mat4::ZERO), None);
2051 assert_eq!(visible_world_rect(Mat4::from_scale(Vec3::ZERO)), None);
2052 }
2053
2054 #[test]
2055 fn occluder_contains_matches_solid_polygons() {
2056 let wall = Occluder2d::rect(Vec2::new(2.0, -2.0), Vec2::new(5.0, 2.0));
2057 assert!(wall.contains(Vec2::new(3.0, 0.0)));
2058 assert!(!wall.contains(Vec2::new(1.0, 0.0)));
2059 assert!(!Occluder2d::polygon(vec![Vec2::ZERO, Vec2::X]).contains(Vec2::ZERO));
2060 let cell = Occluder2d::cell([-1, 2], 8.0);
2062 assert_eq!(cell.aabb(), [-8.0, 16.0, 0.0, 24.0]);
2063 }
2064
2065 fn point_in_quad(p: Vec2, a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> bool {
2067 let side = |p: Vec2, q: Vec2, r: Vec2| cross(q - p, r - p);
2068 let s1 = side(a, b, p) >= 0.0;
2069 let s2 = side(b, c, p) >= 0.0;
2070 let s3 = side(c, d, p) >= 0.0;
2071 let s4 = side(d, a, p) >= 0.0;
2072 (s1 && s2 && s3 && s4) || (!s1 && !s2 && !s3 && !s4)
2073 }
2074}