Perspective

Screen shot of a perspective example
perspective.py
  1# flake8: noqa
  2"""
  3Perspective example using the lower level rendering API.
  4
  5This is definitely in the advanced section, but it can be
  6a useful tool to learn. Sometimes we want perspective
  7projection for things like backgrounds. This can be
  8done very efficiently with shaders.
  9
 10In this example we render content into a framebuffer /
 11virtual screen and map that on a texture we can rotate
 12in 3D.
 13
 14If Python and Arcade are installed, this example can be run from the command line with:
 15python -m arcade.examples.perspective
 16"""
 17
 18from array import array
 19
 20import arcade
 21from pyglet.math import Mat4
 22from arcade.gl import BufferDescription
 23
 24
 25class Perspective(arcade.Window):
 26
 27    def __init__(self):
 28        super().__init__(800, 600, "Perspective", resizable=True)
 29        # Simple texture shader for the plane.
 30        # It support projection and model matrix
 31        # and a scroll value for texture coordinates
 32        self.program = self.ctx.program(
 33            vertex_shader="""
 34            #version 330
 35
 36            uniform mat4 projection;
 37            uniform mat4 model;
 38
 39            in vec3 in_pos;
 40            in vec2 in_uv;
 41
 42            out vec2 uv;
 43
 44            void main() {
 45                gl_Position = projection * 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        # # Matrix for perspective projection
 65        self.proj = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75)
 66        # # Configure the projection in the shader
 67        self.program["projection"] = self.proj
 68
 69        # Framebuffer / virtual screen to render the contents into
 70        self.fbo = self.ctx.framebuffer(
 71            color_attachments=self.ctx.texture(size=(1024, 1024))
 72        )
 73
 74        # Set up the geometry buffer for the plane.
 75        # This is four points with texture coordinates
 76        # creating a rectangle
 77        buffer = self.ctx.buffer(
 78            data=array(
 79                'f',
 80                [
 81                    # x  y   z  u  v 
 82                    -1,  1, 0, 0, 1,  # Top Left     
 83                    -1, -1, 0, 0, 0,  # Bottom Left
 84                     1,  1, 0, 1, 1,  # Top Right
 85                     1, -1, 0, 1, 0,  # Bottom right
 86                ]
 87            )
 88        )
 89        # Make this into a geometry object we can draw-
 90        # Here we describe the contents of the buffer so the shader can understand it
 91        self.geometry = self.ctx.geometry(
 92            content=[BufferDescription(buffer, "3f 2f", ("in_pos", "in_uv"))],
 93            mode=self.ctx.TRIANGLE_STRIP,
 94        )
 95
 96        # Create some sprites
 97        self.spritelist = arcade.SpriteList()
 98        for y in range(8):
 99            for x in range(8):
100                self.spritelist.append(
101                    arcade.Sprite(
102                        ":resources:images/tiles/boxCrate_double.png",
103                        center_x=64 + x * 128,
104                        center_y=64 + y * 128,
105                    )
106                ) 
107        self.time = 0
108
109        self.offscreen_cam = arcade.camera.Camera2D(
110            position=(0.0, 0.0),
111            viewport=arcade.LBWH(0, 0, self.fbo.width, self.fbo.height),
112            projection=arcade.LRBT(0, self.fbo.width, 0, self.fbo.height)
113        )
114
115    def on_draw(self):
116        # Every frame we can update the offscreen texture if needed
117        self.draw_offscreen()
118        # Clear the window
119        self.clear()
120
121        # Bind the texture containing the offscreen data to channel 0
122        self.fbo.color_attachments[0].use(unit=0)
123
124        # Move the plane into camera view and rotate it
125        translate = Mat4.from_translation((0, 0, -2))
126        rotate = Mat4.from_rotation(self.time / 2, (1, 0, 0))
127        self.program["model"] = translate @ rotate
128
129        # Scroll the texture coordinates
130        self.program["scroll"] = 0, -self.time / 5
131
132        # Draw the plane
133        self.geometry.render(self.program)
134
135    def on_update(self, delta_time: float):
136        self.time += delta_time
137
138    def draw_offscreen(self):
139        """Render into the texture mapped """
140        # Activate the offscreen framebuffer and draw the sprites into it
141        with self.fbo.activate() as fbo:
142            fbo.clear()
143            self.offscreen_cam.use()
144            self.spritelist.draw()
145
146    def on_resize(self, width: int, height: int):
147        super().on_resize(width, height)
148        self.program["projection"] = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75)
149
150
151def main():
152    Perspective().run()
153
154
155if __name__ == "__main__":
156    main()