Skip to content

Instantly share code, notes, and snippets.

@KatieFrogs
Last active July 29, 2021 23:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KatieFrogs/b8c1540165a56be1133bacd01c883056 to your computer and use it in GitHub Desktop.
Save KatieFrogs/b8c1540165a56be1133bacd01c883056 to your computer and use it in GitHub Desktop.
.nut texture extractor and encoder, based on Smash Forge code
#!/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