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()