Perspective

Screen shot of a perspective example
perspective.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# flake8: noqa
"""
Perspective example using the lower level rendering API.

This is definitely in the advanced section, but it can be
a useful tool to learn. Sometimes we want perspective
projection for things like backgrounds. This can be
done very efficiently with shaders.

In this example we render content into a framebuffer /
virtual screen and map that on a texture we can rotate
in 3D.
"""

from array import array

import arcade
from pyglet.math import Mat4
from arcade.gl import BufferDescription


class Perspective(arcade.Window):

    def __init__(self):
        super().__init__(800, 600, "Perspective", resizable=True)
        # Simple texture shader for the plane.
        # It support projection and model matrix
        # and a scroll value for texture coordinates
        self.program = self.ctx.program(
            vertex_shader="""
            #version 330

            uniform mat4 projection;
            uniform mat4 model;

            in vec3 in_pos;
            in vec2 in_uv;

            out vec2 uv;

            void main() {
                gl_Position = projection * model * vec4(in_pos, 1.0);
                uv = in_uv;
            }
            """,
            fragment_shader="""
            #version 330

            uniform sampler2D layer;
            uniform vec2 scroll;

            in vec2 uv;
            out vec4 fragColor;

            void main() {
                fragColor = texture(layer, uv + scroll);
            }
            """,
        )

        # # Matrix for perspective projection
        self.proj = Mat4.perspective_projection(0, self.width, 0, self.height, 0.1, 100, fov=75)
        # # Configure the projection in the shader
        self.program["projection"] = self.proj

        # Framebuffer / virtual screen to render the contents into
        self.fbo = self.ctx.framebuffer(
            color_attachments=self.ctx.texture(size=(1024, 1024))
        )

        # Set up the geometry buffer for the plane.
        # This is four points with texture coordinates
        # creating a rectangle
        buffer = self.ctx.buffer(
            data=array(
                'f',
                [
                    # x  y   z  u  v 
                    -1,  1, 0, 0, 1,  # Top Left     
                    -1, -1, 0, 0, 0,  # Bottom Left
                     1,  1, 0, 1, 1,  # Top Right
                     1, -1, 0, 1, 0,  # Bottom right
                ]
            )
        )
        # Make this into a geometry object we can draw-
        # Here we describe the contents of the buffer so the shader can understand it
        self.geometry = self.ctx.geometry(
            content=[BufferDescription(buffer, "3f 2f", ("in_pos", "in_uv"))],
            mode=self.ctx.TRIANGLE_STRIP,
        )

        # Create some sprites
        self.spritelist = arcade.SpriteList()
        for y in range(8):
            for x in range(8):
                self.spritelist.append(
                    arcade.Sprite(
                        ":resources:images/tiles/boxCrate_double.png",
                        center_x=64 + x * 128,
                        center_y=64 + y * 128,
                    )
                ) 
        self.time = 0

    def on_draw(self):
        # Every frame we can update the offscreen texture if needed
        self.draw_offscreen()
        # Clear the window
        self.clear()

        # Bind the texture containing the offscreen data to channel 0
        self.fbo.color_attachments[0].use(unit=0)

        # Move the plane into camera view and rotate it
        translate = Mat4.from_translation((0, 0, -2))
        rotate = Mat4.from_rotation(self.time / 2, (1, 0, 0))
        self.program["model"] = rotate @ translate

        # Scroll the texture coordinates
        self.program["scroll"] = 0, -self.time / 5

        # Draw the plane
        self.geometry.render(self.program)

    def on_update(self, delta_time: float):
        self.time += delta_time

    def draw_offscreen(self):
        """Render into the texture mapped """
        # Activate the offscreen framebuffer and draw the sprites into it
        with self.fbo.activate() as fbo:
            fbo.clear()
            arcade.set_viewport(0, self.fbo.width, 0, self.fbo.height)
            self.spritelist.draw()

    def on_resize(self, width: float, height: float):
        super().on_resize(width, height)
        self.program["projection"] = Mat4.perspective_projection(0, self.width, 0, self.height, 0.1, 100, fov=75)


Perspective().run()