Calculating Visibility in Counter-Strike 2

Knowing whether or not two players are visible to each other opens up exciting analysis opportunities. It is also somewhat difficult to calculate, and existing in game flags like isSpotted can be unreliable. Awpy provides a fast approach to determining visibility in Counter-Strike 2 (CS2). While our approach is not 100% foolproof, it is a start and applicable in many cases.

Triangles are the fundamental building blocks of 3D models, including those used in Counter-Strike maps. In computer graphics, complex surfaces are often broken down into smaller, flat polygons, typically triangles, because they are computationally efficient to render and manipulate. A Counter-Strike map consists of many such triangles forming walls, floors, objects, and other geometry. These triangles are used not only for visual rendering but also for collision detection and gameplay mechanics, such as determining visibility and movement constraints.

To determine if two points are visible, our approach checks whether the straight line segment connecting them intersects with any of the triangles in the map. This process involves representing the map’s triangles in a bounding volume hierarchy (BVH) tree, which organizes the triangles into nested groups to optimize intersection tests. The algorithm first queries the BVH to quickly identify potential collision candidates. Then, it performs precise intersection tests between the line segment and the relevant triangles. If no intersection is found, the points are visible to each other; otherwise, they are not. This approach balances accuracy and performance, making it suitable for real-time applications in games.

Acquiring .tri files

Awpy parses CS2 maps and produces .tri files, which are binary files containing the triangle information. Across all maps, the compressed .tri files are about 20 megabytes, so they are ultimately still quite small. To download the .tri files to your awpy data directory, you can run awpy get tris to get the relevant files. Below, we show where the files are located.

[1]:
# Import the Awpy data directory
from awpy.data import TRIS_DIR

# List files in the data directory
for file in TRIS_DIR.iterdir():
    print(file)
C:\Users\pnxen\.awpy\tris\.patch
C:\Users\pnxen\.awpy\tris\ar_baggage.tri
C:\Users\pnxen\.awpy\tris\ar_shoots.tri
C:\Users\pnxen\.awpy\tris\cs_italy.tri
C:\Users\pnxen\.awpy\tris\cs_office.tri
C:\Users\pnxen\.awpy\tris\de_ancient.tri
C:\Users\pnxen\.awpy\tris\de_anubis.tri
C:\Users\pnxen\.awpy\tris\de_dust2.tri
C:\Users\pnxen\.awpy\tris\de_inferno.tri
C:\Users\pnxen\.awpy\tris\de_mirage.tri
C:\Users\pnxen\.awpy\tris\de_nuke.tri
C:\Users\pnxen\.awpy\tris\de_overpass.tri
C:\Users\pnxen\.awpy\tris\de_train.tri
C:\Users\pnxen\.awpy\tris\de_vertigo.tri
C:\Users\pnxen\.awpy\tris\lobby_mapveto.tri

Constructing the VisibilityChecker

To get started, you must first create a VisibilityChecker. You can do so by passing a path to a .tri file or you can pass in a list of awpy.visibility.Triangle objects. The below options can take about 30 seconds or so.

[2]:
from awpy.visibility import VisibilityChecker

de_dust2_tri = TRIS_DIR / "de_dust2.tri"

# Create VC object with a file path
vc = VisibilityChecker(path=de_dust2_tri)

# Create VC object with a list of triangles
tris = VisibilityChecker.read_tri_file(de_dust2_tri)
vc = VisibilityChecker(triangles=tris)
print(vc)
VisibilityChecker(n_triangles=326265)

Calculating Visibility

We can now calculate visibility (yes/no) rather simply. We just need to provide two points, which we can do using a tuple for each.

[3]:
t_spawn_pos_1 = (-680, 834, 180)
t_spawn_pos_2 = (-1349, 814, 180)
ct_spawn_pos = (15, 2168, -65)

print(f"T spawn 1 is visible from T spawn 2: {vc.is_visible(t_spawn_pos_1, t_spawn_pos_2)}")
print(f"T spawn 1 is visible from CT spawn: {vc.is_visible(t_spawn_pos_1, ct_spawn_pos)}")
T spawn 1 is visible from T spawn 2: True
T spawn 1 is visible from CT spawn: False

Keep in mind that, due to the BVH we create, these calculations are fast. The visibility calculation for two points that are visible will always take the longest. For ones that aren’t visible, it should be much shorter. Below, we show that confirming that you cannot see CT spawn from T spawn is roughly 3x faster than confirming that you can see one T spawn position from another.

[4]:
%timeit vc.is_visible(t_spawn_pos_1, t_spawn_pos_2)
177 μs ± 5 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
[5]:
%timeit vc.is_visible(t_spawn_pos_1, ct_spawn_pos)
65.4 μs ± 2.24 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

Most of the time is really spent creating the BVH tree. Below is the time to do so for the smallest and largest maps:

[6]:
%timeit VisibilityChecker(path=TRIS_DIR / "de_mirage.tri")
744 ms ± 37.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
[7]:
%timeit VisibilityChecker(path=TRIS_DIR / "de_inferno.tri")
9.62 s ± 175 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

If you want to check positions in a local server, you can set sv_cheats 1 and then type getpos in your game’s console.

Pitfalls

Keep in mind that things like smokes, flashes, and props may impact visibility.