using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace Barotrauma.Lights { class ConvexHullList { private List list; public HashSet IsHidden; public readonly Submarine Submarine; public List List { get { return list; } set { Debug.Assert(value != null); Debug.Assert(!list.Contains(null)); list = value; IsHidden.RemoveWhere(ch => !list.Contains(ch)); } } public ConvexHullList(Submarine submarine) { Submarine = submarine; list = new List(); IsHidden = new HashSet(); } } class Segment { public SegmentPoint Start; public SegmentPoint End; public ConvexHull ConvexHull; public bool IsHorizontal; public bool IsAxisAligned; public Segment(SegmentPoint start, SegmentPoint end, ConvexHull convexHull) { if (start.Pos.Y > end.Pos.Y) { var temp = start; start = end; end = temp; } Start = start; End = end; ConvexHull = convexHull; start.ConvexHull = convexHull; end.ConvexHull = convexHull; IsHorizontal = Math.Abs(start.Pos.X - end.Pos.X) > Math.Abs(start.Pos.Y - end.Pos.Y); IsAxisAligned = Math.Abs(start.Pos.X - end.Pos.X) < 0.1f || Math.Abs(start.Pos.Y - end.Pos.Y) < 0.001f; } } struct SegmentPoint { public Vector2 Pos; public Vector2 WorldPos; public ConvexHull ConvexHull; public SegmentPoint(Vector2 pos, ConvexHull convexHull) { Pos = pos; WorldPos = pos; ConvexHull = convexHull; } public override string ToString() { return Pos.ToString(); } } class ConvexHull { public static List HullLists = new List(); public static BasicEffect shadowEffect; public static BasicEffect penumbraEffect; private Segment[] segments = new Segment[4]; private SegmentPoint[] vertices = new SegmentPoint[4]; private SegmentPoint[] losVertices = new SegmentPoint[4]; private readonly bool[] backFacing; private readonly bool[] ignoreEdge; private readonly bool isHorizontal; public VertexPositionColor[] ShadowVertices { get; private set; } public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } public MapEntity ParentEntity { get; private set; } private bool enabled; public bool Enabled { get { return enabled; } set { if (enabled == value) return; enabled = value; LastVertexChangeTime = (float)Timing.TotalTime; } } /// /// The elapsed gametime when the vertices of this hull last changed /// public float LastVertexChangeTime { get; private set; } public Rectangle BoundingBox { get; private set; } public ConvexHull(Vector2[] points, Color color, MapEntity parent) { if (shadowEffect == null) { shadowEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) { VertexColorEnabled = true }; } if (penumbraEffect == null) { penumbraEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) { TextureEnabled = true, LightingEnabled = false, Texture = TextureLoader.FromFile("Content/Lights/penumbra.png") }; } ParentEntity = parent; ShadowVertices = new VertexPositionColor[6 * 2]; PenumbraVertices = new VertexPositionTexture[6]; backFacing = new bool[4]; ignoreEdge = new bool[4]; SetVertices(points); Enabled = true; isHorizontal = BoundingBox.Width > BoundingBox.Height; if (ParentEntity is Structure structure) { isHorizontal = structure.IsHorizontal; } else if (ParentEntity is Item item) { var door = item.GetComponent(); if (door != null) { isHorizontal = door.IsHorizontal; } } var chList = HullLists.Find(x => x.Submarine == parent.Submarine); if (chList == null) { chList = new ConvexHullList(parent.Submarine); HullLists.Add(chList); } foreach (ConvexHull ch in chList.List) { MergeOverlappingSegments(ch); ch.MergeOverlappingSegments(this); } chList.List.Add(this); } private void MergeOverlappingSegments(ConvexHull ch) { if (ch == this) return; if (isHorizontal == ch.isHorizontal) { //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) float mergeDist = 32; float mergeDistSqr = mergeDist * mergeDist; for (int i = 0; i < segments.Length; i++) { for (int j = 0; j < ch.segments.Length; j++) { if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; } //the segments must be at different sides of the convex hulls to be merged //(e.g. the right edge of a wall piece and the left edge of another one) var segment1Center = (segments[i].Start.Pos + segments[i].End.Pos) / 2.0f; var segment2Center = (ch.segments[j].Start.Pos + ch.segments[j].End.Pos) / 2.0f; if (Vector2.Dot(segment1Center - BoundingBox.Center.ToVector2(), segment2Center - ch.BoundingBox.Center.ToVector2()) > 0) { continue; } if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < mergeDistSqr && Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < mergeDistSqr) { ignoreEdge[i] = true; ch.ignoreEdge[j] = true; MergeSegments(segments[i], ch.segments[j], true); } else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < mergeDistSqr && Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < mergeDistSqr) { ignoreEdge[i] = true; ch.ignoreEdge[j] = true; MergeSegments(segments[i], ch.segments[j], false); } } } } else { //TODO: do something to corner areas where a vertical wall meets a horizontal one } //ignore edges that are inside some other convex hull for (int i = 0; i < vertices.Length; i++) { if (vertices[i].Pos.X >= ch.BoundingBox.X && vertices[i].Pos.X <= ch.BoundingBox.Right && vertices[i].Pos.Y >= ch.BoundingBox.Y && vertices[i].Pos.Y <= ch.BoundingBox.Bottom) { Vector2 p = vertices[(i + 1) % vertices.Length].Pos; if (p.X >= ch.BoundingBox.X && p.X <= ch.BoundingBox.Right && p.Y >= ch.BoundingBox.Y && p.Y <= ch.BoundingBox.Bottom) { ignoreEdge[i] = true; } } } } private void MergeSegments(Segment segment1, Segment segment2, bool startPointsMatch) { int startPointIndex = -1, endPointIndex = -1; for (int i = 0; i < vertices.Length; i++) { if (vertices[i].Pos.NearlyEquals(segment1.Start.Pos)) startPointIndex = i; else if (vertices[i].Pos.NearlyEquals(segment1.End.Pos)) endPointIndex = i; } if (startPointIndex == -1 || endPointIndex == -1) { return; } int startPoint2Index = -1, endPoint2Index = -1; for (int i = 0; i < segment2.ConvexHull.vertices.Length; i++) { if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.Start.Pos)) startPoint2Index = i; else if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.End.Pos)) endPoint2Index = i; } if (startPoint2Index == -1 || endPoint2Index == -1) { return; } if (startPointsMatch) { losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = (segment1.Start.Pos + segment2.Start.Pos) / 2.0f; losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = (segment1.End.Pos + segment2.End.Pos) / 2.0f; } else { losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = (segment1.Start.Pos + segment2.End.Pos) / 2.0f; losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = (segment1.End.Pos + segment2.Start.Pos) / 2.0f; } } public void Rotate(Vector2 origin, float amount) { Matrix rotationMatrix = Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateRotationZ(amount) * Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix); } private void CalculateDimensions() { float minX = vertices[0].Pos.X, minY = vertices[0].Pos.Y, maxX = vertices[0].Pos.X, maxY = vertices[0].Pos.Y; for (int i = 1; i < vertices.Length; i++) { if (vertices[i].Pos.X < minX) minX = vertices[i].Pos.X; if (vertices[i].Pos.Y < minY) minY = vertices[i].Pos.Y; if (vertices[i].Pos.X > maxX) maxX = vertices[i].Pos.X; if (vertices[i].Pos.Y > minY) maxY = vertices[i].Pos.Y; } BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); } public void Move(Vector2 amount) { for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos += amount; losVertices[i].Pos += amount; segments[i].Start.Pos += amount; segments[i].End.Pos += amount; } LastVertexChangeTime = (float)Timing.TotalTime; CalculateDimensions(); } public void SetVertices(Vector2[] points, Matrix? rotationMatrix = null) { Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); LastVertexChangeTime = (float)Timing.TotalTime; for (int i = 0; i < 4; i++) { vertices[i] = new SegmentPoint(points[i], this); losVertices[i] = new SegmentPoint(points[i], this); } for (int i = 0; i < 4; i++) { ignoreEdge[i] = false; } int margin = 0; if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y)) { losVertices[0].Pos = new Vector2(points[0].X + margin, points[0].Y); losVertices[1].Pos = new Vector2(points[1].X + margin, points[1].Y); losVertices[2].Pos = new Vector2(points[2].X - margin, points[2].Y); losVertices[3].Pos = new Vector2(points[3].X - margin, points[3].Y); } else { losVertices[0].Pos = new Vector2(points[0].X, points[0].Y + margin); losVertices[1].Pos = new Vector2(points[1].X, points[1].Y - margin); losVertices[2].Pos = new Vector2(points[2].X, points[2].Y - margin); losVertices[3].Pos = new Vector2(points[3].X, points[3].Y + margin); } if (rotationMatrix.HasValue) { for (int i = 0; i < vertices.Length; i++) { vertices[i].Pos = Vector2.Transform(vertices[i].Pos, rotationMatrix.Value); losVertices[i].Pos = Vector2.Transform(losVertices[i].Pos, rotationMatrix.Value); } } for (int i = 0; i < 4; i++) { segments[i] = new Segment(vertices[i], vertices[(i + 1) % 4], this); } CalculateDimensions(); if (ParentEntity == null) return; var chList = HullLists.Find(x => x.Submarine == ParentEntity.Submarine); if (chList != null) { foreach (ConvexHull ch in chList.List) { MergeOverlappingSegments(ch); } } } public bool Intersects(Rectangle rect) { if (!Enabled) return false; Rectangle transformedBounds = BoundingBox; if (ParentEntity != null && ParentEntity.Submarine != null) { transformedBounds.X += (int)ParentEntity.Submarine.Position.X; transformedBounds.Y += (int)ParentEntity.Submarine.Position.Y; } return transformedBounds.Intersects(rect); } /// /// Returns the segments that are facing towards viewPosition /// public void GetVisibleSegments(Vector2 viewPosition, List visibleSegments, bool ignoreEdges) { for (int i = 0; i < 4; i++) { if (ignoreEdge[i] && ignoreEdges) continue; Vector2 pos1 = vertices[i].WorldPos; Vector2 pos2 = vertices[(i + 1) % 4].WorldPos; Vector2 middle = (pos1 + pos2) / 2; Vector2 L = viewPosition - middle; Vector2 N = new Vector2( -(pos2.Y - pos1.Y), pos2.X - pos1.X); if (Vector2.Dot(N, L) > 0) { visibleSegments.Add(segments[i]); } } } public void RefreshWorldPositions() { for (int i = 0; i < 4; i++) { vertices[i].WorldPos = vertices[i].Pos; segments[i].Start.WorldPos = segments[i].Start.Pos; segments[i].End.WorldPos = segments[i].End.Pos; } if (ParentEntity == null || ParentEntity.Submarine == null) { return; } for (int i = 0; i < 4; i++) { vertices[i].WorldPos += ParentEntity.Submarine.DrawPosition; segments[i].Start.WorldPos += ParentEntity.Submarine.DrawPosition; segments[i].End.WorldPos += ParentEntity.Submarine.DrawPosition; } } public void CalculateShadowVertices(Vector2 lightSourcePos, bool los = true) { Vector3 offset = Vector3.Zero; if (ParentEntity != null && ParentEntity.Submarine != null) { offset = new Vector3(ParentEntity.Submarine.DrawPosition.X, ParentEntity.Submarine.DrawPosition.Y, 0.0f); } ShadowVertexCount = 0; var vertices = los ? losVertices : this.vertices; //compute facing of each edge, using N*L for (int i = 0; i < 4; i++) { if (ignoreEdge[i]) { backFacing[i] = false; continue; } Vector2 firstVertex = vertices[i].Pos; Vector2 secondVertex = vertices[(i+1) % 4].Pos; Vector2 L = lightSourcePos - ((firstVertex + secondVertex) / 2.0f); Vector2 N = new Vector2( -(secondVertex.Y - firstVertex.Y), secondVertex.X - firstVertex.X); backFacing[i] = (Vector2.Dot(N, L) < 0) == los; } //find beginning and ending vertices which //belong to the shadow int startingIndex = 0; int endingIndex = 0; for (int i = 0; i < 4; i++) { int currentEdge = i; int nextEdge = (i + 1) % 4; if (backFacing[currentEdge] && !backFacing[nextEdge]) endingIndex = nextEdge; if (!backFacing[currentEdge] && backFacing[nextEdge]) startingIndex = nextEdge; } //nr of vertices that are in the shadow if (endingIndex > startingIndex) ShadowVertexCount = endingIndex - startingIndex + 1; else ShadowVertexCount = 4 + 1 - startingIndex + endingIndex; //shadowVertices = new VertexPositionColor[shadowVertexCount * 2]; //create a triangle strip that has the shape of the shadow int currentIndex = startingIndex; int svCount = 0; while (svCount != ShadowVertexCount * 2) { Vector3 vertexPos = new Vector3(vertices[currentIndex].Pos, 0.0f); int i = los ? svCount : svCount + 1; int j = los ? svCount + 1 : svCount; //one vertex on the hull ShadowVertices[i] = new VertexPositionColor { Color = los ? Color.Black : Color.Transparent, Position = vertexPos + offset }; //one extruded by the light direction ShadowVertices[j] = new VertexPositionColor { Color = ShadowVertices[i].Color }; Vector3 L2P = vertexPos - new Vector3(lightSourcePos, 0); L2P.Normalize(); ShadowVertices[j].Position = new Vector3(lightSourcePos, 0) + L2P * 9000 + offset; svCount += 2; currentIndex = (currentIndex + 1) % 4; } if (los) { CalculatePenumbraVertices(startingIndex, endingIndex, lightSourcePos, los); } } private void CalculatePenumbraVertices(int startingIndex, int endingIndex, Vector2 lightSourcePos, bool los) { var vertices = los ? losVertices : this.vertices; Vector3 offset = Vector3.Zero; if (ParentEntity != null && ParentEntity.Submarine != null) { offset = new Vector3(ParentEntity.Submarine.DrawPosition.X, ParentEntity.Submarine.DrawPosition.Y, 0.0f); } for (int n = 0; n < 4; n += 3) { Vector3 penumbraStart = new Vector3((n == 0) ? vertices[startingIndex].Pos : vertices[endingIndex].Pos, 0.0f); PenumbraVertices[n] = new VertexPositionTexture { Position = penumbraStart + offset, TextureCoordinate = new Vector2(0.0f, 1.0f) }; for (int i = 0; i < 2; i++) { PenumbraVertices[n + i + 1] = new VertexPositionTexture(); Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0); vertexDir.Normalize(); Vector3 normal = (i == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; if (n > 0) normal = -normal; vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); vertexDir.Normalize(); PenumbraVertices[n + i + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset; if (los) { PenumbraVertices[n + i + 1].TextureCoordinate = (i == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f); } else { PenumbraVertices[n + i + 1].TextureCoordinate = (i == 0) ? new Vector2(1.0f, 0.0f) : Vector2.Zero; } } if (n > 0) { var temp = PenumbraVertices[4]; PenumbraVertices[4] = PenumbraVertices[5]; PenumbraVertices[5] = temp; } } } public static List GetHullsInRange(Vector2 position, float range, Submarine ParentSub) { List list = new List(); foreach (ConvexHullList chList in HullLists) { Vector2 lightPos = position; if (ParentSub == null) { //light and the convexhull are both outside if (chList.Submarine == null) { list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); } //light is outside, convexhull inside a sub else { Rectangle subBorders = chList.Submarine.Borders; subBorders.Y -= chList.Submarine.Borders.Height; if (!MathUtils.CircleIntersectsRectangle(lightPos - chList.Submarine.WorldPosition, range, subBorders)) continue; lightPos -= (chList.Submarine.WorldPosition - chList.Submarine.HiddenSubPosition); list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); } } else { //light is inside, convexhull outside if (chList.Submarine == null) { lightPos += (ParentSub.WorldPosition - ParentSub.HiddenSubPosition); HashSet visibleRuins = new HashSet(); foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) { if (!MathUtils.CircleIntersectsRectangle(lightPos, range, ruin.Area)) { continue; } visibleRuins.Add(ruin); } list.AddRange(chList.List.FindAll(ch => ch.ParentEntity?.ParentRuin != null && visibleRuins.Contains(ch.ParentEntity.ParentRuin))); continue; } //light and convexhull are both inside the same sub if (chList.Submarine == ParentSub) { list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); } //light and convexhull are inside different subs else { lightPos -= (chList.Submarine.Position - ParentSub.Position); Rectangle subBorders = chList.Submarine.Borders; subBorders.Location += chList.Submarine.HiddenSubPosition.ToPoint() - new Point(0, chList.Submarine.Borders.Height); if (!MathUtils.CircleIntersectsRectangle(lightPos, range, subBorders)) continue; list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); } } } return list; } public void Remove() { var chList = HullLists.Find(x => x.Submarine == ParentEntity.Submarine); if (chList != null) { chList.List.Remove(this); if (chList.List.Count == 0) { HullLists.Remove(chList); } } } } }