1"""
2Shows simple use of transform feedback.
3
4Transforming is similar to rendering except that the output
5or the shader is a buffer instead of a framebuffer/screen.
6
7This examples shows a common ping-pong technique were we
8transform a buffer with positions and velocities between
9two buffers so we always work on the previous state.
10
11* A list of N points are initialized with random positions and velocities
12* A point of gravity is moving around on the screen affecting the points
13
14Using transforms in this way makes us able to process
15a system that is reacting to external forces in this way.
16There are no predetermined paths and they system just lives on its own.
17
18If Python and Arcade are installed, this example can be run from the command line with:
19python -m arcade.examples.transform_feedback
20"""
21from __future__ import annotations
22
23from array import array
24import math
25import time
26import random
27import arcade
28from arcade.gl import BufferDescription
29
30# Do the math to figure out our screen dimensions
31SCREEN_WIDTH = 800
32SCREEN_HEIGHT = 600
33SCREEN_TITLE = "Transform Feedback"
34
35
36class MyGame(arcade.Window):
37
38 def __init__(self, width, height, title):
39 super().__init__(width, height, title, resizable=True)
40 self.time = 0
41
42 # Program to visualize the points
43 self.points_program = self.ctx.program(
44 vertex_shader="""
45 #version 330
46 in vec2 in_pos;
47 out vec3 color;
48 void main() {
49 // Let's just give them a "random" color based on the vertex id
50 color = vec3(
51 mod(float(gl_VertexID * 100 % 11) / 10.0, 1.0),
52 mod(float(gl_VertexID * 100 % 27) / 10.0, 1.0),
53 mod(float(gl_VertexID * 100 % 71) / 10.0, 1.0));
54 // Pass the point position to primitive assembly
55 gl_Position = vec4(in_pos, 0.0, 1.0);
56 }
57 """,
58 fragment_shader="""
59 #version 330
60
61 // Color passed in from the vertex shader
62 in vec3 color;
63 // The pixel we are writing to in the framebuffer
64 out vec4 fragColor;
65
66 void main() {
67 // Fill the point
68 fragColor = vec4(color, 1.0);
69 }
70 """,
71 )
72
73 # A program transforming points being affected by a gravity point
74 self.gravity_program = self.ctx.program(
75 vertex_shader="""
76 #version 330
77
78 // Delta time (since last frame)
79 uniform float dt;
80 // Strength of gravity
81 uniform float force;
82 // Position of gravity
83 uniform vec2 gravity_pos;
84
85 // The format of the data in our transform buffer(s)
86 in vec2 in_pos;
87 in vec2 in_vel;
88
89 // We are writing to a buffer of the same format
90 out vec2 out_pos;
91 out vec2 out_vel;
92
93 void main() {
94 // Simplified gravity calculations
95 vec2 dir = normalize(gravity_pos - in_pos) * force;
96 vec2 vel = in_vel + dir / length(dir) * 0.01;
97
98 // Write to the output buffer
99 out_vel = vel;
100 out_pos = in_pos + vel * dt;
101 }
102 """,
103 )
104 N = 50_000
105 # Make two buffers we transform between so we can work on the previous result
106 self.buffer_1 = self.ctx.buffer(data=array('f', self.gen_initial_data(N)))
107 self.buffer_2 = self.ctx.buffer(reserve=self.buffer_1.size)
108
109 # We also need to be able to visualize both versions (draw to the screen)
110 self.vao_1 = self.ctx.geometry([BufferDescription(self.buffer_1, '2f 2x4', ['in_pos'])])
111 self.vao_2 = self.ctx.geometry([BufferDescription(self.buffer_2, '2f 2x4', ['in_pos'])])
112
113 # We need to be able to transform both buffers (ping-pong)
114 self.gravity_1 = self.ctx.geometry([BufferDescription(self.buffer_1, '2f 2f', ['in_pos', 'in_vel'])])
115 self.gravity_2 = self.ctx.geometry([BufferDescription(self.buffer_2, '2f 2f', ['in_pos', 'in_vel'])])
116
117 self.ctx.enable_only() # Ensure no context flags are set
118 self.time = time.time()
119
120 def gen_initial_data(self, count):
121 for _ in range(count):
122 yield random.uniform(-1.2, 1.2) # pos x
123 yield random.uniform(-1.2, 1.2) # pos y
124 yield random.uniform(-.3, .3) # velocity x
125 yield random.uniform(-.3, .3) # velocity y
126
127 def on_draw(self):
128 self.clear()
129 self.ctx.point_size = 2 * self.get_pixel_ratio()
130
131 # Calculate the actual delta time and current time
132 t = time.time()
133 frame_time = t - self.time
134 self.time = t
135
136 # Set uniforms in the program
137 self.gravity_program['dt'] = frame_time
138 self.gravity_program['force'] = 0.25
139 self.gravity_program['gravity_pos'] = math.sin(self.time * 0.77) * 0.25, math.cos(self.time) * 0.25
140
141 # Transform data in buffer_1 into buffer_2
142 self.gravity_1.transform(self.gravity_program, self.buffer_2)
143 # Render the result (Draw buffer_2)
144 self.vao_2.render(self.points_program, mode=self.ctx.POINTS)
145
146 # Swap around stuff around so we transform back and fourth between the two buffers
147 self.gravity_1, self.gravity_2 = self.gravity_2, self.gravity_1
148 self.vao_1, self.vao_2 = self.vao_2, self.vao_1
149 self.buffer_1, self.buffer_2 = self.buffer_2, self.buffer_1
150
151
152if __name__ == "__main__":
153 window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
154 window.center_window()
155 arcade.run()