2023-06-12 12:37:12 +00:00
|
|
|
Title: 2D Visibility
|
|
|
|
Brief: Visibility triangles from 2D occluding segment geometry in GDScript.
|
|
|
|
Date: 1686547796
|
|
|
|
Tags: Programming, Godot, GDScript
|
|
|
|
CSS: /style.css
|
|
|
|
|
|
|
|
![](/articles/2d-visibility/example.gif)
|
|
|
|
|
|
|
|
Based on [Redblobgames' visibility article and Haxe reference implementation](https://www.redblobgames.com/articles/visibility)
|
|
|
|
|
2023-07-10 13:23:11 +00:00
|
|
|
Full usable code is [here](/articles/2d-visibility/Visibility2D.gd.txt).
|
2023-06-12 12:37:12 +00:00
|
|
|
|
|
|
|
### Explanation ###
|
|
|
|
|
|
|
|
First step is determining angles for each segment point as well as denoting
|
|
|
|
which one gets encountered first.
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
for segment in range(0, _endpoints.size(), 2):
|
|
|
|
var p1 := _endpoints[segment] as EndPoint
|
|
|
|
var p2 := _endpoints[segment + 1] as EndPoint
|
|
|
|
p1.angle = (p1.point - center).angle()
|
|
|
|
p2.angle = (p2.point - center).angle()
|
|
|
|
var da := p2.angle - p1.angle
|
|
|
|
if da <= PI: da += TAU
|
|
|
|
if da > PI: da -= TAU
|
|
|
|
p1.begin = da > 0.0
|
|
|
|
p2.begin = not p1.begin
|
|
|
|
```
|
|
|
|
|
|
|
|
Then points are sorted by angle and beginning:
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
static func sort(p_a: EndPoint, p_b: EndPoint) -> bool:
|
|
|
|
if p_a.angle > p_b.angle: return true
|
|
|
|
elif p_a.angle < p_b.angle: return false
|
|
|
|
elif not p_a.begin and p_b.begin: return true
|
|
|
|
else: return false
|
|
|
|
```
|
|
|
|
|
|
|
|
Then in two passes:
|
|
|
|
- Walk over sorted points.
|
|
|
|
- When nearest segment end or another more nearest encountered, -
|
|
|
|
remember the starting angle and only emit two points representing the visible portion of segment on second pass.
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
var start_angle := 0.0
|
|
|
|
|
|
|
|
for n_pass in range(2):
|
|
|
|
for p_idx in range(_sorted_endpoints.size() - 1, -1, -1):
|
|
|
|
var p := _sorted_endpoints[p_idx] as EndPoint
|
|
|
|
var old := -1 if _open.empty() else _open[0]
|
|
|
|
|
|
|
|
if p.begin:
|
|
|
|
var idx := 0
|
|
|
|
while idx < _open.size() and _is_segment_in_front(p.segment, _open[idx]):
|
|
|
|
idx += 1
|
|
|
|
_open.insert(idx, p.segment)
|
|
|
|
else:
|
|
|
|
var idx := _open.rfind(p.segment)
|
|
|
|
if idx != -1: _open.remove(idx)
|
|
|
|
|
|
|
|
if old != (-1 if _open.empty() else _open[0]):
|
|
|
|
if n_pass == 1:
|
|
|
|
var p3 := _endpoints[old].point as Vector2 if old != -1 else \
|
|
|
|
center + Vector2(cos(start_angle), sin(start_angle)) * 500.0
|
|
|
|
var t2 := Vector2(cos(p.angle), sin(p.angle))
|
|
|
|
var p4 := p3.direction_to(_endpoints[old + 1].point) if old != -1 else t2
|
|
|
|
|
|
|
|
# note: Checks are in case of parallel lines.
|
|
|
|
var l = Geometry.line_intersects_line_2d(p3, p4, center,
|
|
|
|
Vector2(cos(start_angle), sin(start_angle)))
|
|
|
|
if l != null: output.append(l)
|
|
|
|
l = Geometry.line_intersects_line_2d(p3, p4, center, t2)
|
|
|
|
if l != null: output.append(l)
|
|
|
|
|
|
|
|
start_angle = p.angle
|
|
|
|
```
|
|
|
|
|
|
|
|
Where segment front deciding algorithm is as follows, using cross products:
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
func _is_segment_in_front(p_segment1: int, p_segment2: int) -> bool:
|
|
|
|
var s1p1 := _endpoints[p_segment1].point as Vector2
|
|
|
|
var s1p2 := _endpoints[p_segment1 + 1].point as Vector2
|
|
|
|
var s2p1 := _endpoints[p_segment2].point as Vector2
|
|
|
|
var s2p2 := _endpoints[p_segment2 + 1].point as Vector2
|
|
|
|
|
|
|
|
var d := s1p2 - s1p1
|
|
|
|
var p := s2p1.linear_interpolate(s2p2, 0.01)
|
|
|
|
var a1 := (d.x * (p.y - s1p1.y) \
|
|
|
|
- d.y * (p.x - s1p1.x)) < 0.0
|
|
|
|
p = s2p2.linear_interpolate(s2p1, 0.01)
|
|
|
|
var a2 := (d.x * (p.y - s1p1.y) \
|
|
|
|
- d.y * (p.x - s1p1.x)) < 0.0
|
|
|
|
var a3 := (d.x * (center.y - s1p1.y) \
|
|
|
|
- d.y * (center.x - s1p1.x)) < 0.0
|
|
|
|
|
|
|
|
if a1 == a2 and a2 == a3: return true
|
|
|
|
|
|
|
|
d = s2p2 - s2p1
|
|
|
|
p = s1p1.linear_interpolate(s1p2, 0.01)
|
|
|
|
var b1 := (d.x * (p.y - s2p1.y) \
|
|
|
|
- d.y * (p.x - s2p1.x)) < 0.0
|
|
|
|
p = s1p2.linear_interpolate(s1p1, 0.01)
|
|
|
|
var b2 := (d.x * (p.y - s2p1.y) \
|
|
|
|
- d.y * (p.x - s2p1.x)) < 0.0
|
|
|
|
var b3 := (d.x * (center.y - s2p1.y) \
|
|
|
|
- d.y * (center.x - s2p1.x)) < 0.0
|
|
|
|
|
|
|
|
return b1 == b2 and b2 != b3
|
|
|
|
```
|
|
|
|
|
|
|
|
### Usage example ###
|
|
|
|
Visibility2D.gd class implements builder interface to make it slightly easier to work with.
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
func _process(_delta):
|
|
|
|
$Visibility2D.init_builder() \
|
|
|
|
.view_point(get_global_mouse_position()) \
|
|
|
|
.bounds(get_viewport_rect()) \
|
|
|
|
.occluder($Line2D) \
|
|
|
|
.finalize()
|
|
|
|
|
|
|
|
for child in $Cones.get_children():
|
|
|
|
child.queue_free()
|
|
|
|
|
|
|
|
var edges = $Visibility2D.sweep()
|
|
|
|
for i in range(0, edges.size() - 1, 2):
|
|
|
|
var polygon := Polygon2D.new()
|
|
|
|
polygon.polygon = PoolVector2Array([$Visibility2D.center, edges[i], edges[i + 1]])
|
|
|
|
$Cones.add_child(polygon)
|
|
|
|
```
|