Add switch to DXT1 encoder to select block mode (With alpha/Without alpha)

This commit is contained in:
REDxEYE 2022-08-14 21:11:18 +03:00
parent a634a44a9c
commit c3457f10eb
13 changed files with 94 additions and 93 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -63,7 +63,7 @@ def test_get_mipmap_count(size: Tuple[int, int], expected_count: int):
],
)
def test_get_texture_size(
pixel_format: VtfPF, size: Tuple[int, int], expected_size: int
pixel_format: VtfPF, size: Tuple[int, int], expected_size: int
):
assert _get_texture_size(pixel_format, *size) == expected_size
@ -72,17 +72,18 @@ def test_get_texture_size(
("etalon_path", "file_path", "expected_mode", "epsilon"),
[
("Tests/images/vtf_i8.png", "Tests/images/vtf_i8.vtf", "L", 0.0),
("Tests/images/vtf_a8.png", "Tests/images/vtf_a8.vtf", "L", 0.0),
("Tests/images/vtf_a8.png", "Tests/images/vtf_a8.vtf", "RGBA", 0.0),
("Tests/images/vtf_ia88.png", "Tests/images/vtf_ia88.vtf", "LA", 0.0),
("Tests/images/vtf_uv88.png", "Tests/images/vtf_uv88.vtf", "RGB", 0.0),
("Tests/images/vtf_rgb888.png", "Tests/images/vtf_rgb888.vtf", "RGB", 0.0),
("Tests/images/vtf_bgr888.png", "Tests/images/vtf_bgr888.vtf", "RGB", 0.0),
("Tests/images/vtf_dxt1.png", "Tests/images/vtf_dxt1.vtf", "RGBA", 3.0),
("Tests/images/vtf_dxt1A.png", "Tests/images/vtf_dxt1A.vtf", "RGBA", 8.0),
("Tests/images/vtf_rgba8888.png", "Tests/images/vtf_rgba8888.vtf", "RGBA", 0),
],
)
def test_vtf_loading(
etalon_path: str, file_path: str, expected_mode: str, epsilon: float
def test_vtf_read(
etalon_path: str, file_path: str, expected_mode: str, epsilon: float
):
e = Image.open(etalon_path)
f = Image.open(file_path)
@ -92,3 +93,32 @@ def test_vtf_loading(
assert_image_equal(e, f)
else:
assert_image_similar(e, f, epsilon)
@pytest.mark.parametrize(
("pixel_format", "file_path", "expected_mode", "epsilon"),
[
(VtfPF.I8, "Tests/images/vtf_i8.png", "L", 0.0),
(VtfPF.A8, "Tests/images/vtf_a8.png", "RGBA", 0.0),
(VtfPF.IA88, "Tests/images/vtf_ia88.png", "LA", 0.0),
(VtfPF.UV88, "Tests/images/vtf_uv88.png", "RGB", 0.0),
(VtfPF.RGB888, "Tests/images/vtf_rgb888.png", "RGB", 0.0),
(VtfPF.BGR888, "Tests/images/vtf_bgr888.png", "RGB", 0.0),
(VtfPF.DXT1, "Tests/images/vtf_dxt1.png", "RGBA", 3.0),
(VtfPF.DXT1_ONEBITALPHA, "Tests/images/vtf_dxt1A.png", "RGBA", 8.0),
(VtfPF.RGBA8888, "Tests/images/vtf_rgba8888.png", "RGBA", 0),
],
)
def test_vtf_save(pixel_format: VtfPF, file_path: str,
expected_mode: str, epsilon: float, tmp_path):
f: Image.Image = Image.open(file_path)
out = (tmp_path / "tmp.vtf").as_posix()
f.save(out, pixel_format=pixel_format)
if pixel_format == VtfPF.DXT1:
f = f.convert("RGBA")
e = Image.open(out)
assert e.mode == expected_mode
if epsilon == 0:
assert_image_equal(e, f)
else:
assert_image_similar(e, f, epsilon)

View File

@ -76,29 +76,29 @@ class VtfPF(IntEnum):
ABGR8888 = 1
RGB888 = 2
BGR888 = 3
RGB565 = 4
# RGB565 = 4
I8 = 5
IA88 = 6
P8 = 7
# P8 = 7
A8 = 8
RGB888_BLUESCREEN = 9
BGR888_BLUESCREEN = 10
# RGB888_BLUESCREEN = 9
# BGR888_BLUESCREEN = 10
ARGB8888 = 11
BGRA8888 = 12
DXT1 = 13
DXT3 = 14
DXT5 = 15
BGRX8888 = 16
BGR565 = 17
BGRX5551 = 18
BGRA4444 = 19
# BGR565 = 17
# BGRX5551 = 18
# BGRA4444 = 19
DXT1_ONEBITALPHA = 20
BGRA5551 = 21
# BGRA5551 = 21
UV88 = 22
UVWQ8888 = 23
RGBA16161616F = 24
RGBA16161616 = 25
UVLX8888 = 26
# UVWQ8888 = 23
# RGBA16161616F = 24
# RGBA16161616 = 25
# UVLX8888 = 26
VTFHeader = NamedTuple(
@ -125,26 +125,8 @@ VTFHeader = NamedTuple(
("resource_count", int),
],
)
RGB_FORMATS = (
VtfPF.DXT1,
VtfPF.RGB888,
VtfPF.BGR888,
VtfPF.UV88,
)
RGBA_FORMATS = (
VtfPF.DXT1_ONEBITALPHA,
VtfPF.DXT3,
VtfPF.DXT5,
VtfPF.RGBA8888,
)
L_FORMATS = (
VtfPF.A8,
VtfPF.I8,
)
LA_FORMATS = (VtfPF.IA88,)
BLOCK_COMPRESSED = (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT3, VtfPF.DXT5)
SUPPORTED_FORMATS = RGBA_FORMATS + RGB_FORMATS + LA_FORMATS + L_FORMATS
HEADER_V70 = "<I2HI2H4x3f4xfIbI2b"
HEADER_V72 = "<I2HI2H4x3f4xfIbI2bH"
HEADER_V73 = "<I2HI2H4x3f4xfIbI2bH3xI8x"
@ -153,22 +135,13 @@ HEADER_V73 = "<I2HI2H4x3f4xfIbI2bH3xI8x"
def _get_texture_size(pixel_format: VtfPF, width, height):
if pixel_format in (VtfPF.DXT1, VtfPF.DXT1_ONEBITALPHA):
return width * height // 2
elif (
pixel_format
in (
VtfPF.DXT3,
VtfPF.DXT5,
)
+ L_FORMATS
):
elif pixel_format in (VtfPF.DXT3, VtfPF.DXT5):
return width * height
elif pixel_format == VtfPF.UV88:
elif pixel_format in (VtfPF.A8, VtfPF.I8,):
return width * height
elif pixel_format in (VtfPF.UV88, VtfPF.IA88):
return width * height * 2
elif pixel_format in LA_FORMATS:
return width * height * 2
elif pixel_format == VtfPF.RGB888:
return width * height * 3
elif pixel_format == VtfPF.BGR888:
elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888):
return width * height * 3
elif pixel_format == VtfPF.RGBA8888:
return width * height * 4
@ -190,36 +163,28 @@ def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF):
if pixel_format == VtfPF.DXT1:
encoder = "bcn"
encoder_args = (1, "DXT1")
im = im.convert("RGB")
im = im.convert("RGBA")
elif pixel_format == VtfPF.DXT1_ONEBITALPHA:
encoder = "bcn"
encoder_args = (1, "DXT1A")
im = im.convert("RGBA")
elif pixel_format == VtfPF.DXT3:
encoder = "bcn"
encoder_args = (3, "DXT3")
im = im.convert("RGBA")
elif pixel_format == VtfPF.DXT5:
encoder = "bcn"
encoder_args = (5, "DXT5")
im = im.convert("RGBA")
elif pixel_format == VtfPF.RGB888:
encoder = "raw"
encoder_args = ("RGB", 0, 0)
im = im.convert("RGB")
elif pixel_format == VtfPF.BGR888:
encoder = "raw"
encoder_args = ("BGR", 0, 0)
im = im.convert("RGB")
elif pixel_format == VtfPF.RGBA8888:
encoder = "raw"
encoder_args = ("RGBA", 0, 0)
im = im.convert("RGBA")
elif pixel_format == VtfPF.A8:
encoder = "raw"
encoder_args = ("L", 0, 0)
*_, a = im.split()
im = Image.merge("L", (a,))
encoder_args = ("A", 0, 0)
elif pixel_format == VtfPF.I8:
encoder = "raw"
encoder_args = ("L", 0, 0)
@ -240,7 +205,7 @@ def _write_image(fp: BufferedIOBase, im: Image.Image, pixel_format: VtfPF):
def _closest_power(x):
possible_results = round(log(x, 2)), ceil(log(x, 2))
return 2 ** min(possible_results, key=lambda z: abs(x - 2**z))
return 2 ** min(possible_results, key=lambda z: abs(x - 2 ** z))
class VtfImageFile(ImageFile.ImageFile):
@ -281,15 +246,14 @@ class VtfImageFile(ImageFile.ImageFile):
# flags = CompiledVtfFlags(header.flags)
pixel_format = VtfPF(header.pixel_format)
low_format = VtfPF(header.low_pixel_format)
if pixel_format == VtfPF.DXT1: # Special case for DXT1
if pixel_format in (VtfPF.DXT1_ONEBITALPHA, VtfPF.DXT1, VtfPF.DXT3, VtfPF.DXT5,
VtfPF.RGBA8888, VtfPF.BGRA8888,VtfPF.A8):
self.mode = "RGBA"
elif pixel_format in RGB_FORMATS:
elif pixel_format in (VtfPF.RGB888, VtfPF.BGR888, VtfPF.UV88):
self.mode = "RGB"
elif pixel_format in RGBA_FORMATS:
self.mode = "RGBA"
elif pixel_format in L_FORMATS:
elif pixel_format == VtfPF.I8:
self.mode = "L"
elif pixel_format in LA_FORMATS:
elif pixel_format == VtfPF.IA88:
self.mode = "LA"
else:
raise VTFException(f"Unsupported VTF pixel format: {pixel_format}")
@ -311,19 +275,21 @@ class VtfImageFile(ImageFile.ImageFile):
tile = ("bcn", (0, 0) + self.size, data_start, (2, "DXT3"))
elif pixel_format == VtfPF.DXT5:
tile = ("bcn", (0, 0) + self.size, data_start, (3, "DXT5"))
elif pixel_format in (VtfPF.RGBA8888,):
elif pixel_format == VtfPF.RGBA8888:
tile = ("raw", (0, 0) + self.size, data_start, ("RGBA", 0, 1))
elif pixel_format in (VtfPF.RGB888,):
elif pixel_format == VtfPF.RGB888:
tile = ("raw", (0, 0) + self.size, data_start, ("RGB", 0, 1))
elif pixel_format in (VtfPF.BGR888,):
elif pixel_format == VtfPF.BGR888:
tile = ("raw", (0, 0) + self.size, data_start, ("BGR", 0, 1))
elif pixel_format in (VtfPF.BGRA8888,):
elif pixel_format == VtfPF.BGRA8888:
tile = ("raw", (0, 0) + self.size, data_start, ("BGRA", 0, 1))
elif pixel_format in (VtfPF.UV88,):
elif pixel_format == VtfPF.UV88:
tile = ("raw", (0, 0) + self.size, data_start, ("RG", 0, 1))
elif pixel_format in L_FORMATS:
elif pixel_format == VtfPF.I8:
tile = ("raw", (0, 0) + self.size, data_start, ("L", 0, 1))
elif pixel_format in LA_FORMATS:
elif pixel_format == VtfPF.A8:
tile = ("raw", (0, 0) + self.size, data_start, ("A", 0, 1))
elif pixel_format == VtfPF.IA88:
tile = ("raw", (0, 0) + self.size, data_start, ("LA", 0, 1))
else:
raise VTFException(f"Unsupported VTF pixel format: {pixel_format}")
@ -343,15 +309,12 @@ def _save(im, fp, filename):
if pixel_format == VtfPF.DXT1_ONEBITALPHA:
flags |= CompiledVtfFlags.ONEBITALPHA
elif pixel_format == VtfPF.A8:
elif pixel_format in (VtfPF.DXT3, VtfPF.DXT5,
VtfPF.RGBA8888, VtfPF.BGRA8888,
VtfPF.A8, VtfPF.IA88):
flags |= CompiledVtfFlags.EIGHTBITALPHA
elif pixel_format in RGBA_FORMATS + LA_FORMATS:
flags |= CompiledVtfFlags.EIGHTBITALPHA
elif pixel_format in RGB_FORMATS + L_FORMATS:
pass
else:
raise VTFException("Unhandled case")
pass
im = im.resize((_closest_power(im.width), _closest_power(im.height)))
width, height = im.size

View File

@ -111,7 +111,7 @@ get_closest_color_index(const UINT16 *colors, UINT16 color) {
if (error == 0) {
return color_id;
}
if (error < color_error) {
if (error <= color_error) {
color_error = error;
lowest_id = color_id;
}
@ -122,10 +122,10 @@ get_closest_color_index(const UINT16 *colors, UINT16 color) {
int
encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT8 *dst = buf;
UINT8 no_alpha = 0;
UINT8 alpha = 0;
INT32 block_index;
if (strcmp(((BCNSTATE *)state->context)->pixel_format, "DXT1A") != 0) {
no_alpha = 1;
if (strcmp(((BCNSTATE *)state->context)->pixel_format, "DXT1A") == 0) {
alpha = 1;
}
INT32 block_count = (im->xsize * im->ysize) / 16;
if (block_count * sizeof(bc1_color) > bytes) {
@ -143,6 +143,7 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT16 unique_colors[16];
UINT8 color_frequency[16];
UINT8 opaque[16];
UINT8 local_alpha = 0;
memset(all_colors, 0, sizeof(all_colors));
memset(unique_colors, 0, sizeof(unique_colors));
memset(color_frequency, 0, sizeof(color_frequency));
@ -157,7 +158,8 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT8 b = im->image[y][x * im->pixelsize + 0];
UINT8 a = im->image[y][x * im->pixelsize + 3];
UINT16 color = PACK_SHORT_565(r, g, b);
opaque[bx + by * 4] = a >= 127;
opaque[bx + by * 4] = a >= 128;
local_alpha |= a <= 128;
all_colors[bx + by * 4] = color;
UINT8 new_color = 1;
@ -179,17 +181,23 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT16 c0 = 0, c1 = 0;
pick_2_major_colors(unique_colors, color_frequency, unique_count, &c0, &c1);
if (c0 < c1 && no_alpha) {
SWAP(UINT16, c0, c1);
if (alpha && local_alpha) {
if (c0 > c1) {
SWAP(UINT16, c0, c1);
}
} else {
if (c0 < c1) {
SWAP(UINT16, c0, c1);
}
}
UINT16 palette[4] = {c0, c1, 0, 0};
if (no_alpha) {
palette[2] = rgb565_lerp(c0, c1, 2, 1);
palette[3] = rgb565_lerp(c0, c1, 1, 2);
} else {
if (alpha && local_alpha) {
palette[2] = rgb565_lerp(c0, c1, 1, 1);
palette[3] = 0;
} else {
palette[2] = rgb565_lerp(c0, c1, 2, 1);
palette[3] = rgb565_lerp(c0, c1, 1, 2);
}
bc1_color block = {0};
block.c0 = c0;
@ -197,10 +205,10 @@ encode_bc1(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
UINT32 color_id;
for (color_id = 0; color_id < 16; ++color_id) {
UINT8 bc_color_id;
if (opaque[color_id] || no_alpha) {
bc_color_id = get_closest_color_index(palette, all_colors[color_id]);
} else {
if ((alpha && local_alpha) && !opaque[color_id]) {
bc_color_id = 3;
} else {
bc_color_id = get_closest_color_index(palette, all_colors[color_id]);
}
SET_BITS(block.lut, color_id * 2, 2, bc_color_id);
}