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