1

TL; DR:使用 (py)sdl2,我正在尝试绘制一个圆形轮廓,其内部是透明的,因此显示了在其后面绘制的对象。基本上我需要找到一种方法来擦除填充圆圈内部的像素(或将它们绘制成透明像素/将它们设置回透明像素)。我想尽可能使用纹理而不是表面。

我正在尝试实现一些在概念上看起来非常简单的东西,但我就是无法实现。

我想在 python 中用 sdl2 绘制一个圆形轮廓。当然,这用 sdl2gfx 函数实现起来很简单circleRGBA(),但它只允许你画出线宽为 1px 的轮廓。用较粗的轮廓画圆似乎是完全不可能的。

我之前使用透明颜色键(受此方法启发)使用 pygame 表面做过类似的事情,我知道表面也可用于 SDL2,但需要注意的是我试图坚持使用更快的纹理,这确实不提供色键机制。

为了在视觉上更好地解释事情:我想要实现的是:

目标

一个大于 1px 宽的圆(环),其内部是透明的,这样在圆后面绘制的项目将在内部可见。

我过去使用的一个技巧是只绘制 2 个实心圆圈,其中第二个较小的圆圈与背景颜色相同。但是,这会掩盖圆圈后面的所有内容(在本例中为白线)。下面是一个使用这种方法的函数示例:

@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

导致:

第一次尝试

我还尝试了各种混合模式,这是我能得到的最好结果:

@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

这导致:

第二次尝试混合

关闭,但没有雪茄,因为这条线现在似乎在圆圈的前面而不是在它的后面,据我所知,添加剂/屏幕混合,这是预期的行为。

我知道还有函数SDL_SetRenderDrawBlendMode,但是 sdl2gfx 绘图函数似乎忽略了您使用此函数设置的任何内容。

有没有比我更有经验的人以前做过类似的事情,谁能为我指明如何应对这一挑战的正确方向?

4

2 回答 2

1

我认为你必须创建一个SDL_Surface,为它创建一个软件渲染器,在它上面绘制,SDL_ColorKey()用来去除内部颜色,然后将它转换回SDL_Texture. 也许这不是最快的方法,但它有效。您始终可以创建透明的 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()

在此处输入图像描述

于 2017-03-07T20:52:50.390 回答
0

@tntxtnt 刚刚击败了我,但我自己以类似的方式解决了这个问题。我的解决方案也不能满足仅使用纹理的需求,并且确实回退到使用表面。我不知道是否可以仅使用纹理来解决这个问题,或者使用表面是否会对性能产生很大影响,但至少这是可行的。

@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
于 2017-03-07T21:07:34.153 回答