File: pygame2_simple.py

package info (click to toggle)
python-moderngl-window 2.4.6-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 69,220 kB
  • sloc: python: 11,387; makefile: 21
file content (191 lines) | stat: -rw-r--r-- 7,456 bytes parent folder | download
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""
Based partly on BlubberQuark's blog:
https://blubberquark.tumblr.com/post/185013752945/using-moderngl-for-post-processing-shaders-with

Basic example showing how to efficiently render a pygame surface with moderngl
in the most efficient way. We include alpha channel as well since this
is a very common use case.

This involves to steps:
* Copy the surface data from system memory into graphics memory (texture)
* Render this texture to the screen with some simple geometry

There are two common ways to get the pixel data from a pygame surface:
* pygame.image.tostring(surface, "RGBA", ...)
* surface.get_view("1")

We're using get_view() here because it's faster and more efficient.
In fact about 700+ times faster than tostring() since get_view() doesn't
copy or transform the data in any way.
This however comes with a caveat:

* The raw data of the surface is in BGRA format instead of RGBA so
  we need to set a swizzle on the OpenGL texture to swap the channels.
  This just means OpenGL will swap the channels when reading the data.
  It's pretty much a "free" operation.
* OpenGL are storing textures upside down so we usually need to flip
  the texture. The raw surface data is not flipped, but we can flip
  the texture coordinates instead.

To be as explicit as possible we're not using any shortcuts and include
our own shader program and geometry to render the texture to the screen.
In other words: we are using none of the shortcuts in moderngl-window.

Also note that this example can easily be tweaked to only use RGB data
instead of RGBA if alpha channel is not needed.

Other notes:
* We don't use any projection in this example working directly in
  normalized device coordinates. Meaning we are working in the range
  [-1, 1] for both x and y.
* Texture coordinates are in the [0.0, 1.0] range.
"""
import math
from array import array

import pygame

import moderngl
import moderngl_window


class Pygame(moderngl_window.WindowConfig):
    """
    Example drawing a pygame surface with moderngl.
    """
    title = "Pygame"
    window_size = 1280, 720

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        if self.wnd.name != 'pygame2':
            raise RuntimeError('This example only works with --window pygame2 option')

        # The resolution of the pygame surface
        self.pg_res = 320, 180
        # Create a 24bit (rgba) offscreen surface pygame can render to
        self.pg_screen = pygame.Surface(self.pg_res, flags=pygame.SRCALPHA)
        # 32 bit (rgba) moderngl texture (4 channels, RGBA)
        self.pg_texture = self.ctx.texture(self.pg_res, 4)
        # Change the texture filtering to NEAREST for pixelated look.
        self.pg_texture.filter = moderngl.NEAREST, moderngl.NEAREST
        # The pygame surface is stored in BGRA format but RGBA
        # so we simply change the order of the channels of the texture
        self.pg_texture.swizzle = 'BGRA'

        # Let's make a custom texture shader rendering the surface
        self.texture_program = self.ctx.program(
            vertex_shader="""
                #version 330
                // Vertex shader runs once for each vertex in the geometry

                in vec2 in_vert;
                in vec2 in_texcoord;
                out vec2 uv;

                void main() {
                    // Send the texture coordinates to the fragment shader
                    uv = in_texcoord;
                    // Resolve the vertex position
                    gl_Position = vec4(in_vert, 0.0, 1.0);
                }
            """,
            fragment_shader="""
                #version 330
                // Fragment shader runs once for each pixel in the triangles.
                // We are drawing two triangles here creating a quad.
                // In values are interpolated between the vertices.

                // Sampler reading from a texture channel 0
                uniform sampler2D surface;

                // The pixel we are writing to the screen
                out vec4 f_color;
                // Interpolated texture coordinates
                in vec2 uv;

                void main() {
                    // Simply look up the color from the texture
                    f_color = texture(surface, uv);
                }
            """,
        )
        # Explicitly configure the sampler to read from texture channel 0.
        # Most hardware today supports 8-16 different channels for multi-texturing.
        self.texture_program['surface'] = 0

        # Geometry to render the texture to the screen.
        # This is simply a "quad" covering the entire screen.
        # This is rendered as a triangle strip.
        # NOTE: using array.array is a simple way to create a buffer data
        buffer = self.ctx.buffer(
            data=array('f', [
                # Position (x, y) , Texture coordinates (x, y)
                -1.0, 1.0, 0.0, 1.0,  # upper left
                -1.0, -1.0, 0.0, 0.0,  # lower left
                1.0, 1.0, 1.0, 1.0,  # upper right
                1.0, -1.0, 1.0, 0.0,  # lower right
            ])
        )
        # Create a vertex array describing the buffer layout.
        # The shader program is also passed in there to sanity check
        # the attribute names.
        self.quad_fs = self.ctx.vertex_array(
            self.texture_program,
            [
                (
                    # The buffer containing the data
                    buffer,
                    # Format of the two attributes. 2 floats for position, 2 floats for texture coordinates
                    "2f 2f",
                    # Names of the attributes in the shader program
                    "in_vert", "in_texcoord",
                )
            ],
        )

    def render(self, time: float, frame_time: float):
        """Called every frame"""
        self.render_pygame(time)

        # Clear the screen
        self.ctx.clear(
            (math.sin(time) + 1.0) / 2,
            (math.sin(time + 2) + 1.0) / 2,
            (math.sin(time + 3) + 1.0) / 2,
        )
    
        # Enable blending for transparency
        self.ctx.enable(moderngl.BLEND)
        # Bind the texture to texture channel 0
        self.pg_texture.use(location=0)
        # Render the quad to the screen. Will use the texture we bound above.
        self.quad_fs.render(mode=moderngl.TRIANGLE_STRIP)
        # Disable blending
        self.ctx.disable(moderngl.BLEND)

    def render_pygame(self, time: float):
        """Render to offscreen surface and copy result into moderngl texture"""
        self.pg_screen.fill((0, 0, 0, 0))  # Make sure we clear with alpha 0!
        # Draw some simple circles to the surface
        N = 8
        for i in range(N):
            time_offset = 6.28 / N * i
            pygame.draw.circle(
                self.pg_screen,
                ((i * 50) % 255, (i * 100) % 255, (i * 20) % 255),
                (
                    math.sin(time + time_offset) * 55 + self.pg_res[0] // 2,
                    math.cos(time + time_offset) * 55 + self.pg_res[1] // 2),
                math.sin(time) * 4 + 15,
            )

        # Get the buffer view of the Surface's pixels
        # and write this data into the texture
        texture_data = self.pg_screen.get_view('1')
        self.pg_texture.write(texture_data)


if __name__ == '__main__':
    moderngl_window.run_window_config(Pygame, args=('--window', 'pygame2'))