ZenGL#
ZenGL is a minimalist Python module providing exactly one way to render scenes with OpenGL.
pip install zengl
ZenGL is …
high-performance
simple - buffers, images, pipelines and there you go
easy-to-learn - it is simply OpenGL with no magic added
verbose - most common mistakes are catched and reported in a clear and understandable way
robust - there is no global state or external trouble-maker affecting the render
backward-compatible - it requires OpenGL 3.3 - it is just enough
cached - most OpenGL objects are reused between renders
zen - there is one way to do it
- class Context#
- class Buffer#
- class Image#
- class Pipeline#
Concept#
Context#
All interactions with OpenGL are done by a Context object. Only the first call to this method creates a new context. Multiple calls return the previously created context.
- zengl.loader(headless: bool = False) ContextLoader #
This method provides a default context loader. Headless contexts require glcontext to be installed.
Note
Implementing a context loader enables zengl to run in custom environments. ZenGL uses a subset of the OpenGL 3.3 core, the list of methods can be found in the project source. The implementation takes into account the OpenGL ES compatibility and can also work with a WebGL2 backend.
- zengl.init(loader: ContextLoader)#
Initialize the OpenGL bindings.
This method is automatically called by zengl.context()
.
Context for a window
ctx = zengl.context()
Context for headless rendering
zengl.init(zengl.loader(headless=True))
ctx = zengl.context()
Rendering
- Context.new_frame(reset: bool = True, clear: bool = True, frame_time: bool = False)#
- reset
- A boolean to clear ZenGL internals assuming OpenGL global state.
- clear
- A boolean to clear the default framebuffer.
- frame_time
- A boolean to start a query with
GL_TIME_ELAPSED
.TheContext.frame_time
is set byContext.end_frame()
.
- Context.end_frame(clean: bool = True, flush: bool = True, sync: bool = False)#
- clean
- A boolean to unset OpenGL object bindings managed by ZenGL.The values are not restored from any previous states, they are set to zero.
- flush
- A boolean to call
glFlush
. - sync
- A boolean to wait for a
glFenceSync
.
Buffer#
vertex_buffer = ctx.buffer(open('sphere.mesh', 'rb').read())
vertex_buffer = ctx.buffer(np.array([0.0, 0.0, 1.0, 1.0], 'f4'))
index_buffer = ctx.buffer(np.array([0, 1, 2], 'i4'))
vertex_buffer = ctx.buffer(size=1024)
- data
- The buffer content, represented as
bytes
or a buffer for example a numpy array.If the data is None the content of the buffer will be uninitialized and the size is mandatory.The default value is None. - size
- The size of the buffer. It must be None if the data parameter was provided.The default value is None and it means the size of the data.
- dynamic
- A boolean to enable
GL_DYNAMIC_DRAW
on buffer creation.When this flag is False theGL_STATIC_DRAW
is used.The default value is True. - index
- Modifies the write operation to use the element array buffer binding.Only useful for the webgl compatibility.The default value is False.
- uniform
- Modifies the write operation to use the uniform buffer binding.Only useful for the webgl compatibility.The default value is False.
- external
- An OpenGL Buffer Object returned by glGenBuffers.The default value is 0.
- Buffer.write(data, offset)#
- data
- The content to be written into the buffer, represented as
bytes
or a buffer. - offset
- An int, representing the write offset in bytes.
- Buffer.map(size, offset, discard) memoryview #
- size
- An int, representing the size of the buffer in bytes to be mapped.The default value is None and it means the entire buffer.
- offset
- An int, representing the offset in bytes for the mapping.When the offset is not None the size must also be defined.The default value is None and it means the beginning of the buffer.
- discard
- A boolean to enable the
GL_MAP_INVALIDATE_RANGE_BIT
When this flag is True, the content of the buffer is undefined.The default value is False.
- Buffer.unmap()#
Unmap the buffer.
- Buffer.size#
An int, representing the size of the buffer in bytes.
Image#
render targets
image = ctx.image(window.size, 'rgba8unorm', samples=4)
depth = ctx.image(window.size, 'depth24plus', samples=4)
framebuffer = [image, depth]
textures
img = Image.open('example.png').convert('RGBA')
texture = ctx.image(img.size, 'rgba8unorm', img.tobytes())
- size
- The image size as a tuple of two ints.
- format
- The image format represented as string. (list of image format)The two most common are
'rgba8unorm'
and'depth24plus'
- data
- The image content, represented as
bytes
or a buffer for example a numpy array.If the data is None the content of the image will be uninitialized. The default value is None. - samples
- The number of samples for the image. Multisample render targets must have samples > 1.Textures must have samples = 1. Only powers of two are possible. The default value is 1.For multisampled rendering usually 4 is a good choice.
- array
- The number of array layers for the image. For non-array textures, the value must be 0.The default value is 0.
- texture
- A boolean representing the image to be sampled from shaders or not.For textures, this flag must be True, for render targets it should be False.Multisampled textures to be sampled from the shaders are not supported.The default is None and it means to be determined from the image type.
- cubemap
- A boolean representing the image to be a cubemap texture. The default value is False.
- external
- An OpenGL Texture Object returned by glGenTextures.The default value is 0.
- Image.blit(target, target_viewport, source_viewport, filter, srgb)#
- target
- The target image to copy to. The default value is None and it means to copy to the screen.
- target_viewport and source_viewport
- The source and target viewports defined as tuples of four ints in (x, y, width, height) format.
- filter
- A boolean to enable linear filtering for scaled images. By default it is True. It has no effect if the source and target viewports have the same size.
- srgb
- A boolean to enable linear to srgb conversion.By default it is None and it means False except for srgb source images.
- Image.clear()#
Clear the image with the Image.clear_value
- Image.mipmaps(base, levels)#
Generate mipmaps for the image.
- base
- The base image level. The default value is 0.
- levels
- The number of mipmap levels to generate starting from the base.The default is None and it means to generate mipmaps all the mipmap levels.
- Image.read(size, offset) bytes #
- size and offset
- The size and offset, defining a sub-part of the image to be read.Both the size and offset are tuples of two ints.The size is mandatory when the offset is not None.By default the size is None and it means the full size of the image.By default the offset is None and it means a zero offset.
- Image.write(data, size, offset, layer, level) bytes #
- data
- The content to be written to the image represented as
bytes
or a buffer for example a numpy array. - size and offset
- The size and offset, defining a sub-part of the image to be read.Both the size and offset are tuples of two ints.The size is mandatory when the offset is not None.By default the size is None and it means the full size of the image.By default the offset is None and it means a zero offset.
- layer
- An int representing the layer to be written to.This value must be None for non-layered textures.For array and cubemap textures, the layer must be specified.The default value is None and it mean all the layers.
- level
- An int representing the mipmap level to be written to.The default value is 0.
- Image.clear_value#
Image.clear()
depth24plus-stencil8
format is a tuple of float and int.- Image.size#
- Image.samples#
- Image.color#
Pipeline#
- Context.pipeline(vertex_shader, fragment_shader, layout, resources, depth, stencil, blending, polygon_offset, color_mask, framebuffer, vertex_buffers, index_buffer, short_index, primitive_restart, cull_face, topology, vertex_count, instance_count, first_vertex, viewport, skip_validation) Pipeline #
- vertex_shader
- The vertex shader code.
- fragment_shader
- The fragment shader code.
- layout
- Layout binding definition for the uniform buffers and samplers.
- resources
- The list of uniform buffers and samplers to be bound.
- uniforms
- The default values for uniforms.
- depth
- The depth settings
- stencil
- The stencil settings
- blend
- The blend settings
- polygon_offset
- The polygon offset
- color_mask
- The color mask, defined as a single integer.The bits of the color mask grouped in fours represent the color mask for the attachments.The bits in the groups of four represent the mask for the red, green, blue, and alpha channels.It is easier to understand it from the implementation.
- framebuffer
- A list of images representing the framebuffer for the rendering.The depth or stencil attachment must be the last one in the list.The size and number of samples of the images must match.
- vertex_buffers
- A list of vertex attribute bindings with the following keys:buffer: A buffer to be used as the vertex attribute sourceformat: The vertex attribute format. (list of vertex formats)location: The vertex attribute locationoffset: The buffer offset in bytesstride: The stride in bytesstep:
'vertex'
for per-vertex attributes.'instance'
for per-instance attributesThe
zengl.bind()
method produces this list in a more compact form. - index_buffer
- A buffer object to be used as the index buffer.The default value is None and it means to disable indexed rendering.
- short_index
- A boolean to enable
GL_UNSIGNED_SHORT
as the index type.When this flag is False theGL_UNSIGNED_INT
is used.The default value is False. - cull_face
- A string representing the cull face. It must be
'front'
,'back'
or'none'
The default value is'none'
- topology
- A string representing the rendered primitive topology.It must be one of the following:
'points'
'lines'
'line_loop'
'line_strip'
'triangles'
'triangle_strip'
'triangle_fan'
The default value is'triangles'
- vertex_count
- The number of vertices or the number of elements to draw.
- instance_count
- The number of instances to draw.
- first_vertex
- The first vertex or the first index to start drawing from.The default value is 0. This is a mutable parameter at runtime.
- viewport
- The render viewport, defined as tuples of four ints in (x, y, width, height) format.The default is the full size of the framebuffer.
- Pipeline.vertex_count#
- The number of vertices or the number of elements to draw.
- Pipeline.instance_count#
- The number of instances to draw.
- Pipeline.first_vertex#
- The first vertex or the first index to start drawing from.
- Pipeline.viewport#
- The render viewport, defined as tuples of four ints in (x, y, width, height) format.
- Pipeline.uniforms#
- The uniform values as memoryviews.
- Pipeline.render()#
- Execute the rendering pipeline.
Shader Code#
do use
#version 330
as the first line in the shader.do use
layout (std140)
for uniform buffers.do use
layout (location = ...)
for the vertex shader inputs.do use
layout (location = ...)
for the fragment shader outputs.don’t use
layout (location = ...)
for the vertex shader outputs or the fragment shader inputs. Matching name and order are sufficient and much more readable.don’t use
layout (binding = ...)
for the uniform buffers or samplers. It is not a core feature in OpenGL 3.3 and ZenGL enforces the program layout from the pipeline parameters.do use uniform buffers, use a single one if possible.
don’t use uniforms, use uniform buffers instead.
don’t put constants in uniform buffers, use
#include
and string formatting.don’t over-use the
#include
statement.do use includes without extensions.
do arrange pipelines in such an order to minimize framebuffer then program changes.
Shader Includes#
Context.includes
.h
- Context.includes#
- A string to string mapping dict.
Example
ctx.includes['common'] = '...'
pipeline = ctx.pipeline(
vertex_shader='''
#version 330
#include "common"
#include "qtransform"
void main() {
}
''',
)
Include Patterns#
common uniform buffer
ctx.includes['common'] = '''
layout (std140) uniform Common {
mat4 mvp;
};
'''
quaternion transform
ctx.includes['qtransform'] = '''
vec3 qtransform(vec4 q, vec3 v) {
return v + 2.0 * cross(cross(v, q.xyz) - q.w * v, q.xyz);
}
'''
gaussian filter
def kernel(s):
x = np.arange(-s, s + 1)
y = np.exp(-x * x / (s * s / 4))
y /= y.sum()
v = ', '.join(f'{t:.8f}' for t in y)
return f'const int N = {s * 2 + 1};\nfloat coeff[N] = float[]({v});'
ctx.includes['kernel'] = kernel(19)
hsv to rgb
ctx.includes['hsv2rgb'] = '''
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
'''
Rendering to Texture#
Rendering to texture is supported. However, multisampled images must be downsampled before being used as textures.
In that case, an intermediate render target must be samples > 1 and texture = False.
Then this image can be downsampled with Image.blit()
to another image with samples = 1 and texture = True.
Cleanup#
Clean only if necessary. It is ok not to clean up before the program ends.
This method releases the OpenGL resources associated with the parameter. OpenGL resources are not released automatically on garbage collection. Release Pipelines before the Images and Buffers they use.
When the string shader_cache
is passed to this method,
it calls glDeleteShader for all the previously created vertex and fragment shader modules.
When the string all
is passed to this method, it releases all the resources allocated from this context.
Interoperability#
Context.screen
to the frambuffer object.Pipeline._framebuffer
. It is for a different purpose.zengl.inspect()
.Returns an object with all of the OpenGL objects.
- Context.screen#
- Pipeline._framebuffer#
- Context.reset()#
Utils#
- Context.info#
- Context.limits#
max_uniform_buffer_bindings
max_uniform_block_size
max_combined_uniform_blocks
max_combined_texture_image_units
max_vertex_attribs
max_draw_buffers
max_samples
- Context.frame_time#
Context.new_frame()
and Context.end_frame()
.- zengl.camera(eye, target, up, fov, aspect, near, far, size, clip) bytes #
Buffer.write()
.mvp = zengl.camera(eye=(4.0, 3.0, 2.0), target=(0.0, 0.0, 0.0), aspect=16.0 / 9.0, fov=45.0)
/i
is allowed in the layout to represent per instance stepping.- zengl.calcsize(layout: str) int #
Image Formats#
ZenGL format |
internal format |
format |
type |
---|---|---|---|
r8unorm |
GL_R8 |
GL_RED |
GL_UNSIGNED_BYTE |
rg8unorm |
GL_RG8 |
GL_RG |
GL_UNSIGNED_BYTE |
rgba8unorm |
GL_RGBA8 |
GL_RGBA |
GL_UNSIGNED_BYTE |
r8snorm |
GL_R8_SNORM |
GL_RED |
GL_UNSIGNED_BYTE |
rg8snorm |
GL_RG8_SNORM |
GL_RG |
GL_UNSIGNED_BYTE |
rgba8snorm |
GL_RGBA8_SNORM |
GL_RGBA |
GL_UNSIGNED_BYTE |
r8uint |
GL_R8UI |
GL_RED_INTEGER |
GL_UNSIGNED_BYTE |
rg8uint |
GL_RG8UI |
GL_RG_INTEGER |
GL_UNSIGNED_BYTE |
rgba8uint |
GL_RGBA8UI |
GL_RGBA_INTEGER |
GL_UNSIGNED_BYTE |
r16uint |
GL_R16UI |
GL_RED_INTEGER |
GL_UNSIGNED_SHORT |
rg16uint |
GL_RG16UI |
GL_RG_INTEGER |
GL_UNSIGNED_SHORT |
rgba16uint |
GL_RGBA16UI |
GL_RGBA_INTEGER |
GL_UNSIGNED_SHORT |
r32uint |
GL_R32UI |
GL_RED_INTEGER |
GL_UNSIGNED_INT |
rg32uint |
GL_RG32UI |
GL_RG_INTEGER |
GL_UNSIGNED_INT |
rgba32uint |
GL_RGBA32UI |
GL_RGBA_INTEGER |
GL_UNSIGNED_INT |
r8sint |
GL_R8I |
GL_RED_INTEGER |
GL_BYTE |
rg8sint |
GL_RG8I |
GL_RG_INTEGER |
GL_BYTE |
rgba8sint |
GL_RGBA8I |
GL_RGBA_INTEGER |
GL_BYTE |
r16sint |
GL_R16I |
GL_RED_INTEGER |
GL_SHORT |
rg16sint |
GL_RG16I |
GL_RG_INTEGER |
GL_SHORT |
rgba16sint |
GL_RGBA16I |
GL_RGBA_INTEGER |
GL_SHORT |
r32sint |
GL_R32I |
GL_RED_INTEGER |
GL_INT |
rg32sint |
GL_RG32I |
GL_RG_INTEGER |
GL_INT |
rgba32sint |
GL_RGBA32I |
GL_RGBA_INTEGER |
GL_INT |
r16float |
GL_R16F |
GL_RED |
GL_FLOAT |
rg16float |
GL_RG16F |
GL_RG |
GL_FLOAT |
rgba16float |
GL_RGBA16F |
GL_RGBA |
GL_FLOAT |
r32float |
GL_R32F |
GL_RED |
GL_FLOAT |
rg32float |
GL_RG32F |
GL_RG |
GL_FLOAT |
rgba32float |
GL_RGBA32F |
GL_RGBA |
GL_FLOAT |
rgba8unorm-srgb |
GL_RGBA8 |
GL_RGBA |
GL_UNSIGNED_BYTE |
depth16unorm |
GL_DEPTH_COMPONENT16 |
GL_DEPTH_COMPONENT |
GL_UNSIGNED_SHORT |
depth24plus |
GL_DEPTH_COMPONENT24 |
GL_DEPTH_COMPONENT |
GL_UNSIGNED_INT |
depth24plus-stencil8 |
GL_DEPTH_COMPONENT24 |
GL_DEPTH_COMPONENT |
GL_UNSIGNED_INT |
depth32float |
GL_DEPTH_COMPONENT32F |
GL_DEPTH_COMPONENT |
GL_FLOAT |
Vertex Formats#
ZenGL bind |
ZenGL format |
type |
size |
normalized |
---|---|---|---|---|
1f |
float32 |
GL_FLOAT |
1 |
no |
2f |
float32x2 |
GL_FLOAT |
2 |
no |
3f |
float32x3 |
GL_FLOAT |
3 |
no |
4f |
float32x4 |
GL_FLOAT |
4 |
no |
1u |
uint32 |
GL_UNSIGNED_INT |
1 |
no |
2u |
uint32x2 |
GL_UNSIGNED_INT |
2 |
no |
3u |
uint32x3 |
GL_UNSIGNED_INT |
3 |
no |
4u |
uint32x4 |
GL_UNSIGNED_INT |
4 |
no |
1i |
sint32 |
GL_INT |
1 |
no |
2i |
sint32x2 |
GL_INT |
2 |
no |
3i |
sint32x3 |
GL_INT |
3 |
no |
4i |
sint32x4 |
GL_INT |
4 |
no |
2u1 |
uint8x2 |
GL_UNSIGNED_BYTE |
2 |
no |
4u1 |
uint8x4 |
GL_UNSIGNED_BYTE |
4 |
no |
2i1 |
sint8x2 |
GL_BYTE |
2 |
no |
4i1 |
sint8x4 |
GL_BYTE |
4 |
no |
2h |
float16x2 |
GL_HALF_FLOAT |
2 |
no |
4h |
float16x4 |
GL_HALF_FLOAT |
4 |
no |
2nu1 |
unorm8x2 |
GL_UNSIGNED_BYTE |
2 |
yes |
4nu1 |
unorm8x4 |
GL_UNSIGNED_BYTE |
4 |
yes |
2ni1 |
snorm8x2 |
GL_BYTE |
2 |
yes |
4ni1 |
snorm8x4 |
GL_BYTE |
4 |
yes |
2u2 |
uint16x2 |
GL_UNSIGNED_SHORT |
2 |
no |
4u2 |
uint16x4 |
GL_UNSIGNED_SHORT |
4 |
no |
2i2 |
sint16x2 |
GL_SHORT |
2 |
no |
4i2 |
sint16x4 |
GL_SHORT |
4 |
no |
2nu2 |
unorm16x2 |
GL_UNSIGNED_SHORT |
2 |
yes |
4nu2 |
unorm16x4 |
GL_UNSIGNED_SHORT |
4 |
yes |
2ni2 |
snorm16x2 |
GL_SHORT |
2 |
yes |
4ni2 |
snorm16x4 |
GL_SHORT |
4 |
yes |