Client LuaCsForBarotrauma
LightSource.cs
2 using Microsoft.Xna.Framework;
3 using Microsoft.Xna.Framework.Graphics;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Text;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma.Lights
11 {
13  {
14  public string Name => "Light Source";
15 
16  public bool Persistent;
17 
18 
19  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; } = new Dictionary<Identifier, SerializableProperty>();
20 
21  [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable]
22  public Color Color
23  {
24  get;
25  set;
26  }
27 
28  private float range;
29 
30  [Serialize(100.0f, IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)]
31  public float Range
32  {
33  get { return range; }
34  set
35  {
36  range = MathHelper.Clamp(value, 0.0f, 4096.0f);
37  TextureRange = range;
38  if (OverrideLightTexture != null)
39  {
40  TextureRange *= 1.0f + Math.Max(
41  Math.Abs(OverrideLightTexture.RelativeOrigin.X - 0.5f),
42  Math.Abs(OverrideLightTexture.RelativeOrigin.Y - 0.5f));
43  }
44  }
45  }
46 
47  [Serialize(1f, IsPropertySaveable.Yes), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)]
48  public float Scale { get; set; }
49 
50  [Serialize("0, 0", IsPropertySaveable.Yes), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)]
51  public Vector2 Offset { get; set; }
52 
53  [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)]
54  public float Rotation { get; set; }
55 
56  [Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+
57  " Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")]
58  public bool Directional { get; set; }
59 
60  public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation)));
61 
62  private float flicker;
63  [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")]
64  public float Flicker
65  {
66  get { return flicker; }
67  set
68  {
69  flicker = MathHelper.Clamp(value, 0.0f, 1.0f);
70  }
71  }
72 
73  [Editable, Serialize(1.0f, IsPropertySaveable.No, description: "How fast the light flickers.")]
74  public float FlickerSpeed
75  {
76  get;
77  set;
78  }
79 
80  private float pulseFrequency;
81  [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")]
82  public float PulseFrequency
83  {
84  get { return pulseFrequency; }
85  set
86  {
87  pulseFrequency = MathHelper.Clamp(value, 0.0f, 60.0f);
88  }
89  }
90 
91  private float pulseAmount;
92  [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")]
93  public float PulseAmount
94  {
95  get { return pulseAmount; }
96  set
97  {
98  pulseAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
99  }
100  }
101 
102  private float blinkFrequency;
103  [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")]
104  public float BlinkFrequency
105  {
106  get { return blinkFrequency; }
107  set
108  {
109  blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f);
110  }
111  }
112 
113  public float TextureRange
114  {
115  get;
116  private set;
117  }
118 
120  {
121  get;
122  private set;
123  }
124  //Additional sprite drawn on top of the lightsource. Ignores shadows.
125  //Can be used to make lamp sprites glow for example.
127  {
128  get;
129  private set;
130  }
131 
133  {
134  get;
135  private set;
136  }
137 
138  //Override the alpha value of the light sprite (if not set, the alpha of the light color is used)
139  //Can be used to make lamp sprites glow at full brightness even if the light itself is dim.
141 
143  {
144  Deserialize(element);
145 
146  foreach (var subElement in element.Elements())
147  {
148  switch (subElement.Name.ToString().ToLowerInvariant())
149  {
150  case "sprite":
151  case "lightsprite":
152  {
153  LightSprite = new Sprite(subElement);
154  float spriteAlpha = subElement.GetAttributeFloat("alpha", -1.0f);
155  if (spriteAlpha >= 0.0f)
156  {
157  OverrideLightSpriteAlpha = spriteAlpha;
158  }
159  }
160  break;
161  case "deformablesprite":
162  {
163  DeformableLightSpriteElement = subElement;
164  float spriteAlpha = subElement.GetAttributeFloat("alpha", -1.0f);
165  if (spriteAlpha >= 0.0f)
166  {
167  OverrideLightSpriteAlpha = spriteAlpha;
168  }
169  }
170  break;
171  case "lighttexture":
172  OverrideLightTexture = new Sprite(subElement);
173  //refresh TextureRange
174  Range = range;
175  break;
176  }
177  }
178  }
179 
180  public LightSourceParams(float range, Color color)
181  {
183  Range = range;
184  Color = color;
185  }
186 
187  public bool Deserialize(XElement element)
188  {
190  return SerializableProperties != null;
191  }
192 
193  public void Serialize(XElement element)
194  {
195  SerializableProperty.SerializeProperties(this, element, true);
196  }
197  }
198 
200  {
201  //how many pixels the position of the light needs to change for the light volume to be recalculated
202  const float MovementRecalculationThreshold = 10.0f;
203  //how many radians the light needs to rotate for the light volume to be recalculated
204  const float RotationRecalculationThreshold = 0.02f;
205 
206  private static Texture2D lightTexture;
207 
208  private VertexPositionColorTexture[] vertices;
209  private short[] indices;
210 
211  private readonly List<ConvexHullList> convexHullsInRange;
212 
213  private readonly HashSet<ConvexHull> visibleConvexHulls = new HashSet<ConvexHull>();
214 
215  public Texture2D texture;
216 
217  public SpriteEffects LightSpriteEffect;
218 
220 
221  private bool castShadows;
222  public bool CastShadows
223  {
224  get { return castShadows && !IsBackground; }
225  set { castShadows = value; }
226  }
227 
228  //what was the range of the light when lightvolumes were last calculated
229  private float prevCalculatedRange;
230  private Vector2 prevCalculatedPosition;
231 
232  //Which submarines' convex hulls are up to date? Resets when the item moves/rotates relative to the submarine.
233  //Can contain null (means convex hulls that aren't part of any submarine).
234  public HashSet<Submarine> HullsUpToDate = new HashSet<Submarine>();
235 
236  //do we need to recalculate the vertices of the light volume
237  private bool needsRecalculation;
238  private bool needsRecalculationWhenUpToDate;
239  public bool NeedsRecalculation
240  {
241  get
242  {
243  if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight") { return true; }
244  return needsRecalculation;
245  }
246  set
247  {
248  if (value)
249  {
250  foreach (ConvexHullList chList in convexHullsInRange)
251  {
252  chList.IsHidden.Clear();
253  }
254  }
255  needsRecalculation = value;
256  if (needsRecalculation && state != LightVertexState.UpToDate)
257  {
258  //if we're currently recalculating light vertices, mark that we need to recalculate them again after it's done
259  needsRecalculationWhenUpToDate = true;
260  }
261  }
262  }
263 
264 
265  //when were the vertices of the light volume last calculated
266  public float LastRecalculationTime { get; private set; }
267 
268 
269  private enum LightVertexState
270  {
271  UpToDate,
272  PendingRayCasts,
273  PendingVertexRecalculation,
274  }
275 
276  private LightVertexState state;
277 
278  private Vector2 calculatedDrawPos;
279 
280  private readonly Dictionary<Submarine, Vector2> diffToSub;
281 
282  private DynamicVertexBuffer lightVolumeBuffer;
283  private DynamicIndexBuffer lightVolumeIndexBuffer;
284  private int vertexCount;
285  private int indexCount;
286 
287  private Vector2 translateVertices;
288 
289  private readonly LightSourceParams lightSourceParams;
290 
291  public LightSourceParams LightSourceParams => lightSourceParams;
292 
293  private Vector2 position;
294  public Vector2 Position
295  {
296  get { return position; }
297  set
298  {
299  Vector2 moveAmount = value - position;
300  if (Math.Abs(moveAmount.X) < 0.1f && Math.Abs(moveAmount.Y) < 0.1f) { return; }
301  position = value;
302 
303  //translate light volume manually instead of doing a full recalculation when moving by a small amount
304  if (Vector2.DistanceSquared(prevCalculatedPosition, position) < MovementRecalculationThreshold * MovementRecalculationThreshold && vertices != null)
305  {
306  translateVertices = position - prevCalculatedPosition;
307  return;
308  }
309 
310  HullsUpToDate.Clear();
311  NeedsRecalculation = true;
312  }
313  }
314 
315  private float prevCalculatedRotation;
316  private float rotation;
317  public float Rotation
318  {
319  get { return rotation; }
320  set
321  {
322  if (Math.Abs(value - rotation) < 0.001f) { return; }
323  rotation = value;
324 
325  dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation));
326 
327  if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null)
328  {
329  return;
330  }
331 
332  HullsUpToDate.Clear();
333  NeedsRecalculation = true;
334  }
335  }
336 
337  private Vector2 dir = Vector2.UnitX;
338 
339  private Vector2 _spriteScale = Vector2.One;
340 
341  public Vector2 SpriteScale
342  {
343  get { return _spriteScale * lightSourceParams.Scale; }
344  set { _spriteScale = value; }
345  }
346 
348  {
349  get { return lightSourceParams.OverrideLightSpriteAlpha; }
350  set { lightSourceParams.OverrideLightSpriteAlpha = value; }
351  }
352 
353  public Vector2 WorldPosition
354  {
355  get { return (ParentSub == null) ? position : position + ParentSub.Position; }
356  }
357 
358  public static Texture2D LightTexture
359  {
360  get
361  {
362  if (lightTexture == null)
363  {
364  lightTexture = TextureLoader.FromFile("Content/Lights/pointlight_bright.png");
365  }
366 
367  return lightTexture;
368  }
369  }
370 
372  {
373  get { return lightSourceParams.OverrideLightTexture; }
374  }
375 
377  {
378  get { return lightSourceParams.LightSprite; }
379  }
380 
381  private Vector2 OverrideLightTextureOrigin => OverrideLightTexture.Origin + LightSourceParams.Offset;
382 
383  public Color Color
384  {
385  get { return lightSourceParams.Color; }
386  set { lightSourceParams.Color = value; }
387  }
388 
389  public float CurrentBrightness
390  {
391  get;
392  private set;
393  }
394 
395  public float Range
396  {
397  get { return lightSourceParams.Range; }
398  set
399  {
400 
401  lightSourceParams.Range = value;
402  if (Math.Abs(prevCalculatedRange - lightSourceParams.Range) < 10.0f) return;
403 
404  HullsUpToDate.Clear();
405  NeedsRecalculation = true;
406  prevCalculatedRange = lightSourceParams.Range;
407  }
408  }
409 
410  public float Priority;
411 
412  public float PriorityMultiplier = 1.0f;
413 
414  private Vector2 lightTextureTargetSize;
415 
416  public Vector2 LightTextureTargetSize
417  {
418  get => lightTextureTargetSize;
419  set
420  {
421  NeedsRecalculation = true;
422  lightTextureTargetSize = value;
423  HullsUpToDate.Clear();
424  }
425  }
426 
427  public Vector2 LightTextureOffset { get; set; }
428  public Vector2 LightTextureScale { get; set; } = Vector2.One;
429 
430  public float TextureRange
431  {
432  get
433  {
434  return lightSourceParams.TextureRange;
435  }
436  }
437 
441  public bool IsBackground
442  {
443  get;
444  set;
445  }
446 
448  {
449  get;
450  set;
451  }
452 
454  {
455  get;
456  private set;
457  }
458 
459  public bool Enabled = true;
460 
461  private readonly ISerializableEntity conditionalTarget;
462  private readonly PropertyConditional.LogicalOperatorType logicalOperator;
463  private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
464 
465  public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null)
466  : this(Vector2.Zero, 100.0f, Color.White, null)
467  {
468  lightSourceParams = new LightSourceParams(element);
469  CastShadows = element.GetAttributeBool("castshadows", true);
470  logicalOperator = element.GetAttributeEnum("comparison", logicalOperator);
471 
472  if (lightSourceParams.DeformableLightSpriteElement != null)
473  {
474  DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true);
475  }
476 
477  this.conditionalTarget = conditionalTarget;
478  foreach (var subElement in element.Elements())
479  {
480  switch (subElement.Name.ToString().ToLowerInvariant())
481  {
482  case "conditional":
483  conditionals.AddRange(PropertyConditional.FromXElement(subElement));
484  break;
485  }
486  }
487  }
488 
489  public LightSource(LightSourceParams lightSourceParams)
490  : this(Vector2.Zero, 100.0f, Color.White, null)
491  {
492  this.lightSourceParams = lightSourceParams;
493  lightSourceParams.Persistent = true;
494  if (lightSourceParams.DeformableLightSpriteElement != null)
495  {
496  DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true);
497  }
498  }
499 
500  public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true)
501  {
502  convexHullsInRange = new List<ConvexHullList>();
503  this.ParentSub = submarine;
504  this.position = position;
505  lightSourceParams = new LightSourceParams(range, color);
506  CastShadows = true;
508  diffToSub = new Dictionary<Submarine, Vector2>();
509  if (addLight) { GameMain.LightManager.AddLight(this); }
510  }
511 
512  public void Update(float time)
513  {
514  float brightness = 1.0f;
515  if (lightSourceParams.BlinkFrequency > 0.0f)
516  {
517  float blinkTimer = (time * lightSourceParams.BlinkFrequency) % 1.0f;
518  if (blinkTimer > 0.5f)
519  {
520  CurrentBrightness = 0.0f;
521  return;
522  }
523  }
524  if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f)
525  {
526  float pulseState = (time * lightSourceParams.PulseFrequency) % 1.0f;
527  //oscillate between 0-1
528  brightness *= 1.0f - (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount;
529  }
530  if (lightSourceParams.Flicker > 0.0f && lightSourceParams.FlickerSpeed > 0.0f)
531  {
532  float flickerState = (time * lightSourceParams.FlickerSpeed) % 255;
533  brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker;
534  }
535  CurrentBrightness = brightness;
536  }
537 
541  private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub)
542  {
543  var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub);
544  if (fullChList == null) { return; }
545 
546  //used to check whether the lightsource hits the target hull if the light is directional
547  Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange;
548  Vector2 normal = new Vector2(-ray.Y, ray.X);
549 
550  chList.List.Clear();
551  foreach (var convexHull in fullChList.List)
552  {
553  if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; }
554  if (lightSourceParams.Directional)
555  {
556  Rectangle bounds = convexHull.BoundingBox;
557  //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left
558  bounds.Y -= bounds.Height;
559 
560  //the ray can't hit if
561  // center is in the opposite direction from the ray (cheapest check first)
562  if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 &&
563  /*ray doesn't hit the convex hull*/
564  !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) &&
565  /*normal vectors of the ray don't hit the convex hull */
566  !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _))
567  {
568  continue;
569  }
570  }
571  chList.List.Add(convexHull);
572  }
573  chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch));
574  chList.HasBeenVisible.RemoveWhere(ch => !chList.List.Contains(ch));
575  HullsUpToDate.Add(sub);
576  }
577 
582  private void CheckConvexHullsInRange()
583  {
584  foreach (Submarine sub in Submarine.Loaded)
585  {
586  CheckHullsInRange(sub);
587  }
588  //check convex hulls that aren't in any sub
589  CheckHullsInRange(null);
590  }
591 
592  private void CheckHullsInRange(Submarine sub)
593  {
594  //find the list of convexhulls that belong to the sub
595  ConvexHullList chList = convexHullsInRange.FirstOrDefault(chList => chList.Submarine == sub);
596 
597  //not found -> create one
598  if (chList == null)
599  {
600  chList = new ConvexHullList(sub);
601  convexHullsInRange.Add(chList);
602  NeedsRecalculation = true;
603  }
604 
605  foreach (var ch in chList.List)
606  {
607  if (ch.LastVertexChangeTime > LastRecalculationTime &&
608  (!chList.IsHidden.Contains(ch) || chList.HasBeenVisible.Contains(ch)))
609  {
610  NeedsRecalculation = true;
611  break;
612  }
613  }
614 
615  Vector2 lightPos = position;
616  if (ParentSub == null)
617  {
618  //light and the convexhulls are both outside
619  if (sub == null)
620  {
621  if (!HullsUpToDate.Contains(null))
622  {
623  RefreshConvexHullList(chList, lightPos, null);
624  }
625  }
626  //light is outside, convexhulls inside a sub
627  else
628  {
629  lightPos -= sub.Position;
630 
631  Rectangle subBorders = sub.Borders;
632  subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height);
633 
634  //only draw if the light overlaps with the sub
635  if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders))
636  {
637  if (chList.List.Count > 0) { NeedsRecalculation = true; }
638  chList.List.Clear();
639  return;
640  }
641 
642  RefreshConvexHullList(chList, lightPos, sub);
643  }
644  }
645  else
646  {
647  //light is inside, convexhull outside
648  if (sub == null) { return; }
649 
650  //light and convexhull are both inside the same sub
651  if (sub == ParentSub)
652  {
653  if (!HullsUpToDate.Contains(sub))
654  {
655  RefreshConvexHullList(chList, lightPos, sub);
656  }
657  }
658  //light and convexhull are inside different subs
659  else
660  {
661  if (sub.DockedTo.Contains(ParentSub) && HullsUpToDate.Contains(sub)) { return; }
662 
663  lightPos -= (sub.Position - ParentSub.Position);
664 
665  Rectangle subBorders = sub.Borders;
666  subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height);
667 
668  //don't draw any shadows if the light doesn't overlap with the borders of the sub
669  if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders))
670  {
671  if (chList.List.Count > 0) { NeedsRecalculation = true; }
672  chList.List.Clear();
673  return;
674  }
675 
676  //recalculate vertices if the subs have moved > 5 px relative to each other
677  Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition;
678  if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff))
679  {
680  diffToSub.Add(sub, diff);
681  NeedsRecalculation = true;
682  }
683  else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f)
684  {
685  diffToSub[sub] = diff;
686  NeedsRecalculation = true;
687  }
688 
689  RefreshConvexHullList(chList, lightPos, sub);
690  }
691  }
692  }
693 
694  private static readonly object mutex = new object();
695 
696  private readonly List<Segment> visibleSegments = new List<Segment>();
697  private readonly List<SegmentPoint> points = new List<SegmentPoint>();
698  private readonly List<Vector2> verts = new List<Vector2>();
699  private readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4];
700  private void FindRaycastHits()
701  {
702  if (!CastShadows || Range < 1.0f || Color.A < 1)
703  {
704  state = LightVertexState.PendingVertexRecalculation;
705  return;
706  }
707 
708  Vector2 drawPos = position;
709  if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
710 
711  visibleSegments.Clear();
712  foreach (ConvexHullList chList in convexHullsInRange)
713  {
714  foreach (ConvexHull hull in chList.List)
715  {
716  if (hull.IsInvalid || !hull.Enabled) { continue; }
717  if (!chList.IsHidden.Contains(hull) || chList.HasBeenVisible.Contains(hull))
718  {
719  //find convexhull segments that are close enough and facing towards the light source
720  lock (mutex)
721  {
722  hull.RefreshWorldPositions();
723  hull.GetVisibleSegments(drawPos, visibleSegments);
724  foreach (var visibleSegment in visibleSegments)
725  {
726  if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null)
727  {
728  visibleSegment.SubmarineDrawPos = visibleSegment.ConvexHull.ParentEntity.Submarine.DrawPosition;
729  }
730  }
731  }
732  }
733  }
734  foreach (ConvexHull hull in chList.List)
735  {
736  if (!hull.Enabled)
737  {
738  //if the hull is not enabled, we cannot determine if it's visible or hidden from the point of view of the light source
739  //so let's not mark it as hidden, but instead consider it as something that has been visible, so we know to recalculate if/when it becomes enabled again
740  chList.IsHidden.Remove(hull);
741  chList.HasBeenVisible.Add(hull);
742  continue;
743  }
744 
745  //mark convex hulls as hidden at this point, they're removed if we find any of the segments to be visible
746  chList.IsHidden.Add(hull);
747  }
748  }
749 
750  state = LightVertexState.PendingRayCasts;
751  GameMain.LightManager.AddRayCastTask(this, drawPos, rotation);
752  }
753 
754  const float MinPointDistance = 6;
755 
756  public void RayCastTask(Vector2 drawPos, float rotation)
757  {
758  visibleConvexHulls.Clear();
759 
760  Vector2 drawOffset = Vector2.Zero;
761  float boundsExtended = TextureRange;
762  if (OverrideLightTexture != null)
763  {
764  float cosAngle = (float)Math.Cos(rotation);
765  float sinAngle = -(float)Math.Sin(rotation);
766 
767  var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
768 
769  Vector2 origin = OverrideLightTextureOrigin;
770 
771  origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
772  origin -= Vector2.One * 0.5f;
773 
774  if (Math.Abs(origin.X) >= 0.45f || Math.Abs(origin.Y) >= 0.45f)
775  {
776  boundsExtended += 5.0f;
777  }
778 
779  origin *= TextureRange;
780 
781  drawOffset.X = -origin.X * cosAngle - origin.Y * sinAngle;
782  drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle;
783  }
784 
785  //add a square-shaped boundary to make sure we've got something to construct the triangles from
786  //even if there aren't enough hull segments around the light source
787 
788  //(might be more effective to calculate if we actually need these extra points)
789  Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended);
790  Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended);
791  boundaryCorners[0] = new SegmentPoint(boundsMax, null);
792  boundaryCorners[1] = new SegmentPoint(new Vector2(boundsMax.X, boundsMin.Y), null);
793  boundaryCorners[2] = new SegmentPoint(boundsMin, null);
794  boundaryCorners[3] = new SegmentPoint(new Vector2(boundsMin.X, boundsMax.Y), null);
795 
796  for (int i = 0; i < 4; i++)
797  {
798  var s = new Segment(boundaryCorners[i], boundaryCorners[(i + 1) % 4], null);
799  visibleSegments.Add(s);
800  }
801 
802  lock (mutex)
803  {
804  //Generate new points at the intersections between segments
805  //This is necessary for the light volume to generate properly on some subs
806  for (int i = 0; i < visibleSegments.Count; i++)
807  {
808  Vector2 p1a = visibleSegments[i].Start.WorldPos;
809  Vector2 p1b = visibleSegments[i].End.WorldPos;
810 
811  for (int j = i + 1; j < visibleSegments.Count; j++)
812  {
813  //ignore intersections between parallel axis-aligned segments
814  if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned &&
815  visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal)
816  {
817  continue;
818  }
819 
820  Vector2 p2a = visibleSegments[j].Start.WorldPos;
821  Vector2 p2b = visibleSegments[j].End.WorldPos;
822 
823  if (Vector2.DistanceSquared(p1a, p2a) < 5.0f ||
824  Vector2.DistanceSquared(p1a, p2b) < 5.0f ||
825  Vector2.DistanceSquared(p1b, p2a) < 5.0f ||
826  Vector2.DistanceSquared(p1b, p2b) < 5.0f)
827  {
828  continue;
829  }
830 
831  bool intersects;
832  Vector2 intersection = Vector2.Zero;
833  if (visibleSegments[i].IsAxisAligned)
834  {
835  intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection);
836  }
837  else if (visibleSegments[j].IsAxisAligned)
838  {
839  intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection);
840  }
841  else
842  {
843  intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection);
844  }
845 
846  if (intersects)
847  {
848  SegmentPoint start = visibleSegments[i].Start;
849  SegmentPoint end = visibleSegments[i].End;
850  SegmentPoint mid = new SegmentPoint(intersection, null);
851  mid.Pos -= visibleSegments[i].SubmarineDrawPos;
852 
853  if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f ||
854  Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f)
855  {
856  continue;
857  }
858 
859  Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull)
860  {
861  IsHorizontal = visibleSegments[i].IsHorizontal,
862  };
863 
864  Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull)
865  {
866  IsHorizontal = visibleSegments[i].IsHorizontal
867  };
868 
869  visibleSegments[i] = seg1;
870  visibleSegments.Insert(i + 1, seg2);
871  i--;
872  break;
873  }
874  }
875  }
876 
877  points.Clear();
878  //remove segments that fall out of bounds
879  for (int i = 0; i < visibleSegments.Count; i++)
880  {
881  Segment s = visibleSegments[i];
882  if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
883  Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f ||
884  Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
885  Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f)
886  {
887  visibleSegments.RemoveAt(i);
888  i--;
889  }
890  else
891  {
892  points.Add(s.Start);
893  points.Add(s.End);
894  }
895  }
896 
897  //remove points that are very close to each other
898  //+= 2 because the points are added in pairs above, i.e. 0 and 1 belong to the same segment
899  for (int i = 0; i < points.Count; i += 2)
900  {
901  for (int j = Math.Min(i + 2, points.Count - 1); j > i; j--)
902  {
903  if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance &&
904  Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance)
905  {
906  points.RemoveAt(j);
907  }
908  }
909  }
910 
911  try
912  {
913  var compareCW = new CompareSegmentPointCW(drawPos);
914  points.Sort(compareCW);
915  }
916  catch (Exception e)
917  {
918  StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, Hull verts:\n");
919  foreach (SegmentPoint sp in points)
920  {
921  sb.AppendLine(sp.Pos.ToString());
922  }
923  DebugConsole.ThrowError(sb.ToString(), e);
924  }
925 
926  visibleSegments.Sort((s1, s2) =>
927  MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos)
928  .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos)));
929 
930  verts.Clear();
931  foreach (SegmentPoint p in points)
932  {
933  Vector2 diff = p.WorldPos - drawPos;
934  float dist = diff.Length();
935  //light source exactly at the segment point, don't cast a shadow (normalizing the vector would lead to NaN)
936  if (dist <= 0.0001f) { continue; }
937  Vector2 dir = diff / dist;
938  Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance;
939 
940  //do two slightly offset raycasts to hit the segment itself and whatever's behind it
941  var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments);
942  if (intersection1.index < 0) { return; }
943  var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments);
944  if (intersection2.index < 0) { return; }
945 
946  Segment seg1 = visibleSegments[intersection1.index];
947  Segment seg2 = visibleSegments[intersection2.index];
948 
949  bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f;
950  bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f;
951 
952  bool markAsVisible = false;
953  if (isPoint1 && isPoint2)
954  {
955  //hit at the current segmentpoint -> place the segmentpoint into the list
956  verts.Add(p.WorldPos);
957  markAsVisible = true;
958  }
959  else if (intersection1.index != intersection2.index)
960  {
961  //the raycasts landed on different segments
962  //we definitely want to generate new geometry here
963  if (isPoint1)
964  {
965  TryAddPoints(intersection2.pos, p.WorldPos, drawPos, verts);
966  markAsVisible = true;
967  }
968  else if (isPoint2)
969  {
970  TryAddPoints(intersection1.pos, p.WorldPos, drawPos, verts);
971  markAsVisible = true;
972  }
973  else
974  {
975  //didn't hit either point, completely obstructed
976  verts.Add(intersection1.pos);
977  verts.Add(intersection2.pos);
978  }
979  static void TryAddPoints(Vector2 intersection, Vector2 point, Vector2 refPos, List<Vector2> verts)
980  {
981  //* 0.8f because we don't care about obstacles that are very close (intersecting walls),
982  //only about obstacles that are clearly between the point and the refPos
983  bool intersectionCloserThanPoint = Vector2.DistanceSquared(intersection, refPos) < Vector2.DistanceSquared(point, refPos) * 0.8f;
984  //if the raycast hit a segment that's closer than the point we're aiming towards,
985  //it means we didn't hit a segment behind the point, but something that's obstructing it
986  //= we don't want to add vertex at that obstructed point, it could make the light go through obstacles
987  if (!intersectionCloserThanPoint)
988  {
989  verts.Add(point);
990  }
991  verts.Add(intersection);
992  }
993  }
994  if (markAsVisible)
995  {
996  visibleConvexHulls.Add(p.ConvexHull);
997  visibleConvexHulls.Add(seg1.ConvexHull);
998  visibleConvexHulls.Add(seg2.ConvexHull);
999  }
1000  //if neither of the conditions above are met, we just assume
1001  //that the raycasts both resulted on the same segment
1002  //and creating geometry here would be wasteful
1003  }
1004  }
1005 
1006  //remove points that are very close to each other
1007  for (int i = 0; i < verts.Count - 1; i++)
1008  {
1009  for (int j = verts.Count - 1; j > i; j--)
1010  {
1011  if (Math.Abs(verts[i].X - verts[j].X) < MinPointDistance &&
1012  Math.Abs(verts[i].Y - verts[j].Y) < MinPointDistance)
1013  {
1014  verts.RemoveAt(j);
1015  }
1016  }
1017  }
1018 
1019  try
1020  {
1021  var compareCW = new CompareCW(drawPos);
1022  verts.Sort(compareCW);
1023  }
1024  catch (Exception e)
1025  {
1026  StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, verts:\n");
1027  foreach (Vector2 v in verts)
1028  {
1029  sb.AppendLine(v.ToString());
1030  }
1031  DebugConsole.ThrowError(sb.ToString(), e);
1032  }
1033 
1034 
1035  calculatedDrawPos = drawPos;
1036  state = LightVertexState.PendingVertexRecalculation;
1037  }
1038 
1039  private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List<Segment> segments)
1040  {
1041  Vector2? closestIntersection = null;
1042  int segment = -1;
1043 
1044  float minX = Math.Min(rayStart.X, rayEnd.X);
1045  float maxX = Math.Max(rayStart.X, rayEnd.X);
1046  float minY = Math.Min(rayStart.Y, rayEnd.Y);
1047  float maxY = Math.Max(rayStart.Y, rayEnd.Y);
1048 
1049  for (int i = 0; i < segments.Count; i++)
1050  {
1051  Segment s = segments[i];
1052 
1053  //segment's end position always has a higher or equal y coordinate than the start position
1054  //so we can do this comparison and skip segments that are at the wrong side of the ray
1055  /*if (s.End.WorldPos.Y < s.Start.WorldPos.Y)
1056  {
1057  System.Diagnostics.Debug.Assert(s.End.WorldPos.Y >= s.Start.WorldPos.Y,
1058  "LightSource raycast failed. Segment's end positions should never be below the start position. Parent entity: " + (s.ConvexHull?.ParentEntity == null ? "null" : s.ConvexHull.ParentEntity.ToString()));
1059  }*/
1060  if (s.Start.WorldPos.Y > maxY || s.End.WorldPos.Y < minY) { continue; }
1061  //same for the x-axis
1062  if (s.Start.WorldPos.X > s.End.WorldPos.X)
1063  {
1064  if (s.Start.WorldPos.X < minX) { continue; }
1065  if (s.End.WorldPos.X > maxX) { continue; }
1066  }
1067  else
1068  {
1069  if (s.End.WorldPos.X < minX) { continue; }
1070  if (s.Start.WorldPos.X > maxX) { continue; }
1071  }
1072 
1073  bool intersects;
1074  Vector2 intersection;
1075  if (s.IsAxisAligned)
1076  {
1077  intersects = MathUtils.GetAxisAlignedLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, s.IsHorizontal, out intersection);
1078  }
1079  else
1080  {
1081  intersects = MathUtils.GetLineSegmentIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection);
1082  }
1083 
1084  if (intersects)
1085  {
1086  closestIntersection = intersection;
1087 
1088  rayEnd = intersection;
1089  minX = Math.Min(rayStart.X, rayEnd.X);
1090  maxX = Math.Max(rayStart.X, rayEnd.X);
1091  minY = Math.Min(rayStart.Y, rayEnd.Y);
1092  maxY = Math.Max(rayStart.Y, rayEnd.Y);
1093 
1094  segment = i;
1095  }
1096  }
1097 
1098  return (segment, closestIntersection == null ? rayEnd : (Vector2)closestIntersection);
1099  }
1100 
1101 
1102  private void CalculateLightVertices(List<Vector2> rayCastHits)
1103  {
1104  vertexCount = rayCastHits.Count * 2 + 1;
1105  indexCount = (rayCastHits.Count) * 9;
1106 
1107  //recreate arrays if they're too small or excessively large
1108  if (vertices == null || vertices.Length < vertexCount || vertices.Length > vertexCount * 3)
1109  {
1110  vertices = new VertexPositionColorTexture[vertexCount];
1111  indices = new short[indexCount];
1112  }
1113 
1114  Vector2 drawPos = calculatedDrawPos;
1115 
1116  Vector2 uvOffset = Vector2.Zero;
1117  Vector2 overrideTextureDims = Vector2.One;
1118  Vector2 dir = this.dir;
1119  if (OverrideLightTexture != null)
1120  {
1121  overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
1122 
1123  Vector2 origin = OverrideLightTextureOrigin;
1124  if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
1125  {
1126  origin.X = OverrideLightTexture.SourceRect.Width - origin.X;
1127  dir = -dir;
1128  }
1129  if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; }
1130  uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f);
1131  }
1132 
1133  // Add a vertex for the center of the mesh
1134  vertices[0] = new VertexPositionColorTexture(new Vector3(position.X, position.Y, 0),
1135  Color.White, GetUV(new Vector2(0.5f, 0.5f) + uvOffset, LightSpriteEffect));
1136 
1137  //hacky fix to exc excessively large light volumes (they used to be up to 4x the range of the light if there was nothing to block the rays).
1138  //might want to tweak the raycast logic in a way that this isn't necessary
1139  /*float boundRadius = Range * 1.1f / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
1140  Rectangle boundArea = new Rectangle((int)(drawPos.X - boundRadius), (int)(drawPos.Y + boundRadius), (int)(boundRadius * 2), (int)(boundRadius * 2));
1141  for (int i = 0; i < rayCastHits.Count; i++)
1142  {
1143  if (MathUtils.GetLineRectangleIntersection(drawPos, rayCastHits[i], boundArea, out Vector2 intersection))
1144  {
1145  rayCastHits[i] = intersection;
1146  }
1147  }*/
1148 
1149  // Add all the other encounter points as vertices
1150  // storing their world position as UV coordinates
1151  for (int i = 0; i < rayCastHits.Count; i++)
1152  {
1153  Vector2 vertex = rayCastHits[i];
1154 
1155  //we'll use the previous and next vertices to calculate the normals
1156  //of the two segments this vertex belongs to
1157  //so we can add new vertices based on these normals
1158  Vector2 prevVertex = rayCastHits[i > 0 ? i - 1 : rayCastHits.Count - 1];
1159  Vector2 nextVertex = rayCastHits[i < rayCastHits.Count - 1 ? i + 1 : 0];
1160 
1161  Vector2 rawDiff = vertex - drawPos;
1162 
1163  //calculate normal of first segment
1164  Vector2 nDiff1 = vertex - nextVertex;
1165  nDiff1 = new Vector2(-nDiff1.Y, nDiff1.X);
1166  nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y));
1167  //if the normal is pointing towards the light origin
1168  //rather than away from it, invert it
1169  if (Vector2.DistanceSquared(nDiff1, rawDiff) > Vector2.DistanceSquared(-nDiff1, rawDiff)) nDiff1 = -nDiff1;
1170 
1171  //calculate normal of second segment
1172  Vector2 nDiff2 = prevVertex - vertex;
1173  nDiff2 = new Vector2(-nDiff2.Y, nDiff2.X);
1174  nDiff2 /= Math.Max(Math.Abs(nDiff2.X), Math.Abs(nDiff2.Y));
1175  //if the normal is pointing towards the light origin
1176  //rather than away from it, invert it
1177  if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2;
1178 
1179  //add the normals together and use some magic numbers to create
1180  //a somewhat useful/good-looking blur
1181  float blurDistance = 25.0f;
1182  Vector2 nDiff = nDiff1 * blurDistance;
1183  if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection))
1184  {
1185  nDiff = intersection - vertex;
1186  if (nDiff.LengthSquared() > 100.0f * 100.0f)
1187  {
1188  nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y));
1189  nDiff *= 100.0f;
1190  }
1191  }
1192 
1193  Vector2 diff = rawDiff;
1194  diff /= Range * 2.0f;
1195  if (OverrideLightTexture != null)
1196  {
1197  //calculate texture coordinates based on the light's rotation
1198  Vector2 originDiff = diff;
1199 
1200  diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y;
1201  diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X;
1202  diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
1203  diff += uvOffset;
1204  }
1205 
1206  //finally, create the vertices
1207  VertexPositionColorTexture fullVert = new VertexPositionColorTexture(new Vector3(position.X + rawDiff.X, position.Y + rawDiff.Y, 0),
1208  Color.White, GetUV(new Vector2(0.5f, 0.5f) + diff, LightSpriteEffect));
1209  VertexPositionColorTexture fadeVert = new VertexPositionColorTexture(new Vector3(position.X + rawDiff.X + nDiff.X, position.Y + rawDiff.Y + nDiff.Y, 0),
1210  Color.White * 0.0f, GetUV(new Vector2(0.5f, 0.5f) + diff, LightSpriteEffect));
1211 
1212  vertices[1 + i * 2] = fullVert;
1213  vertices[1 + i * 2 + 1] = fadeVert;
1214  }
1215 
1216  // Compute the indices to form triangles
1217  for (int i = 0; i < rayCastHits.Count - 1; i++)
1218  {
1219  //main light body
1220  indices[i * 9] = 0;
1221  indices[i * 9 + 1] = (short)((i * 2 + 3) % vertexCount);
1222  indices[i * 9 + 2] = (short)((i * 2 + 1) % vertexCount);
1223 
1224  //faded light
1225  indices[i * 9 + 3] = (short)((i * 2 + 1) % vertexCount);
1226  indices[i * 9 + 4] = (short)((i * 2 + 3) % vertexCount);
1227  indices[i * 9 + 5] = (short)((i * 2 + 4) % vertexCount);
1228 
1229  indices[i * 9 + 6] = (short)((i * 2 + 2) % vertexCount);
1230  indices[i * 9 + 7] = (short)((i * 2 + 1) % vertexCount);
1231  indices[i * 9 + 8] = (short)((i * 2 + 4) % vertexCount);
1232  }
1233 
1234  //main light body
1235  indices[(rayCastHits.Count - 1) * 9] = 0;
1236  indices[(rayCastHits.Count - 1) * 9 + 1] = (short)(1);
1237  indices[(rayCastHits.Count - 1) * 9 + 2] = (short)(vertexCount - 2);
1238 
1239  //faded light
1240  indices[(rayCastHits.Count - 1) * 9 + 3] = (short)(1);
1241  indices[(rayCastHits.Count - 1) * 9 + 4] = (short)(vertexCount - 1);
1242  indices[(rayCastHits.Count - 1) * 9 + 5] = (short)(vertexCount - 2);
1243 
1244  indices[(rayCastHits.Count - 1) * 9 + 6] = (short)(1);
1245  indices[(rayCastHits.Count - 1) * 9 + 7] = (short)(2);
1246  indices[(rayCastHits.Count - 1) * 9 + 8] = (short)(vertexCount - 1);
1247 
1248  //TODO: a better way to determine the size of the vertex buffer and handle changes in size?
1249  //now we just create a buffer for 64 verts and make it larger if needed
1250  if (lightVolumeBuffer == null)
1251  {
1252  lightVolumeBuffer = new DynamicVertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, Math.Max(64, (int)(vertexCount * 1.5)), BufferUsage.None);
1253  lightVolumeIndexBuffer = new DynamicIndexBuffer(GameMain.Instance.GraphicsDevice, typeof(short), Math.Max(64 * 3, (int)(indexCount * 1.5)), BufferUsage.None);
1254  }
1255  else if (vertexCount > lightVolumeBuffer.VertexCount || indexCount > lightVolumeIndexBuffer.IndexCount)
1256  {
1257  lightVolumeBuffer.Dispose();
1258  lightVolumeIndexBuffer.Dispose();
1259 
1260  lightVolumeBuffer = new DynamicVertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, (int)(vertexCount * 1.5), BufferUsage.None);
1261  lightVolumeIndexBuffer = new DynamicIndexBuffer(GameMain.Instance.GraphicsDevice, typeof(short), (int)(indexCount * 1.5), BufferUsage.None);
1262  }
1263 
1264  lightVolumeBuffer.SetData<VertexPositionColorTexture>(vertices, 0, vertexCount);
1265  lightVolumeIndexBuffer.SetData<short>(indices, 0, indexCount);
1266 
1267  static Vector2 GetUV(Vector2 vert, SpriteEffects effects)
1268  {
1269  if (effects == SpriteEffects.FlipHorizontally)
1270  {
1271  vert.X = 1.0f - vert.X;
1272  }
1273  else if (effects == SpriteEffects.FlipVertically)
1274  {
1275  vert.Y = 1.0f - vert.Y;
1276  }
1277  else if (effects == (SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically))
1278  {
1279  vert.X = 1.0f - vert.X;
1280  vert.Y = 1.0f - vert.Y;
1281  }
1282  vert.Y = 1.0f - vert.Y;
1283  return vert;
1284  }
1285 
1286  translateVertices = Vector2.Zero;
1287  prevCalculatedPosition = position;
1288  prevCalculatedRotation = rotation;
1289  }
1290 
1295  public void DrawSprite(SpriteBatch spriteBatch, Camera cam)
1296  {
1297  //uncomment if you want to visualize the bounds of the light volume
1298  /*if (GameMain.DebugDraw)
1299  {
1300  Vector2 drawPos = position;
1301  if (ParentSub != null)
1302  {
1303  drawPos += ParentSub.DrawPosition;
1304  }
1305  drawPos.Y = -drawPos.Y;
1306 
1307  float bounds = TextureRange;
1308 
1309  if (OverrideLightTexture != null)
1310  {
1311  var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
1312 
1313  Vector2 origin = OverrideLightTextureOrigin;
1314 
1315  origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
1316  origin *= TextureRange;
1317 
1318  drawPos.X += origin.X * dir.Y + origin.Y * dir.X;
1319  drawPos.Y += origin.X * dir.X + origin.Y * dir.Y;
1320  }
1321 
1322  //add a square-shaped boundary to make sure we've got something to construct the triangles from
1323  //even if there aren't enough hull segments around the light source
1324 
1325  //(might be more effective to calculate if we actually need these extra points)
1326  var boundaryCorners = new SegmentPoint[] {
1327  new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y + bounds), null),
1328  new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y - bounds), null),
1329  new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y - bounds), null),
1330  new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y + bounds), null)
1331  };
1332 
1333  for (int i = 0; i < 4; i++)
1334  {
1335  GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3);
1336  }
1337  }*/
1338 
1339  if (DeformableLightSprite != null)
1340  {
1342  Vector2 drawPos = position;
1343  if (ParentSub != null)
1344  {
1345  drawPos += ParentSub.DrawPosition;
1346  }
1347  if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
1348  {
1349  origin.X = DeformableLightSprite.Sprite.SourceRect.Width - origin.X;
1350  }
1351  if (LightSpriteEffect == SpriteEffects.FlipVertically)
1352  {
1353  origin.Y = DeformableLightSprite.Sprite.SourceRect.Height - origin.Y;
1354  }
1355 
1357  cam, new Vector3(drawPos, 0.0f),
1358  origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale,
1359  new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness),
1360  LightSpriteEffect == SpriteEffects.FlipVertically);
1361  }
1362 
1363  if (LightSprite != null)
1364  {
1365  Vector2 origin = LightSprite.Origin + LightSourceParams.GetOffset();
1366  if ((LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally)
1367  {
1368  origin.X = LightSprite.SourceRect.Width - origin.X;
1369  }
1370  if ((LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically)
1371  {
1372  origin.Y = LightSprite.SourceRect.Height - origin.Y;
1373  }
1374 
1375  Vector2 drawPos = position;
1376  if (ParentSub != null)
1377  {
1378  drawPos += ParentSub.DrawPosition;
1379  }
1380  drawPos.Y = -drawPos.Y;
1381 
1382  Color color = new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness);
1383 
1384  if (LightTextureTargetSize != Vector2.Zero)
1385  {
1386  LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color: color, startOffset: LightTextureOffset, textureScale: LightTextureScale);
1387  }
1388  else
1389  {
1390  LightSprite.Draw(
1391  spriteBatch, drawPos,
1392  color,
1393  origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale, LightSpriteEffect);
1394  }
1395  }
1396 
1397  if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f)
1398  {
1399  Vector2 drawPos = position;
1400  if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
1401  drawPos.Y = -drawPos.Y;
1402 
1404  {
1405  GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, GUIStyle.Orange, isFilled: false);
1406  GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, GUIStyle.Orange);
1407  GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, GUIStyle.Orange);
1408  }
1409 
1410  //visualize light recalculations
1411  float timeSinceRecalculation = (float)Timing.TotalTime - LastRecalculationTime;
1412  if (timeSinceRecalculation < 0.1f)
1413  {
1414  GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUIStyle.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true);
1415  GUI.DrawLine(spriteBatch, drawPos - Vector2.One * Range, drawPos + Vector2.One * Range, Color);
1416  GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * Range, drawPos + new Vector2(1.0f, -1.0f) * Range, Color);
1417  }
1418  }
1419  }
1420 
1421  public void CheckConditionals()
1422  {
1423  if (conditionals.None()) { return; }
1424  if (conditionalTarget == null) { return; }
1425  Enabled = PropertyConditional.CheckConditionals(conditionalTarget, conditionals, logicalOperator);
1426  }
1427 
1428  public void DebugDrawVertices(SpriteBatch spriteBatch)
1429  {
1430  if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; }
1431 
1432  //commented out because this is mostly just useful in very specific situations, otherwise it just makes debugdraw very messy
1433  //(you may also need to add a condition here that only draws this for the specific light you're interested in)
1434  if (GameMain.DebugDraw && vertices != null)
1435  {
1436  if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight")
1437 
1438  for (int i = 1; i < vertices.Length - 1; i += 2)
1439  {
1440  Vector2 vert1 = new Vector2(vertices[i].Position.X, vertices[i].Position.Y);
1441  int nextIndex = (i + 2) % vertices.Length;
1442  //the first vertex is the one at the position of the light source, skip that one
1443  //(we just want to draw lines between the vertices at the circumference of the light volume)
1444  if (nextIndex == 0) { nextIndex++; }
1445  Vector2 vert2 = new Vector2(vertices[nextIndex].Position.X, vertices[nextIndex].Position.Y);
1446  if (ParentSub != null)
1447  {
1448  vert1 += ParentSub.DrawPosition;
1449  vert2 += ParentSub.DrawPosition;
1450  }
1451  vert1.Y = -vert1.Y;
1452  vert2.Y = -vert2.Y;
1453 
1454  var randomColor = ToolBox.GradientLerp(i / (float)vertices.Length, Color.Magenta, Color.Blue, Color.Yellow, Color.Green, Color.Cyan, Color.Red, Color.Purple, Color.Yellow);
1455  GUI.DrawLine(spriteBatch, vert1, vert2, randomColor * 0.8f, width: 2);
1456  }
1457  }
1458  }
1459 
1460  public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount)
1461  {
1462  if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; }
1463 
1464  //if the light doesn't cast shadows, we can simply render the texture without having to calculate the light volume
1465  if (!CastShadows)
1466  {
1467  Texture2D currentTexture = texture ?? LightTexture;
1468  if (OverrideLightTexture != null) { currentTexture = OverrideLightTexture.Texture; }
1469 
1470  Vector2 center = OverrideLightTexture == null ?
1471  new Vector2(currentTexture.Width / 2, currentTexture.Height / 2) :
1473  float scale = Range / (currentTexture.Width / 2.0f);
1474 
1475  Vector2 drawPos = position;
1476  if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
1477  drawPos.Y = -drawPos.Y;
1478 
1479  spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation + MathHelper.ToRadians(LightSourceParams.Rotation), center, scale, SpriteEffects.None, 1);
1480  return;
1481  }
1482 
1483  CheckConvexHullsInRange();
1484 
1485  if (NeedsRecalculation && allowRecalculation)
1486  {
1487  if (state == LightVertexState.UpToDate)
1488  {
1489  recalculationCount++;
1490  FindRaycastHits();
1491  }
1492  else if (state == LightVertexState.PendingVertexRecalculation)
1493  {
1494  if (verts == null)
1495  {
1496  #if DEBUG
1497  DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}");
1498  #endif
1499  Enabled = false;
1500  return;
1501  }
1502 
1503  foreach (var visibleConvexHull in visibleConvexHulls)
1504  {
1505  foreach (var convexHullList in convexHullsInRange)
1506  {
1507  convexHullList.IsHidden.Remove(visibleConvexHull);
1508  convexHullList.HasBeenVisible.Add(visibleConvexHull);
1509  }
1510  }
1511 
1512  CalculateLightVertices(verts);
1513 
1514  LastRecalculationTime = (float)Timing.TotalTime;
1515  NeedsRecalculation = needsRecalculationWhenUpToDate;
1516  needsRecalculationWhenUpToDate = false;
1517 
1518  state = LightVertexState.UpToDate;
1519  }
1520  }
1521 
1522  if (vertexCount == 0) { return; }
1523 
1524  Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition;
1525  lightEffect.World =
1526  Matrix.CreateTranslation(-new Vector3(position, 0.0f)) *
1527  Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) *
1528  Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) *
1529  transform;
1530 
1531 
1532  lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f;
1533  if (OverrideLightTexture != null)
1534  {
1535  lightEffect.Texture = OverrideLightTexture.Texture;
1536  }
1537  else
1538  {
1539  lightEffect.Texture = texture ?? LightTexture;
1540  }
1541  lightEffect.CurrentTechnique.Passes[0].Apply();
1542 
1543  GameMain.Instance.GraphicsDevice.SetVertexBuffer(lightVolumeBuffer);
1544  GameMain.Instance.GraphicsDevice.Indices = lightVolumeIndexBuffer;
1545 
1546  GameMain.Instance.GraphicsDevice.DrawIndexedPrimitives
1547  (
1548  PrimitiveType.TriangleList, 0, 0, indexCount / 3
1549  );
1550  }
1551 
1552  public void Reset()
1553  {
1554  HullsUpToDate.Clear();
1555  convexHullsInRange.Clear();
1556  diffToSub.Clear();
1557  NeedsRecalculation = true;
1558 
1559  vertexCount = 0;
1560  if (lightVolumeBuffer != null)
1561  {
1562  lightVolumeBuffer.Dispose();
1563  lightVolumeBuffer = null;
1564  }
1565 
1566  indexCount = 0;
1567  if (lightVolumeIndexBuffer != null)
1568  {
1569  lightVolumeIndexBuffer.Dispose();
1570  lightVolumeIndexBuffer = null;
1571  }
1572  }
1573 
1574  public void Remove()
1575  {
1576  if (!lightSourceParams.Persistent)
1577  {
1578  LightSprite?.Remove();
1580  }
1581 
1583  DeformableLightSprite = null;
1584 
1585  lightVolumeBuffer?.Dispose();
1586  lightVolumeBuffer = null;
1587 
1588  lightVolumeIndexBuffer?.Dispose();
1589  lightVolumeIndexBuffer = null;
1590 
1591  GameMain.LightManager.RemoveLight(this);
1592  }
1593  }
1594 }
float? Zoom
Definition: Camera.cs:78
float GetAttributeFloat(string key, float def)
bool GetAttributeBool(string key, bool def)
void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool mirror=false, bool invert=false)
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static bool DebugDraw
Definition: GameMain.cs:29
static GameMain Instance
Definition: GameMain.cs:144
static List< ConvexHullList > HullLists
Definition: ConvexHull.cs:83
readonly List< ConvexHull > List
Definition: ConvexHull.cs:16
HashSet< ConvexHull > IsHidden
Definition: ConvexHull.cs:14
readonly Submarine Submarine
Definition: ConvexHull.cs:13
HashSet< ConvexHull > HasBeenVisible
Definition: ConvexHull.cs:15
LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true)
Definition: LightSource.cs:500
LightSource(ContentXElement element, ISerializableEntity conditionalTarget=null)
Definition: LightSource.cs:465
void RayCastTask(Vector2 drawPos, float rotation)
Definition: LightSource.cs:756
void DebugDrawVertices(SpriteBatch spriteBatch)
DeformableSprite DeformableLightSprite
Definition: LightSource.cs:454
HashSet< Submarine > HullsUpToDate
Definition: LightSource.cs:234
LightSource(LightSourceParams lightSourceParams)
Definition: LightSource.cs:489
static Texture2D LightTexture
Definition: LightSource.cs:359
LightSourceParams LightSourceParams
Definition: LightSource.cs:291
void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount)
bool IsBackground
Background lights are drawn behind submarines and they don't cast shadows.
Definition: LightSource.cs:442
void DrawSprite(SpriteBatch spriteBatch, Camera cam)
Draws the optional "light sprite", just a simple sprite with no shadows
LightSourceParams(float range, Color color)
Definition: LightSource.cs:180
bool Deserialize(XElement element)
Definition: LightSource.cs:187
void Serialize(XElement element)
Definition: LightSource.cs:193
ContentXElement DeformableLightSpriteElement
Definition: LightSource.cs:133
Dictionary< Identifier, SerializableProperty > SerializableProperties
Definition: LightSource.cs:19
LightSourceParams(ContentXElement element)
Definition: LightSource.cs:142
readonly Identifier Identifier
Definition: Prefab.cs:34
Conditionals are used by some in-game mechanics to require one or more conditions to be met for those...
static bool CheckConditionals(ISerializableEntity conditionalTarget, IEnumerable< PropertyConditional > conditionals, LogicalOperatorType logicalOperator)
static IEnumerable< PropertyConditional > FromXElement(ContentXElement element, Predicate< XAttribute >? predicate=null)
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault=false, bool ignoreEditable=false)
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
void DrawTiled(ISpriteBatch spriteBatch, Vector2 position, Vector2 targetSize, float rotation=0f, Vector2? origin=null, Color? color=null, Vector2? startOffset=null, Vector2? textureScale=null, float? depth=null, SpriteEffects? spriteEffects=null)