Search code examples
pythonopengl-esglsleglpython-moderngl

How to enable Anti-aliasing in Moderngl EGL backend?


This code renders a colored triangle when there is no anti-aliasing (samples=0). But when I turn on anti-aliasing (samples=1...32) it can't render anything. How to make it work with anti-aliasing? Perhaps I cannot read pixels from multisample fbos or textures directly but I don't know how to fix that.

import numpy as np
from PIL import Image
import moderngl

ctx = moderngl.create_standalone_context(backend='egl')
fbo = ctx.framebuffer(
        color_attachments=ctx.texture((512, 512), 4, samples=2),
        depth_attachment=ctx.depth_texture((512, 512), samples=2)
    )
fbo.use()

vertices = np.array([
    -1.0,  -1.0,   1.0, 0.0, 0.0,
     1.0,  -1.0,   0.0, 1.0, 0.0,
     0.0,   1.0,   0.0, 0.0, 1.0],
    dtype='f4',
)

prog = ctx.program(vertex_shader="""
#version 330
in vec2 in_vert;
in vec3 in_color;
out vec3 color;
void main() {
    gl_Position = vec4(in_vert, 0.0, 1.0);
    color = in_color;
}
""",
fragment_shader="""
#version 330
out vec4 fragColor;
in vec3 color;
void main() {
    fragColor = vec4(color, 1.0);
}
""",
)
vao = ctx.simple_vertex_array(prog, ctx.buffer(vertices), 'in_vert', 'in_color')
vao.render(mode=moderngl.TRIANGLES)

image = Image.frombytes('RGBA', (512, 512), fbo.read(components=4))
image = image.transpose(Image.FLIP_TOP_BOTTOM)
image.save('triangle.png', format='png')

Solution

  • It is not possible to read data from a multisample framebuffer directly. Note, in a multisample framebuffer the pixels are stored for each sample. The color for each sample has to be mixed to a single color. That can be achieved by glBlitFramebuffer.

    Create 2 framebuffers. Create a framebuffer with samples=0, this framebuffer is used to read the pixel data. Create a mutlisample framebuffer, which is the target of the rendering.

    fbo = ctx.framebuffer(
        color_attachments=ctx.texture((512, 512), 4, samples=0),
    )
    
    fbo_msaa = ctx.framebuffer(
        color_attachments=ctx.texture((512, 512), 4, samples=8),
    )
    

    Copy the pixel data from the multisample framebuffer to the single sample framebuffer. I have not found any ModernGL class or method which provides that.
    Anyway the multisample framebuffer has to be bound for reading and the single sample framebuffer has to be bound for writing and the color data has to be copied. In native OpenGL this looks as follows:

    gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, fbo_msaa.glo)
    gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, fbo.glo)
    gl.glBlitFramebuffer(0, 0, 512, 512, 0, 0, 512, 512, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR)
    

    All together:

    import numpy as np
    from PIL import Image
    import moderngl
    import OpenGL.GL as gl
    
    ctx = moderngl.create_standalone_context(backend='egl')
    
    fbo = ctx.framebuffer(
            color_attachments=ctx.texture((512, 512), 4, samples=0)
        )
    
    fbo_msaa = ctx.framebuffer(
            color_attachments=ctx.texture((512, 512), 4, samples=8)
        )
    fbo_msaa.use()
    
    vertices = np.array([
        -1.0,  -1.0,   1.0, 0.0, 0.0,
         1.0,  -1.0,   0.0, 1.0, 0.0,
         0.0,   1.0,   0.0, 0.0, 1.0],
        dtype='f4',
    )
    
    prog = ctx.program(vertex_shader="""
    #version 330
    in vec2 in_vert;
    in vec3 in_color;
    out vec3 color;
    void main() {
        gl_Position = vec4(in_vert, 0.0, 1.0);
        color = in_color;
    }
    """,
    fragment_shader="""
    #version 330
    out vec4 fragColor;
    in vec3 color;
    void main() {
        fragColor = vec4(color, 1.0);
    }
    """,
    )
    vao = ctx.simple_vertex_array(prog, ctx.buffer(vertices), 'in_vert', 'in_color')
    vao.render(mode=moderngl.TRIANGLES)
    
    gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, fbo_msaa.glo)
    gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, fbo.glo)
    gl.glBlitFramebuffer(0, 0, 512, 512, 0, 0, 512, 512, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR)
    
    image = Image.frombytes('RGBA', (512, 512), fbo.read(components=4))
    image = image.transpose(Image.FLIP_TOP_BOTTOM)
    image.save('triangle.png', format='png')
    

    Sadly I encountered a new issue. If the multisampe framebuffer has a depth buffer, too, then glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_msaa.glo) fails, for whatever reason.
    This needs to be investigated further.

    Edit:

    I just figured out, that there is not any issue, when using a Renderbuffer rather than a Texture for the depth buffer:

    fbo = ctx.framebuffer(
        color_attachments=ctx.texture((512, 512), 4, samples=0),
    )
    
    fbo_msaa = ctx.framebuffer(
        color_attachments=ctx.texture((512, 512), 4, samples=8),
        depth_attachment=ctx.depth_renderbuffer((512, 512), samples=8)
    )
    

    Hence, it seams to be a bug in ModernGL, related to multisample texture depth buffer attachment.