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