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 (!needsRecalculation && 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 (!convexHull.Enabled) { continue; }
554  if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; }
555  if (lightSourceParams.Directional)
556  {
557  Rectangle bounds = convexHull.BoundingBox;
558  //invert because GetLineRectangleIntersection uses the messed up rects that start from top-left
559  bounds.Y -= bounds.Height;
560 
561  //the ray can't hit if
562  // center is in the opposite direction from the ray (cheapest check first)
563  if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 &&
564  /*ray doesn't hit the convex hull*/
565  !MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) &&
566  /*normal vectors of the ray don't hit the convex hull */
567  !MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _))
568  {
569  continue;
570  }
571  }
572  chList.List.Add(convexHull);
573  }
574  chList.IsHidden.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 && !chList.IsHidden.Contains(ch))
608  {
609  NeedsRecalculation = true;
610  break;
611  }
612  }
613 
614  Vector2 lightPos = position;
615  if (ParentSub == null)
616  {
617  //light and the convexhulls are both outside
618  if (sub == null)
619  {
620  if (!HullsUpToDate.Contains(null))
621  {
622  RefreshConvexHullList(chList, lightPos, null);
623  }
624  }
625  //light is outside, convexhulls inside a sub
626  else
627  {
628  lightPos -= sub.Position;
629 
630  Rectangle subBorders = sub.Borders;
631  subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height);
632 
633  //only draw if the light overlaps with the sub
634  if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders))
635  {
636  if (chList.List.Count > 0) { NeedsRecalculation = true; }
637  chList.List.Clear();
638  return;
639  }
640 
641  RefreshConvexHullList(chList, lightPos, sub);
642  }
643  }
644  else
645  {
646  //light is inside, convexhull outside
647  if (sub == null) { return; }
648 
649  //light and convexhull are both inside the same sub
650  if (sub == ParentSub)
651  {
652  if (!HullsUpToDate.Contains(sub))
653  {
654  RefreshConvexHullList(chList, lightPos, sub);
655  }
656  }
657  //light and convexhull are inside different subs
658  else
659  {
660  if (sub.DockedTo.Contains(ParentSub) && HullsUpToDate.Contains(sub)) { return; }
661 
662  lightPos -= (sub.Position - ParentSub.Position);
663 
664  Rectangle subBorders = sub.Borders;
665  subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height);
666 
667  //don't draw any shadows if the light doesn't overlap with the borders of the sub
668  if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders))
669  {
670  if (chList.List.Count > 0) { NeedsRecalculation = true; }
671  chList.List.Clear();
672  return;
673  }
674 
675  //recalculate vertices if the subs have moved > 5 px relative to each other
676  Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition;
677  if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff))
678  {
679  diffToSub.Add(sub, diff);
680  NeedsRecalculation = true;
681  }
682  else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f)
683  {
684  diffToSub[sub] = diff;
685  NeedsRecalculation = true;
686  }
687 
688  RefreshConvexHullList(chList, lightPos, sub);
689  }
690  }
691  }
692 
693  private static readonly object mutex = new object();
694 
695  private readonly List<Segment> visibleSegments = new List<Segment>();
696  private readonly List<SegmentPoint> points = new List<SegmentPoint>();
697  private readonly List<Vector2> verts = new List<Vector2>();
698  private readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4];
699  private void FindRaycastHits()
700  {
701  if (!CastShadows || Range < 1.0f || Color.A < 1)
702  {
703  state = LightVertexState.PendingVertexRecalculation;
704  return;
705  }
706 
707  Vector2 drawPos = position;
708  if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
709 
710  visibleSegments.Clear();
711  foreach (ConvexHullList chList in convexHullsInRange)
712  {
713  foreach (ConvexHull hull in chList.List)
714  {
715  if (hull.IsInvalid) { continue; }
716  if (!chList.IsHidden.Contains(hull))
717  {
718  //find convexhull segments that are close enough and facing towards the light source
719  lock (mutex)
720  {
721  hull.RefreshWorldPositions();
722  hull.GetVisibleSegments(drawPos, visibleSegments);
723  foreach (var visibleSegment in visibleSegments)
724  {
725  if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null)
726  {
727  visibleSegment.SubmarineDrawPos = visibleSegment.ConvexHull.ParentEntity.Submarine.DrawPosition;
728  }
729  }
730  }
731  }
732  }
733  foreach (ConvexHull hull in chList.List)
734  {
735  chList.IsHidden.Add(hull);
736  }
737  }
738 
739  state = LightVertexState.PendingRayCasts;
740  GameMain.LightManager.AddRayCastTask(this, drawPos, rotation);
741  }
742 
743  const float MinPointDistance = 6;
744 
745  public void RayCastTask(Vector2 drawPos, float rotation)
746  {
747  visibleConvexHulls.Clear();
748 
749  Vector2 drawOffset = Vector2.Zero;
750  float boundsExtended = TextureRange;
751  if (OverrideLightTexture != null)
752  {
753  float cosAngle = (float)Math.Cos(rotation);
754  float sinAngle = -(float)Math.Sin(rotation);
755 
756  var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
757 
758  Vector2 origin = OverrideLightTextureOrigin;
759 
760  origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
761  origin -= Vector2.One * 0.5f;
762 
763  if (Math.Abs(origin.X) >= 0.45f || Math.Abs(origin.Y) >= 0.45f)
764  {
765  boundsExtended += 5.0f;
766  }
767 
768  origin *= TextureRange;
769 
770  drawOffset.X = -origin.X * cosAngle - origin.Y * sinAngle;
771  drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle;
772  }
773 
774  //add a square-shaped boundary to make sure we've got something to construct the triangles from
775  //even if there aren't enough hull segments around the light source
776 
777  //(might be more effective to calculate if we actually need these extra points)
778  Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended);
779  Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended);
780  boundaryCorners[0] = new SegmentPoint(boundsMax, null);
781  boundaryCorners[1] = new SegmentPoint(new Vector2(boundsMax.X, boundsMin.Y), null);
782  boundaryCorners[2] = new SegmentPoint(boundsMin, null);
783  boundaryCorners[3] = new SegmentPoint(new Vector2(boundsMin.X, boundsMax.Y), null);
784 
785  for (int i = 0; i < 4; i++)
786  {
787  var s = new Segment(boundaryCorners[i], boundaryCorners[(i + 1) % 4], null);
788  visibleSegments.Add(s);
789  }
790 
791  lock (mutex)
792  {
793  //Generate new points at the intersections between segments
794  //This is necessary for the light volume to generate properly on some subs
795  for (int i = 0; i < visibleSegments.Count; i++)
796  {
797  Vector2 p1a = visibleSegments[i].Start.WorldPos;
798  Vector2 p1b = visibleSegments[i].End.WorldPos;
799 
800  for (int j = i + 1; j < visibleSegments.Count; j++)
801  {
802  //ignore intersections between parallel axis-aligned segments
803  if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned &&
804  visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal)
805  {
806  continue;
807  }
808 
809  Vector2 p2a = visibleSegments[j].Start.WorldPos;
810  Vector2 p2b = visibleSegments[j].End.WorldPos;
811 
812  if (Vector2.DistanceSquared(p1a, p2a) < 5.0f ||
813  Vector2.DistanceSquared(p1a, p2b) < 5.0f ||
814  Vector2.DistanceSquared(p1b, p2a) < 5.0f ||
815  Vector2.DistanceSquared(p1b, p2b) < 5.0f)
816  {
817  continue;
818  }
819 
820  bool intersects;
821  Vector2 intersection = Vector2.Zero;
822  if (visibleSegments[i].IsAxisAligned)
823  {
824  intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection);
825  }
826  else if (visibleSegments[j].IsAxisAligned)
827  {
828  intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection);
829  }
830  else
831  {
832  intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection);
833  }
834 
835  if (intersects)
836  {
837  SegmentPoint start = visibleSegments[i].Start;
838  SegmentPoint end = visibleSegments[i].End;
839  SegmentPoint mid = new SegmentPoint(intersection, null);
840  mid.Pos -= visibleSegments[i].SubmarineDrawPos;
841 
842  if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f ||
843  Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f)
844  {
845  continue;
846  }
847 
848  Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull)
849  {
850  IsHorizontal = visibleSegments[i].IsHorizontal,
851  };
852 
853  Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull)
854  {
855  IsHorizontal = visibleSegments[i].IsHorizontal
856  };
857 
858  visibleSegments[i] = seg1;
859  visibleSegments.Insert(i + 1, seg2);
860  i--;
861  break;
862  }
863  }
864  }
865 
866  points.Clear();
867  //remove segments that fall out of bounds
868  for (int i = 0; i < visibleSegments.Count; i++)
869  {
870  Segment s = visibleSegments[i];
871  if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
872  Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f ||
873  Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
874  Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f)
875  {
876  visibleSegments.RemoveAt(i);
877  i--;
878  }
879  else
880  {
881  points.Add(s.Start);
882  points.Add(s.End);
883  }
884  }
885 
886  //remove points that are very close to each other
887  //+= 2 because the points are added in pairs above, i.e. 0 and 1 belong to the same segment
888  for (int i = 0; i < points.Count; i += 2)
889  {
890  for (int j = Math.Min(i + 2, points.Count - 1); j > i; j--)
891  {
892  if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < MinPointDistance &&
893  Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < MinPointDistance)
894  {
895  points.RemoveAt(j);
896  }
897  }
898  }
899 
900  try
901  {
902  var compareCW = new CompareSegmentPointCW(drawPos);
903  points.Sort(compareCW);
904  }
905  catch (Exception e)
906  {
907  StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, Hull verts:\n");
908  foreach (SegmentPoint sp in points)
909  {
910  sb.AppendLine(sp.Pos.ToString());
911  }
912  DebugConsole.ThrowError(sb.ToString(), e);
913  }
914 
915  visibleSegments.Sort((s1, s2) =>
916  MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos)
917  .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos)));
918 
919  verts.Clear();
920  foreach (SegmentPoint p in points)
921  {
922  Vector2 diff = p.WorldPos - drawPos;
923  float dist = diff.Length();
924  //light source exactly at the segment point, don't cast a shadow (normalizing the vector would lead to NaN)
925  if (dist <= 0.0001f) { continue; }
926  Vector2 dir = diff / dist;
927  Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * MinPointDistance;
928 
929  //do two slightly offset raycasts to hit the segment itself and whatever's behind it
930  var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments);
931  if (intersection1.index < 0) { return; }
932  var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments);
933  if (intersection2.index < 0) { return; }
934 
935  Segment seg1 = visibleSegments[intersection1.index];
936  Segment seg2 = visibleSegments[intersection2.index];
937 
938  bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f;
939  bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f;
940 
941  bool markAsVisible = false;
942  if (isPoint1 && isPoint2)
943  {
944  //hit at the current segmentpoint -> place the segmentpoint into the list
945  verts.Add(p.WorldPos);
946  markAsVisible = true;
947  }
948  else if (intersection1.index != intersection2.index)
949  {
950  //the raycasts landed on different segments
951  //we definitely want to generate new geometry here
952  if (isPoint1)
953  {
954  TryAddPoints(intersection2.pos, p.WorldPos, drawPos, verts);
955  markAsVisible = true;
956  }
957  else if (isPoint2)
958  {
959  TryAddPoints(intersection1.pos, p.WorldPos, drawPos, verts);
960  markAsVisible = true;
961  }
962  else
963  {
964  //didn't hit either point, completely obstructed
965  verts.Add(intersection1.pos);
966  verts.Add(intersection2.pos);
967  }
968  static void TryAddPoints(Vector2 intersection, Vector2 point, Vector2 refPos, List<Vector2> verts)
969  {
970  //* 0.8f because we don't care about obstacles that are very close (intersecting walls),
971  //only about obstacles that are clearly between the point and the refPos
972  bool intersectionCloserThanPoint = Vector2.DistanceSquared(intersection, refPos) < Vector2.DistanceSquared(point, refPos) * 0.8f;
973  //if the raycast hit a segment that's closer than the point we're aiming towards,
974  //it means we didn't hit a segment behind the point, but something that's obstructing it
975  //= we don't want to add vertex at that obstructed point, it could make the light go through obstacles
976  if (!intersectionCloserThanPoint)
977  {
978  verts.Add(point);
979  }
980  verts.Add(intersection);
981  }
982  }
983  if (markAsVisible)
984  {
985  visibleConvexHulls.Add(p.ConvexHull);
986  visibleConvexHulls.Add(seg1.ConvexHull);
987  visibleConvexHulls.Add(seg2.ConvexHull);
988  }
989  //if neither of the conditions above are met, we just assume
990  //that the raycasts both resulted on the same segment
991  //and creating geometry here would be wasteful
992  }
993  }
994 
995  //remove points that are very close to each other
996  for (int i = 0; i < verts.Count - 1; i++)
997  {
998  for (int j = verts.Count - 1; j > i; j--)
999  {
1000  if (Math.Abs(verts[i].X - verts[j].X) < MinPointDistance &&
1001  Math.Abs(verts[i].Y - verts[j].Y) < MinPointDistance)
1002  {
1003  verts.RemoveAt(j);
1004  }
1005  }
1006  }
1007 
1008  try
1009  {
1010  var compareCW = new CompareCW(drawPos);
1011  verts.Sort(compareCW);
1012  }
1013  catch (Exception e)
1014  {
1015  StringBuilder sb = new StringBuilder($"Constructing light volumes failed ({nameof(CompareSegmentPointCW)})! Light pos: {drawPos}, verts:\n");
1016  foreach (Vector2 v in verts)
1017  {
1018  sb.AppendLine(v.ToString());
1019  }
1020  DebugConsole.ThrowError(sb.ToString(), e);
1021  }
1022 
1023 
1024  calculatedDrawPos = drawPos;
1025  state = LightVertexState.PendingVertexRecalculation;
1026  }
1027 
1028  private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List<Segment> segments)
1029  {
1030  Vector2? closestIntersection = null;
1031  int segment = -1;
1032 
1033  float minX = Math.Min(rayStart.X, rayEnd.X);
1034  float maxX = Math.Max(rayStart.X, rayEnd.X);
1035  float minY = Math.Min(rayStart.Y, rayEnd.Y);
1036  float maxY = Math.Max(rayStart.Y, rayEnd.Y);
1037 
1038  for (int i = 0; i < segments.Count; i++)
1039  {
1040  Segment s = segments[i];
1041 
1042  //segment's end position always has a higher or equal y coordinate than the start position
1043  //so we can do this comparison and skip segments that are at the wrong side of the ray
1044  /*if (s.End.WorldPos.Y < s.Start.WorldPos.Y)
1045  {
1046  System.Diagnostics.Debug.Assert(s.End.WorldPos.Y >= s.Start.WorldPos.Y,
1047  "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()));
1048  }*/
1049  if (s.Start.WorldPos.Y > maxY || s.End.WorldPos.Y < minY) { continue; }
1050  //same for the x-axis
1051  if (s.Start.WorldPos.X > s.End.WorldPos.X)
1052  {
1053  if (s.Start.WorldPos.X < minX) { continue; }
1054  if (s.End.WorldPos.X > maxX) { continue; }
1055  }
1056  else
1057  {
1058  if (s.End.WorldPos.X < minX) { continue; }
1059  if (s.Start.WorldPos.X > maxX) { continue; }
1060  }
1061 
1062  bool intersects;
1063  Vector2 intersection;
1064  if (s.IsAxisAligned)
1065  {
1066  intersects = MathUtils.GetAxisAlignedLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, s.IsHorizontal, out intersection);
1067  }
1068  else
1069  {
1070  intersects = MathUtils.GetLineSegmentIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection);
1071  }
1072 
1073  if (intersects)
1074  {
1075  closestIntersection = intersection;
1076 
1077  rayEnd = intersection;
1078  minX = Math.Min(rayStart.X, rayEnd.X);
1079  maxX = Math.Max(rayStart.X, rayEnd.X);
1080  minY = Math.Min(rayStart.Y, rayEnd.Y);
1081  maxY = Math.Max(rayStart.Y, rayEnd.Y);
1082 
1083  segment = i;
1084  }
1085  }
1086 
1087  return (segment, closestIntersection == null ? rayEnd : (Vector2)closestIntersection);
1088  }
1089 
1090 
1091  private void CalculateLightVertices(List<Vector2> rayCastHits)
1092  {
1093  vertexCount = rayCastHits.Count * 2 + 1;
1094  indexCount = (rayCastHits.Count) * 9;
1095 
1096  //recreate arrays if they're too small or excessively large
1097  if (vertices == null || vertices.Length < vertexCount || vertices.Length > vertexCount * 3)
1098  {
1099  vertices = new VertexPositionColorTexture[vertexCount];
1100  indices = new short[indexCount];
1101  }
1102 
1103  Vector2 drawPos = calculatedDrawPos;
1104 
1105  Vector2 uvOffset = Vector2.Zero;
1106  Vector2 overrideTextureDims = Vector2.One;
1107  Vector2 dir = this.dir;
1108  if (OverrideLightTexture != null)
1109  {
1110  overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
1111 
1112  Vector2 origin = OverrideLightTextureOrigin;
1113  if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
1114  {
1115  origin.X = OverrideLightTexture.SourceRect.Width - origin.X;
1116  dir = -dir;
1117  }
1118  if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; }
1119  uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f);
1120  }
1121 
1122  // Add a vertex for the center of the mesh
1123  vertices[0] = new VertexPositionColorTexture(new Vector3(position.X, position.Y, 0),
1124  Color.White, GetUV(new Vector2(0.5f, 0.5f) + uvOffset, LightSpriteEffect));
1125 
1126  //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).
1127  //might want to tweak the raycast logic in a way that this isn't necessary
1128  /*float boundRadius = Range * 1.1f / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
1129  Rectangle boundArea = new Rectangle((int)(drawPos.X - boundRadius), (int)(drawPos.Y + boundRadius), (int)(boundRadius * 2), (int)(boundRadius * 2));
1130  for (int i = 0; i < rayCastHits.Count; i++)
1131  {
1132  if (MathUtils.GetLineRectangleIntersection(drawPos, rayCastHits[i], boundArea, out Vector2 intersection))
1133  {
1134  rayCastHits[i] = intersection;
1135  }
1136  }*/
1137 
1138  // Add all the other encounter points as vertices
1139  // storing their world position as UV coordinates
1140  for (int i = 0; i < rayCastHits.Count; i++)
1141  {
1142  Vector2 vertex = rayCastHits[i];
1143 
1144  //we'll use the previous and next vertices to calculate the normals
1145  //of the two segments this vertex belongs to
1146  //so we can add new vertices based on these normals
1147  Vector2 prevVertex = rayCastHits[i > 0 ? i - 1 : rayCastHits.Count - 1];
1148  Vector2 nextVertex = rayCastHits[i < rayCastHits.Count - 1 ? i + 1 : 0];
1149 
1150  Vector2 rawDiff = vertex - drawPos;
1151 
1152  //calculate normal of first segment
1153  Vector2 nDiff1 = vertex - nextVertex;
1154  nDiff1 = new Vector2(-nDiff1.Y, nDiff1.X);
1155  nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y));
1156  //if the normal is pointing towards the light origin
1157  //rather than away from it, invert it
1158  if (Vector2.DistanceSquared(nDiff1, rawDiff) > Vector2.DistanceSquared(-nDiff1, rawDiff)) nDiff1 = -nDiff1;
1159 
1160  //calculate normal of second segment
1161  Vector2 nDiff2 = prevVertex - vertex;
1162  nDiff2 = new Vector2(-nDiff2.Y, nDiff2.X);
1163  nDiff2 /= Math.Max(Math.Abs(nDiff2.X), Math.Abs(nDiff2.Y));
1164  //if the normal is pointing towards the light origin
1165  //rather than away from it, invert it
1166  if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2;
1167 
1168  //add the normals together and use some magic numbers to create
1169  //a somewhat useful/good-looking blur
1170  float blurDistance = 25.0f;
1171  Vector2 nDiff = nDiff1 * blurDistance;
1172  if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection))
1173  {
1174  nDiff = intersection - vertex;
1175  if (nDiff.LengthSquared() > 100.0f * 100.0f)
1176  {
1177  nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y));
1178  nDiff *= 100.0f;
1179  }
1180  }
1181 
1182  Vector2 diff = rawDiff;
1183  diff /= Range * 2.0f;
1184  if (OverrideLightTexture != null)
1185  {
1186  //calculate texture coordinates based on the light's rotation
1187  Vector2 originDiff = diff;
1188 
1189  diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y;
1190  diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X;
1191  diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
1192  diff += uvOffset;
1193  }
1194 
1195  //finally, create the vertices
1196  VertexPositionColorTexture fullVert = new VertexPositionColorTexture(new Vector3(position.X + rawDiff.X, position.Y + rawDiff.Y, 0),
1197  Color.White, GetUV(new Vector2(0.5f, 0.5f) + diff, LightSpriteEffect));
1198  VertexPositionColorTexture fadeVert = new VertexPositionColorTexture(new Vector3(position.X + rawDiff.X + nDiff.X, position.Y + rawDiff.Y + nDiff.Y, 0),
1199  Color.White * 0.0f, GetUV(new Vector2(0.5f, 0.5f) + diff, LightSpriteEffect));
1200 
1201  vertices[1 + i * 2] = fullVert;
1202  vertices[1 + i * 2 + 1] = fadeVert;
1203  }
1204 
1205  // Compute the indices to form triangles
1206  for (int i = 0; i < rayCastHits.Count - 1; i++)
1207  {
1208  //main light body
1209  indices[i * 9] = 0;
1210  indices[i * 9 + 1] = (short)((i * 2 + 3) % vertexCount);
1211  indices[i * 9 + 2] = (short)((i * 2 + 1) % vertexCount);
1212 
1213  //faded light
1214  indices[i * 9 + 3] = (short)((i * 2 + 1) % vertexCount);
1215  indices[i * 9 + 4] = (short)((i * 2 + 3) % vertexCount);
1216  indices[i * 9 + 5] = (short)((i * 2 + 4) % vertexCount);
1217 
1218  indices[i * 9 + 6] = (short)((i * 2 + 2) % vertexCount);
1219  indices[i * 9 + 7] = (short)((i * 2 + 1) % vertexCount);
1220  indices[i * 9 + 8] = (short)((i * 2 + 4) % vertexCount);
1221  }
1222 
1223  //main light body
1224  indices[(rayCastHits.Count - 1) * 9] = 0;
1225  indices[(rayCastHits.Count - 1) * 9 + 1] = (short)(1);
1226  indices[(rayCastHits.Count - 1) * 9 + 2] = (short)(vertexCount - 2);
1227 
1228  //faded light
1229  indices[(rayCastHits.Count - 1) * 9 + 3] = (short)(1);
1230  indices[(rayCastHits.Count - 1) * 9 + 4] = (short)(vertexCount - 1);
1231  indices[(rayCastHits.Count - 1) * 9 + 5] = (short)(vertexCount - 2);
1232 
1233  indices[(rayCastHits.Count - 1) * 9 + 6] = (short)(1);
1234  indices[(rayCastHits.Count - 1) * 9 + 7] = (short)(2);
1235  indices[(rayCastHits.Count - 1) * 9 + 8] = (short)(vertexCount - 1);
1236 
1237  //TODO: a better way to determine the size of the vertex buffer and handle changes in size?
1238  //now we just create a buffer for 64 verts and make it larger if needed
1239  if (lightVolumeBuffer == null)
1240  {
1241  lightVolumeBuffer = new DynamicVertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, Math.Max(64, (int)(vertexCount * 1.5)), BufferUsage.None);
1242  lightVolumeIndexBuffer = new DynamicIndexBuffer(GameMain.Instance.GraphicsDevice, typeof(short), Math.Max(64 * 3, (int)(indexCount * 1.5)), BufferUsage.None);
1243  }
1244  else if (vertexCount > lightVolumeBuffer.VertexCount || indexCount > lightVolumeIndexBuffer.IndexCount)
1245  {
1246  lightVolumeBuffer.Dispose();
1247  lightVolumeIndexBuffer.Dispose();
1248 
1249  lightVolumeBuffer = new DynamicVertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, (int)(vertexCount * 1.5), BufferUsage.None);
1250  lightVolumeIndexBuffer = new DynamicIndexBuffer(GameMain.Instance.GraphicsDevice, typeof(short), (int)(indexCount * 1.5), BufferUsage.None);
1251  }
1252 
1253  lightVolumeBuffer.SetData<VertexPositionColorTexture>(vertices, 0, vertexCount);
1254  lightVolumeIndexBuffer.SetData<short>(indices, 0, indexCount);
1255 
1256  static Vector2 GetUV(Vector2 vert, SpriteEffects effects)
1257  {
1258  if (effects == SpriteEffects.FlipHorizontally)
1259  {
1260  vert.X = 1.0f - vert.X;
1261  }
1262  else if (effects == SpriteEffects.FlipVertically)
1263  {
1264  vert.Y = 1.0f - vert.Y;
1265  }
1266  else if (effects == (SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically))
1267  {
1268  vert.X = 1.0f - vert.X;
1269  vert.Y = 1.0f - vert.Y;
1270  }
1271  vert.Y = 1.0f - vert.Y;
1272  return vert;
1273  }
1274 
1275  translateVertices = Vector2.Zero;
1276  prevCalculatedPosition = position;
1277  prevCalculatedRotation = rotation;
1278  }
1279 
1284  public void DrawSprite(SpriteBatch spriteBatch, Camera cam)
1285  {
1286  //uncomment if you want to visualize the bounds of the light volume
1287  /*if (GameMain.DebugDraw)
1288  {
1289  Vector2 drawPos = position;
1290  if (ParentSub != null)
1291  {
1292  drawPos += ParentSub.DrawPosition;
1293  }
1294  drawPos.Y = -drawPos.Y;
1295 
1296  float bounds = TextureRange;
1297 
1298  if (OverrideLightTexture != null)
1299  {
1300  var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
1301 
1302  Vector2 origin = OverrideLightTextureOrigin;
1303 
1304  origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
1305  origin *= TextureRange;
1306 
1307  drawPos.X += origin.X * dir.Y + origin.Y * dir.X;
1308  drawPos.Y += origin.X * dir.X + origin.Y * dir.Y;
1309  }
1310 
1311  //add a square-shaped boundary to make sure we've got something to construct the triangles from
1312  //even if there aren't enough hull segments around the light source
1313 
1314  //(might be more effective to calculate if we actually need these extra points)
1315  var boundaryCorners = new SegmentPoint[] {
1316  new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y + bounds), null),
1317  new SegmentPoint(new Vector2(drawPos.X + bounds, drawPos.Y - bounds), null),
1318  new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y - bounds), null),
1319  new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y + bounds), null)
1320  };
1321 
1322  for (int i = 0; i < 4; i++)
1323  {
1324  GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3);
1325  }
1326  }*/
1327 
1328  if (DeformableLightSprite != null)
1329  {
1331  Vector2 drawPos = position;
1332  if (ParentSub != null)
1333  {
1334  drawPos += ParentSub.DrawPosition;
1335  }
1336  if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
1337  {
1338  origin.X = DeformableLightSprite.Sprite.SourceRect.Width - origin.X;
1339  }
1340  if (LightSpriteEffect == SpriteEffects.FlipVertically)
1341  {
1342  origin.Y = DeformableLightSprite.Sprite.SourceRect.Height - origin.Y;
1343  }
1344 
1346  cam, new Vector3(drawPos, 0.0f),
1347  origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale,
1348  new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness),
1349  LightSpriteEffect == SpriteEffects.FlipVertically);
1350  }
1351 
1352  if (LightSprite != null)
1353  {
1354  Vector2 origin = LightSprite.Origin + LightSourceParams.GetOffset();
1355  if ((LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally)
1356  {
1357  origin.X = LightSprite.SourceRect.Width - origin.X;
1358  }
1359  if ((LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically)
1360  {
1361  origin.Y = LightSprite.SourceRect.Height - origin.Y;
1362  }
1363 
1364  Vector2 drawPos = position;
1365  if (ParentSub != null)
1366  {
1367  drawPos += ParentSub.DrawPosition;
1368  }
1369  drawPos.Y = -drawPos.Y;
1370 
1371  Color color = new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness);
1372 
1373  if (LightTextureTargetSize != Vector2.Zero)
1374  {
1375  LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color: color, startOffset: LightTextureOffset, textureScale: LightTextureScale);
1376  }
1377  else
1378  {
1379  LightSprite.Draw(
1380  spriteBatch, drawPos,
1381  color,
1382  origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale, LightSpriteEffect);
1383  }
1384  }
1385 
1386  if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f)
1387  {
1388  Vector2 drawPos = position;
1389  if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
1390  drawPos.Y = -drawPos.Y;
1391 
1393  {
1394  GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, GUIStyle.Orange, isFilled: false);
1395  GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, GUIStyle.Orange);
1396  GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, GUIStyle.Orange);
1397  }
1398 
1399  //visualize light recalculations
1400  float timeSinceRecalculation = (float)Timing.TotalTime - LastRecalculationTime;
1401  if (timeSinceRecalculation < 0.1f)
1402  {
1403  GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUIStyle.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true);
1404  GUI.DrawLine(spriteBatch, drawPos - Vector2.One * Range, drawPos + Vector2.One * Range, Color);
1405  GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * Range, drawPos + new Vector2(1.0f, -1.0f) * Range, Color);
1406  }
1407  }
1408  }
1409 
1410  public void CheckConditionals()
1411  {
1412  if (conditionals.None()) { return; }
1413  if (conditionalTarget == null) { return; }
1414  if (logicalOperator == PropertyConditional.LogicalOperatorType.And)
1415  {
1416  Enabled = conditionals.All(c => c.Matches(conditionalTarget));
1417  }
1418  else
1419  {
1420  Enabled = conditionals.Any(c => c.Matches(conditionalTarget));
1421  }
1422  }
1423 
1424  public void DebugDrawVertices(SpriteBatch spriteBatch)
1425  {
1426  if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; }
1427 
1428  //commented out because this is mostly just useful in very specific situations, otherwise it just makes debugdraw very messy
1429  //(you may also need to add a condition here that only draws this for the specific light you're interested in)
1430  if (GameMain.DebugDraw && vertices != null)
1431  {
1432  if (ParentBody?.UserData is Item it && it.Prefab.Identifier == "flashlight")
1433 
1434  for (int i = 1; i < vertices.Length - 1; i += 2)
1435  {
1436  Vector2 vert1 = new Vector2(vertices[i].Position.X, vertices[i].Position.Y);
1437  int nextIndex = (i + 2) % vertices.Length;
1438  //the first vertex is the one at the position of the light source, skip that one
1439  //(we just want to draw lines between the vertices at the circumference of the light volume)
1440  if (nextIndex == 0) { nextIndex++; }
1441  Vector2 vert2 = new Vector2(vertices[nextIndex].Position.X, vertices[nextIndex].Position.Y);
1442  if (ParentSub != null)
1443  {
1444  vert1 += ParentSub.DrawPosition;
1445  vert2 += ParentSub.DrawPosition;
1446  }
1447  vert1.Y = -vert1.Y;
1448  vert2.Y = -vert2.Y;
1449 
1450  var randomColor = ToolBox.GradientLerp(i / (float)vertices.Length, Color.Magenta, Color.Blue, Color.Yellow, Color.Green, Color.Cyan, Color.Red, Color.Purple, Color.Yellow);
1451  GUI.DrawLine(spriteBatch, vert1, vert2, randomColor * 0.8f, width: 2);
1452  }
1453  }
1454  }
1455 
1456  public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform, bool allowRecalculation, ref int recalculationCount)
1457  {
1458  if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; }
1459 
1460  //if the light doesn't cast shadows, we can simply render the texture without having to calculate the light volume
1461  if (!CastShadows)
1462  {
1463  Texture2D currentTexture = texture ?? LightTexture;
1464  if (OverrideLightTexture != null) { currentTexture = OverrideLightTexture.Texture; }
1465 
1466  Vector2 center = OverrideLightTexture == null ?
1467  new Vector2(currentTexture.Width / 2, currentTexture.Height / 2) :
1469  float scale = Range / (currentTexture.Width / 2.0f);
1470 
1471  Vector2 drawPos = position;
1472  if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
1473  drawPos.Y = -drawPos.Y;
1474 
1475  spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation + MathHelper.ToRadians(LightSourceParams.Rotation), center, scale, SpriteEffects.None, 1);
1476  return;
1477  }
1478 
1479  CheckConvexHullsInRange();
1480 
1481  if (NeedsRecalculation && allowRecalculation)
1482  {
1483  if (state == LightVertexState.UpToDate)
1484  {
1485  recalculationCount++;
1486  FindRaycastHits();
1487  }
1488  else if (state == LightVertexState.PendingVertexRecalculation)
1489  {
1490  if (verts == null)
1491  {
1492  #if DEBUG
1493  DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}");
1494  #endif
1495  Enabled = false;
1496  return;
1497  }
1498 
1499  foreach (var visibleConvexHull in visibleConvexHulls)
1500  {
1501  foreach (var convexHullList in convexHullsInRange)
1502  {
1503  convexHullList.IsHidden.Remove(visibleConvexHull);
1504  }
1505  }
1506 
1507  CalculateLightVertices(verts);
1508 
1509  LastRecalculationTime = (float)Timing.TotalTime;
1510  NeedsRecalculation = needsRecalculationWhenUpToDate;
1511  needsRecalculationWhenUpToDate = false;
1512 
1513  state = LightVertexState.UpToDate;
1514  }
1515  }
1516 
1517  if (vertexCount == 0) { return; }
1518 
1519  Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition;
1520  lightEffect.World =
1521  Matrix.CreateTranslation(-new Vector3(position, 0.0f)) *
1522  Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) *
1523  Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) *
1524  transform;
1525 
1526 
1527  lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f;
1528  if (OverrideLightTexture != null)
1529  {
1530  lightEffect.Texture = OverrideLightTexture.Texture;
1531  }
1532  else
1533  {
1534  lightEffect.Texture = texture ?? LightTexture;
1535  }
1536  lightEffect.CurrentTechnique.Passes[0].Apply();
1537 
1538  GameMain.Instance.GraphicsDevice.SetVertexBuffer(lightVolumeBuffer);
1539  GameMain.Instance.GraphicsDevice.Indices = lightVolumeIndexBuffer;
1540 
1541  GameMain.Instance.GraphicsDevice.DrawIndexedPrimitives
1542  (
1543  PrimitiveType.TriangleList, 0, 0, indexCount / 3
1544  );
1545  }
1546 
1547  public void Reset()
1548  {
1549  HullsUpToDate.Clear();
1550  convexHullsInRange.Clear();
1551  diffToSub.Clear();
1552  NeedsRecalculation = true;
1553 
1554  vertexCount = 0;
1555  if (lightVolumeBuffer != null)
1556  {
1557  lightVolumeBuffer.Dispose();
1558  lightVolumeBuffer = null;
1559  }
1560 
1561  indexCount = 0;
1562  if (lightVolumeIndexBuffer != null)
1563  {
1564  lightVolumeIndexBuffer.Dispose();
1565  lightVolumeIndexBuffer = null;
1566  }
1567  }
1568 
1569  public void Remove()
1570  {
1571  if (!lightSourceParams.Persistent)
1572  {
1573  LightSprite?.Remove();
1575  }
1576 
1578  DeformableLightSprite = null;
1579 
1580  lightVolumeBuffer?.Dispose();
1581  lightVolumeBuffer = null;
1582 
1583  lightVolumeIndexBuffer?.Dispose();
1584  lightVolumeIndexBuffer = null;
1585 
1586  GameMain.LightManager.RemoveLight(this);
1587  }
1588  }
1589 }
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:15
readonly Submarine Submarine
Definition: ConvexHull.cs:14
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:745
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 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)