Perspective

perspective.py
1"""
2Perspective example using the PerspectiveProjectionCamera
3
4This is definitely in the advanced section, but it can be
5a useful tool to learn. Sometimes we want perspective
6projection for things like backgrounds. This can be
7done very efficiently with shaders.
8
9In this example we render content into a framebuffer /
10virtual screen and map that on a texture we can rotate
11in 3D.
12
13If Python and Arcade are installed, this example can be run from the command line with:
14python -m arcade.examples.perspective
15"""
16
17from array import array
18
19import arcade
20from arcade.gl import BufferDescription
21
22
23class GameView(arcade.View):
24
25 def __init__(self):
26 super().__init__()
27 # Simple texture shader for the plane.
28 # It support projection and model matrix
29 # and a scroll value for texture coordinates
30 self.program = self.window.ctx.program(
31 vertex_shader="""
32 #version 330
33
34 uniform WindowBlock {
35 mat4 projection;
36 mat4 model;
37 } window;
38
39 in vec3 in_pos;
40 in vec2 in_uv;
41
42 out vec2 uv;
43
44 void main() {
45 gl_Position = window.projection * window.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 # Configure and create the perspective projector
65 self.perspective_data = arcade.camera.PerspectiveProjectionData(
66 self.window.aspect_ratio, # The ratio between window width and height
67 75, # The angle between things at the top of the screen, and the bottom
68 0.1, # Anything within 0.1 units of the camera won't be visible
69 100.0 # Anything past 100.0 units of the camera won't be visible
70 )
71 self.projector = arcade.camera.PerspectiveProjector()
72
73 # Framebuffer / virtual screen to render the contents into
74 self.fbo = self.window.ctx.framebuffer(
75 color_attachments=self.window.ctx.texture(size=(1024, 1024))
76 )
77
78 # Set up the geometry buffer for the plane.
79 # This is four points with texture coordinates
80 # creating a rectangle
81 buffer = self.window.ctx.buffer(
82 data=array(
83 'f',
84 [
85 # x y z u v
86 -1, 1, 0, 0, 1, # Top Left
87 -1, -1, 0, 0, 0, # Bottom Left
88 1, 1, 0, 1, 1, # Top Right
89 1, -1, 0, 1, 0, # Bottom right
90 ]
91 )
92 )
93 # Make this into a geometry object we can draw-
94 # Here we describe the contents of the buffer so the shader can understand it
95 self.geometry = self.window.ctx.geometry(
96 content=[BufferDescription(buffer, "3f 2f", ("in_pos", "in_uv"))],
97 mode=arcade.gl.TRIANGLE_STRIP,
98 )
99
100 # Create some sprites
101 self.spritelist = arcade.SpriteList()
102 for y in range(8):
103 for x in range(8):
104 self.spritelist.append(
105 arcade.Sprite(
106 ":resources:images/tiles/boxCrate_double.png",
107 center_x=64 + x * 128,
108 center_y=64 + y * 128,
109 )
110 )
111
112 # Create a 2D camera for rendering to the fbo
113 # by setting the camera's render target it will automatically
114 # size and position itself correctly
115 self.offscreen_cam = arcade.camera.Camera2D(
116 render_target=self.fbo
117 )
118
119 def on_update(self, delta_time: float):
120 # Rotate the perspective camera around the plane
121 view_data = self.projector.view
122 view_data.position = arcade.math.quaternion_rotation(
123 (1.0, 0.0, 0.0), (0, 0, 3), 180 * self.window.time
124 )
125 view_data.forward, view_data.up = arcade.camera.grips.look_at(view_data, (0.0, 0.0, 0.0))
126 print(view_data)
127
128
129 def on_draw(self):
130 # Every frame we can update the offscreen texture if needed
131 self.draw_offscreen()
132 # Clear the window
133 self.clear()
134
135 with self.projector.activate():
136 # Bind the texture containing the offscreen data to channel 0
137 self.fbo.color_attachments[0].use(unit=0)
138
139 # Scroll the texture coordinates
140 self.program["scroll"] = 0, -self.window.time / 5
141
142 # Draw the plane
143 self.geometry.render(self.program)
144
145 def draw_offscreen(self):
146 """Render into the texture mapped """
147 # Activate the offscreen cam, this also activates it's render target
148 with self.offscreen_cam.activate():
149 self.fbo.clear()
150 self.offscreen_cam.use()
151 self.spritelist.draw()
152
153 def on_resize(self, width: int, height: int):
154 super().on_resize(width, height)
155 self.perspective_data.aspect = height / width
156
157
158def main():
159 """ Main function """
160 # Create a window class. This is what actually shows up on screen
161 window = arcade.Window(1280, 720, "Perspective Example", resizable=True)
162
163 # Create and setup the GameView
164 game = GameView()
165
166 # Show GameView on screen
167 window.show_view(game)
168
169 # Start the arcade game loop
170 arcade.run()
171
172
173if __name__ == "__main__":
174 main()