add DXT5 to DXT1 conversion tool
This commit is contained in:
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('--convertcmd', type=str, metavar='CMD', default=dds.convertcmd, help="name of imagemagick's convert tool (default: %(default)s)")
|
parser.add_argument('--formats', nargs='+', default = ['DXT1', 'DXT5'],
|
||||||
parser.add_argument('--infocmd', type=str, metavar='CMD', default=dds.infocmd, help="name of the nvidia dds info tool (default: %(default)s)")
|
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)")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
dds.convertcmd = args.convertcmd
|
dds.convertcmd = args.convertcmd
|
||||||
dds.infocmd = args.infocmd
|
|
||||||
|
|
||||||
for argv in args.files:
|
def printerror(path, shortpath, message):
|
||||||
file = os.path.abspath(argv)
|
"""either prints a message or just the path depending on if stdout is a tty"""
|
||||||
|
if sys.stdout.isatty():
|
||||||
info = dds.nvinfo(file)
|
print(f'[{shortpath}]: {message}')
|
||||||
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)
|
||||||
|
40
Scripts/dxt5to1.py
Executable file
40
Scripts/dxt5to1.py
Executable file
@ -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)
|
139
Scripts/s3tc.py
139
Scripts/s3tc.py
@ -1,18 +1,102 @@
|
|||||||
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)
|
||||||
|
return [values[0:4], values[4:8]]
|
||||||
|
|
||||||
|
def triple_merge(rows):
|
||||||
|
values = rows[0] + rows[1]
|
||||||
|
return bit_slice(bit_merge(values, 3), 8, 3)
|
||||||
|
|
||||||
|
class DXT1Block:
|
||||||
|
size = 8
|
||||||
|
|
||||||
|
def __init__(self, file = None):
|
||||||
|
self.color0 = Color()
|
||||||
|
self.color1 = Color()
|
||||||
|
self.indices = [[0] * 4] * 4
|
||||||
|
|
||||||
|
if file:
|
||||||
|
self.read(file)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.__dict__)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.indices}'
|
||||||
|
|
||||||
|
def read(self, file):
|
||||||
|
block = struct.unpack_from('<2H4B', file.read(self.size))
|
||||||
|
|
||||||
|
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):
|
||||||
|
file.write(struct.pack('<2H4B',
|
||||||
|
self.color0.to_565(), self.color1.to_565(),
|
||||||
|
*(bit_merge(row, 2) for row in self.indices)))
|
||||||
|
|
||||||
|
|
||||||
|
class DXT5Block:
|
||||||
|
size = 16
|
||||||
|
|
||||||
|
def __init__(self, file = None):
|
||||||
|
self.color0 = Color()
|
||||||
|
self.color1 = Color()
|
||||||
|
self.indices = [[0] * 4] * 4
|
||||||
|
|
||||||
|
self.alpha0 = 1
|
||||||
|
self.alpha1 = 1
|
||||||
|
self.alpha_indices = [[0] * 4] * 4
|
||||||
|
|
||||||
|
if file:
|
||||||
|
self.read(file)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.__dict__)
|
||||||
|
|
||||||
|
def read(self, file):
|
||||||
|
block = struct.unpack_from('<2B6B2H4B', file.read(self.size))
|
||||||
|
|
||||||
|
self.alpha0 = block[0] / 0xFF
|
||||||
|
self.alpha1 = block[1] / 0xFF
|
||||||
|
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):
|
||||||
|
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.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.block_width = math.ceil(width / 4)
|
self.block_width = math.ceil(width / 4)
|
||||||
@ -28,35 +112,22 @@ class DXT1Texture:
|
|||||||
|
|
||||||
def read(self, file):
|
def read(self, file):
|
||||||
for x in range(self.block_count):
|
for x in range(self.block_count):
|
||||||
self.blocks.append(DXT1Block(file))
|
self.blocks.append(self.block_type(file))
|
||||||
|
|
||||||
def write(self, file):
|
def write(self, file):
|
||||||
for block in self.blocks:
|
for block in self.blocks:
|
||||||
block.write(file)
|
block.write(file)
|
||||||
|
|
||||||
class DXT1Block:
|
class DXT1Texture(BlockTexture):
|
||||||
size = 8
|
def __init__(self, width = 4, height = 4, file = None):
|
||||||
|
super().__init__(DXT1Block, width, height, file)
|
||||||
|
|
||||||
def __init__(self, file = None):
|
class DXT5Texture(BlockTexture):
|
||||||
self.color0 = Color()
|
def __init__(self, width = 4, height = 4, file = None):
|
||||||
self.color1 = Color()
|
super().__init__(DXT5Block, width, height, file)
|
||||||
self.indices = [[0] * 4] * 4
|
|
||||||
|
|
||||||
if file:
|
def to_dxt1(self):
|
||||||
self.read(file)
|
dxt1 = DXT1Texture(self.width, self.height)
|
||||||
|
dxt1.blocks = [block.to_dxt1() for block in self.blocks]
|
||||||
def __repr__(self):
|
# print(f'size:{dxt1.width}x{dxt1.height}, {len(dxt1.blocks)} blocks')
|
||||||
return f'color0: ({repr(self.color0)}) color1: ({repr(self.color1)}), indices:{self.indices}'
|
return dxt1
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.indices}'
|
|
||||||
|
|
||||||
def read(self, file):
|
|
||||||
block = struct.unpack_from('<HHBBBB', file.read(self.size))
|
|
||||||
|
|
||||||
self.color0 = Color.from_565(block[0])
|
|
||||||
self.color1 = Color.from_565(block[1])
|
|
||||||
self.indices = list(map(byte_slice, block[2:6]))
|
|
||||||
|
|
||||||
def write(self, file):
|
|
||||||
file.write(struct.pack('<HHBBBB', self.color0.to_565(), self.color1.to_565(), *map(byte_unslice, self.indices)))
|
|
||||||
|
Loading…
Reference in New Issue
Block a user