Perspective

Screen shot of a perspective example
perspective.py
  1"""
  2Perspective example using the PerspectiveProjectionCamera
  3
  4This is definitely in the advanced section, but it can be
  5a useful tool to learn. Sometimes we want perspective
  6projection for things like backgrounds. This can be
  7done very efficiently with shaders.
  8
  9In this example we render content into a framebuffer /
 10virtual screen and map that on a texture we can rotate
 11in 3D.
 12
 13If Python and Arcade are installed, this example can be run from the command line with:
 14python -m arcade.examples.perspective
 15"""
 16
 17from array import array
 18
 19import arcade
 20from arcade.gl import BufferDescription
 21
 22
 23class GameView(arcade.View):
 24
 25    def __init__(self):
 26        super().__init__()
 27        # Simple texture shader for the plane.
 28        # It support projection and model matrix
 29        # and a scroll value for texture coordinates
 30        self.program = self.window.ctx.program(
 31            vertex_shader="""
 32            #version 330
 33
 34            uniform WindowBlock {
 35                mat4 projection;
 36                mat4 model;
 37            } window;
 38
 39            in vec3 in_pos;
 40            in vec2 in_uv;
 41
 42            out vec2 uv;
 43
 44            void main() {
 45                gl_Position = window.projection * window.model * vec4(in_pos, 1.0);
 46                uv = in_uv;
 47            }
 48            """,
 49            fragment_shader="""
 50            #version 330
 51
 52            uniform sampler2D layer;
 53            uniform vec2 scroll;
 54
 55            in vec2 uv;
 56            out vec4 fragColor;
 57
 58            void main() {
 59                fragColor = texture(layer, uv + scroll);
 60            }
 61            """,
 62        )
 63
 64        # Configure and create the perspective projector
 65        self.perspective_data = arcade.camera.PerspectiveProjectionData(
 66            self.window.aspect_ratio, # The ratio between window width and height
 67            75, # The angle  between things at the top of the screen, and the bottom
 68            0.1, # Anything within 0.1 units of the camera won't be visible
 69            100.0 # Anything past 100.0 units of the camera won't be visible
 70        )
 71        self.projector = arcade.camera.PerspectiveProjector()
 72
 73        # Framebuffer / virtual screen to render the contents into
 74        self.fbo = self.window.ctx.framebuffer(
 75            color_attachments=self.window.ctx.texture(size=(1024, 1024))
 76        )
 77
 78        # Set up the geometry buffer for the plane.
 79        # This is four points with texture coordinates
 80        # creating a rectangle
 81        buffer = self.window.ctx.buffer(
 82            data=array(
 83                'f',
 84                [
 85                    # x  y   z  u  v
 86                    -1,  1, 0, 0, 1,  # Top Left
 87                    -1, -1, 0, 0, 0,  # Bottom Left
 88                     1,  1, 0, 1, 1,  # Top Right
 89                     1, -1, 0, 1, 0,  # Bottom right
 90                ]
 91            )
 92        )
 93        # Make this into a geometry object we can draw-
 94        # Here we describe the contents of the buffer so the shader can understand it
 95        self.geometry = self.window.ctx.geometry(
 96            content=[BufferDescription(buffer, "3f 2f", ("in_pos", "in_uv"))],
 97            mode=arcade.gl.TRIANGLE_STRIP,
 98        )
 99
100        # Create some sprites
101        self.spritelist = arcade.SpriteList()
102        for y in range(8):
103            for x in range(8):
104                self.spritelist.append(
105                    arcade.Sprite(
106                        ":resources:images/tiles/boxCrate_double.png",
107                        center_x=64 + x * 128,
108                        center_y=64 + y * 128,
109                    )
110                )
111
112        # Create a 2D camera for rendering to the fbo
113        # by setting the camera's render target it will automatically
114        # size and position itself correctly
115        self.offscreen_cam = arcade.camera.Camera2D(
116            render_target=self.fbo
117        )
118
119    def on_update(self, delta_time: float):
120        # Rotate the perspective camera around the plane
121        view_data = self.projector.view
122        view_data.position = arcade.math.quaternion_rotation(
123            (1.0, 0.0, 0.0), (0, 0, 3), 180 * self.window.time
124        )
125        view_data.forward, view_data.up = arcade.camera.grips.look_at(view_data, (0.0, 0.0, 0.0))
126        print(view_data)
127
128
129    def on_draw(self):
130        # Every frame we can update the offscreen texture if needed
131        self.draw_offscreen()
132        # Clear the window
133        self.clear()
134
135        with self.projector.activate():
136            # Bind the texture containing the offscreen data to channel 0
137            self.fbo.color_attachments[0].use(unit=0)
138
139            # Scroll the texture coordinates
140            self.program["scroll"] = 0, -self.window.time / 5
141
142            # Draw the plane
143            self.geometry.render(self.program)
144
145    def draw_offscreen(self):
146        """Render into the texture mapped """
147        # Activate the offscreen cam, this also activates it's render target
148        with self.offscreen_cam.activate():
149            self.fbo.clear()
150            self.offscreen_cam.use()
151            self.spritelist.draw()
152
153    def on_resize(self, width: int, height: int):
154        super().on_resize(width, height)
155        self.perspective_data.aspect = height / width
156
157
158def main():
159    """ Main function """
160    # Create a window class. This is what actually shows up on screen
161    window = arcade.Window(1280, 720, "Perspective Example", resizable=True)
162
163    # Create and setup the GameView
164    game = GameView()
165
166    # Show GameView on screen
167    window.show_view(game)
168
169    # Start the arcade game loop
170    arcade.run()
171
172
173if __name__ == "__main__":
174    main()