Last active
July 29, 2021 23:01
-
-
Save KatieFrogs/b8c1540165a56be1133bacd01c883056 to your computer and use it in GitHub Desktop.
.nut texture extractor and encoder, based on Smash Forge code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import os | |
import struct | |
class Dds: | |
class DdsFormat: | |
Rgba = 0 | |
Dxt1 = 1 | |
Dxt3 = 2 | |
Dxt5 = 3 | |
Ati1 = 4 | |
Ati2 = 5 | |
class CubemapFace: | |
PosX = 0 | |
NegX = 1 | |
PosY = 2 | |
NegY = 3 | |
PosZ = 4 | |
NegZ = 5 | |
class Ddsd: | |
Caps = 0x1 | |
Height = 0x2 | |
Width = 0x4 | |
Pitch = 0x8 | |
Pixelformat = 0x1000 | |
Mipmapcount = 0x20000 | |
Linearsize = 0x80000 | |
Depth = 0x800000 | |
class Ddpf: | |
Alphapixels = 0x1 | |
Alpha = 0x2 | |
Fourcc = 0x4 | |
Rgb = 0x40 | |
Yuv = 0x200 | |
Luminance = 0x20000 | |
class Ddscaps: | |
Complex = 0x8 | |
Texture = 0x1000 | |
Mipmap = 0x400000 | |
class Ddscaps2: | |
Cubemap = 0x200 | |
CubemapPositivex = 0x600 | |
CubemapNegativex = 0xa00 | |
CubemapPositivey = 0x1200 | |
CubemapNegativey = 0x2200 | |
CubemapPositivez = 0x4200 | |
CubemapNegativez = 0x8200 | |
CubemapAllfaces = 0xfe00 | |
Volume = 0x200000 | |
def GetFormatSize(self, fourCc): | |
if fourCc == b"\0\0\0\0": | |
#RGBA | |
return 0x4 | |
elif fourCc in (b"DXT1", b"ATI1", b"BC4U"): | |
return 0x8 | |
elif fourCc in (b"DXT3", b"DXT5", b"ATI2", b"BC5U"): | |
return 0x10 | |
else: | |
return 0x0 | |
Magic = b"DDS " | |
class Header: | |
size = 0x7c | |
flags = 0 | |
height = 0 | |
width = 0 | |
pitchOrLinearSize = 0 | |
depth = 0 | |
mipmapCount = 0 | |
reserved1 = [0] * 11 | |
class DdsPixelFormat: | |
size = 0x20 | |
flags = 0 | |
fourCc = b"\0\0\0\0" | |
rgbBitCount = 0 | |
rBitMask = 0 | |
gBitMask = 0 | |
bBitMask = 0 | |
aBitMask = 0 | |
def __init__(self): | |
self.ddspf = self.DdsPixelFormat() | |
caps = 0 | |
caps2 = 0 | |
caps3 = 0 | |
caps4 = 0 | |
reserved2 = 0 | |
bdata = [] | |
def __init__(self): | |
self.header = self.Header() | |
def ImportDds(self, inputFile): | |
if type(inputFile) is str: | |
file = open(inputFile, "rb") | |
else: | |
file = inputFile | |
order = "<" | |
def readStruct(format, seek=None): | |
if seek != None: | |
file.seek(seek) | |
return struct.unpack(order + format, file.read(struct.calcsize(order + format))) | |
if readStruct("4s")[0] != self.Magic: | |
raise Exception("The file does not appear to be a valid DDS file.") | |
( | |
self.header.size, | |
self.header.flags, | |
self.header.height, | |
self.header.width, | |
self.header.pitchOrLinearSize, | |
self.header.depth, | |
self.header.mipmapCount | |
) = readStruct("7I") | |
self.header.reserved1 = readStruct("11I") | |
( | |
self.header.ddspf.size, | |
self.header.ddspf.flags, | |
self.header.ddspf.fourCc, | |
self.header.ddspf.rgbBitCount, | |
self.header.ddspf.rBitMask, | |
self.header.ddspf.gBitMask, | |
self.header.ddspf.bBitMask, | |
self.header.ddspf.aBitMask, | |
self.header.caps, | |
self.header.caps2, | |
self.header.caps3, | |
self.header.caps4, | |
self.header.reserved2 | |
) = readStruct("II4s10I") | |
file.seek(4 + self.header.size) | |
self.bdata = file.read() | |
def Save(self, outputFile=None): | |
import io | |
f = b"" | |
order = "<" | |
def write(format, *args): | |
return struct.pack(order + format, *args) | |
f += write("4s20I4s10I", | |
self.Magic, | |
self.header.size, | |
self.header.flags, | |
self.header.height, | |
self.header.width, | |
self.header.pitchOrLinearSize, | |
self.header.depth, | |
self.header.mipmapCount, | |
*self.header.reserved1, | |
self.header.ddspf.size, | |
self.header.ddspf.flags, | |
self.header.ddspf.fourCc, | |
self.header.ddspf.rgbBitCount, | |
self.header.ddspf.rBitMask, | |
self.header.ddspf.gBitMask, | |
self.header.ddspf.bBitMask, | |
self.header.ddspf.aBitMask, | |
self.header.caps, | |
self.header.caps2, | |
self.header.caps3, | |
self.header.caps4, | |
self.header.reserved2 | |
) | |
f += self.bdata | |
if type(outputFile) is str: | |
file = open(outputFile, "wb+") | |
else: | |
file = outputFile | |
if type(outputFile) is io.TextIOWrapper: | |
sys.stdout.buffer.write(f) | |
elif file: | |
file.write(f) | |
file.close() | |
else: | |
return f | |
def FromNutTexture(self, tex, disableMipmaps=False): | |
self.header.flags = self.Ddsd.Caps | self.Ddsd.Height | self.Ddsd.Width | self.Ddsd.Pixelformat | self.Ddsd.Linearsize | |
self.header.width = tex.Width | |
self.header.height = tex.Height | |
self.header.pitchOrLinearSize = tex.ImageSize | |
self.header.mipmapCount = len(tex.surfaces[0].mipmaps) | |
self.header.caps2 = tex.DdsCaps2 | |
isCubemap = self.header.caps2 & self.Ddscaps2.Cubemap == self.Ddscaps2.Cubemap | |
self.header.caps = self.Ddscaps.Texture | |
if not disableMipmaps: | |
self.header.flags |=self.Ddsd.Mipmapcount | |
if self.header.mipmapCount > 1: | |
self.header.caps |= self.Ddscaps.Complex | self.Ddscaps.Mipmap | |
if isCubemap: | |
self.header.caps |= self.Ddscaps.Complex | |
pix = tex.pixelInternalFormat | |
if pix == "dxt1": | |
self.header.ddspf.fourCc = b"DXT1" | |
self.header.ddspf.flags = self.Ddpf.Fourcc | |
elif pix == "dxt3": | |
self.header.ddspf.fourCc = b"DXT3" | |
self.header.ddspf.flags = self.Ddpf.Fourcc | |
elif pix == "dxt5": | |
self.header.ddspf.fourCc = b"DXT5" | |
self.header.ddspf.flags = self.Ddpf.Fourcc | |
elif pix == "rgtc1": | |
self.header.ddspf.fourCc = b"ATI1" | |
self.header.ddspf.flags = self.Ddpf.Fourcc | |
elif pix == "rgtc2": | |
self.header.ddspf.fourCc = b"ATI2" | |
self.header.ddspf.flags = self.Ddpf.Fourcc | |
elif pix == "rgba": | |
self.header.ddspf.fourCc = b"\0\0\0\0" | |
if tex.pixelFormat == "rgba": | |
self.header.ddspf.flags = self.Ddpf.Rgb | self.Ddpf.Alphapixels | |
self.header.ddspf.rgbBitCount = 0x8 * 4 | |
self.header.ddspf.rBitMask = 0xff | |
self.header.ddspf.gBitMask = 0xff << 8 | |
self.header.ddspf.bBitMask = 0xff << 16 | |
self.header.ddspf.aBitMask = 0xff << 24 | |
else: | |
raise Exception("Unknown pixel format {}".format(tex.pixelInternalFormat)) | |
d = [] | |
for b in tex.GetAllMipmaps(): | |
d.append(b) | |
if disableMipmaps: | |
break | |
self.bdata = b"".join(d) | |
def ToNutTexture(self): | |
tex = NutTexture() | |
tex.isDds = True | |
tex.HashId = struct.unpack(">I", b"HASH")[0] | |
tex.Height = self.header.height | |
tex.Width = self.header.width | |
surfaceCount = 1 | |
isCubemap = self.header.caps2 & self.Ddscaps2.Cubemap == self.Ddscaps2.Cubemap | |
if isCubemap: | |
if self.header.caps2 & self.Ddscaps2.CubemapAllfaces == self.Ddscaps2.CubemapAllfaces: | |
surfaceCount = 6 | |
else: | |
raise Exception("Unsupported cubemap face amount for texture. Six faces are required.") | |
isBlock = True | |
fourCc = self.header.ddspf.fourCc | |
if fourCc == b"\0\0\0\0": | |
#RGBA | |
isBlock = False | |
tex.pixelInternalFormat = "rgba" | |
tex.pixelFormat = "rgba" | |
elif fourCc == b"DXT1": | |
tex.pixelInternalFormat = "dxt1" | |
elif fourCc == b"DXT3": | |
tex.pixelInternalFormat = "dxt3" | |
elif fourCc == b"DXT5": | |
tex.pixelInternalFormat = "dxt5" | |
elif fourCc == b"ATI1" or fourCC == b"BC4U": | |
tex.pixelInternalFormat = "rgtc1" | |
elif fourCc == b"ATI2" or fourCC == b"BC5U": | |
tex.pixelInternalFormat = "rgtc2" | |
else: | |
raise Exception("Unsupported DDS format '{}'".format(fourCc.decode(errors="ignore"))) | |
formatSize = self.GetFormatSize(fourCc) | |
if self.header.mipmapCount == 0: | |
self.header.mipmapCount = 1 | |
off = 0 | |
for i in range(surfaceCount): | |
surface = TextureSurface() | |
w = self.header.width | |
h = self.header.height | |
for j in range(self.header.mipmapCount): | |
if (tex.pixelInternalFormat == "dxt3" or tex.pixelInternalFormat == "dxt5") and tex.Width != tex.Height and (w < 4 or h < 4): | |
break | |
s = w * h | |
if isBlock: | |
s = s * (formatSize / 0x10) | |
if s < formatSize: | |
s = formatSize | |
w //= 2 | |
h //= 2 | |
surface.mipmaps.append(self.bdata[off:off+int(s)]) | |
off += s | |
tex.surfaces.append(surface) | |
return tex | |
def ToBrtiTexture(self): | |
raise Exception("ToBrtiTexture is not implemented") | |
def ToBitmap(self): | |
from PIL import Image | |
if self.header.ddspf.fourCc == b"DXT1": | |
pixels = self.DecodeDxt1(self.bdata, self.header.width, self.header.height) | |
elif self.header.ddspf.fourCc == b"DXT3": | |
pixels = self.DecodeDxt3(self.bdata, self.header.width, self.header.height) | |
elif self.header.ddspf.fourCc == b"DXT5": | |
pixels = self.DecodeDxt5(self.bdata, self.header.width, self.header.height) | |
else: | |
raise Exception("Unknown DDS format '{}'".format(self.header.ddspf.fourCc.decode(errors="ignore"))) | |
bmp = Image.new("RGBA", (self.header.width, self.header.height)) | |
bmp.putdata(pixels) | |
return bmp | |
def DecodeDxt1(self): | |
raise Exception("DecodeDxt1 is not implemented") | |
def DecodeDxt3(self): | |
raise Exception("DecodeDxt3 is not implemented") | |
def DecodeDxt5(self, data, width, height): | |
pixels = [0] * (width * height) | |
x = 0 | |
y = 0 | |
p = 0 | |
while True: | |
# Alpha | |
block = [] | |
for i in range(8): | |
block.append(data[p + i]) | |
p += 8 | |
a1 = block[0] & 0xff | |
a2 = block[1] & 0xff | |
aWord1 = (block[2] & 0xff) | ((block[3] & 0xff) << 8) | ((block[4] & 0xff) << 16) | |
aWord2 = (block[5] & 0xff) | ((block[6] & 0xff) << 8) | ((block[7] & 0xff) << 16) | |
a = [] | |
for i in range(16): | |
if i < 8: | |
code = aWord1 & 0x7 | |
aWord1 >>= 3 | |
a.append(self.GetDxtaWord(code, a1, a2) & 0xff) | |
else: | |
code = aWord2 & 0x7 | |
aWord2 >>= 3 | |
a.append(self.GetDxtaWord(code, a1, a2) & 0xff) | |
# Color | |
block = [] | |
blockp = 0 | |
for i in range(8): | |
block.append(data[p + i]) | |
p += 8 | |
pal = [ | |
self.MakeColor565(block[0] & 0xff, block[1] & 0xff), | |
self.MakeColor565(block[2] & 0xff, block[3] & 0xff) | |
] | |
r = (2 * self.GetRed(pal[0]) + self.GetRed(pal[1])) // 3 | |
g = (2 * self.GetGreen(pal[0]) + self.GetGreen(pal[1])) // 3 | |
b = (2 * self.GetBlue(pal[0]) + self.GetBlue(pal[1])) // 3 | |
pal.append((0xff << 24) | (r << 16) | (g << 8) | b) | |
r = (2 * self.GetRed(pal[1]) + self.GetRed(pal[0])) // 3 | |
g = (2 * self.GetGreen(pal[1]) + self.GetGreen(pal[0])) // 3 | |
b = (2 * self.GetBlue(pal[1]) + self.GetBlue(pal[0])) // 3 | |
pal.append((0xff << 24) | (r << 16) | (g << 8) | b) | |
index = [] | |
for i in range(4): | |
by = block[i + 4] & 0xff | |
index.append(by & 0x03) | |
index.append((by & 0x0c) >> 2) | |
index.append((by & 0x30) >> 4) | |
index.append((by & 0xc0) >> 6) | |
# End | |
for h in range(4): | |
for w in range(4): | |
color = (a[w + h * 4] << 24) | (pal[index[w + h * 4]] & 0xffffff) | |
pixels[(w + x) + (h + y) * width] = ( | |
((color >> 16) & 0xff), | |
((color >> 8) & 0xff), | |
(color & 0xff), | |
self.GetAlpha(color) | |
) | |
# End positioning | |
x += 4 | |
if x >= width: | |
x = 0 | |
y += 4 | |
if y >= height: | |
break | |
return pixels | |
def GetDxtaWord(self, code, alpha0, alpha1): | |
if alpha0 > alpha1: | |
if code == 0: | |
return alpha0 | |
elif code == 1: | |
return alpha1 | |
elif code == 2: | |
return (6 * alpha0 + 1 * alpha1) // 7 | |
elif code == 3: | |
return (5 * alpha0 + 2 * alpha1) // 7 | |
elif code == 4: | |
return (4 * alpha0 + 3 * alpha1) // 7 | |
elif code == 5: | |
return (3 * alpha0 + 4 * alpha1) // 7 | |
elif code == 6: | |
return (2 * alpha0 + 5 * alpha1) // 7 | |
elif code == 7: | |
return (1 * alpha0 + 6 * alpha1) // 7 | |
else: | |
if code == 0: | |
return alpha0 | |
elif code == 1: | |
return alpha1 | |
elif code == 2: | |
return (4 * alpha0 + 1 * alpha1) // 5 | |
elif code == 3: | |
return (3 * alpha0 + 2 * alpha1) // 5 | |
elif code == 4: | |
return (2 * alpha0 + 3 * alpha1) // 5 | |
elif code == 5: | |
return (1 * alpha0 + 4 * alpha1) // 5 | |
elif code == 6: | |
return 0 | |
elif code == 7: | |
return 0xff | |
return 0 | |
def GetAlpha(self, c): | |
return c >> 24 & 0xff | |
def GetRed(self, c): | |
return c >> 16 & 0xff | |
def GetGreen(self, c): | |
return c >> 8 & 0xff | |
def GetBlue(self, c): | |
return c & 0xff | |
def MakeColor565(self, b1, b2): | |
bt = (b2 << 8) | b1 | |
a = 0xff | |
r = (bt >> 11) & 0x1f | |
g = (bt >> 5) & 0x3f | |
b = bt & 0x1f | |
r = (r << 3) | (r >> 2) | |
g = (g << 2) | (g >> 4) | |
b = (b << 3) | (b >> 2) | |
return (a << 24) | (r << 16) | (g << 8) | b | |
class NutTexture: | |
@property | |
def MipMapsPerSurface(self): | |
return len(self.surfaces[0].mipmaps) | |
id = 0 | |
@property | |
def HashId(self): | |
return self.id | |
@HashId.setter | |
def HashId(self, value): | |
self.Text = "{:X}".format(value) | |
self.id = value | |
isDds = False | |
Width = 0 | |
Height = 0 | |
pixelInternalFormat = "" | |
pixelFormat = "" | |
pixelType = "unsigned" | |
@property | |
def DdsCaps2(self): | |
if len(self.surfaces) == 6: | |
return Dds.Ddscaps2.CubemapAllfaces | |
else: | |
return 0 | |
def GetAllMipmaps(self): | |
mipmaps = [] | |
for surface in self.surfaces: | |
for mipmap in surface.mipmaps: | |
mipmaps.append(mipmap) | |
return mipmaps | |
def SwapChannelOrderUp(self): | |
for surface in self.surfaces: | |
for i in range(len(surface.mipmaps)): | |
mip = list(surface.mipmaps[i]) | |
for j in range(len(mip) // 4): | |
t = j * 4 | |
t1 = mip[t] | |
t2 = mip[t + 1] | |
t3 = mip[t + 2] | |
t4 = mip[t + 3] | |
mip[t] = t4 | |
mip[t + 1] = t3 | |
mip[t + 2] = t2 | |
mip[t + 3] = t1 | |
surface.mipmaps[i] = bytes(mip) | |
def SwapChannelOrderDown(self): | |
for surface in self.surfaces: | |
for i in range(len(surface.mipmaps)): | |
mip = list(surface.mipmaps[i]) | |
for j in range(len(mip) // 4): | |
t = j * 4 | |
t1 = mip[t + 3] | |
mip[t + 3] = mip[t + 2] | |
mip[t + 2] = mip[t + 1] | |
mip[t + 1] = mip[t] | |
mip[t] = t1 | |
surface.mipmaps[i] = bytes(mip) | |
def __init__(self): | |
self.ImageKey = "texture" | |
self.SelectedImageKey = "texture" | |
self.surfaces = [] | |
def __repr__(self): | |
return "NutTexture {:X}".format(self.HashId) | |
@property | |
def ImageSize(self): | |
pix = self.pixelInternalFormat | |
if pix == "rgtc1" or pix == "dxt1": | |
return self.Width * self.Height / 2 | |
elif pix == "rgtc2" or pix == "dxt3" or pix == "dxt5": | |
return self.Width * self.Height | |
elif pix == "rgba16": | |
return len(self.surfaces[0].mipmaps[0]) / 2 | |
else: | |
return len(self.surfaces[0].mipmaps[0]) | |
def getNutFormat(self): | |
pix = self.pixelInternalFormat | |
if pix == "dxt1": | |
return 0 | |
elif pix == "dxt3": | |
return 1 | |
elif pix == "dxt5": | |
return 2 | |
elif pix == "rgb16": | |
return 8 | |
elif pix == "rgba": | |
if self.pixelFormat == "rgba": | |
return 14 | |
elif self.pixelFormat == "abgr": | |
return 16 | |
else: | |
return 17 | |
elif pix == "rgtc1": | |
return 21 | |
elif pix == "rgtc2": | |
return 22 | |
else: | |
raise Exception("Unknown pixel format {}".format(pix)) | |
def setPixelFormatFromNutFormat(self, typet): | |
if typet == 0x0: | |
self.pixelInternalFormat = "dxt1" | |
elif typet == 0x1: | |
self.pixelInternalFormat = "dxt3" | |
elif typet == 0x2: | |
self.pixelInternalFormat = "dxt5" | |
elif typet == 0x8: | |
self.pixelInternalFormat = "rgb16" | |
self.pixelFormat = "rgb" | |
self.pixelType = "565" | |
elif typet == 0xc: | |
self.pixelInternalFormat = "rgba16" | |
self.pixelFormat = "rgba" | |
elif typet == 0xe: | |
self.pixelInternalFormat = "rgba" | |
self.pixelFormat = "rgba" | |
elif typet == 0x10: | |
self.pixelInternalFormat = "rgba" | |
self.pixelFormat = "abgr" | |
elif typet == 0x11: | |
self.pixelInternalFormat = "rgba" | |
self.pixelFormat = "rgba" | |
elif typet == 0x15: | |
self.pixelInternalFormat = "rgtc1" | |
elif typet == 0x16: | |
self.pixelInternalFormat = "rgtc2" | |
else: | |
raise Exception("Unknown nut texture format 0x{:x}".format(typet)) | |
class TextureSurface: | |
def __init__(self): | |
self.mipmaps = [] | |
self.cubemapFace = 0 | |
class NUT: | |
Version = 0x0200 | |
Endian = "Big" | |
def __init__(self): | |
self.Text = "model.nut" | |
self.ImageKey = "nut" | |
self.SelectedImageKey = "nut" | |
self.Nodes = [] | |
self.glTexByHashId = {} | |
def getTextureByID(self, hash): | |
for t in self.Nodes: | |
if t.HashId == hash: | |
return t | |
return None | |
def ConvertToDdsNut(self): | |
for i in range(len(self.Nodes)): | |
originalTexture = self.Nodes[i] | |
dds = Dds() | |
dds.FromNutTexture(originalTexture) | |
ddsTexture = dds.ToNutTexture() | |
ddsTexture.HashId = originalTexture.HashId | |
if self.regenerateMipMaps: | |
self.RegenerateMipmapsFromTexture2D(ddsTexture) | |
self.Nodes[i] = ddsTexture | |
def Save(self, outputFile=None): | |
import io | |
if type(outputFile) is str: | |
file = open(outputFile, "wb+") | |
else: | |
file = outputFile | |
nutContents = self.Rebuild() | |
if type(outputFile) is io.TextIOWrapper: | |
sys.stdout.buffer.write(nutContents) | |
elif file: | |
file.write(nutContents) | |
file.close() | |
else: | |
return nutContents | |
def Rebuild(self): | |
o = b"" | |
data = b"" | |
order = ">" | |
def write(format, *args): | |
return struct.pack(order + format, *args) | |
def align(file, pos): | |
initial = len(file) | |
output = initial | |
while output % pos != 0: | |
output += 1 | |
return b"\0" * (initial - output) | |
if self.Endian == "big": | |
o += b"NTP3" | |
else: | |
o += b"NTWD" | |
if self.Version > 0x200: | |
self.Version = 0x200 | |
o += write("H", self.Version) | |
if self.Endian == "big": | |
order = ">" | |
else: | |
order = "<" | |
o += write("H", len(self.Nodes)) | |
o += b"\0" * 8 | |
headerLength = 0 | |
for texture in self.Nodes: | |
surfaceCount = len(texture.surfaces) | |
isCubemap = surfaceCount == 6 | |
if surfaceCount < 1 or surfaceCount > 6: | |
raise Exception("Unsupported surface amount {} for texture with hash 0x{:x}. 1 to 6 faces are required.".format(surfaceCount, texture.HashId)) | |
elif surfaceCount > 1 and surfaceCount < 6: | |
raise Exception("Unsupported cubemap face amount for texture with hash 0x{:x}. Six faces are required.".format(texture.HashId)) | |
mipmapCount = len(texture.surfaces[0].mipmaps) | |
headerSize = 0x50 | |
if isCubemap: | |
headerSize += 0x10 | |
if mipmapCount > 1: | |
headerSize += mipmapCount * 4 | |
while headerSize % 0x10 != 0: | |
headerSize += 1 | |
headerLength += headerSize | |
for texture in self.Nodes: | |
surfaceCount = len(texture.surfaces) | |
isCubemap = surfaceCount == 6 | |
mipmapCount = len(texture.surfaces[0].mipmaps) | |
dataSize = 0 | |
for mip in texture.GetAllMipmaps(): | |
dataSize += len(mip) | |
while dataSize % 0x10 != 0: | |
dataSize += 1 | |
headerSize = 0x50 | |
if isCubemap: | |
headerSize += 0x10 | |
if mipmapCount > 1: | |
headerSize += mipmapCount * 4 | |
while headerSize % 0x10 != 0: | |
headerSize += 1 | |
o += write("IIIHHbbbbhhiIIiii", | |
dataSize + headerSize, | |
0, | |
dataSize, | |
headerSize, | |
0, | |
0, | |
mipmapCount, | |
0, | |
texture.getNutFormat(), | |
texture.Width, | |
texture.Height, | |
0, | |
texture.DdsCaps2, | |
0 if self.Version < 0x200 else headerLength + len(data), | |
0, | |
0, | |
0 | |
) | |
headerLength -= headerSize | |
if isCubemap: | |
o += write("iiii", | |
len(texture.surfaces[0].mipmaps[0]), | |
len(texture.surfaces[0].mipmaps[0]), | |
0, | |
0 | |
) | |
if texture.getNutFormat() == 0xe or texture.getNutFormat() == 0x11: | |
texture.SwapChannelOrderDown() | |
for surfaceLevel in range(surfaceCount): | |
for mipLevel in range(mipmapCount): | |
ds = len(data) | |
data += texture.surfaces[surfaceLevel].mipmaps[mipLevel] | |
data += align(data, 0x10) | |
if mipmapCount > 1 and surfaceLevel == 0: | |
o += write("i", len(data) - ds) | |
o += align(o, 0x10) | |
if texture.getNutFormat() == 0xe or texture.getNutFormat() == 0x11: | |
texture.SwapChannelOrderUp() | |
o += b"eXt\0" | |
o += write("iii", 0x20, 0x10, 0x00) | |
o += b"GIDX" | |
o += write("iii", 0x10, texture.HashId, 0x00) | |
if self.Version < 0x0200: | |
o += data | |
data = b"" | |
if self.Version >= 0x0200: | |
o += data | |
return o | |
def Read(self, inputFile): | |
if type(inputFile) is str: | |
file = open(inputFile, "rb") | |
else: | |
file = inputFile | |
inputFileName = os.path.split(file.name)[1] | |
size = os.fstat(file.fileno()).st_size | |
self.Endian = "big" | |
order = ">" | |
def readStruct(format, seek=None): | |
if seek != None: | |
file.seek(seek) | |
return struct.unpack(order + format, file.read(struct.calcsize(order + format))) | |
def skip(pos): | |
file.seek(pos, os.SEEK_CUR) | |
def align(pos): | |
output = file.tell() | |
while output % pos != 0: | |
output += 1 | |
file.seek(output) | |
magic,version = readStruct("4sH") | |
self.Version = version | |
if magic == b"NTP3": | |
count = readStruct("H", 0x6)[0] | |
headerPtr = 0x10 | |
for i in range(count): | |
file.seek(headerPtr) | |
tex = NutTexture() | |
tex.isDds = True | |
tex.pixelInternalFormat = "rgba32" | |
( | |
totalSize, | |
dataSize, | |
headerSize, | |
mipmapCount, | |
nutFormat, | |
tex.Width, | |
tex.Height, | |
caps2 | |
) = readStruct("i4xiH3xbxbHH4xI") | |
tex.setPixelFormatFromNutFormat(nutFormat) | |
isCubemap = False | |
surfaceCount = 1 | |
if caps2 & Dds.Ddscaps2.Cubemap == Dds.Ddscaps2.Cubemap: | |
if caps2 & Dds.Ddscaps2.CubemapAllfaces == Dds.Ddscaps2.CubemapAllfaces: | |
isCubemap = True | |
surfaceCount = 6 | |
else: | |
raise Exception("Unsupported cubemap face amount for texture {i} with hash 0x{:x}. Six faces are required.".format(i, tex.HashId)) | |
dataOffset = 0 | |
if self.Version < 0x0200: | |
dataOffset = headerPtr + headerSize | |
skip(0x4) | |
elif self.Version >= 0x0200: | |
dataOffset = readStruct("i")[0] + headerPtr | |
skip(0xc) | |
cmapSize1 = 0 | |
cmapSize2 = 0 | |
if isCubemap: | |
cmapSize1,cmapSize2 = readStruct("ii") | |
skip(0x8) | |
mipSizes = [0] * mipmapCount | |
if mipmapCount == 1: | |
if isCubemap: | |
mipSizes[0] = cmapSize1 | |
else: | |
mipSizes[0] = dataSize | |
else: | |
for mipLevel in range(mipmapCount): | |
mipSizes[mipLevel] = readStruct("i") | |
align(0x10) | |
skip(0x18) | |
tex.HashId = readStruct("i")[0] | |
skip(0x4) | |
for surfaceLevel in range(surfaceCount): | |
surface = TextureSurface() | |
file.seek(dataOffset) | |
for mipLevel in range(mipmapCount): | |
texArray = file.read(mipSizes[mipLevel]) | |
surface.mipmaps.append(texArray) | |
tex.surfaces.append(surface) | |
if tex.getNutFormat() == 0xe or tex.getNutFormat() == 0x11: | |
tex.SwapChannelOrderUp() | |
if self.Version < 0x0200: | |
headerPtr += totalSize | |
else: | |
headerPtr += headerSize | |
self.Nodes.append(tex) | |
elif magic == b"NTWU": | |
raise Exception("NTWU is not implemented") | |
elif magic == b"NTWD": | |
self.Endian = "little" | |
order = "<" | |
raise Exception("NTWD is not implemented") | |
else: | |
raise Exception("Unknown header: '{}'".format(magic.decode(errors="ignore"))) | |
def RefreshGlTexturesByHashId(self): | |
self.glTexByHashId = {} | |
for tex in self.Nodes: | |
if tex.HashId not in self.glTexByHashId: | |
if len(tex.surfaces) == 6: | |
self.glTexByHashId[tex.HashId] = self.CreateTextureCubeMap(tex) | |
else: | |
self.glTexByHashId[tex.HashId] = self.CreateTexture2D(tex) | |
def RegenerateMipmapsFromTexture2D(self, tex): | |
raise Exception("RegenerateMipmapsFromTexture2D is not implemented") | |
def texIdUsed(self, texId): | |
raise Exception("texIdUsed is not implemented") | |
def ChangeTextureIds(self, newTexId): | |
if TexIdDuplicate4thByte(): | |
raise Exception("Duplicate Texture ID - The first six digits should be the same for all textures to prevent duplicate IDs after changing the Tex ID.") | |
for tex in self.Nodes: | |
originalTexture = self.glTexByHashId[tex.HashId] | |
del self.glTexByHashId[tex.HashId] | |
tex.HashId = tex.HashId & 0xff | |
first3Bytes = newTexId & 0xffffff00 | |
tex.HashId = tex.HashId | first3Bytes | |
self.glTexByHashId[tex.HashId] = originalTexture | |
def TexIdDuplicate4thByte(self): | |
previous4thBytes = [] | |
for tex in self.Nodes: | |
fourthByte = tex.HashId & 0xff | |
if fourthByte not in previous4thBytes: | |
previous4thBytes.append(fourthByte) | |
else: | |
return True | |
return False | |
def __repr__(self): | |
return "NUT" | |
def CreateTexture2D(self, nutTexture, surfaceIndex=0): | |
compressedFormatWithMipMaps = TextureFormatTools.IsCompressed(nutTexture.pixelInternalFormat) | |
mipmaps = nutTexture.surfaces[surfaceIndex].mipmaps | |
if compressedFormatWithMipMaps: | |
if len(nutTexture.surfaces[0].mipmaps) > 1 and nutTexture.isDds and nutTexture.Width == nutTexture.Height: | |
raise Exception("CreateTexture2D is not implemented") | |
else: | |
raise Exception("CreateTexture2D is not implemented") | |
else: | |
raise Exception("CreateTexture2D is not implemented") | |
def ContainsGtxTextures(self): | |
for texture in self.Nodes: | |
if not texture.isDds: | |
return True | |
return False | |
def CreateTextureCubeMap(self, t): | |
if TextureFormatTools.IsCompressed(t.pixelInternalFormat): | |
raise Exception("CreateTextureCubeMap is not implemented") | |
else: | |
raise Exception("CreateTextureCubeMap is not implemented") | |
class TextureFormatTools: | |
def IsCompressed(format): | |
return format in ("dxt1", "dxt3", "dxt5", "rgtc1", "rgtc2") | |
def existingFile(arg): | |
if os.path.isfile(arg): | |
return arg | |
else: | |
raise argparse.ArgumentTypeError("File not found: '{}'".format(arg)) | |
if __name__ == "__main__": | |
import argparse, sys | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
"input", | |
help="Path to the NUT/DDS texture file", | |
type=existingFile | |
) | |
parser.add_argument( | |
"-o", | |
metavar="output", | |
help="Converted output file (NUT/DDS/PNG)" | |
) | |
if len(sys.argv) == 1: | |
parser.print_help() | |
else: | |
args = parser.parse_args() | |
inputFile = args.input | |
inPath,inName = os.path.split(inputFile) | |
inName,inExt = os.path.splitext(inName) | |
outputFile = args.o | |
outputStdout = outputFile == "-" | |
if inExt == ".nut": | |
nut = NUT() | |
nut.Read(inputFile) | |
if outputStdout: | |
outExt = ".png" | |
elif not outputFile: | |
outputFile = "{}.png".format(os.path.join(inPath, inName)) | |
elif inExt == ".dds": | |
dds = Dds() | |
dds.ImportDds(inputFile) | |
if outputStdout: | |
outExt = ".nut" | |
elif not outputFile: | |
outputFile = "{}.nut".format(os.path.join(inPath, inName)) | |
if not outputStdout: | |
outPath,outName = os.path.split(outputFile) | |
outName,outExt = os.path.splitext(outName) | |
if outExt == ".dds" or outExt == ".png": | |
if inExt == ".nut": | |
nodeLength = len(nut.Nodes) | |
for i in range(nodeLength): | |
originalTexture = nut.Nodes[i] | |
dds = Dds() | |
dds.FromNutTexture(originalTexture, disableMipmaps=False) | |
if nodeLength == 1 or outputStdout: | |
savePath = outputFile | |
else: | |
savePath = "{}_{i}{}".format(*os.path.splitext(outputFile), i=i) | |
if outExt == ".png": | |
try: | |
dds.ToBitmap().save(savePath) | |
except Exception: | |
if nodeLength != 1 and not outputStdout: | |
savePath = "{}_{i}{}".format(os.path.splitext(outputFile)[0], ".dds", i=i) | |
dds.Save(savePath) | |
elif outExt == ".dds": | |
dds.Save(savePath) | |
elif inExt == ".dds": | |
if outExt == ".png": | |
try: | |
dds.ToBitmap().save(outputFile) | |
except Exception: | |
if type(outputFile) is str: | |
outputFile = "{}.dds".format(os.path.join(inPath, inName)) | |
dds.Save(outputFile) | |
elif outExt == ".dds": | |
dds.Save(outputFile) | |
elif outExt == ".nut": | |
if inExt == ".dds": | |
nut = NUT() | |
ddsTexture = dds.ToNutTexture() | |
nut.Nodes = [ddsTexture] | |
nut.Endian = "big" | |
nut.Save(outputFile) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment