简体   繁体   English

用sdl2绘制轮廓圆

[英]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. TL; DR:我正在尝试使用(py)sdl2绘制一个圆形轮廓,该轮廓的内部是透明的,从而显示在其后面绘制的对象。 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. 我想在python中使用sdl2绘制圆形轮廓。 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. 当然,使用sdl2gfx函数circleRGBA()可以很容易地实现这一点,但是它仅允许您绘制线宽为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. 之前我已经使用透明颜色键( 受此方法启发 )对pygame曲面进行了类似的操作,并且我知道SDL2中也提供了曲面,但是需要注意的是,我试图坚持更快的纹理,没有提供颜色键机制。

To explain things a bit better visually: what I'm trying to achieve is: 为了从视觉上更好地说明问题,我想要实现的目标是:

目标

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. 一个大于1px宽的圆圈(环),内部透明,使在圆圈后面绘制的项目在内部可见。

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. 我过去使用的一个技巧是仅绘制2个实心圆,其中第二个较小的圆与背景颜色相同。 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: 导致:

第一次尝试

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: 结果是:

混合的第二次尝试

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. 我知道也有函数SDL_SetRenderDrawBlendMode ,但是sdl2gfx绘图函数似乎忽略了您使用此函数设置的任何内容。

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? 有没有比我有更多经验的人,以前做过这样的事情,并且可以为我指出如何应对这一挑战的正确方向?

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 . 我认为您必须创建一个SDL_Surface ,为其创建一个软件渲染器,在其上绘制,使用SDL_ColorKey()摆脱内部颜色,然后将其转换回SDL_Texture Maybe it's not the fastest way but it works. 也许这不是最快的方法,但是它可以工作。 You can always create a transparent PNG image and load from it. 您始终可以创建透明的PNG图像并从中加载。

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()

在此处输入图片说明

@tntxtnt just beat me to it, but I solved this problem myself in a similar fashion. @tntxtnt击败了我,但是我自己以类似的方式解决了这个问题。 My solution also doesn't meet the demand to only use textures, and does fall back to using surfaces. 我的解决方案还不能满足仅使用纹理的需求,而只能使用表面。 I don't know if it is possible to solve this problem using only textures, or if using surfaces will have a large impact on performance, but at least this works. 我不知道是否可以仅使用纹理来解决此问题,或者使用表面是否会对性能产生很大影响,但至少可以奏效。

@tntxtnt solution is very good, but I'll add mine here too since I have a slightly different approach by relying a bit more on pysdl2's utility functions: @tntxtnt解决方案非常好,但是我也将在这里添加我,因为我的方法略有不同,因为它更多地依赖pysdl2的实用程序函数:

class FrameBuffer(object):

    # ... omitted for brevity

    @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)

        # 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 only a filled circle needs to be drawn, 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, simply use sdl2gfx's 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.
                # To ensure that the interior of the circle is transparent, we will
                # have to work with multiple textures and blending.
                outer_r, inner_r = int(r+penwidth*.5), int(r-penwidth*.5)

                # Calculate the required dimensions of the extra texture we are
                # going to draw the circle on. Add 1 pixel to account for division
                # errors (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_sprite = self.environment.texture_factory.create_software_sprite(
                    size=(c_width, c_height),
                )
                # Create a renderer to draw to the circle sprite
                sprite_renderer = sdl2.ext.Renderer(circle_sprite)

                # Determine the optimal color key to use for this operation
                colorkey_color = self.determine_optimal_colorkey(color, self.background_color)

                # Clear the sprite with the colorkey color
                sprite_renderer.clear(colorkey_color)

                # Draw the annulus:
                sdlgfx.filledCircleRGBA(sprite_renderer.sdlrenderer, outer_r, outer_r,
                    outer_r, color.r, color.g, color.b, 255)
                # Antialias if desired
                if aa:
                    for i in range(-1,1):
                        for j in range(2):
                            sdlgfx.aacircleRGBA(sprite_renderer.sdlrenderer, outer_r,
                                outer_r, outer_r+i, color.r, color.g, color.b, 255)

                # Draw the hole
                sdlgfx.filledCircleRGBA(sprite_renderer.sdlrenderer, outer_r, outer_r,
                    inner_r, colorkey_color.r, colorkey_color.g, colorkey_color.b, 255)
                # Antialias if desired
                if aa:
                    for i in range(0,2):
                        for j in range(2):
                            sdlgfx.aacircleRGBA(sprite_renderer.sdlrenderer, outer_r,
                                outer_r, inner_r+i, color.r, color.g, color.b, 255)

                # Optimize drawing of transparent pixels
                sdl2.SDL_SetSurfaceRLE(circle_sprite.surface, 1)

                # Convert the colorkey to a format understandable by the 
                # SDL_SetColorKey function
                colorkey = sdl2.SDL_MapRGB(circle_sprite.surface.format, 
                    colorkey_color.r, colorkey_color.g, colorkey_color.b)

                # Set transparency color key
                sdl2.SDL_SetColorKey(circle_sprite.surface, sdl2.SDL_TRUE, colorkey)

                # Create a texture from the circle sprite
                circle_texture = self.environment.texture_factory.from_surface(
                    circle_sprite.surface
                )

                # Set the desired transparency value to the texture
                sdl2.SDL_SetTextureAlphaMod(circle_texture.texture, opacity)

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

                # Cleanup
                del(circle_sprite)
                del(sprite_renderer)

        return self

    def determine_optimal_colorkey(self, drawing_color, dev_offset=5):
        """ Determines the optimal color to use for the transparent color key."""

        # If foreground and background colors are not identical, simply return
        # the background color
        if drawing_color != self.background_color:
            return self.background_color

        if not 1 <= dev_offset <= 25:
            raise ValueError("dev_offset should be a value between 1 and 25")

        # Create offset_color
        offset_color = sdl2.ext.Color(dev_offset, dev_offset, dev_offset, 0)
        # Create white_color
        white = sdl2.ext.Color(255,255,255,255)

        color_key = self.background_color + offset_color
        if color_key != white:
            return color_key
        else:
            return self.background_color - offset_color

    def opacity(self, value):
        """ Convert float values to opacity range between 0 and 255. """
        if type(value) == float and 0.0 <= value <= 1.0:
            # This is maybe a bit iffy, because it does not allow opacity values
            # in the 0 to 255 range between 0 and 1 (it maybe undesiredly converts
            # it to value*255).
            # TODO: Think about a solution for this
            return int(value*255)
        elif type(value) in [int, float]:
            if 0 <= value <= 255:
                return int(value)
            else:
                raise ValueError("Invalid opacity value")
        else:
            raise TypeError("Incorrect type or value passed for opacity.")

    # ... ommitted for brevity

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM