GPU Based Line of Sight#

Screenshot of line of sight

Calculate line-of-sight with the GPU

spritelist_interaction_visualize_dist_los.py#
  1"""
  2Shows how we can use shaders using existing spritelist data.
  3
  4This examples renders a line between the player position
  5and nearby sprites when they are within a certain distance.
  6
  7This builds on a previous example adding line of sight (LoS)
  8checks by using texture lookups. We our walls into a
  9texture and read the pixels in a line between the
 10player and the target sprite to check if the path is
 11colliding with something.
 12
 13If Python and Arcade are installed, this example can be run from the command line with:
 14python -m arcade.examples.gl.spritelist_interaction_visualize_dist_los
 15"""
 16from __future__ import annotations
 17
 18import random
 19import arcade
 20
 21WINDOW_WIDTH = 800
 22WINDOW_HEIGHT = 600
 23NUM_COINS = 500
 24NUM_WALLS = 75
 25INTERACTION_RADIUS = 300
 26
 27
 28class SpriteListInteraction(arcade.Window):
 29
 30    def __init__(self):
 31        super().__init__(WINDOW_WIDTH, WINDOW_HEIGHT, "SpriteList Interaction - LoS")
 32
 33        # Player
 34        self.player = arcade.Sprite(
 35            ":resources:images/animated_characters/female_person/femalePerson_idle.png",
 36            scale=0.25,
 37        )
 38
 39        # Wall sprites we are checking collision against
 40        self.walls = arcade.SpriteList()
 41        for _ in range(NUM_WALLS):
 42            self.walls.append(
 43                arcade.Sprite(
 44                    ":resources:images/tiles/boxCrate_double.png",
 45                    center_x=random.randint(0, WINDOW_WIDTH),
 46                    center_y=random.randint(0, WINDOW_HEIGHT),
 47                    scale=0.25,
 48                )
 49            )
 50
 51        # Generate some random coins.
 52        # We make sure they are not placed inside a wall.
 53        # We give the coins one chance to appear outside walls
 54        self.coins = arcade.SpriteList()
 55        for _ in range(NUM_COINS):
 56            coin = arcade.Sprite(
 57                ":resources:images/items/coinGold.png",
 58                center_x=random.randint(0, WINDOW_WIDTH),
 59                center_y=random.randint(0, WINDOW_HEIGHT),
 60                scale=0.25,
 61            )
 62            if arcade.check_for_collision_with_list(coin, self.walls):
 63                continue
 64
 65            self.coins.append(coin)
 66
 67        # This program draws lines from the player/origin
 68        # to sprites that are within a certain distance.
 69        # The main action here happens in the geometry shader.
 70        # It creates lines when a sprite is within the maxDistance.
 71        self.program_visualize_dist = self.ctx.program(
 72            vertex_shader="""
 73            #version 330
 74
 75            // Sprite positions from SpriteList
 76            in vec3 in_pos;
 77
 78            // Output to geometry shader
 79            out vec3 v_position;
 80
 81            void main() {
 82                // This shader just forwards info to geo shader
 83                v_position = in_pos;
 84            }
 85            """,
 86            geometry_shader="""
 87            #version 330
 88
 89            // This is how we access pyglet's global projection matrix
 90            uniform WindowBlock {
 91                mat4 projection;
 92                mat4 view;
 93            } window;
 94
 95            // The position we measure distance from
 96            uniform vec2 origin;
 97            // The maximum distance
 98            uniform float maxDistance;
 99            // Sampler for reading wall data
100            uniform sampler2D walls;
101
102            // These configure the geometry shader to process a points
103            // and allows it to emit lines. It runs for every sprite
104            // in the spritelist.
105            layout (points) in;
106            layout (line_strip, max_vertices = 2) out;
107
108            // The position input from vertex shader.
109            // It's an array because geo shader can take more than one input
110            in vec3 v_position[];
111
112            // Helper function converting screen coordinates to texture coordinates.
113            // Texture coordinates are normalized (0.0 -> 1.0) were 0,0 is in the
114            vec2 screen2texcoord(vec2 pos) {
115                return vec2(pos / vec2(textureSize(walls, 0).xy));
116            }
117
118            void main() {
119                // ONLY emit a line between the sprite and origin when within the distance
120                if (distance(v_position[0].xy, origin) > maxDistance) return;
121
122                // Read samples from the wall texture in a line looking for obstacles
123                // We simply make a vector between the original and the sprite location
124                // and trace pixels in this path with a reasonable step.
125                int numSteps = int(maxDistance / 2.0);
126                vec2 dir = v_position[0].xy - origin;
127                for (int i = 0; i < numSteps; i++) {
128                    // Read pixels along the vector
129                    vec2 pos = origin + dir * (float(i) / float(numSteps));
130                    vec4 color = texture(walls, screen2texcoord(pos));
131                    // If we find non-zero pixel data we have obstacles in our path!
132                    if (color != vec4(0.0)) return;
133                }
134
135                // First line segment position (origin)
136                gl_Position = window.projection * window.view * vec4(origin, 0.0, 1.0);
137                EmitVertex();
138                // Second line segment position (sprite position)
139                gl_Position = window.projection * window.view * vec4(v_position[0].xy, 0.0, 1.0);
140                EmitVertex();
141                EndPrimitive();
142            }
143            """,
144            fragment_shader="""
145            #version 330
146            // The fragment shader just runs for every pixel of the line segment.
147
148            // Reference to the pixel we are writing to in the framebuffer
149            out vec4 fragColor;
150
151            void main() {
152                // All the pixels in the line should just be white
153                fragColor = vec4(1.0, 1.0, 1.0, 1.0);
154            }
155            """
156        )
157        # Configure program with maximum distance
158        self.program_visualize_dist["maxDistance"] = INTERACTION_RADIUS
159
160        # Lookup texture/framebuffer for walls so we can trace pixels in the shader.
161        # It contains a texture attachment with the same size as the window.
162        # We draw only the walls into this one as a line of sight lookup
163        self.walls_fbo = self.ctx.framebuffer(
164            color_attachments=[self.ctx.texture((WINDOW_WIDTH, WINDOW_HEIGHT))]
165        )
166        # Draw the walls into the framebuffer
167        with self.walls_fbo.activate() as fbo:
168            fbo.clear()
169            self.walls.draw()
170
171    def on_draw(self):
172        self.clear()
173
174        self.walls.draw()
175        self.coins.draw()
176        # Bind the wall texture to texture channel 0 so we can read it in the shader
177        self.walls_fbo.color_attachments[0].use(0)
178        # We already have a geometry instance in the spritelist we can
179        # use to run our shader/gpu program. It only requires that we
180        # use correctly named input name(s). in_pos in this example
181        # what will automatically map in the position buffer to the vertex shader.
182        self.coins.geometry.render(self.program_visualize_dist, vertices=len(self.coins))
183        self.player.draw()
184
185        # Visualize the interaction radius
186        arcade.draw_circle_filled(self.player.center_x, self.player.center_y, INTERACTION_RADIUS, (255, 255, 255, 64))
187
188    def on_mouse_motion(self, x: float, y: float, dx: float, dy: float):
189        # Move the sprite to mouse position
190        self.player.position = x, y
191        # Update the program with a new origin
192        self.program_visualize_dist["origin"] = x, y
193
194
195SpriteListInteraction().run()