Skip to content

Commit

Permalink
Enhance the pygame example
Browse files Browse the repository at this point in the history
  • Loading branch information
einarf committed Oct 15, 2022
1 parent aeea62f commit 32c83f0
Showing 1 changed file with 130 additions and 16 deletions.
146 changes: 130 additions & 16 deletions examples/advanced/pygame2_simple.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,174 @@
"""
Based on BlubberQuark's blog:
Based partly on BlubberQuark's blog:
https://blubberquark.tumblr.com/post/185013752945/using-moderngl-for-post-processing-shaders-with
Clears the screen using opengl with a constantly changing
color value and alpha blend a pygame surface on top.
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 pathlib import Path
from array import array

import pygame

import moderngl
import moderngl_window
from moderngl_window import geometry


class Pygame(moderngl_window.WindowConfig):
"""
Example using pygame with moderngl.
Needs to run with ``--window pygame2`` option.
Example drawing a pygame surface with moderngl.
"""
title = "Pygame"
window_size = 1280, 720
resource_dir = (Path(__file__) / '../../resources').absolute()

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

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

self.pg_res = (320, 180)
# 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)
# 24 bit (rgba) moderngl texture
# 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
self.texture_program = self.load_program('programs/texture.glsl')
self.quad_fs = geometry.quad_fs()
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, frametime):
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)
self.pg_texture.use()
self.quad_fs.render(self.texture_program)
# 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):
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
Expand Down

0 comments on commit 32c83f0

Please sign in to comment.