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