#Bullet Witch cpr extraction and dds conversion from inc_noesis import * import time #registerNoesisTypes is called by Noesis to allow the script to register formats. #Do not implement this function in script files unless you want them to be dedicated format modules! def registerNoesisTypes(): handle = noesis.register("Bullet Witch CPR Model/Textures", ".cpr;.czr") noesis.setHandlerTypeCheck(handle, cprCheckType) noesis.setHandlerLoadModel(handle, cprLoadModel) #noesis.logPopup() return 1 CPR_HEADER_ID = 0x31525043 #"CPR1" CPR_ENTRY_SIZE = 48 CPR_TYPE_DDS = 0x20534444 #"DDS " CPR_TYPE_RESU = 0x52455355 #"RESU" CPR_TYPE_BVXD = 0x42565844 #"BVXD" CPR_TYPE_BIXD = 0x42495844 #"BIXD" CPR_MDLHDR_ID = 0x56465832 #"VFX2" CPR_CHUNKHDR_ID = 0x434E4B48 #"CNKH" CPR_TEXLISTHDR_ID = 0x544C5354 #"TLST" CPR_SKELHDR_ID = 0x5F424C42 #"_BLB" CZR_COMPRESSION_MARKER = 0x484C5A34 #"HLZ4" class CprEntry: def __init__(self, data, nameStream): self.entryData = data entryInfo = noeUnpackFrom(">iii", data, 0) self.type = entryInfo[0] nameStream.seek(entryInfo[1], NOESEEK_ABS) #offset to name string self.name = nameStream.readString() nameStream.seek(entryInfo[2], NOESEEK_ABS) #offset to directory name string self.dirName = nameStream.readString().replace(".", "/") entryInfo = noeUnpackFrom(">ii", data, 24) self.infoOfs = entryInfo[0] self.infoSize = entryInfo[1] entryInfo = noeUnpackFrom(">ii", data, 32) self.dataOfs = entryInfo[0] self.dataSize = entryInfo[1] def __repr__(self): return self.name + "(" + repr(self.dataOfs) + "," + repr(self.dataSize) + ")" class CprIDX: def __init__(self, bufferIndex): self.bufferIndex = bufferIndex class CprVTX: def __init__(self, rawBytes, isShape): entryInfo = noeUnpackFrom(">iiii", rawBytes, 0) self.bufferIndex = entryInfo[1] self.vertSize = entryInfo[3] self.isShape = isShape class CprTexRef: def __init__(self, texType, texIdx, texIdxB): self.texType = texType self.texIdx = texIdx self.texIdxB = texIdxB def __repr__(self): return "(tref:" + self.texType + ",idx:" + repr(self.texIdx) + ")" class CprKNS2: def __init__(self, texRefs): self.texRefs = texRefs def __repr__(self): return repr(self.texRefs) #contains information about each vertex component class CprVDCLEntry: def __init__(self, rawBytes): dclInfo = noeUnpackFrom(">HHiHH", rawBytes, 0) self.shapeFlag = dclInfo[0] self.cmpOfs = dclInfo[1] self.cmpType = dclInfo[2] self.cmpElemType = dclInfo[3] def __repr__(self): return "(elemtype:" + repr(self.cmpElemType) + ",ofs:" + repr(self.cmpOfs) + ",type:" + repr(self.cmpType) + ",shape:" + repr(self.shapeFlag) + ")" class CprVDCL: def __init__(self, rawBytes): self.decodedNormals = None #keep a decoded buffer on each vdcl self.entries = [] self.shapeIndex = -1 maxEntries = (len(rawBytes)-4) // 12 for i in range(0, maxEntries): eofs = 4 + i*12 self.entries.append(CprVDCLEntry(rawBytes[eofs:eofs+12])) def __repr__(self): return repr(self.entries) def createBuffers(self, cprFile, shapesOfs): idx = self.idx vtx = self.vtx if idx.bufferIndex < 0 or idx.bufferIndex >= len(cprFile.idxEntries): noesis.doException("Index buffer out of range") if vtx.bufferIndex < 0 or vtx.bufferIndex >= len(cprFile.vtxEntries): noesis.doException("Vertex buffer out of range") self.vdclPos = cprFindDecl(self.entries, 0, 2761657) if self.vdclPos is None: return self.vdclNrm = cprFindDecl(self.entries, 3, 2761095) if self.vdclNrm is not None and self.vdclNrm.shapeFlag != 0: print("WARNING: Found shape flag on normal component, this is not handled") #haven't seen this happen, but it seems possible/reasonable self.vdclNrm = None self.vdclUV = cprFindDecl(self.entries, 5, 2892639) self.vdclBoneIdx = cprFindDecl(self.entries, 1, 1712774) self.vdclBoneWgt = cprFindDecl(self.entries, 1, 1712262) vtxEntry = cprFile.vtxArEntries[vtx.bufferIndex] self.vtxSize = vtx.vertSize self.vtxBuffer = cprFile.getEntryBytes(vtxEntry) if self.vdclPos.shapeFlag != 0: #use a shape buffer if self.shapeIndex < 0: noesis.doException("Vertex decl unexpectedly sets shape flag without a shape index") vtxEntryPos = cprFile.vtxArEntries[shapesOfs + self.shapeIndex] self.vtxSizePos = 12 self.vtxBufferPos = cprFile.getEntryBytes(vtxEntryPos) else: vtxEntryPos = cprFile.vtxArEntries[vtx.bufferIndex] self.vtxSizePos = vtx.vertSize self.vtxBufferPos = self.vtxBuffer idxEntry = cprFile.idxArEntries[idx.bufferIndex] self.idxBuffer = cprFile.getEntryBytes(idxEntry) if self.vdclNrm is not None: self.decodedNormals = rapi.decodeNormals32(self.vtxBuffer[self.vdclNrm.cmpOfs:], self.vtxSize, -10, -10, -10, NOE_BIGENDIAN) #convert encoded normals into an array of floats self.decodedNormals = rapi.swapEndianArray(self.decodedNormals, 4) #swap endianness back for each float before drawing in big-endian mode else: self.decodedNormals = None class CprDIP: def __init__(self, rawBytes, boneMap): dipInfo = noeUnpackFrom(">iiiiii", rawBytes, 0) self.unkA = dipInfo[0] self.vertStart = dipInfo[1] self.unkC = dipInfo[2] self.unkD = dipInfo[3] self.triStart = dipInfo[4] self.numTris = dipInfo[5] self.boneMap = boneMap class CprDGRP: def __init__(self, dipList, activeKNS2, activeVDCL): self.dipList = dipList self.kns2 = activeKNS2 self.vdcl = activeVDCL #see if the file in question is a valid cpr file class CprFile: def __init__(self, bs): self.bs = bs self.texNameList = [] self.boneList = [] self.vtxArEntries = [] self.idxArEntries = [] self.entries = [] self.vdclEntries = [] self.vtxEntries = [] self.svtxEntries = [] self.idxEntries = [] self.dgrpEntries = [] def parseHeader(self): bs = self.bs if bs.getSize() < 96: return 0 bs.seek(0, NOESEEK_ABS) ver = bs.read(">iiii") if ver[0] != CPR_HEADER_ID or ver[1] != 1 or ver[2] != 0x11223344 or ver[3] != 0: return 0 hdrInfo = bs.read(">iiiiiiii") self.entriesSize = hdrInfo[0] self.unkSize = hdrInfo[1] self.namesSize = hdrInfo[2] self.finfoSize = hdrInfo[3] self.dataSize = hdrInfo[4] #self.dataOfs = hdrInfo[7] #hdrInfo[7] seemed to be correct for Bullet Witch, but that is no longer the case in Xegapain XOR self.dataOfs = self.entriesSize + self.unkSize + self.namesSize + self.finfoSize if self.entriesSize <= 0 or self.namesSize <= 0 or self.finfoSize <= 0 or self.dataSize <= 0 or self.dataOfs <= 0: return 0 self.arcBase = bs.tell() + 16 + 32 #skip over archive name (always seems to be "unknown") return 1 #looks valid def decompressInPlace(self): bs = self.bs bs.seek(self.arcBase) decompSizes = (self.entriesSize+self.unkSize+self.namesSize+self.finfoSize, self.dataSize) decompData = bytearray() for decompSize in decompSizes: cmprId = bs.readInt() if cmprId != CZR_COMPRESSION_MARKER: break numChunks = bs.readInt() chunkSize = 0 for i in range(0, numChunks): chunkSize += bs.readInt() cmpData = bs.readBytes(chunkSize) decompData += rapi.decompLZHMelt(cmpData, decompSize) #swap out the bitstream in place with the old header and the new decompressed data bs.seek(0, NOESEEK_ABS) self.bs = NoeBitStream(bs.readBytes(self.arcBase) + decompData, NOE_BIGENDIAN) def parseEntries(self): bs = self.bs bs.seek(self.arcBase, NOESEEK_ABS) cmprTest = bs.readInt() if cmprTest == CZR_COMPRESSION_MARKER: self.decompressInPlace() bs = self.bs bs.seek(self.arcBase+self.entriesSize+self.unkSize, NOESEEK_ABS) self.nameStream = NoeBitStream(bs.readBytes(self.namesSize), NOE_BIGENDIAN) self.finfoStream = NoeBitStream(bs.readBytes(self.finfoSize), NOE_BIGENDIAN) bs.seek(self.arcBase, NOESEEK_ABS) self.numEntries = self.entriesSize // CPR_ENTRY_SIZE print("Parsing", self.numEntries, "entries.") for i in range(0, self.numEntries): entry = CprEntry(bs.readBytes(CPR_ENTRY_SIZE), self.nameStream) self.entries.append(entry) #print(repr(i) + " - " + entry.dirName + "/" + entry.name) if entry.type == CPR_TYPE_BVXD: self.vtxArEntries.append(entry) elif entry.type == CPR_TYPE_BIXD: self.idxArEntries.append(entry) return 1 def loadTextures(self, texList): bs = self.bs fi = self.finfoStream for i in range(0, self.numEntries): entry = self.entries[i] bs.seek(self.arcBase+self.dataOfs+entry.dataOfs, NOESEEK_ABS) if entry.type != CPR_TYPE_DDS: continue data = bs.readBytes(entry.dataSize) fi.seek(entry.infoOfs, NOESEEK_ABS) infoData = fi.readBytes(entry.infoSize) texture = cprLoadTexture(data, entry, infoData) if texture is not None: texList.append(texture) return 1 def getEntryBytes(self, entry): hdrOfs = self.arcBase+self.dataOfs+entry.dataOfs self.bs.seek(hdrOfs, NOESEEK_ABS) #print(hdrOfs) return self.bs.readBytes(entry.dataSize) def findEntry(self, entryName): for entry in self.entries: if entry.name == entryName: return entry return None def parseVDCL(self, subSize): if (subSize-4)%12 != 0: noesis.doException("Unexpected VDCL chunk size: " + repr(subSize)) return CprVDCL(self.bs.readBytes(subSize)) def parseIDX(self, subSize): if subSize != 4: noesis.doException("Unexpected IDX chunk size: " + repr(subSize)) return CprIDX(self.bs.readInt()) def parseVTX(self, subSize, isShape): if subSize != 16: noesis.doException("Unexpected VTX chunk size: " + repr(subSize)) return CprVTX(self.bs.readBytes(16), isShape) def parseKNS2(self, subSize): bs = NoeBitStream(self.bs.readBytes(subSize), NOE_BIGENDIAN) texRefs = [] while bs.tell() < bs.getSize(): knsType = bs.readBytes(4).decode("ASCII") knsLen = bs.readInt() if knsLen <= 0: noesis.doException("Unexpected sub-KNS2 length") knsStart = bs.tell() if knsType == "MCTL": bs.seek(0x08, NOESEEK_REL) elif knsType == "TR ": #texture references bs.seek(0x04, NOESEEK_REL) numRefs = bs.readInt() for i in range(0, numRefs): texType = bs.readBytes(8).decode("ASCII").rstrip("\0") texIdx = bs.readInt() texIdx2 = bs.readInt() texRefs.append(CprTexRef(texType, texIdx, texIdx2)) else: bs.seek(knsLen, NOESEEK_REL) return CprKNS2(texRefs) def parseCM1RBoneMap(self, rawBytes): bs = NoeBitStream(rawBytes, NOE_BIGENDIAN) bs.seek(0x04, NOESEEK_REL) numBones = bs.readInt() boneMap = [] for i in range(0, numBones): type = bs.readBytes(4).decode("ASCII") if type != "JINT": print("WARNING: Prematurely ran out of JINT blocks") break unk = bs.readInt() mapTo = bs.readInt() mapFrom = bs.readInt() if len(boneMap) <= mapFrom: boneMap.extend([0]*(mapFrom+1-len(boneMap))) boneMap[mapFrom] = mapTo return boneMap def parseDGRP(self, subSize, activeKNS2, activeVDCL): if activeVDCL is None: noesis.doException("Encountered DGRP without active vertex info") bs = NoeBitStream(self.bs.readBytes(subSize), NOE_BIGENDIAN) bs.seek(0x04, NOESEEK_REL) dipList = [] boneMap = None while bs.tell() < bs.getSize(): grpType = bs.readBytes(4).decode("ASCII") grpLen = bs.readInt() if grpLen <= 0: noesis.doException("Unexpected sub-DGRP length") grpStart = bs.tell() if grpType == "DVOL": bs.seek(0x04, NOESEEK_REL) elif grpType == "knsC": bs.seek(0, NOESEEK_REL) elif grpType == "MCTL": bs.seek(0x08, NOESEEK_REL) elif grpType == "CM1R": #joint map boneMap = self.parseCM1RBoneMap(bs.readBytes(grpLen)) elif grpType == "DIP ": if grpLen != 24: noesis.doException("Unexpected DIP size (that's what she said)") dipList.append(CprDIP(bs.readBytes(grpLen), boneMap)) else: bs.seek(grpLen, NOESEEK_REL) return CprDGRP(dipList, activeKNS2, activeVDCL) def parseTexNameList(self, tlEntry, matList): bs = self.bs hdrOfs = self.arcBase+self.dataOfs+tlEntry.dataOfs bs.seek(hdrOfs, NOESEEK_ABS) hdrID = bs.readInt() if hdrID != CPR_TEXLISTHDR_ID: return 0 numEntries = bs.readInt() for i in range(0, numEntries): unk = bs.readInt() str = bs.readBytes(32).decode("ASCII").rstrip("\0") self.texNameList.append(str) return 1 def parseSkeleton(self, sklEntry): bs = self.bs hdrOfs = self.arcBase+self.dataOfs+sklEntry.dataOfs bs.seek(hdrOfs, NOESEEK_ABS) hdrID = bs.readInt() if hdrID != CPR_SKELHDR_ID: return 0 numBones = bs.readInt() for i in range(0, numBones): unk = bs.readInt() boneParent = bs.readInt() unk = bs.readInt() unk = bs.readInt() unk = bs.readInt() unk = bs.readInt() bs.seek(0x28, NOESEEK_REL) #skip the first matrix - it's parent-relative, which is unnecessary (although a "self.boneList = rapi.multiplyBones(self.boneList)" would allow them to be used) quat = NoeQuat.fromBytes(bs.readBytes(16), NOE_BIGENDIAN) trn = NoeVec3.fromBytes(bs.readBytes(12), NOE_BIGENDIAN) scl = NoeVec3.fromBytes(bs.readBytes(12), NOE_BIGENDIAN) mat = quat.toMat43(1) mat[0] *= scl mat[1] *= scl mat[2] *= scl mat[3] = trn #mat = mat.swapHandedness() self.boneList.append(NoeBone(i, "bone%03i"%i, mat, None, boneParent)) return 1 def parseModelHeader(self, hdrEntry, matList): bs = self.bs hdrOfs = self.arcBase+self.dataOfs+hdrEntry.dataOfs bs.seek(hdrOfs, NOESEEK_ABS) hdrID = bs.readInt() if hdrID != CPR_MDLHDR_ID: noesis.doException("Unexpected model header: " + repr(hdrID)) bs.seek(0x58, NOESEEK_REL) chunkOfsTo = bs.readInt() bs.seek(chunkOfsTo - 0x0C, NOESEEK_REL) while bs.tell() < bs.getSize(): chunkIndex = bs.readInt() chunkID = bs.readInt() if chunkID != CPR_CHUNKHDR_ID: break chunkSize = bs.readInt() chunkDataOfs = bs.tell() activeVDCL = None activeKNS2 = None #fixme - VDCL etc. can actually be under DVOL/DGRP in some zegapain xor files, this chunk logic all needs to be restructured and put into a single switch while bs.tell() < chunkDataOfs+chunkSize: subType = bs.readBytes(4).decode("ASCII") subSize = bs.readInt() if subType == "VDCL": activeVDCL = self.parseVDCL(subSize) self.vdclEntries.append(activeVDCL) elif subType == "IDX ": activeVDCL.idx = self.parseIDX(subSize) self.idxEntries.append(activeVDCL.idx) elif subType == "VTX ": activeVDCL.vtx = self.parseVTX(subSize, 0) self.vtxEntries.append(activeVDCL.vtx) elif subType == "VTXS": activeVDCL.shapeIndex = len(self.svtxEntries) activeVDCL.vtx = self.parseVTX(subSize, 1) self.svtxEntries.append(activeVDCL.vtx) elif subType == "kns2" or subType == "knsC": activeKNS2 = self.parseKNS2(subSize) elif subType == "DGRP": self.dgrpEntries.append(self.parseDGRP(subSize, activeKNS2, activeVDCL)) else: bs.seek(subSize, NOESEEK_REL) self.numMeshes = len(self.idxEntries) #print(len(self.idxEntries), len(self.vdclEntries), len(self.vtxEntries), len(self.svtxEntries)) #print(len(self.idxArEntries), len(self.vtxArEntries)) if len(self.vdclEntries) != self.numMeshes or len(self.vtxEntries) != self.numMeshes or len(self.idxArEntries) != self.numMeshes or len(self.vtxArEntries) < self.numMeshes: noesis.doException("Unexpected vdecl/buffer counts: " + repr(len(self.vdclEntries)) + "," + repr(self.numMeshes) + "," + repr(len(self.idxArEntries)) + "," + repr(len(self.vtxArEntries)) ) shapesOfs = len(self.vdclEntries) for vdcl in self.vdclEntries: vdcl.createBuffers(self, shapesOfs) #no need to explicitly free the context (created contexts are auto-freed after the handler), but DO NOT hold any references to it outside of this method ctx = rapi.rpgCreateContext() rapi.rpgSetOption(noesis.RPGOPT_BIGENDIAN, 1) #rapi.rpgSetOption(noesis.RPGOPT_SWAPHANDEDNESS, 1) for dgrp in self.dgrpEntries: vdcl = dgrp.vdcl material = NoeMaterial("bwitchmat%03i"%len(matList), "") #create a material for each dgrp if dgrp.kns2 != None: for tref in dgrp.kns2.texRefs: if tref.texIdx >= 0 and tref.texIdx < len(self.texNameList): if tref.texType == "BTX0": material.setTexture(self.texNameList[tref.texIdx]) elif tref.texType == "NMAP": material.setNormalTexture(self.texNameList[tref.texIdx]) rapi.rpgSetMaterial(material.name) matList.append(material) vdclPos = vdcl.vdclPos if vdclPos is None: print("WARNING: Couldn't find position element for vdecl") continue vdclUV = vdcl.vdclUV vdclBoneIdx = vdcl.vdclBoneIdx vdclBoneWgt = vdcl.vdclBoneWgt vtxSize = vdcl.vtxSize vtxSizePos = vdcl.vtxSizePos for dip in dgrp.dipList: rapi.rpgSetBoneMap(dip.boneMap) baseOfs = dip.vertStart*vtxSize baseOfsPos = dip.vertStart*vtxSizePos rapi.rpgBindPositionBufferOfs(vdcl.vtxBufferPos, noesis.RPGEODATA_FLOAT, vtxSizePos, baseOfsPos+vdclPos.cmpOfs) if vdcl.decodedNormals is not None: rapi.rpgBindNormalBufferOfs(vdcl.decodedNormals, noesis.RPGEODATA_FLOAT, 12, dip.vertStart*12) if vdclUV is not None: rapi.rpgBindUV1BufferOfs(vdcl.vtxBuffer, noesis.RPGEODATA_HALFFLOAT, vtxSize, baseOfs+vdclUV.cmpOfs) if vdclBoneIdx is not None and vdclBoneWgt is not None: rapi.rpgBindBoneIndexBufferOfs(vdcl.vtxBuffer, noesis.RPGEODATA_UBYTE, vtxSize, baseOfs+vdclBoneIdx.cmpOfs, 4) rapi.rpgBindBoneWeightBufferOfs(vdcl.vtxBuffer, noesis.RPGEODATA_UBYTE, vtxSize, baseOfs+vdclBoneWgt.cmpOfs, 4) numIdx = dip.numTris*3 idxOfs = dip.triStart*2 idxEnd = idxOfs + numIdx*2 rapi.rpgCommitTriangles(vdcl.idxBuffer[idxOfs:idxEnd], noesis.RPGEODATA_USHORT, numIdx, noesis.RPGEO_TRIANGLE, 1) rapi.rpgClearBufferBinds() #rapi.rpgSmoothTangents() #rapi.rpgOptimize() #return rapi.rpgConstructModel() return rapi.rpgConstructModelSlim() def cprCheckType(data): cpr = CprFile(NoeBitStream(data, NOE_BIGENDIAN)) if cpr.parseHeader() == 0: return 0 return 1 #load the model/textures def cprLoadModel(data, mdlList): startTime = time.time() cpr = CprFile(NoeBitStream(data, NOE_BIGENDIAN)) if cpr.parseHeader() == 0: return 0 if cpr.parseEntries() == 0 or len(cpr.entries) <= 0: return 0 texList = [] if cpr.loadTextures(texList) == 0: return 0 matList = [] firstEntry = cpr.entries[0] if firstEntry.type == CPR_TYPE_RESU and firstEntry.name == "header.bin": tlEntry = cpr.findEntry("texlist.bin") #"anim_mat.bin" also sometimes seems to be a texture list if tlEntry is not None and tlEntry.type == CPR_TYPE_RESU: cpr.parseTexNameList(tlEntry, matList) sklEntry = cpr.findEntry("motion._bl") if sklEntry is not None and sklEntry.type == CPR_TYPE_RESU: cpr.parseSkeleton(sklEntry) mdl = cpr.parseModelHeader(firstEntry, matList) mdl.setBones(cpr.boneList) #this is an example of how to use procedural animations to test your weighting (this assumes bone 11=right shoulder and bone 16=left shoulder) #panims = [NoeProceduralAnim("bone011", 30.0, 1, 0.1), NoeProceduralAnim("bone016", 60.0, 1, 0.1)] #anims = rapi.createProceduralAnim(cpr.boneList, panims, 100) #mdl.setAnims(anims) else: mdl = NoeModel() if len(matList) > 0 and len(texList) <= 0: #try grabbing a texture package on a relative path cprTexData = None texFileName = rapi.getDirForFilePath(rapi.getInputName()) + "../texture/texture.cpr" if (rapi.checkFileExists(texFileName)): cprTexData = rapi.loadIntoByteArray(texFileName) else: texFileName = rapi.getDirForFilePath(rapi.getInputName()) + "../texture/texture.czr" if (rapi.checkFileExists(texFileName)): cprTexData = rapi.loadIntoByteArray(texFileName) if cprTexData is not None: texCpr = CprFile(NoeBitStream(cprTexData, NOE_BIGENDIAN)) if texCpr.parseHeader() != 0 and texCpr.parseEntries() != 0 and len(texCpr.entries) > 0: print("Got texture package from relative path.") texCpr.loadTextures(texList) mdl.setModelMaterials(NoeModelMaterials(texList, matList)) mdlList.append(mdl) timeTaken = time.time() - startTime print("Model loaded in", timeTaken, "seconds.") rapi.setPreviewOption("setAngOfs", "0 90 90") return 1 #take appropriate action to convert texture data def cprLoadTexture(data, entry, infoData): imgDims = noeUnpackFrom(">i", infoData, 36)[0] imgWidth = (imgDims & 4095) + 1 imgHeight = ((imgDims>>13) & 4095) + 1 imgFmt = noeUnpackFrom(">B", infoData, 35)[0] texFmt = 0 #DXT1 if imgFmt == 0x52: data = rapi.imageUntile360DXT(rapi.swapEndianArray(data, 2), imgWidth, imgHeight, 8) texFmt = noesis.NOESISTEX_DXT1 #DXT5 elif imgFmt == 0x54: data = rapi.imageUntile360DXT(rapi.swapEndianArray(data, 2), imgWidth, imgHeight, 16) texFmt = noesis.NOESISTEX_DXT5 #DXT5 packed normal map elif imgFmt == 0x71: data = rapi.imageUntile360DXT(rapi.swapEndianArray(data, 2), imgWidth, imgHeight, 16) data = rapi.imageDecodeDXT(data, imgWidth, imgHeight, noesis.FOURCC_ATI2) texFmt = noesis.NOESISTEX_RGBA32 #DXT1 packed normal map elif imgFmt == 0x7C: data = rapi.imageUntile360DXT(rapi.swapEndianArray(data, 2), imgWidth, imgHeight, 8) data = rapi.imageDecodeDXT(data, imgWidth, imgHeight, noesis.FOURCC_DXT1NORMAL) texFmt = noesis.NOESISTEX_RGBA32 #raw elif imgFmt == 0x86: data = rapi.imageUntile360Raw(rapi.swapEndianArray(data, 4), imgWidth, imgHeight, 4) texFmt = noesis.NOESISTEX_RGBA32 #unknown, not handled else: print("WARNING: Unhandled image format " + repr(imgFmt) + " - " + repr(imgWidth) + "x" + repr(imgHeight) + " - " + repr(len(data))) return None return NoeTexture(entry.name, imgWidth, imgHeight, data, texFmt) def cprFindDecl(entries, elemType, type): for component in entries: if component.cmpElemType == elemType and component.cmpType == type: return component return None