{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Calculating Visibility in Counter-Strike 2\n", "\n", "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.\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Acquiring `.tri` files\n", "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." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "C:\\Users\\pnxen\\.awpy\\tris\\.patch\n", "C:\\Users\\pnxen\\.awpy\\tris\\ar_baggage.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\ar_shoots.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\cs_italy.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\cs_office.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_ancient.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_anubis.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_dust2.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_inferno.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_mirage.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_nuke.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_overpass.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_train.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\de_vertigo.tri\n", "C:\\Users\\pnxen\\.awpy\\tris\\lobby_mapveto.tri\n" ] } ], "source": [ "# Import the Awpy data directory\n", "from awpy.data import TRIS_DIR\n", "\n", "# List files in the data directory\n", "for file in TRIS_DIR.iterdir():\n", " print(file)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Constructing the `VisibilityChecker`\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "VisibilityChecker(n_triangles=326265)\n" ] } ], "source": [ "from awpy.visibility import VisibilityChecker\n", "\n", "de_dust2_tri = TRIS_DIR / \"de_dust2.tri\"\n", "\n", "# Create VC object with a file path\n", "vc = VisibilityChecker(path=de_dust2_tri)\n", "\n", "# Create VC object with a list of triangles\n", "tris = VisibilityChecker.read_tri_file(de_dust2_tri)\n", "vc = VisibilityChecker(triangles=tris)\n", "print(vc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Calculating Visibility\n", "\n", "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.\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "T spawn 1 is visible from T spawn 2: True\n", "T spawn 1 is visible from CT spawn: False\n" ] } ], "source": [ "t_spawn_pos_1 = (-680, 834, 180)\n", "t_spawn_pos_2 = (-1349, 814, 180)\n", "ct_spawn_pos = (15, 2168, -65)\n", "\n", "print(f\"T spawn 1 is visible from T spawn 2: {vc.is_visible(t_spawn_pos_1, t_spawn_pos_2)}\")\n", "print(f\"T spawn 1 is visible from CT spawn: {vc.is_visible(t_spawn_pos_1, ct_spawn_pos)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "177 μs ± 5 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" ] } ], "source": [ "%timeit vc.is_visible(t_spawn_pos_1, t_spawn_pos_2)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "65.4 μs ± 2.24 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" ] } ], "source": [ "%timeit vc.is_visible(t_spawn_pos_1, ct_spawn_pos)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Most of the time is really spent creating the BVH tree. Below is the time to do so for the smallest and largest maps:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "744 ms ± 37.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%timeit VisibilityChecker(path=TRIS_DIR / \"de_mirage.tri\")" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "9.62 s ± 175 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%timeit VisibilityChecker(path=TRIS_DIR / \"de_inferno.tri\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pitfalls\n", "\n", "Keep in mind that things like smokes, flashes, and props may impact visibility.\n" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 2 }