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    def on_draw(self):
110        # Every frame we can update the offscreen texture if needed
111        self.draw_offscreen()
112        # Clear the window
113        self.clear()
114
115        # Bind the texture containing the offscreen data to channel 0
116        self.fbo.color_attachments[0].use(unit=0)
117
118        # Move the plane into camera view and rotate it
119        translate = Mat4.from_translation((0, 0, -2))
120        rotate = Mat4.from_rotation(self.time / 2, (1, 0, 0))
121        self.program["model"] = translate @ rotate
122
123        # Scroll the texture coordinates
124        self.program["scroll"] = 0, -self.time / 5
125
126        # Draw the plane
127        self.geometry.render(self.program)
128
129    def on_update(self, delta_time: float):
130        self.time += delta_time
131
132    def draw_offscreen(self):
133        """Render into the texture mapped """
134        # Activate the offscreen framebuffer and draw the sprites into it
135        with self.fbo.activate() as fbo:
136            fbo.clear()
137            arcade.set_viewport(0, self.fbo.width, 0, self.fbo.height)
138            self.spritelist.draw()
139
140    def on_resize(self, width: int, height: int):
141        super().on_resize(width, height)
142        self.program["projection"] = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75)
143
144
145Perspective().run()