Textures and Rendering
This page covers GPU-side imaging in pyglet 3.0: drawing images, working with textures, texture atlases, and framebuffers.
Texture classes moved from pyglet.image to pyglet.graphics.texture in
pyglet 3.0. Use Texture and related classes
for GPU resources, and keep pyglet.image for CPU-side image
data (see Images and Image Data).
Drawing images
For most users, the Sprite class is the best way to
draw an image.
Note
image.blit no longer exists in pyglet 3.0. Use sprites for drawing.
Example:
import pyglet
window = pyglet.window.Window(800, 600)
image = pyglet.image.load("kitten.png")
sprite = pyglet.sprite.Sprite(image, x=100, y=80)
@window.event
def on_draw():
window.clear()
sprite.draw()
pyglet.app.run()
Sprites can be created from either ImageData,
Animation, or an existing texture object.
Batched sprites
If you need to draw many sprites, use a Batch:
batch = pyglet.graphics.Batch()
sprites = [pyglet.sprite.Sprite(image, batch=batch) for _ in range(100)]
@window.event
def on_draw():
window.clear()
batch.draw()
When using batches, draw order can be controlled with
Group objects:
batch = pyglet.graphics.Batch()
background = pyglet.graphics.Group(order=0)
foreground = pyglet.graphics.Group(order=1)
pyglet.sprite.Sprite(image, batch=batch, group=background)
pyglet.sprite.Sprite(image, batch=batch, group=foreground)
To reduce texture switches, prefer atlased textures (for example via
texture()) or use explicit texture bins/atlases as
described below.
Converting images and textures
CPU image objects can be uploaded to the GPU with
get_texture():
img = pyglet.image.load('kitten.png')
texture = img.get_texture()
If you need finer control over texture creation (for example filtering,
address mode, texture type, or internal format settings), use
Texture.create_from_image(...) instead of get_texture().
Reading texture pixels back to CPU memory can be done with
Texture.fetch():
image_data = texture.fetch()
fetch() is intended for image-oriented use. It returns unsigned-byte
ImageData and does not convert floating-point or
integer textures. Use Texture.read_pixels() when typed values are needed:
pixels = texture.read_pixels()
float_values = memoryview(pixels.data).cast("f")
The returned PixelData contains tightly
packed raw bytes together with their component format and data type. Neither
method should generally be called every frame because GPU readback can be
expensive.
Writing into textures
To write CPU image data into an existing texture, use Texture.upload(...).
This method was previously named blit_into.
image_data = pyglet.image.load('overlay.png').get_image_data()
texture = pyglet.graphics.Texture.create(512, 512)
texture.upload(image_data, x=0, y=0, z=0)
Texture uploads replace texel data in the target region. They do not perform alpha blending with existing texels. Blending only applies when drawing geometry/sprites with blend state enabled. For render-based updates, see Drawing into a texture.
Image sequences and atlases
Sometimes a single source image is used to hold many sub-images (for example, sprite sheets). pyglet provides helpers for this workflow.
Texture grids
You can define the CPU-side grid first with ImageGrid
(see Image grids in Images and Image Data). Then convert it to a
GPU-side TextureGrid for efficient
rendering:
explosion = pyglet.image.load('explosion.png')
explosion_grid = pyglet.image.ImageGrid(explosion, 1, 8)
explosion_tex_grid = pyglet.graphics.TextureGrid.from_image_grid(explosion_grid)
first_frame = explosion_tex_grid[0]
TextureGrid can also be created directly
with the same grid arguments as ImageGrid
(rows, columns, optional item_width/item_height, and padding),
but with a texture as the first argument:
texture = explosion.get_texture()
explosion_tex_grid = pyglet.graphics.TextureGrid(texture, 1, 8)
pyglet.graphics.TextureGrid is the same class alias exposed as
TextureGrid.
TextureGrid items are
TextureRegion objects.
Texture bins and atlases
A TextureAtlas is a large texture that packs
many smaller images. A TextureBin manages
multiple atlases as needed.
images = [
pyglet.image.load('img1.png'),
pyglet.image.load('img2.png'),
]
bin = pyglet.graphics.atlas.TextureBin()
regions = [bin.add(image) for image in images]
The result of TextureBin.add is typically a
TextureRegion.
3D textures
You can create a Texture3D from a sequence
of images or from an
ImageGrid (see Image grids):
explosion_3d = pyglet.graphics.texture.Texture3D.create_for_image_grid(explosion_grid)
Slicing a Texture3D returns
TextureRegion objects for layers.
Framebuffers
For explicit framebuffer objects, use Framebuffer and Renderbuffer in
pyglet.graphics.framebuffer.
import pyglet
from pyglet.enums import TextureFilter, FramebufferAttachment, ComponentFormat
from pyglet.graphics import Texture, Renderbuffer, Framebuffer
window = pyglet.window.Window()
color_buffer = Texture.create(width, height, filters=TextureFilter.NEAREST)
depth_buffer = Renderbuffer(window.context, width, height,
component_format=ComponentFormat.D, bit_size=24)
framebuffer = Framebuffer(context=window.context)
framebuffer.attach_texture(color_buffer, attachment=FramebufferAttachment.COLOR0)
framebuffer.attach_renderbuffer(depth_buffer, attachment=FramebufferAttachment.DEPTH)
framebuffer.bind()
Drawing into a texture
Use a texture render target when you want to render into a texture, for example for post-processing, compositing, minimaps, or dynamic texture generation.
Warning
Render targets allocate GPU resources and drawing into them submits GPU work. Creating targets or generating many textures every frame can be slow. Reuse can reduce setup overhead, but it does not eliminate texture allocation or rendering costs.
Persistent render textures
RenderTexture owns a fixed color
texture and the framebuffer and camera used to draw into it. It is useful when
the same texture is updated repeatedly:
import pyglet
window = pyglet.window.Window(800, 600)
scene_batch = pyglet.graphics.Batch()
target = pyglet.graphics.RenderTexture(512, 512)
with target:
scene_batch.draw()
# Use the result like any other texture.
result_sprite = pyglet.sprite.Sprite(target.texture)
Entering the target clears it and temporarily installs a camera and viewport matching its dimensions. On exit, the previous framebuffer, camera, viewport, and scissor state are restored.
Deleting the target releases its framebuffer, camera-related state, and optional depth buffer, but retains the color texture by default:
texture = target.texture
target.delete()
# The texture is still valid.
result_sprite = pyglet.sprite.Sprite(texture)
# Delete it explicitly when it is no longer needed.
texture.delete()
Pass delete_texture=True to
delete() when the color
texture should be deleted with the target.
Generating independent textures efficiently
Each output must have its own texture if several results need to remain valid,
but the framebuffer and camera do not need to be recreated for every output.
TextureRenderTarget keeps that target
state and attaches a fresh texture for each render scope:
target = pyglet.graphics.TextureRenderTarget()
textures = []
for batch, width, height in jobs:
with target.render_to_texture(width, height) as texture:
batch.draw()
textures.append(texture)
# This does not delete any successfully returned textures.
target.delete()
for texture in textures:
texture.delete()
The reusable target updates its camera and viewport for each output size. If depth buffering is enabled, a same-sized depth buffer is reused and is recreated only when the dimensions change. If drawing raises an exception, the incomplete output texture is deleted and the target remains reusable.
Only the framebuffer, camera, and compatible depth buffer are reused. Every successful call still allocates and renders into a new GPU texture so that previous results remain independent.
Text layouts accept a reusable target through
get_as_texture():
target = pyglet.graphics.TextureRenderTarget()
textures = [label.get_as_texture(target) for label in labels]
target.delete()
Without an argument, get_as_texture creates and deletes a temporary target,
which is convenient for a single conversion:
texture = label.get_as_texture()
The returned textures are always caller-owned and must eventually be deleted.
Note
Render targets follow the same context rules as other graphics resources.
They do not switch windows automatically. In a multi-window application,
call window.switch_to() before constructing or using a target belonging
to that window.
Low-level framebuffer usage
For direct control, create a framebuffer and attach textures and renderbuffers manually.
import pyglet
from pyglet.enums import FramebufferAttachment, TextureFilter
window = pyglet.window.Window(800, 600)
source_image = pyglet.image.load('source.png')
source_sprite = pyglet.sprite.Sprite(source_image, x=100, y=120)
target_texture = pyglet.graphics.Texture.create(
800, 600, filters=TextureFilter.NEAREST
)
framebuffer = pyglet.graphics.Framebuffer(context=window.context)
framebuffer.attach_texture(target_texture, attachment=FramebufferAttachment.COLOR0)
# This sprite displays the texture we rendered into:
result_sprite = pyglet.sprite.Sprite(target_texture, x=0, y=0)
@window.event
def on_draw():
# Pass 1: render into texture:
framebuffer.bind()
window.clear()
source_sprite.draw()
framebuffer.unbind()
# Pass 2: render texture to window:
window.clear()
result_sprite.draw()
pyglet.app.run()
This is different from Texture.upload(...): framebuffer rendering runs the
normal draw pipeline (shaders, blending, draw order), while upload is a
direct texel data replacement.
To capture the default framebuffer to an image:
from pyglet.graphics.framebuffer import get_screenshot
screenshot = get_screenshot()
screenshot.save('screenshot.png')
OpenGL texture access
This section assumes familiarity with texture mapping in OpenGL (for example, chapter 9 of the OpenGL Programming Guide).
To create a texture from image data:
kitten = pyglet.image.load('kitten.jpg')
texture = kitten.get_texture()
A Texture has a target and id:
from pyglet.graphics.api.gl import *
glBindTexture(texture.target, texture.handle)
Texture filtering
By default, all textures are created with smooth (LINEAR)
filtering.
To use a different filter for a specific texture, pass the filtering constant(s)
to Texture.create via the filter arguments or
Texture.create_from_image.
Pixel art
To enable nearest-neighbor filtering for retro-style games, set the
corresponding variables of pyglet.graphics.texture.Texture to
NEAREST:
from pyglet.enums import TextureFilter
pyglet.graphics.Texture.default_filters = (TextureFilter.NEAREST, TextureFilter.NEAREST)
Afterward, all textures pyglet creates will default to nearest-neighbor sampling.