Seeing the world through triangles
Give a game character eyes and you have to answer a geometry question sixty times a second: which parts of the map can this point see right now? The brute-force answer — cast a ray to every pixel and check what it hits — is far too slow. The efficient answer is shadow casting: cast rays only to the corners of obstacles, sort the hit points by angle around the viewer, and connect them into a single polygon. Everything inside that polygon is visible; everything outside it is hidden behind a wall.
This is the same family of algorithm behind fog of war in strategy games, flashlight cones in horror games, and the vision checks that make stealth games work: if the polygon that contains the player also contains a guard's position, the guard can, in principle, see the player.
Why corners, not pixels
A visibility polygon is entirely determined by the corners of every obstacle in the scene, plus the two extreme rays that graze each obstacle's silhouette. For each corner, a ray from the viewer either stops exactly at that corner (nothing beyond it is visible in that direction) or continues past it until it hits something else. So the algorithm casts one ray directly at every corner and two more rays at a tiny angle either side of it, to catch the sliver of space that opens up just past the edge:
for each obstacle corner c: cast ray at angle(c) - epsilon cast ray at angle(c) cast ray at angle(c) + epsilon sort all hit points by angle connect them in order → visibility polygon
Sorting by angle around the NPC is what turns a scatter of ray/wall intersections into a single connected shape. Fan-sort the hit points, walk around them once, and each consecutive pair plus the viewer forms one triangle of the polygon — draw all the triangles as a filled shape and you get the light-coloured wedge the player actually sees, complete with sharp shadow edges exactly at obstacle corners.
Adding a field-of-view cone and a range
A full 360° visibility polygon models an omniscient sensor; real NPCs look in one direction with a limited cone and a maximum sight distance. Both are cheap additions: clip the ray-casting to angles inside [facing − fov/2, facing + fov/2], and cap every ray's length at the sight radius, treating the range boundary itself as a curved "obstacle" whose arc gets filled in with extra rays so the visibility polygon closes smoothly instead of ending in a jagged edge.
visible = angle_in_fov(target, npc.facing, npc.fovAngle)
and distance(npc.pos, target) < npc.sightRange
and not blocked_by_any_obstacle(npc.pos, target)
From geometry to gameplay
Once you have the visibility polygon, stealth mechanics fall out almost for free. A point-in-polygon test tells you whether the player is currently seen. Accumulating time spent inside the cone before triggering full alert (rather than an instant detection) is what makes sneaking past a guard feel fair instead of arbitrary — most shipped stealth games use a "suspicion meter" that fills while the player is inside the polygon and drains when they leave it, rather than a hard boolean.
The same visibility polygon can drive sound and light propagation too: a torch's glow, a gunshot's noise radius intersected with line-of-sight, or a security camera's rotating cone are all the identical shadow-casting computation with different rays, ranges and update rates.
Performance notes
For a scene with n obstacle corners the sort-and-sweep algorithm above runs in O(n log n) per query, dominated by the angle sort — fast enough to recompute every frame for dozens of NPCs against a few hundred wall segments, which is exactly what the simulation on this page does with rectangular obstacles you can drag and resize. Large open-world games instead precompute a static visibility structure once (a navmesh's dual graph, or a portal/cell decomposition of the level) and only run per-frame ray casts for moving obstacles, because re-sorting thousands of corners sixty times a second for every guard does add up.
Frequently asked questions
What's the difference between field of view and a visibility polygon?
Field of view (FOV) is the angular cone an NPC looks through — typically 90 to 150 degrees. The visibility polygon is the actual visible area within that cone once obstacles are accounted for; it is usually a much smaller, irregular shape carved out by shadow casting.
Why cast rays at corners instead of a fixed grid of angles?
A fixed angular grid either wastes rays on empty space or misses thin obstacles between samples, and it never lines up exactly with an obstacle's edge, so shadows look soft and imprecise. Casting at each corner (plus a hair either side) guarantees the polygon's boundary is exact wherever the scene geometry actually changes.
Does this technique work in 3D?
The same idea generalises to 3D shadow volumes and portal culling, but it is much more expensive — 2D visibility polygons are exact and cheap because the world is a set of line segments, while 3D requires reasoning about occluding surfaces and silhouette edges in a full solid geometry, which is why real-time 3D engines usually approximate visibility with shadow maps or precomputed potentially-visible sets instead.
Try it live
Everything above runs in your browser — open Field of View and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open Field of View simulation