1
0
mirror of synced 2024-11-27 23:50:47 +01:00

Fix anti-aliasing of rectangles so that they are always crisp.

This commit is contained in:
Jennifer Taylor 2021-08-09 19:09:00 +00:00
parent ec3453ae54
commit 73a36e17c2
7 changed files with 102 additions and 57 deletions

View File

@ -1,9 +1,9 @@
import multiprocessing
import signal
from PIL import Image # type: ignore
from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
from typing import Any, Callable, List, Optional, Sequence, Union
from ..types import Color, Matrix, Point
from ..types import Color, Matrix, Point, AAMode
from .perspective import perspective_calculate
@ -201,14 +201,14 @@ def pixel_renderer(
texheight: int,
xscale: float,
yscale: float,
callback: Callable[[Point], Tuple[Optional[Point], bool]],
callback: Callable[[Point], Optional[Point]],
add_color: Color,
mult_color: Color,
blendfunc: int,
imgbytes: Union[bytes, bytearray],
texbytes: Union[bytes, bytearray],
maskbytes: Optional[Union[bytes, bytearray]],
enable_aa: bool,
aa_mode: int,
) -> Sequence[int]:
# Determine offset
maskoff = imgx + (imgy * imgwidth)
@ -218,7 +218,7 @@ def pixel_renderer(
# This pixel is masked off!
return imgbytes[imgoff:(imgoff + 4)]
if enable_aa:
if aa_mode != AAMode.NONE:
r = 0
g = 0
b = 0
@ -229,17 +229,21 @@ def pixel_renderer(
# Essentially what we're doing here is calculating the scale, clamping it at 1.0 as the
# minimum and then setting the AA sample swing accordingly. This has the effect of anti-aliasing
# scaled up images a bit softer than would otherwise be achieved.
xswing = 0.5 * max(1.0, xscale)
yswing = 0.5 * max(1.0, yscale)
if aa_mode == AAMode.UNSCALED_SSAA_ONLY:
xswing = 0.5
yswing = 0.5
else:
xswing = 0.5 * max(1.0, xscale)
yswing = 0.5 * max(1.0, yscale)
xpoints = [0.5 - xswing, 0.5 - (xswing / 2.0), 0.5, 0.5 + (xswing / 2.0), 0.5 + xswing]
ypoints = [0.5 - yswing, 0.5 - (yswing / 2.0), 0.5, 0.5 + (yswing / 2.0), 0.5 + yswing]
# First, figure out if we can use bilinear resampling.
bilinear = False
if xscale >= 1.0 and yscale >= 1.0:
aaloc, enable_bilinear = callback(Point(imgx + 0.5, imgy + 0.5))
if aaloc is not None and enable_bilinear:
if aa_mode == AAMode.SSAA_OR_BILINEAR and xscale >= 1.0 and yscale >= 1.0:
aaloc = callback(Point(imgx + 0.5, imgy + 0.5))
if aaloc is not None:
aax, aay, _ = aaloc.as_tuple()
if not (aax <= 0 or aay <= 0 or aax >= (texwidth - 1) or aay >= (texheight - 1)):
bilinear = True
@ -247,7 +251,7 @@ def pixel_renderer(
# Now perform the desired AA operation.
if bilinear:
# Calculate the pixel we're after, and what percentage into the pixel we are.
texloc, _ = callback(Point(imgx + 0.5, imgy + 0.5))
texloc = callback(Point(imgx + 0.5, imgy + 0.5))
if texloc is None:
raise Exception("Logic error!")
aax, aay, _ = texloc.as_tuple()
@ -293,7 +297,7 @@ def pixel_renderer(
else:
for addy in ypoints:
for addx in xpoints:
texloc, _ = callback(Point(imgx + addx, imgy + addy))
texloc = callback(Point(imgx + addx, imgy + addy))
denom += 1
if texloc is None:
@ -340,7 +344,7 @@ def pixel_renderer(
return blend_point(add_color, mult_color, average, imgbytes[imgoff:(imgoff + 4)], blendfunc)
else:
# Calculate what texture pixel data goes here.
texloc, _ = callback(Point(imgx + 0.5, imgy + 0.5))
texloc = callback(Point(imgx + 0.5, imgy + 0.5))
if texloc is None:
return imgbytes[imgoff:(imgoff + 4)]
@ -370,7 +374,7 @@ def affine_line_renderer(
imgbytes: Union[bytes, bytearray],
texbytes: Union[bytes, bytearray],
maskbytes: Optional[Union[bytes, bytearray]],
enable_aa: bool,
aa_mode: int,
) -> None:
while True:
imgy = work.get()
@ -392,14 +396,14 @@ def affine_line_renderer(
texheight,
1.0 / inverse.xscale,
1.0 / inverse.yscale,
lambda point: (inverse.multiply_point(point), True),
lambda point: inverse.multiply_point(point),
add_color,
mult_color,
blendfunc,
imgbytes,
texbytes,
maskbytes,
enable_aa,
aa_mode,
)
results.put((imgy, bytes(rowbytes)))
@ -414,7 +418,7 @@ def affine_composite(
blendfunc: int,
texture: Image.Image,
single_threaded: bool = False,
enable_aa: bool = True,
aa_mode: int = AAMode.SSAA_OR_BILINEAR,
) -> Image.Image:
# Calculate the inverse so we can map canvas space back to texture space.
try:
@ -477,14 +481,14 @@ def affine_composite(
texheight,
1.0 / inverse.xscale,
1.0 / inverse.yscale,
lambda point: (inverse.multiply_point(point), True),
lambda point: inverse.multiply_point(point),
add_color,
mult_color,
blendfunc,
imgbytes,
texbytes,
maskbytes,
enable_aa,
aa_mode,
)
img = Image.frombytes('RGBA', (imgwidth, imgheight), bytes(imgbytes))
@ -529,7 +533,7 @@ def affine_composite(
imgbytes,
texbytes,
maskbytes,
enable_aa,
aa_mode,
),
)
procs.append(proc)
@ -581,15 +585,15 @@ def perspective_line_renderer(
imgbytes: Union[bytes, bytearray],
texbytes: Union[bytes, bytearray],
maskbytes: Optional[Union[bytes, bytearray]],
enable_aa: bool,
aa_mode: int,
) -> None:
def perspective_inverse(imgpoint: Point) -> Tuple[Optional[Point], bool]:
def perspective_inverse(imgpoint: Point) -> Optional[Point]:
# Calculate the texture coordinate with our perspective interpolation.
texdiv = inverse.multiply_point(imgpoint)
if texdiv.z <= 0.0:
return None, False
return None
return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z), False
return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z)
while True:
imgy = work.get()
@ -618,7 +622,7 @@ def perspective_line_renderer(
imgbytes,
texbytes,
maskbytes,
enable_aa,
aa_mode,
)
results.put((imgy, bytes(rowbytes)))
@ -635,7 +639,7 @@ def perspective_composite(
blendfunc: int,
texture: Image.Image,
single_threaded: bool = False,
enable_aa: bool = True,
aa_mode: int = AAMode.SSAA_ONLY,
) -> Image.Image:
# Warn if we have an unsupported blend.
if blendfunc not in {0, 1, 2, 3, 8, 9, 70, 256, 257}:
@ -664,13 +668,13 @@ def perspective_composite(
else:
maskbytes = None
def perspective_inverse(imgpoint: Point) -> Tuple[Optional[Point], bool]:
def perspective_inverse(imgpoint: Point) -> Optional[Point]:
# Calculate the texture coordinate with our perspective interpolation.
texdiv = inverse_matrix.multiply_point(imgpoint)
if texdiv.z <= 0.0:
return None, False
return None
return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z), False
return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z)
cores = multiprocessing.cpu_count()
if single_threaded or cores < 2:
@ -703,7 +707,7 @@ def perspective_composite(
imgbytes,
texbytes,
maskbytes,
enable_aa,
aa_mode,
)
img = Image.frombytes('RGBA', (imgwidth, imgheight), bytes(imgbytes))
@ -750,7 +754,7 @@ def perspective_composite(
imgbytes,
texbytes,
maskbytes,
enable_aa,
aa_mode,
),
)
procs.append(proc)

View File

@ -13,7 +13,7 @@ def affine_composite(
blendfunc: int,
texture: Image.Image,
single_threaded: bool = ...,
enable_aa: bool = ...,
aa_mode: int = ...
) -> Image.Image:
...
@ -29,6 +29,6 @@ def perspective_composite(
blendfunc: int,
texture: Image.Image,
single_threaded: bool = ...,
enable_aa: bool = ...,
aa_mode: int = ...
) -> Image.Image:
...

View File

@ -2,7 +2,7 @@ import multiprocessing
from PIL import Image # type: ignore
from typing import Optional, Tuple
from ..types import Color, Matrix, Point
from ..types import Color, Matrix, Point, AAMode
from .perspective import perspective_calculate
cdef extern struct floatcolor_t:
@ -45,7 +45,7 @@ cdef extern int composite_fast(
unsigned int texwidth,
unsigned int texheight,
unsigned int threads,
unsigned int enable_aa
unsigned int aa_mode
)
def affine_composite(
@ -57,7 +57,7 @@ def affine_composite(
blendfunc: int,
texture: Image.Image,
single_threaded: bool = False,
enable_aa: bool = True,
aa_mode: int = AAMode.SSAA_OR_BILINEAR,
) -> Image.Image:
# Calculate the inverse so we can map canvas space back to texture space.
try:
@ -141,7 +141,7 @@ def affine_composite(
texwidth,
texheight,
threads,
1 if enable_aa else 0,
aa_mode,
)
if errors != 0:
raise Exception("Error raised in C++!")
@ -164,7 +164,7 @@ def perspective_composite(
blendfunc: int,
texture: Image.Image,
single_threaded: bool = False,
enable_aa: bool = True,
aa_mode: int = AAMode.SSAA_ONLY,
) -> Image.Image:
if blendfunc not in {0, 1, 2, 3, 8, 9, 70, 256, 257}:
print(f"WARNING: Unsupported blend {blendfunc}")
@ -229,7 +229,7 @@ def perspective_composite(
texwidth,
texheight,
threads,
1 if enable_aa else 0,
aa_mode,
)
if errors != 0:
raise Exception("Error raised in C++!")

View File

@ -5,6 +5,11 @@
#define MIN_THREAD_WORK 10
#define AA_MODE_NONE 0
#define AA_MODE_UNSCALED_SSAA_ONLY 1
#define AA_MODE_SSAA_ONLY 2
#define AA_MODE_SSAA_OR_BILINEAR 3
extern "C"
{
typedef struct intcolor {
@ -77,7 +82,7 @@ extern "C"
floatcolor_t mult_color;
int blendfunc;
pthread_t *thread;
int enable_aa;
int aa_mode;
} work_t;
inline unsigned char clamp(float color) {
@ -265,8 +270,16 @@ extern "C"
// costs us almost nothing. Essentially what we're doing here is calculating the scale, clamping it at 1.0 as the
// minimum and then setting the AA sample swing accordingly. This has the effect of anti-aliasing scaled up images
// a bit softer than would otherwise be achieved.
float xswing = 0.5 * fmax(1.0, work->xscale);
float yswing = 0.5 * fmax(1.0, work->yscale);
float xswing;
float yswing;
if (work->aa_mode == AA_MODE_UNSCALED_SSAA_ONLY) {
xswing = 0.5;
yswing = 0.5;
} else {
xswing = 0.5 * fmax(1.0, work->xscale);
yswing = 0.5 * fmax(1.0, work->yscale);
}
for (unsigned int imgy = work->miny; imgy < work->maxy; imgy++) {
for (unsigned int imgx = work->minx; imgx < work->maxx; imgx++) {
@ -280,7 +293,7 @@ extern "C"
}
// Blend for simple anti-aliasing.
if (work->enable_aa) {
if (work->aa_mode != AA_MODE_NONE) {
// Calculate what texture pixel data goes here.
int r = 0;
int g = 0;
@ -292,7 +305,7 @@ extern "C"
// First, figure out if we can use bilinear resampling. Bilinear seems to look
// awful on perspective transforms, so disable it for all of them.
int bilinear = 0;
if (!work->use_perspective && work->xscale >= 1.0 && work->yscale >= 1.0) {
if (work->aa_mode == AA_MODE_SSAA_OR_BILINEAR && work->xscale >= 1.0 && work->yscale >= 1.0) {
point_t aaloc = work->inverse.multiply_point((point_t){(float)(imgx + 0.5), (float)(imgy + 0.5)});
int aax = aaloc.x;
int aay = aaloc.y;
@ -503,7 +516,7 @@ extern "C"
unsigned int texwidth,
unsigned int texheight,
unsigned int threads,
unsigned int enable_aa
unsigned int aa_mode
) {
// Cast to a usable type.
intcolor_t *imgdata = (intcolor_t *)imgbytes;
@ -528,7 +541,7 @@ extern "C"
work.add_color = add_color;
work.mult_color = mult_color;
work.blendfunc = blendfunc;
work.enable_aa = enable_aa;
work.aa_mode = aa_mode;
work.use_perspective = use_perspective;
chunk_composite_fast(&work);
@ -575,7 +588,7 @@ extern "C"
work->mult_color = mult_color;
work->blendfunc = blendfunc;
work->thread = thread;
work->enable_aa = enable_aa;
work->aa_mode = aa_mode;
work->use_perspective = use_perspective;
if (me)

View File

@ -22,6 +22,7 @@ from .types import (
Matrix,
Point,
Rectangle,
AAMode,
AP2Trigger,
AP2Action,
PushAction,
@ -1046,7 +1047,7 @@ class AFPRenderer(VerboseOutput):
257,
mask.rectangle,
single_threaded=self.__single_threaded,
enable_aa=False,
aa_mode=AAMode.NONE,
)
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
if self.__camera is None:
@ -1060,7 +1061,7 @@ class AFPRenderer(VerboseOutput):
257,
mask.rectangle,
single_threaded=self.__single_threaded,
enable_aa=False,
aa_mode=AAMode.NONE,
)
else:
calculated_mask = perspective_composite(
@ -1074,7 +1075,7 @@ class AFPRenderer(VerboseOutput):
257,
mask.rectangle,
single_threaded=self.__single_threaded,
enable_aa=False,
aa_mode=AAMode.NONE,
)
# Composite it onto the current mask.
@ -1087,7 +1088,7 @@ class AFPRenderer(VerboseOutput):
256,
calculated_mask,
single_threaded=self.__single_threaded,
enable_aa=False,
aa_mode=AAMode.NONE,
)
def __render_object(
@ -1162,6 +1163,7 @@ class AFPRenderer(VerboseOutput):
print("WARNING: Unhandled UV coordinate color!")
texture = None
rectangle = False
if params.flags & 0x2:
# We need to look up the texture for this.
if params.region not in self.textures:
@ -1203,9 +1205,15 @@ class AFPRenderer(VerboseOutput):
shape.rectangle = Image.new('RGBA', (int(right - left), int(bottom - top)), (params.blend.as_tuple()))
texture = shape.rectangle
rectangle = True
if texture is not None:
if projection == AP2PlaceObjectTag.PROJECTION_AFFINE:
if self.__enable_aa:
aamode = AAMode.UNSCALED_SSAA_ONLY if rectangle else AAMode.SSAA_OR_BILINEAR
else:
aamode = AAMode.NONE
img = affine_composite(
img,
add_color,
@ -1215,10 +1223,15 @@ class AFPRenderer(VerboseOutput):
blend,
texture,
single_threaded=self.__single_threaded,
enable_aa=self.__enable_aa,
aa_mode=aamode,
)
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
if self.__camera is None:
if self.__enable_aa:
aamode = AAMode.UNSCALED_SSAA_ONLY if rectangle else AAMode.SSAA_OR_BILINEAR
else:
aamode = AAMode.NONE
print("WARNING: Element requests perspective projection but no camera exists!")
img = affine_composite(
img,
@ -1229,9 +1242,14 @@ class AFPRenderer(VerboseOutput):
blend,
texture,
single_threaded=self.__single_threaded,
enable_aa=self.__enable_aa,
aa_mode=aamode,
)
else:
if self.__enable_aa:
aamode = AAMode.UNSCALED_SSAA_ONLY if rectangle else AAMode.SSAA_ONLY
else:
aamode = AAMode.NONE
img = perspective_composite(
img,
add_color,
@ -1243,7 +1261,7 @@ class AFPRenderer(VerboseOutput):
blend,
texture,
single_threaded=self.__single_threaded,
enable_aa=self.__enable_aa,
aa_mode=aamode,
)
elif isinstance(renderable, PlacedImage):
@ -1263,7 +1281,7 @@ class AFPRenderer(VerboseOutput):
blend,
texture,
single_threaded=self.__single_threaded,
enable_aa=self.__enable_aa,
aa_mode=AAMode.SSAA_OR_BILINEAR if self.__enable_aa else AAMode.NONE,
)
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
if self.__camera is None:
@ -1277,7 +1295,7 @@ class AFPRenderer(VerboseOutput):
blend,
texture,
single_threaded=self.__single_threaded,
enable_aa=self.__enable_aa,
aa_mode=AAMode.SSAA_OR_BILINEAR if self.__enable_aa else AAMode.NONE,
)
else:
img = perspective_composite(
@ -1291,7 +1309,7 @@ class AFPRenderer(VerboseOutput):
blend,
texture,
single_threaded=self.__single_threaded,
enable_aa=self.__enable_aa,
aa_mode=AAMode.SSAA_ONLY if self.__enable_aa else AAMode.NONE,
)
elif isinstance(renderable, PlacedDummy):
# Nothing to do!

View File

@ -87,6 +87,7 @@ from .statement import (
AndIf,
OrIf,
)
from .aa import AAMode
__all__ = [
@ -176,4 +177,5 @@ __all__ = [
'GetURL2Action',
'StartDragAction',
'DefineFunction2Action',
'AAMode',
]

View File

@ -0,0 +1,8 @@
from typing_extensions import Final
class AAMode:
NONE: Final[int] = 0
UNSCALED_SSAA_ONLY: Final[int] = 1
SSAA_ONLY: Final[int] = 2
SSAA_OR_BILINEAR: Final[int] = 3