pythongraphicstransparencysdl-2pysdl2

Draw outline circle with sdl2


TL;DR: Using (py)sdl2, I am trying to draw a circle outline of which the interior is transparent and thus shows objects drawn behind it. Basically I need to find a way to erase the pixels of the interior of a filled circle (or draw them over with/set them back to transparent pixels). I'd like to use Textures instead of Surfaces, as far as this is possible.

I am trying to achieve something that seems conceptually very simple, but I just can't make it happen.

I want to draw a circle outline with sdl2 in python. Of course, this is very simple to achieve with the sdl2gfx function circleRGBA(), but it only allows you to draw outlines with a line width of 1px. It seems downright impossible to draw circles with thicker outlines.

I have done something like this before with pygame surfaces using transparent color keys (inspired by this method), and I know surfaces are also available in SDL2, but the caveat is that I'm trying to stick to the much faster textures, which do not provide the color key mechanism.

To explain things a bit better visually: what I'm trying to achieve is:

The goal

a circle (annulus) larger than 1px wide of which the interior is transparent, such that items drawn behind the circle will be visible in the interior.

A trick that I have used in the past is to just draw 2 filled circles, where a second smaller circle has the same color as the background. However, this obscures everything behind the circle (in this case the white line). Here's an example of a function using this approach:

@to_texture
def draw_circle(self, x, y, r, color, opacity=1.0, fill=True, aa=False, penwidth=1):
    # Make sure all spatial parameters are ints
    x, y, r = int(x), int(y), int(r)
    # convert color parameter to sdl2 understandable values
    color = sdl2.ext.convert_to_color(color)
    # convert possible opacity values to the right scale
    opacity = self.opacity(opacity)
    # Get background color set for this texture
    bgcolor = self.__bgcolor

    # Check for invalid penwidth values, and make sure the value is an int
    if penwidth != 1:
        if penwidth < 1:
            raise ValueError("Penwidth cannot be smaller than 1")
        if penwidth > 1:
            penwidth = int(penwidth)

    # if the circle needs to be filled, it's easy
    if fill:
        sdlgfx.filledCircleRGBA(self.sdl_renderer, x, y, r, color.r, color.g, color.b, opacity)
    else:
        # If penwidth is 1, simple use sdl2gfx own functions
        if penwidth == 1:
            if aa:
                sdlgfx.aacircleRGBA(self.sdl_renderer, x, y, r, color.r, color.g, color.b, opacity)
            else:
                sdlgfx.circleRGBA(self.sdl_renderer, x, y, r, color.r, color.g, color.b, opacity)
        else:
            # If the penwidth is larger than 1, things become a bit more complex.
            outer_r = int(r+penwidth*.5)
            inner_r = int(r-penwidth*.5)

            # Draw outer circle
            sdlgfx.filledCircleRGBA(self.sdl_renderer, x, y,
                outer_r, color.r, color.g, color.b, opacity)

            # Draw inner circle
            sdlgfx.filledCircleRGBA(self.sdl_renderer, x, y,
                inner_r, bgcolor.r, bgcolor.g, bgcolor.b, 255)

    return self

Resulting in:

First attempt

I also tried playing around with various blending modes, and this was the best result I could get:

@to_texture
def draw_circle(self, x, y, r, color, opacity=1.0, fill=True, aa=False, penwidth=1):

        # ... omitted for brevity

        else:
            # If the penwidth is larger than 1, things become a bit more complex.
            # To ensure that the interior of the circle is transparent, we will
            # have to work with multiple textures and blending.
            outer_r = int(r+penwidth*.5)
            inner_r = int(r-penwidth*.5)

            # Calculate the required dimensions of the separate texture we are
            # going to draw the circle on. Add 1 pixel to account for division
            # inaccuracies (i.e. dividing an odd number of pixels)
            (c_width, c_height) = (outer_r*2+1, outer_r*2+1)

            # Create the circle texture, and make sure it can be a rendering
            # target by setting the correct access flag.
            circle_texture = self.environment.texture_factory.create_sprite(
                size=(c_width, c_height),
                access=sdl2.SDL_TEXTUREACCESS_TARGET
            )

            # Set renderer target to the circle texture
            if sdl2.SDL_SetRenderTarget(self.sdl_renderer, circle_texture.texture) != 0:
                raise Exception("Could not set circle texture as rendering"
                    " target: {}".format(sdl2.SDL_GetError()))

            # Draw the annulus:
            # as the reference point is the circle center, outer_r
            # can be used for the circles x and y coordinates.
            sdlgfx.filledCircleRGBA(self.sdl_renderer, outer_r, outer_r,
                outer_r, color.r, color.g, color.b, opacity)

            # Draw the hole
            sdlgfx.filledCircleRGBA(self.sdl_renderer, outer_r, outer_r,
                inner_r, 0, 0, 0, 255)

            # Set renderer target back to the FrameBuffer texture
            if sdl2.SDL_SetRenderTarget(self.sdl_renderer, self.surface.texture) != 0:
                raise Exception("Could not unset circle texture as rendering"
                    " target: {}".format(sdl2.SDL_GetError()))

            # Use additive blending when blitting the circle texture on the main texture
            sdl2.SDL_SetTextureBlendMode(circle_texture.texture, sdl2.SDL_BLENDMODE_ADD)

            # Perform the blitting operation
            self.renderer.copy( circle_texture, dstrect=(x-int(c_width/2),
                y-int(c_height/2), c_width, c_height) )

    return self

which results in:

Second attempt with blending

Close, but no cigar, as the line now appears to be in front of the circle instead of behind it, and as far as I understand additive/screen blending, this is the intended behavior.

I know there is also the function SDL_SetRenderDrawBlendMode, but the sdl2gfx drawing functions seem to ignore anything you set with this function.

Is there anyone with more experience than me who has done something like this before and who can point me in the right direction on how to tackle this challenge?


Solution

  • I think you have to create a SDL_Surface, create a software renderer for it, draw on it, use SDL_ColorKey() to get rid of the inner color, then convert it back to SDL_Texture. Maybe it's not the fastest way but it works. You can always create a transparent PNG image and load from it.

    import sys
    import ctypes
    from sdl2 import *
    import sdl2.sdlgfx as sdlgfx
    
    if __name__ == "__main__":
        SDL_Init(SDL_INIT_VIDEO)
    
        window = SDL_CreateWindow("Test",
                                  SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                                  200, 200, SDL_WINDOW_SHOWN)
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED)
    
    
        ############################################################
        surface = SDL_CreateRGBSurface(0, 200, 200, 32,
                                       0xff000000, # r mask
                                       0x00ff0000, # g mask
                                       0x0000ff00, # b mask
                                       0x000000ff) # a mask
        soft_renderer = SDL_CreateSoftwareRenderer(surface)
    
        SDL_SetRenderDrawColor(soft_renderer, 255, 255, 0, 255) #yellow background
        SDL_SetRenderDrawBlendMode(soft_renderer, SDL_BLENDMODE_NONE)
        SDL_RenderClear(soft_renderer)
        sdlgfx.filledCircleRGBA(soft_renderer, 100, 100, 100, 0, 255, 255, 255)
        sdlgfx.filledCircleRGBA(soft_renderer, 100, 100,  80, 255, 255, 0, 255) #yellow
        SDL_SetColorKey(surface, SDL_TRUE, 0xffff00ff) #yellow colorkey
    
        circ = SDL_CreateTextureFromSurface(renderer, surface)
    
        SDL_DestroyRenderer(soft_renderer)
        SDL_FreeSurface(surface)
        ############################################################
    
        running = True
        event = SDL_Event()
        while running:
            # Event loop
            while SDL_PollEvent(ctypes.byref(event)) != 0:
                if event.type == SDL_QUIT:
                    running = False
                    break
            # Rendering
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
            SDL_RenderClear(renderer)
            sdlgfx.thickLineRGBA(renderer, 200, 0, 0, 200, 10, 255, 255, 255, 255)
            SDL_RenderCopy(renderer, circ, None, None)
            SDL_RenderPresent(renderer);
    
        SDL_DestroyTexture(circ)
        SDL_DestroyRenderer(renderer)
        SDL_DestroyWindow(window)
        SDL_Quit()
    

    enter image description here