我正在实施“快速抗锯齿圆形发生器”程序,该程序由Xiaolin Wu在Siggraph[他的论文“An Efficient Anasingiasing Technique”中描述。
这是我使用Python 3和PySDL2编写的代码:
def draw_antialiased_circle(renderer, position, radius):
def _draw_point(renderer, offset, x, y):
sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y + y)
sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y + y)
sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y - y)
sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y - y)
i = 0
j = radius
d = 0
T = 0
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, sdl2.SDL_ALPHA_OPAQUE)
_draw_point(renderer, position, i, j)
while i < j + 1:
i += 1
s = math.sqrt(max(radius * radius - i * i, 0.0))
d = math.floor(sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s) + 0.5)
if d < T:
j -= 1
T = d
if d > 0:
alpha = d
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
_draw_point(renderer, position, i, j)
if i != j:
_draw_point(renderer, position, j, i)
if (sdl2.SDL_ALPHA_OPAQUE - d) > 0:
alpha = sdl2.SDL_ALPHA_OPAQUE - d
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
_draw_point(renderer, position, i, j + 1)
if i != j + 1:
_draw_point(renderer, position, j + 1, i)
这是我认为在他的论文中描述的一个天真的实现,除了我将半径值分配给j
而不是i
,因为我误解了某些东西或那里&#39他的论文中有一个错误。实际上,他使用半径值i
和j
初始化0
,然后定义循环条件i <= j
,只有当半径为{{1}时才能为真}。这一变化促使我对所描述的内容做了一些其他的小修改,我也将0
更改为if d > T
,因为它看起来很糟糕。
除了每个八分圆的开始和结束之外,这个实现工作效果很好,其中会出现一些毛刺。
上面的圆的半径为1.正如您在每个八分圆的开头看到的那样(例如在(0,1)区域中),在循环内绘制的像素未与第一个像素对齐在循环开始之前绘制。在每个八分圆的末尾(例如在if d < T
区域),也会出现严重错误。我设法通过将(sqrt(2) / 2, sqrt(2) / 2)
条件更改为if d < T
来使最后一个问题消失,但同样的问题会出现在每个八分圆的开头。
问题1 :我做错了什么?
问题2 :如果我希望输入位置和半径为浮点,会不会有任何问题?
答案 0 :(得分:2)
让我们解构您的实施,找出您犯的错误:
def draw_antialiased_circle(renderer, position, radius):
第一个问题,radius
是什么意思?绘制一个圆圈是不可能的,你只能绘制一个圆环(圆环/圆环),因为除非你有一些厚度,否则你无法看到它。因此半径是模糊的,是内半径,中点半径还是外半径?如果你没有在变量名中指定,那将会让人感到困惑。也许我们可以找到答案。
def _draw_point(renderer, offset, x, y):
sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y + y)
sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y + y)
sdl2.SDL_RenderDrawPoint(renderer, offset.x - x, offset.y - y)
sdl2.SDL_RenderDrawPoint(renderer, offset.x + x, offset.y - y)
好吧,由于圆圈是对称的,我们一次要画四个地方。为什么不同时出现8个地方?
i = 0
j = radius
d = 0
T = 0
好的,将i
初始化为0并将j
初始化为半径,这些必须是x
和y
坐标。什么是d
和T
?无意义的变量名称没有帮助。复制科学家&#39;使用更长的变量名称实际可以理解的算法!
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, sdl2.SDL_ALPHA_OPAQUE)
_draw_point(renderer, position, i, j)
咦?我们正在绘制一个特例?我们为什么这样做?不确定。这意味着什么?这意味着填充(0, radius)
中具有完全不透明度的方块。所以现在我们知道radius
是什么,它是环的外半径,而环的宽度显然是一个像素。或者至少,这个特例告诉我们的是什么......让我们看看它是否在通用代码中有用。
while i < j + 1:
我们将循环直到我们到达i > j
所在的圆点,然后停止。即我们正在画一个八分圆。
i += 1
所以我们已经在i = 0
位置绘制了我们关注的所有像素,让我们进入下一个像素。
s = math.sqrt(max(radius * radius - i * i, 0.0))
即。 s
是一个浮点数,它是x轴上点i
处八分圆的y分量。即高于x轴的高度。出于某种原因,我们在那里有max
,也许我们担心非常小的圆圈...但是这并不能告诉我们这个点是否在外半径/内半径上。 / p>
d = math.floor(sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s) + 0.5)
将其分解,(math.ceil(s) - s)
给出一个0到1.0之间的数字。当s
减少时,此数字会增加,因此i
会增加,然后一旦达到1.0,将重置为0.0,因此采用锯齿模式。
sdl2.SDL_ALPHA_OPAQUE * (math.ceil(s) - s)
提示我们使用锯齿输入来产生不透明度,即对圆圈进行抗锯齿处理。 0.5不变的添加似乎是不必要的。 floor()
表明我们只对整数值感兴趣 - 可能API只接受整数值。所有这些都表明d
最终是0到SDL_ALPHA_OPAQUE
之间的整数级别,从0开始,然后逐渐建立i
,然后ceil(s) - s
}从1回到0,它也再次降低。
那么这一开始有什么价值呢? s
几乎是radius
,因为i
是1,(假设是一个非平凡的大小圆圈),所以假设我们有一个整数半径(第一个特殊情况代码明确假设){{ 1}}是0
ceil(s) - s
在这里,我们发现 if d < T:
j -= 1
d = T
已经从高位变为低位,我们向下移动屏幕,使我们的d
位置保持在理论上应该位于环的位置附近。
但现在我们也意识到前一个等式的底线是错误的。想象一下,在一次迭代中j
是100.9。然后在下一次迭代中它已经下降,但只有100.1。由于d
和d
相同,因为最低限度已根除了他们的差异,在这种情况下我们不会减少T
,这是至关重要的。我想可能解释了八分圆末端奇怪的曲线。
j
如果我们要使用alpha值0
,只是优化不绘制if d < 0:
但是,在第一次迭代alpha = d
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
_draw_point(renderer, position, i, j)
上保持不变,因此这是一个几乎完全透明的元素d
。但是开始的特殊情况为(1, radius)
画了一个完全不透明的元素,所以很明显这里会出现图形故障。这就是你所看到的。
继续:
(0, radius)
另一个小优化,如果我们要吸引到同一个位置,我们就不会再画画了
if i != j:
这就解释了为什么我们只在_draw_point(renderer, position, j, i)
中绘制4个点,因为你在这里做了另一个对称。它会简化您的代码而不是。
_draw_point()
另一项优化(您不应该过早优化代码!):
if (sdl2.SDL_ALPHA_OPAQUE - d) > 0:
所以在第一次迭代中,我们写入上面的位置,但是完全不透明。这意味着实际上它是我们正在使用的内部半径,而不是特殊情况错误所使用的外部半径。即一种一个一个一个错误。
我的实现(我没有安装库,但你可以从输出中看到它有意义的边界条件):
alpha = sdl2.SDL_ALPHA_OPAQUE - d
sdl2.SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha)
_draw_point(renderer, position, i, j + 1)
最后,如果你想传递一个浮点原点会有陷阱吗?
是的,会的。该算法假定原点位于整数/整数位置,否则完全忽略它。如我们所见,如果传入整数import math
def draw_antialiased_circle(outer_radius):
def _draw_point(x, y, alpha):
# draw the 8 symmetries.
print('%d:%d @ %f' % (x, y, alpha))
i = 0
j = outer_radius
last_fade_amount = 0
fade_amount = 0
MAX_OPAQUE = 100.0
while i < j:
height = math.sqrt(max(outer_radius * outer_radius - i * i, 0))
fade_amount = MAX_OPAQUE * (math.ceil(height) - height)
if fade_amount < last_fade_amount:
# Opaqueness reset so drop down a row.
j -= 1
last_fade_amount = fade_amount
# The API needs integers, so convert here now we've checked if
# it dropped.
fade_amount_i = int(fade_amount)
# We're fading out the current j row, and fading in the next one down.
_draw_point(i, j, MAX_OPAQUE - fade_amount_i)
_draw_point(i, j - 1, fade_amount_i)
i += 1
,算法会在outer_radius
位置绘制100%不透明的正方形。但是,如果您想要转换到(0, outer_radius - 1)
位置,您可能希望圆圈在(0, 0.5)
和(0, outer_radius - 1)
位置平滑地混叠到50%的不透明度,这个算法没有给你,因为它忽略了原点。因此,如果你想准确地使用这个算法,你必须在传入之前对原点进行舍入,因此使用浮点数无法获得任何东西。
答案 1 :(得分:1)
我在我的机器上工作了以下内容:
FirstOrDefault()
以上是下图的简单实现。
此图表中存在错误:最底部的测试应该是def draw_antialiased_circle(renderer, position, radius, alpha_max=sdl2.SDL_ALPHA_OPAQUE):
def _draw_points(i, j):
'''Draws 8 points, one on each octant.'''
#Square symmetry
local_coord = [(i*(-1)**(k%2), j*(-1)**(k//2)) for k in range(4)]
#Diagonal symmetry
local_coord += [(j_, i_) for i_, j_ in local_coord]
for i_, j_ in local_coord:
sdl2.SDL_RenderDrawPoint(renderer, position.x + i_, position.y + j_)
def set_alpha_color(alpha, r=255, g=255, b=255):
'''Sets the render color.'''
sdl2.SDL_SetRenderDrawColor(renderer, r, g, b, alpha)
i, j, T = radius, 0, 0
#Draw the first 4 points
set_alpha_color(alpha_max)
_draw_points(i, 0)
while i > j:
j += 1
d = math.ceil(math.sqrt(radius**2 - j**2))
#Decrement i only if d < T as proved by Xiaolin Wu
i -= 1 if d < T else 0
"""
Draw 8 points for i and i-1 keeping the total opacity constant
and equal to :alpha_max:.
"""
alpha = int(d/radius * alpha_max)
for i_, alpha_ in zip((i, i-1), (alpha, alpha_max - alpha)):
set_alpha_color(alpha_)
_draw_points(i_, j)
#Previous d value goes to T
T = d
而不是D(r,j) < T
,这可以解释为什么您有点困惑。
要回答你的第二个问题,我怀疑你可以传递一些D(r,j) > T
数字作为位置和半径,因为最终C函数float
期望SDL_RenderDrawPoint
参数。
您可以将一些浮点数传递给Python函数,但在使用int
之前必须将它们转换为int
。遗憾的是,没有像半像素那样;)