Added texture handling tools.

Updated README.md to document geo_list.py options '-s' (show scales) and '-t' (show triangle count).
master
TigerKat 5 years ago
parent 7b316f2386
commit 4eb13819c4

@ -19,6 +19,10 @@ The Blender add-on allows you to export .geo files.
2. From the menu select File->Export->"City of Heroes (Feet) (.geo)" (or "City of Heroes (Meters) (.geo)" if your meshes have been scaled in meters).
3. Browse to the file you want to create and click "Export GEO".
## Notes
Models are exported with the name of the mesh they come from, so having the meshes named correctly is recommended.
# Tools
These command line tools allow inspection and modification of .geo files.
@ -60,11 +64,51 @@ Unless noted otherwise, model names and regular expressions are case sensitive.
A command line tool for list the model name inside of 1 or more .geo files.
geo_list.py <file.geo> [<file.geo> ...]
geo_list.py [<output_options>] <file.geo> [<file.geo> ...]
The output format is:
<geo_name> : <model_name>
<output_options> adds additional items that can be outputted:
-t Prints the triangle count of the model. Added output format: ": <tri_count>"
-s Prints the scale of the model. Added output format: ": <sacle_x>, <sacle_y>, <sacle_z>"
## texture.py
Contains the Texture class, which represents .texture files. Can be run to test the reading and writing functionality.
Note: Requires Wand to be installed: http://wand-py.org/
texture.py <infile.texture> [<outfile.texture>]
If only an input .texture is specified, it will read the input file and dump the contents to the console.
If an output .texture is specified, it will read the input.texture file, and write the contents to the output .texture file.
## texture_extract.py
A command line tool for extracting an image from a .texture file.
Note: Requires Wand to be installed: http://wand-py.org/
texture_extract.py <infile.texture> [<outfile>]
Reads the file <infile.texture> and writes the contained image to an output file.
If <outfile> is specified it will write to this file. If no <outfile> is specified it will use the file name contained inside the .texture file, recreating the directory structure.
## dds2tex.py
A command line tool for converting a .dds file into a .texture .
Note: Requires Wand to be installed: http://wand-py.org/
texture_extract.py <infile.dds> [<outfile.texture> [<texture_filename>]]
<infile.dds> is the input file.
<outfile.texture> is the output file. If not specified it will use the same file name as the input except with a .texture extension. If <outfile.texture> is a directory it will use <texture_filename> with a .texture extension as the file name and directory, but placed under the directory <outfile.texture> .
<texture_filename> the filename to store inside the .texture file. If this not specified, it defaults to <infile.dds> . If <infile.dds> contains "texture_library" it will discard everything before "texture_library" use the remainder.
## Known Issues
- Not all structures are handled (reflection quads)
- Not all structures are regenerated when writing a .geo file. (Reductions)

@ -0,0 +1,63 @@
import os.path
import os
import sys
from texture import Texture
#todo:
if __name__ == "__main__":
if len(sys.argv) <= 1 or len(sys.argv) > 4:
print("Usage:")
print(" %s <file_in> [<file_out> [<texture_name>]]" % (sys.argv[0], ))
print("Puts a .dds file inside a .texture file.")
print("<file_out> will default to the same name but with a .texture extension.")
print("<texture_name> will defaul to <file_in> starting 'texture_library'.")
exit(0)
filename_in = sys.argv[1].replace("\\", "/")
fh = open(filename_in, "rb")
image_data = fh.read()
fh.close()
#print("filename_in: '%s'" % (filename_in, ))
filedir_in = os.path.dirname(filename_in)
if "texture_library" in filedir_in.split("/"):
context_name = filename_in[filedir_in.find("texture_library"):]
else:
#todo: warning about not being in the texture_library heirachy?
context_name = filename_in
#print("context_name: '%s'" % (context_name, ))
if len(sys.argv) >= 4:
context_name = sys.argv[3].replace("\\", "/")
#todo: check input and output extensions match
#print("context_name: '%s'" % (context_name, ))
if len(sys.argv) >= 3:
filepath_out = sys.argv[2].replace("\\", "/")
if filepath_out[-1] == "/" or (os.path.exists(filepath_out) and os.path.isdir(filepath_out)):
if filepath_out[-1] == "/":
if not os.path.exists(filepath_out):
os.makedirs(filepath_out)
filename_out = os.path.join(filepath_out, os.path.splitext(context_name)[0] + ".texture")
else:
filename_out = filepath_out
else:
filename_out = os.path.splitext(context_name)[0] + ".texture"
filedir_out = os.path.dirname(filename_out)
if not os.path.exists(filedir_out):
os.makedirs(filedir_out)
#print("filename_out: '%s'" % (filename_out, ))
texture = Texture()
texture.filename = bytes(context_name, "utf-8")
texture.setImageData(image_data)
data = texture.saveToData()
texture.dump()
fh_out = open(filename_out, "wb")
fh_out.write(data)
fh_out.close()

@ -0,0 +1,353 @@
import sys
import struct
import math
#from PIL import Image
from wand.image import Image
import io
#TextureFileHeader
#offset size description
#0 4 i32: header_size
#4 4 i32: file_size
#8 4 i32: width
#12 4 i32: height
#16 4 u32(TexOptFlags): flags
#20 4 f32[2]: fade
#28 1 u8: alpha
#29 3 u8[3]: version "TEX" or "TX2"
#32 variable char[] (ASCIIZ): file path of origianl texture
#? 16 TextureFileMipHeader: mipmap_header (TX2 only) (optional, assumed present if header_size is larger than previous data.)
#? ? MipMapData: All mipmap data that's 8x8 or smaller. (TX2 only) (present if mipmap_header is present)
#TextureFileMipHeader
#offset size description
#0 4 i32: structsize
#4 4 i32: width
#8 4 i32: height
#12 4 i32: format
#16
#MipMapData
#This contains a copy of the mipmap of size mipmap_header.width by mipmap_header.height and all thos smaller than it. The format should match that of the main texture.
#TexReadInfo
#0 4 ptr: data?
#4 4 i32: mip_count
#8 4 i32: format
#12 4 i32: mip_count
#16 4 i32: width
#20 4 i32: height
#24 4 i32: size
#28
#TexReadInfo
GL_FORMATS = {
"GL_COMPRESSED_RGB_S3TC_DXT1_EXT": 0x83F0,
"GL_COMPRESSED_RGBA_S3TC_DXT1_EXT": 0x83F1,
"GL_COMPRESSED_RGBA_S3TC_DXT3_EXT": 0x83F2,
"GL_COMPRESSED_RGBA_S3TC_DXT5_EXT": 0x83F3,
}
GL_FORMATS_LOOKUP = {}
for k, v in GL_FORMATS.items():
GL_FORMATS_LOOKUP[v] = k
TEX_OPT_FLAGS = [
"TEX_ALPHA", # bit 0
"TEX_RGB8", # bit 1
"TEX_COMP4", # bit 2
"TEX_COMP8", # bit 3
"TEX_UNKNOWN_BIT_4", # bit 4
"TEX_TGA", # bit 5
"TEX_DDS", # bit 6
"TEX_UNKNOWN_BIT_7", # bit 7
"TEX_UNKNOWN_BIT_8", # bit 8
"TEX_CUBEMAPFACE", # bit 9
"TEX_REPLACEABLE", # bit 10
"TEX_BUMPMAP", # bit 11
"TEX_UNKNOWN_BIT_12", # bit 12
"TEX_JPEG", # bit 13
]
while len(TEX_OPT_FLAGS) < 32:
TEX_OPT_FLAGS.append("TEX_UNKNOWN_BIT_%d" % len(TEX_OPT_FLAGS))
TEX_OPT_FLAGS_LOOKUP = {}
for i in range(32):
s = TEX_OPT_FLAGS[i]
TEX_OPT_FLAGS_LOOKUP[s] = i
TEX_OPT_FLAGS_LOOKUP[s.lower()] = i
if s.startswith("TEX_"):
TEX_OPT_FLAGS_LOOKUP[s[4:]] = i
TEX_OPT_FLAGS_LOOKUP[s[4:].lower()] = i
def decodeTexOptFlags(bits):
v = []
for i in range(32):
if bits & (1 << i):
v.append(TEX_OPT_FLAGS[i])
return v
def encodeTexOptFlags(flags):
v = 0
for f in flags:
v = v | (1 << TEX_OPT_FLAGS_LOOKUP[f.upper()])
return v
class Texture:
def __init__(self):
self.width = 0
self.height = 0
self.tex_opt_flags = []
self.tex_opt_flags_data = 0
self.fade = [0, 0]
self.alpha = None
self.version = 2
self.version_string = b"TX2"
self.image_data = None
self.filename = None
self.mip_present = False
self.mip_struct_size = 0
self.mip_width = 0
self.mip_height = 0
self.mip_format = 0
self.mip_data = None
#self.header_size = 0
#self.file_size = 0
self.image = None
def extractRawMipMapData(self, cell_w, cell_h, cell_bytes):
#Computes the number of cells of data
cell_count = 0
w = self.width
h = self.height
#Halve width and height until both are 8 pixels or less.
while w > 8 or h > 8:
w = int(math.ceil(w / 2.0))
h = int(math.ceil(h / 2.0))
self.mip_width = w
self.mip_height = h
#Determine the number of cells used by this cell and all those smaller than it.
while True:
#What is the width and height of this mip map, in cells?
#Number has to be rounded up.
cw = int(math.ceil(w / float(cell_w)))
ch = int(math.ceil(h / float(cell_h)))
#Accumulate cells.
cell_count += cw * ch
#Stop if this was 1 by 1 pixel.
if w <= 1 and h <= 1:
break
#Halve width and height for next smaller mip map.
w = int(math.ceil(w / 2.0))
h = int(math.ceil(h / 2.0))
#Return the data for the last cell_count cells in the source file.
return self.image_data[-(cell_count * cell_bytes) : ]
def extractMipMapData(self):
#cell_w, cell_h = 1, 1
#if self.image.alpha_channel:
# cell_bytes = 4
#else:
# cell_bytes = 3
if self.image.format == 'DDS':
#todo: a real lazy kludge to find the format of the image.
img_header = self.image_data[0:512]
if b"DXT1" in img_header:
if self.alpha:
self.mip_format = GL_FORMATS["GL_COMPRESSED_RGBA_S3TC_DXT1_EXT"]
else:
self.mip_format = GL_FORMATS["GL_COMPRESSED_RGB_S3TC_DXT1_EXT"]
cell_bytes = 8
elif b"DXT3" in img_header:
self.mip_format = GL_FORMATS["GL_COMPRESSED_RGBA_S3TC_DXT3_EXT"]
cell_bytes = 16
elif b"DXT5" in img_header:
self.mip_format = GL_FORMATS["GL_COMPRESSED_RGBA_S3TC_DXT5_EXT"]
cell_bytes = 16
else:
self.mip_present = False
return
cell_w, cell_h = 4, 4
else:
self.mip_present = False
return
self.mip_present = True
self.mip_data = self.extractRawMipMapData(cell_w, cell_h, cell_bytes)
def extractImageInfo(self):
self.width = self.image.width
self.height = self.image.height
def setImageData(self, image_data):
self.image_data = image_data
self.image = Image(file = io.BytesIO(self.image_data))
self.width = self.image.width
self.height = self.image.height
if self.image.alpha_channel:
image_raw = self.image.make_blob(format = "RGBA")
alpha_raw = image_raw[3::4]
count = 0
#todo: there's probably a better way than this
for i in range(247, 256):
b = bytes((i, ))
count += alpha_raw.count(b)
self.alpha = count < len(alpha_raw)
else:
self.alpha = False
def loadFromFile(self, fh):
self.loadFromData(fh.read())
def loadFromData(self, data):
main_header_size = struct.calcsize("<iiiiIffB3s")
offset = 0
(
self.header_size,
self.file_size,
self.width,
self.height,
self.tex_opt_flags_data,
self.fade[0],
self.fade[1],
self.alpha,
self.version_string,
) = struct.unpack("<iiiiIffB3s", data[offset : offset + main_header_size])
if self.version_string == b"TEX":
self.version = 1
elif self.version_string == b"TX2":
self.version = 2
else:
self.version = 0
#todo: raise errror
offset += main_header_size
filename_end = data.find(b"\x00", offset)
if filename_end < 0:
filename_end = len(data)
self.filename = data[offset : filename_end]
offset = filename_end + 1
self.mip_struct_size = 0
self.mip_width = 0
self.mip_height = 0
self.mip_format = 0
self.tex_opt_flags = decodeTexOptFlags(self.tex_opt_flags_data)
if self.version == 2:
if self.header_size > offset:
self.mip_present = True
texture_file_mip_header_size = struct.calcsize("<iiii")
(
self.mip_struct_size,
self.mip_width,
self.mip_height,
self.mip_format,
) = struct.unpack("<iiii", data[offset : offset + texture_file_mip_header_size])
offset += self.mip_struct_size #texture_file_mip_header_size
self.mip_data = data[offset : self.header_size]
#mip_size = struct.calcsize("<iiii")
#while offset < len(header_size):
# offset += mip_size
else:
self.mip_present = False
else:
self.mip_data = b""
self.image_data = data[self.header_size:]
#print("%s" % (repr(self.image_data[0:16]), ))
self.image = Image(file = io.BytesIO(self.image_data))
self.extractImageInfo()
def saveToFile(self, fh):
fh.write(self.saveToData)
def saveToData(self):
data = b""
#Set version and version string.
self.version = 2
if self.version == 1:
self.version_string = b"TEX"
elif self.version == 2:
self.version_string = b"TX2"
else:
self.version_string = b"TX2"
#todo: raise error
#extract mipmap data
self.extractMipMapData()
#encode mipmap data and header
if self.version >= 2:
if self.mip_present:
self.mip_struct_size = struct.calcsize("<iiii")
mipmap_data = struct.pack("<iiii",
self.mip_struct_size,
self.mip_width,
self.mip_height,
self.mip_format) + self.mip_data
else:
mipmap_data = b""
#encode filename
filename_data = self.filename + b"\x00"
#compute sizes
main_header_size = struct.calcsize("<iiiiIffB3s")
self.header_size = main_header_size + len(filename_data) + len(mipmap_data)
self.file_size = len(self.image_data)
#encode flags
self.tex_opt_flags_data = encodeTexOptFlags(self.tex_opt_flags)
#encode header
header_data = struct.pack("<iiiiIffB3s",
self.header_size,
self.file_size,
self.width,
self.height,
self.tex_opt_flags_data,
self.fade[0],
self.fade[1],
self.alpha,
self.version_string)
#assemble data
data = header_data + filename_data + mipmap_data + self.image_data
return data
def dump(self):
print("header_size: %d" % (self.header_size, ))
print("image_file_size: %d" % (self.file_size, ))
print("width x height: %d x %d" % (self.width, self.height))
print("tex_opt_flags_data: 0x%8.8x" % (self.tex_opt_flags_data, ))
print("tex_opt_flags: %s" % (self.tex_opt_flags, ))
print("fade: %s" % (self.fade, ))
print("alpha: %s" % (self.alpha, ))
print("version: %s (%s)" % (self.version, self.version_string))
print("filename: %s" % (self.filename, ))
print("mip_struct_size: %d" % (self.mip_struct_size, ))
print("mip_width: %d" % (self.mip_width, ))
print("mip_height: %d" % (self.mip_height, ))
print("mip_format: 0x%8.8x (%s)" % (self.mip_format, GL_FORMATS_LOOKUP[self.mip_format]))
print("mip_data: len: %d" % (self.mip_data and len(self.mip_data) or 0, ))
print("mip_data: %s" % (repr(self.mip_data), ))
print("image_data: len: %d" % (self.image_data and len(self.image_data) or 0, ))
print("image_data: %s" % (repr(self.image_data[0:512]), ))
print("image.format: %s" % (self.image.format, ))
#print("image.compression: %s" % (self.image.compression, ))
image_raw = self.image.make_blob(format = "RGBA")
alpha_raw = image_raw[3::4]
#print("image.blob: %s" % (image_raw, ))
#print("image.blob.alpha: %s" % (alpha_raw, ))
#print("image.channels: %s" % (self.image.channels, ))
if __name__ == "__main__":
if len(sys.argv) <= 1 or len(sys.argv) > 3:
print("Usage:")
print(" %s <file_in> [<file_out>]" % (sys.argv[0], ))
print("Test loads a .texture file, dumps its content, and optionally writes its content out.")
exit(0)
fh = open(sys.argv[1], "rb")
texture = Texture()
texture.loadFromFile(fh)
fh.close()
#print(sys.argv)
if len(sys.argv) <= 2:
fho = None
else:
fho = open(sys.argv[2], "wb")
if fho is not None:
data = texture.saveToData()
#texture.dump()
fho.write(data)
else:
texture.dump()
#print("%s" % [texture.header_data])

@ -0,0 +1,33 @@
import sys
import os
import os.path
from texture import Texture
if __name__ == "__main__":
if len(sys.argv) <= 1 or len(sys.argv) > 3:
print("Usage:")
print(" %s <file_in> [<file_out>]" % (sys.argv[0], ))
print("Reads texture <file_in> and writes the contained image to a file. If <file_out> is given it will write the output to this, otherwise will extract the full contained path into the current directory.")
exit(0)
fh = open(sys.argv[1], "rb")
tex = Texture()
tex.loadFromFile(fh)
fh.close()
#print(sys.argv)
if len(sys.argv) <= 2:
fno = tex.filename
else:
fno = sys.argv[2]
#create paths
fno_path = os.path.dirname(fno)
if not(os.path.exists(fno_path)):
os.makedirs(fno_path)
#write output file
fho = open(fno, "wb")
fho.write(tex.image_data)
fho.close()
print("Wrote: '%s'" % (fno, ))
Loading…
Cancel
Save