pip install zengl
ZenGL
ZenGL is a low level graphics library. Works on all platforms including the browser.
Description
- Context is the root object to access OpenGL
- Image is an OpenGL Texture or Renderbuffer
- Buffer is an OpenGL Buffer
- Pipeline is an OpenGL Program + Vertex Array + Framebuffer + complete state for rendering
ctx = zengl.context()
texture = ctx.image(size, 'rgba8unorm', pixels)
renderbuffer = ctx.image(size, 'rgba8unorm', samples=4)
vertex_buffer = ctx.buffer(vertices)
pipeline = ctx.pipeline(...)
The complete OpenGL state is encapsulated by the Pipeline.
Rendering with multiple pipelines guarantees proper state with minimal changes and api calls.
background.render()
scene.render()
particles.render()
bloom.render()
Pipelines render to framebuffers, Images can be blit to the screen.
pipeline = ctx.pipeline(
framebuffer=[image, depth],
)
image.clear()
depth.clear()
pipeline.render()
image.blit()
Programs are simple, easy, and cached. Unique shader sources are only compiled once.
pipeline = ctx.pipeline(
vertex_shader='''
#version 330 core
void main() {
gl_Position = ...
}
''',
fragment_shader='''
#version 330 core
out vec4 frag_color;
void main() {
frag_color = ...
}
''',
)
Vertex Arrays are simple.
pipeline = ctx.pipeline(
vertex_buffers=zengl.bind(vertex_buffer, '3f 3f 2f', 0, 1, 2),
vertex_count=vertex_buffer.size // zengl.calcsize('3f 3f 2f'),
)
pipeline = ctx.pipeline(
vertex_buffers=zengl.bind(vertex_buffer, '3f 3f 2f', 0, 1, 2),
index_buffer=index_buffer,
vertex_count=index_buffer.size // 4,
)
pipeline = ctx.pipeline(
vertex_buffers=[
*zengl.bind(vertex_buffer, '3f 3f 2f', 0, 1, 2),
*zengl.bind(instance_buffer, '3f 4f /i', 3, 4),
],
vertex_count=vertex_buffer.size // zengl.calcsize('3f 3f 2f'),
instance_count=1000,
)
Uniform Buffer, Texture, and Sampler binding is easy.
pipeline = ctx.pipeline(
layout=[
{
'name': 'Common',
'binding': 0,
},
],
resources=[
{
'type': 'uniform_buffer',
'binding': 0,
'buffer': uniform_buffer,
},
],
)
pipeline = ctx.pipeline(
layout=[
{
'name': 'Texture',
'binding': 0,
},
],
resources=[
{
'type': 'sampler',
'binding': 0,
'image': texture,
'wrap_x': 'clamp_to_edge',
'wrap_y': 'clamp_to_edge',
'min_filter': 'nearest',
'mag_filter': 'nearest',
},
],
)
Postprocessing and Compute can be implemented as rendering a fullscreen quad.
pipeline = ctx.pipeline(
vertex_shader='''
#version 330 core
vec2 vertices[3] = vec2[](
vec2(-1.0, -1.0),
vec2(3.0, -1.0),
vec2(-1.0, 3.0)
);
void main() {
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
}
''',
fragment_shader='''
#version 330 core
out vec4 frag_color;
void main() {
frag_color = ...
}
''',
topology='triangles',
vertex_count=3,
)
particle_system = ctx.pipeline(
vertex_shader=...,
fragment_shader='''
#version 330 core
uniform sampler2D Position;
uniform sampler2D Velocity;
uniform vec3 Acceleration;
layout (location = 0) out vec3 OutputPosition;
layout (location = 1) out vec3 OutputVelocity;
void main() {
ivec2 at = ivec2(gl_FragCoord.xy);
vec3 position = texelFetch(Position, at, 0).xyz;
vec3 velocity = texelFetch(Velocity, at, 0).xyz;
OutputPosition = position + velocity;
OutputVelocity = velocity + Acceleration;
}
''',
)
ZenGL intentionally does not support:
- Transform Feedback
- Geometry Shaders
- Tesselation
- Compute Shaders
- 3D Textures
- Storage Buffers
Most of the above can be implemented in a more hardware friendly way using the existing ZenGL API.
Interoperability with other modules is also possible. Using such may reduce the application's portablity.
It is even possible to use direct OpenGL calls together with ZenGL, however this is likely not necessary.
It is common to render directly to the screen with OpenGL.
With ZenGL, the right way is to render to a framebuffer and blit the final image to the screen.
This allows fine-grained control of the framebuffer format, guaranteed multisampling settings, correct depth/stencil precison.
It is also possible to render directly to the screen, however this feature is designed to be used for the postprocessing step.
This design allows ZenGL to support:
- Rendering without a window
- Rendering to multiple windows
- Rendering to HDR monitors
- Refreshing the screen without re-rendering the scene
- Apply post-processing without changing how the scene is rendered
- Making reusable shaders and components
- Taking screenshots or exporting a video
The default framebuffer in OpenGL is highly dependent on how the Window is created.
It is often necessary to configure the Window to provide the proper depth precision, stencil buffer, multisampling and double buffering.
Often the "best pixel format" lacks all of these features on purpose. ZenGL aims to allow choosing these pixel formats and ensures the user specifies the rendering requirements.
It is even possible to render low-resolution images and upscale them for high-resolution monitors.
Tearing can be easily prevented by decoupling the scene rendering from the screen updates.
ZenGL was designed for Prototyping
It is tempting to start a project with Vulkan, however even getting a simple scene rendered requires tremendous work and advanced tooling to compile shaders ahead of time. ZenGL provides self-contained Pipelines which can be easily ported to Vulkan.
ZenGL code is verbose and easy to read.
ZenGL support multiple design patters
Many libraries enfore certain design patterns.
ZenGL avoids this by providing cached pipeline creation, pipeline templating and lean resourece and framebuffer definition.
It is supported to create pipelines on the fly or template them for certain use-cases.
TODO: examples for such patters
ZenGL emerged from an experimental version of ModernGL.
To keep ModernGL backward compatible, ZenGL was re-designed from the ground-up to support a strict subset of OpenGL.
On the other hand, ModernGL supports a wide variety of OpenGL versions and extensions.
Disambiguation
- ZenGL is a drop-in replacement for pure OpenGL code
- Using ZenGL requires some OpenGL knowledge
- ZenGL Images are OpenGL Texture Objects or Renderbuffer Objects
- ZenGL Buffers are OpenGL Buffer Objects
- ZenGL Pipelines contain an OpenGL Vertex Array Object, a Program Object, and a Framebuffer Object
- ZenGL Pipelines may also contain OpenGL Sampler Objects
- Creating ZenGL Pipelines does not necessarily compile the shader from source
- The ZenGL Shader Cache exists independently from the Pipeline objects
- A Framebuffer is always represented by a Python list of ZenGL Images
- There is no
Pipeline.clear()
method, individual images must be cleared independently - GLSL Uniform Blocks and sampler2D objects are bound in the Pipeline layout
- Textures and Uniform Buffers are bound in the Pipeline resources
Simple Pipeline Definition
pipeline = ctx.pipeline(
vertex_shader='...',
fragment_shader='...',
layout=[
{
'name': 'Uniforms',
'binding': 0,
},
{
'name': 'Texture',
'binding': 0,
},
],
resources=[
{
'type': 'uniform_buffer',
'binding': 0,
'buffer': uniform_buffer,
},
{
'type': 'sampler',
'binding': 0,
'image': texture,
},
],
uniforms={
'color': [0.0, 0.5, 1.0],
'iterations': 10,
},
depth={
'func': 'less',
'write': False,
},
stencil={
'front': {
'fail_op': 'replace',
'pass_op': 'replace',
'depth_fail_op': 'replace',
'compare_op': 'always',
'compare_mask': 1,
'write_mask': 1,
'reference': 1,
},
'back': ...,
'both': ...,
},
blend={
'enable': True,
'src_color': 'src_alpha',
'dst_color': 'one_minus_src_alpha',
},
cull_face='back',
topology='triangles',
framebuffer=[color1, color2, ..., depth],
viewport=(x, y, width, height),
vertex_buffers=[
*zengl.bind(vertex_buffer, '3f 3f', 0, 1),
*zengl.bind(None, '2f', 2),
],
index_buffer=index_buffer,
short_index=False,
vertex_count=...,
instance_count=1,
first_vertex=0,
includes={
'common': '...',
},
)
pipeline.viewport = ...
pipeline.vertex_count = ...
pipeline.uniforms['iterations'][:] = struct.pack('i', 50)
pipeline.render()