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 __future__ import annotations
 19
 20from array import array
 21
 22import arcade
 23from pyglet.math import Mat4
 24from arcade.gl import BufferDescription
 25
 26
 27class Perspective(arcade.Window):
 28
 29    def __init__(self):
 30        super().__init__(800, 600, "Perspective", resizable=True)
 31        # Simple texture shader for the plane.
 32        # It support projection and model matrix
 33        # and a scroll value for texture coordinates
 34        self.program = self.ctx.program(
 35            vertex_shader="""
 36            #version 330
 37
 38            uniform mat4 projection;
 39            uniform mat4 model;
 40
 41            in vec3 in_pos;
 42            in vec2 in_uv;
 43
 44            out vec2 uv;
 45
 46            void main() {
 47                gl_Position = projection * model * vec4(in_pos, 1.0);
 48                uv = in_uv;
 49            }
 50            """,
 51            fragment_shader="""
 52            #version 330
 53
 54            uniform sampler2D layer;
 55            uniform vec2 scroll;
 56
 57            in vec2 uv;
 58            out vec4 fragColor;
 59
 60            void main() {
 61                fragColor = texture(layer, uv + scroll);
 62            }
 63            """,
 64        )
 65
 66        # # Matrix for perspective projection
 67        self.proj = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75)
 68        # # Configure the projection in the shader
 69        self.program["projection"] = self.proj
 70
 71        # Framebuffer / virtual screen to render the contents into
 72        self.fbo = self.ctx.framebuffer(
 73            color_attachments=self.ctx.texture(size=(1024, 1024))
 74        )
 75
 76        # Set up the geometry buffer for the plane.
 77        # This is four points with texture coordinates
 78        # creating a rectangle
 79        buffer = self.ctx.buffer(
 80            data=array(
 81                'f',
 82                [
 83                    # x  y   z  u  v 
 84                    -1,  1, 0, 0, 1,  # Top Left     
 85                    -1, -1, 0, 0, 0,  # Bottom Left
 86                     1,  1, 0, 1, 1,  # Top Right
 87                     1, -1, 0, 1, 0,  # Bottom right
 88                ]
 89            )
 90        )
 91        # Make this into a geometry object we can draw-
 92        # Here we describe the contents of the buffer so the shader can understand it
 93        self.geometry = self.ctx.geometry(
 94            content=[BufferDescription(buffer, "3f 2f", ("in_pos", "in_uv"))],
 95            mode=self.ctx.TRIANGLE_STRIP,
 96        )
 97
 98        # Create some sprites
 99        self.spritelist = arcade.SpriteList()
100        for y in range(8):
101            for x in range(8):
102                self.spritelist.append(
103                    arcade.Sprite(
104                        ":resources:images/tiles/boxCrate_double.png",
105                        center_x=64 + x * 128,
106                        center_y=64 + y * 128,
107                    )
108                ) 
109        self.time = 0
110
111    def on_draw(self):
112        # Every frame we can update the offscreen texture if needed
113        self.draw_offscreen()
114        # Clear the window
115        self.clear()
116
117        # Bind the texture containing the offscreen data to channel 0
118        self.fbo.color_attachments[0].use(unit=0)
119
120        # Move the plane into camera view and rotate it
121        translate = Mat4.from_translation((0, 0, -2))
122        rotate = Mat4.from_rotation(self.time / 2, (1, 0, 0))
123        self.program["model"] = translate @ rotate
124
125        # Scroll the texture coordinates
126        self.program["scroll"] = 0, -self.time / 5
127
128        # Draw the plane
129        self.geometry.render(self.program)
130
131    def on_update(self, delta_time: float):
132        self.time += delta_time
133
134    def draw_offscreen(self):
135        """Render into the texture mapped """
136        # Activate the offscreen framebuffer and draw the sprites into it
137        with self.fbo.activate() as fbo:
138            fbo.clear()
139            arcade.set_viewport(0, self.fbo.width, 0, self.fbo.height)
140            self.spritelist.draw()
141
142    def on_resize(self, width: int, height: int):
143        super().on_resize(width, height)
144        self.program["projection"] = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75)
145
146
147Perspective().run()