add DXT5 to DXT1 conversion tool

main
Andrew Cassidy 3 years ago
parent c89d94717d
commit b6aa96cca3

@ -1,4 +1,5 @@
import os import os
import subprocess
import struct import struct
import re import re
import math import math
@ -74,7 +75,8 @@ class DDSPixelFlags(enum.IntFlag):
texture_formats = { texture_formats = {
'DXT1' : s3tc.DXT1Texture 'DXT1' : s3tc.DXT1Texture,
'DXT5' : s3tc.DXT5Texture
} }
class DDSFile: class DDSFile:

@ -1,43 +1,66 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Checks any number of dds files for common issues"""
import sys import sys
import os import os
import os.path import os.path
import enum
import argparse import argparse
import dds import dds
parser = argparse.ArgumentParser(description="Checks any number of dds files for common issues, including formats not supported by KSP, and DXT5 textures that don't use the alpha channel.") class Check(enum.Enum):
"""Kinds of check to perform"""
TRANPARENCY = 1
FORMAT = 2
parser = argparse.ArgumentParser(description= __doc__)
parser.add_argument('files', type=str, nargs='*', help = "input dds files") parser.add_argument('files', type=str, nargs='*', help = "input dds files")
modes = parser.add_mutually_exclusive_group() parser.add_argument('--transparency', '-t',
modes.add_argument('--transparency', '-t', dest='mode', action='store_const', const="transparency", default="none", help = "Generate a list of files that fail the transparency check") action='append_const', dest='checks', const=Check.TRANPARENCY,
modes.add_argument('--format', '-f', dest='mode', action='store_const', const="format", default="none", help = "Generate a list of files that fail the format check") help = "Check textures for unnecessary transparency map")
parser.add_argument('--format', '-f',
action='append_const', dest='checks', const=Check.FORMAT,
help = "Check texture formats")
parser.add_argument('--all', '-a',
action='store_const', dest='checks', const=[Check.TRANPARENCY, Check.FORMAT],
help = "Perform all checks")
parser.add_argument('--formats', nargs='+', default = ['DXT1', 'DXT5'],
help="Valid texture formats to check against (default: %(default)s)")
parser.add_argument('--convertcmd', type=str, metavar='CMD', default=dds.convertcmd, help="name of imagemagick's convert tool (default: %(default)s)") parser.add_argument('--convertcmd', type=str, metavar='CMD', default=dds.convertcmd,
parser.add_argument('--infocmd', type=str, metavar='CMD', default=dds.infocmd, help="name of the nvidia dds info tool (default: %(default)s)") help="name of imagemagick's convert tool (default: %(default)s)")
args = parser.parse_args() args = parser.parse_args()
dds.convertcmd = args.convertcmd dds.convertcmd = args.convertcmd
dds.infocmd = args.infocmd
def printerror(path, shortpath, message):
for argv in args.files: """either prints a message or just the path depending on if stdout is a tty"""
file = os.path.abspath(argv) if sys.stdout.isatty():
print(f'[{shortpath}]: {message}')
info = dds.nvinfo(file)
format = info["format"]
alpha = dds.alpha(file)
if format == "DXT1":
pass
elif format == "DXT5":
if alpha > 254:
if args.mode == 'none':
print(f'[{argv}]: Image is DXT5 but has no alpha channel')
elif args.mode == 'transparency':
print(file)
else: else:
if args.mode == 'none': sys.stdout.write(path+'\n')
print(f'[{argv}]: incompatible format')
elif args.mode == 'format': def checkfile(shortpath, checks, formats):
print(file) """check a single dds file for common issues"""
path = os.path.abspath(shortpath)
with open(path, 'rb') as file:
ddsfile = dds.DDSFile()
ddsfile.read_header(file)
four_cc = ddsfile.four_cc
if four_cc == '':
four_cc = "uncompressed"
alpha = dds.alpha(path)
if Check.TRANPARENCY in checks and four_cc == 'DXT5' and alpha > 254:
printerror(path, shortpath, 'Image is DXT5 but has no alpha channel')
if Check.FORMAT in checks and four_cc not in formats:
printerror(path, shortpath, f'Incompatible format: {four_cc}')
for shortpath in args.files:
checkfile(shortpath, args.checks, args.formats)

@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""converts DXT5 dds files to DXT1 without generation loss."""
import sys
import os
import os.path
import argparse
import dds
parser = argparse.ArgumentParser(
description=__doc__)
parser.add_argument('files', type=str, nargs='*',
help = "input dds files. Paths are read from stdin if none are given")
args = parser.parse_args()
if len(args.files) < 1 and not sys.stdin.isatty():
args.files = sys.stdin.read().splitlines()
for argv in args.files:
path = os.path.abspath(argv)
# read DDS file
with open(path, 'rb') as file:
ddsfile = dds.DDSFile(file)
# check its actually dxt5
if ddsfile.four_cc != 'DXT5':
print(f'[{argv}] file is not DXT5, aborting.')
continue
print(f'[{argv}] converting to DXT1')
# make modifications
ddsfile.four_cc = 'DXT1'
ddsfile.textures = [tex.to_dxt1() for tex in ddsfile.textures]
ddsfile.linear_size //= 2
# write DDS file
with open(path, 'wb') as file:
ddsfile.write(file)

@ -1,62 +1,133 @@
import struct import struct
import math import math
import operator
from functools import reduce
from color import Color from color import Color
def byte_slice(byte): def bit_slice(value, size, count):
return [byte & 0x03, mask = (2 ** size) - 1
(byte & 0x0C) >> 2, return [(value >> offset) & mask for offset in range(0, size * count, size)]
(byte & 0x30) >> 4,
(byte & 0xC0) >> 6]
def byte_unslice(indices): def bit_merge(values, size):
return indices[0] | (indices[1] << 2) | (indices[2] << 4) | (indices[3] << 6) offsets = range(0, len(values) * size, size)
return reduce(operator.__or__, map(operator.lshift, values, offsets))
class DXT1Texture: def triple_slice(triplet):
def __init__(self, width = 4, height = 4, file = None): values = bit_slice(bit_merge(triplet, 8), 3, 8)
self.width = width return [values[0:4], values[4:8]]
self.height = height
self.block_width = math.ceil(width / 4) def triple_merge(rows):
self.block_height = math.ceil(height / 4) values = rows[0] + rows[1]
self.block_count = self.block_width * self.block_height return bit_slice(bit_merge(values, 3), 8, 3)
self.blocks = []
class DXT1Block:
size = 8
def __init__(self, file = None):
self.color0 = Color()
self.color1 = Color()
self.indices = [[0] * 4] * 4
if file: if file:
self.read(file) self.read(file)
def __repr__(self): def __repr__(self):
return f'{self.block_count} blocks: {self.blocks}' return repr(self.__dict__)
def __str__(self):
return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.indices}'
def read(self, file): def read(self, file):
for x in range(self.block_count): block = struct.unpack_from('<2H4B', file.read(self.size))
self.blocks.append(DXT1Block(file))
self.color0 = Color.from_565(block[0])
self.color1 = Color.from_565(block[1])
self.indices = [bit_slice(row, 2, 4) for row in block[2:6]]
def write(self, file): def write(self, file):
for block in self.blocks: file.write(struct.pack('<2H4B',
block.write(file) self.color0.to_565(), self.color1.to_565(),
*(bit_merge(row, 2) for row in self.indices)))
class DXT1Block:
size = 8 class DXT5Block:
size = 16
def __init__(self, file = None): def __init__(self, file = None):
self.color0 = Color() self.color0 = Color()
self.color1 = Color() self.color1 = Color()
self.indices = [[0] * 4] * 4 self.indices = [[0] * 4] * 4
self.alpha0 = 1
self.alpha1 = 1
self.alpha_indices = [[0] * 4] * 4
if file: if file:
self.read(file) self.read(file)
def __repr__(self): def __repr__(self):
return f'color0: ({repr(self.color0)}) color1: ({repr(self.color1)}), indices:{self.indices}' return repr(self.__dict__)
def __str__(self):
return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.indices}'
def read(self, file): def read(self, file):
block = struct.unpack_from('<HHBBBB', file.read(self.size)) block = struct.unpack_from('<2B6B2H4B', file.read(self.size))
self.color0 = Color.from_565(block[0]) self.alpha0 = block[0] / 0xFF
self.color1 = Color.from_565(block[1]) self.alpha1 = block[1] / 0xFF
self.indices = list(map(byte_slice, block[2:6])) self.alpha_indices = triple_slice(block[2:5]) + triple_slice(block[5:8])
self.color0 = Color.from_565(block[8])
self.color1 = Color.from_565(block[9])
self.indices = [bit_slice(row, 2, 4) for row in block[10:14]]
def write(self, file): def write(self, file):
file.write(struct.pack('<HHBBBB', self.color0.to_565(), self.color1.to_565(), *map(byte_unslice, self.indices))) file.write(struct.pack('<2B6B2H4B',
int(self.alpha0 * 0xFF), int(self.alpha1 * 0xFF),
*triple_merge(self.alpha_indices[0:2]),
*triple_merge(self.alpha_indices[2:4]),
self.color0.to_565(), self.color1.to_565(),
*(bit_merge(row, 2) for row in self.indices)))
def to_dxt1(self):
dxt1 = DXT1Block()
dxt1.color0 = self.color0
dxt1.color1 = self.color1
dxt1.indices = self.indices
return dxt1
class BlockTexture:
def __init__(self, block_type, width = 4, height = 4, file = None):
self.block_type = block_type
self.width = width
self.height = height
self.block_width = math.ceil(width / 4)
self.block_height = math.ceil(height / 4)
self.block_count = self.block_width * self.block_height
self.blocks = []
if file:
self.read(file)
def __repr__(self):
return f'{self.block_count} blocks: {self.blocks}'
def read(self, file):
for x in range(self.block_count):
self.blocks.append(self.block_type(file))
def write(self, file):
for block in self.blocks:
block.write(file)
class DXT1Texture(BlockTexture):
def __init__(self, width = 4, height = 4, file = None):
super().__init__(DXT1Block, width, height, file)
class DXT5Texture(BlockTexture):
def __init__(self, width = 4, height = 4, file = None):
super().__init__(DXT5Block, width, height, file)
def to_dxt1(self):
dxt1 = DXT1Texture(self.width, self.height)
dxt1.blocks = [block.to_dxt1() for block in self.blocks]
# print(f'size:{dxt1.width}x{dxt1.height}, {len(dxt1.blocks)} blocks')
return dxt1

Loading…
Cancel
Save