Merge branch 'master' into pr/cabalex/1

This commit is contained in:
cabalex
2024-04-26 19:26:46 -07:00
10 changed files with 1075 additions and 57 deletions
+28 -20
View File
@@ -1,6 +1,7 @@
import os
import time
import json
from ...wta_wtp.exporter import export_wta_wtp
import bpy
from bpy.props import StringProperty
@@ -16,12 +17,8 @@ class ExportAllSteps(bpy.types.PropertyGroup):
name = "Export WMB",
default = True
)
useWtpStep: bpy.props.BoolProperty(
name = "Export WTP",
default = True
)
useWtaStep: bpy.props.BoolProperty(
name = "Export WTA",
useWtaWtpStep: bpy.props.BoolProperty(
name = "Export WTA + WTP",
default = True
)
useColStep: bpy.props.BoolProperty(
@@ -63,6 +60,16 @@ class ExportAllSteps(bpy.types.PropertyGroup):
default = True
)
exportingForGame: bpy.props.EnumProperty(
name = "Export For Game",
default = "NIER",
items = [
("NIER", "NieR: Automata", "Exports for NieR (WTA: DDS only).", 1),
("NIERSWITCH", "NieR: Automata (Switch)", "Exports for NieR on Nintendo Switch (WTA: DDS/ASTC)", 2),
("ASTRALCHAIN", "Astral Chain", "Exports for Astral Chain (WTA: DDS/ASTC)", 3)
]
)
class DAT_DTT_PT_Export(bpy.types.Panel):
bl_label = "NieR:Automata Export"
bl_space_type = 'PROPERTIES'
@@ -148,8 +155,7 @@ class DAT_DTT_PT_Export(bpy.types.Panel):
row = box.row(align=True)
row.scale_y = 1.125
row.prop(context.scene.ExportAllSteps, "useWmbStep", text="WMB", icon="PANEL_CLOSE" if context.scene.ExportAllSteps.useWmbStep else "ADD")
row.prop(context.scene.ExportAllSteps, "useWtpStep", text="WTP", icon="PANEL_CLOSE" if context.scene.ExportAllSteps.useWtpStep else "ADD")
row.prop(context.scene.ExportAllSteps, "useWtaStep", text="WTA", icon="PANEL_CLOSE" if context.scene.ExportAllSteps.useWtaStep else "ADD")
row.prop(context.scene.ExportAllSteps, "useWtaWtpStep", text="WTA + WTP", icon="PANEL_CLOSE" if context.scene.ExportAllSteps.useWtaWtpStep else "ADD")
row = box.row(align=True)
row.scale_y = 1.125
secondRowItemCount = 0
@@ -175,6 +181,10 @@ class DAT_DTT_PT_Export(bpy.types.Panel):
row.prop(context.scene.ExportAllSteps, "centerOrigins", text="Center Origins", icon="OBJECT_ORIGIN")
row.prop(context.scene.ExportAllSteps, "deleteLoose", text="Delete Loose", icon="SNAP_VERTEX")
row = box.row(align=True)
box = row.box()
box.prop(context.scene.ExportAllSteps, "exportingForGame", text="Export for")
layout.separator()
row = layout.row()
@@ -243,7 +253,9 @@ class ExportAll(bpy.types.Operator):
sarColl = bpy.data.objects["Field-Root"].users_collection[0]
for item in context.scene.DatContents:
if item.filepath.endswith('.wta'):
if item.filepath.endswith('.wmb'):
wmbFilePath = item.filepath
elif item.filepath.endswith('.wta'):
wtaFilePath = item.filepath
elif item.filepath.endswith('.col'):
colFilePath = item.filepath
@@ -276,15 +288,11 @@ class ExportAll(bpy.types.Operator):
bpy.ops.b2n.deleteloosegeometryall()
wmb_exporter.main(wmbFilePath)
exportedFilesCount += 1
from ...wta_wtp.exporter import export_wta, export_wtp
if exportSteps.useWtaStep:
print("Exporting WTA")
export_wta.main(context, wtaFilePath)
exportedFilesCount += 1
if exportSteps.useWtpStep:
print("Exporting WTP")
export_wtp.main(context, wtpFilePath)
exportedFilesCount += 1
from ...wta_wtp.exporter import export_wta_wtp
if exportSteps.useWtaWtpStep:
print("Exporting WTA/WTP")
export_wta_wtp.main(context, wtaFilePath, wtpFilePath, exportSteps.exportingForGame)
exportedFilesCount += 2
from ...col.exporter import col_exporter
if exportSteps.useColStep:
print("Exporting COL")
@@ -326,7 +334,7 @@ class ExportAll(bpy.types.Operator):
fileNames = [os.path.basename(file_path) for file_path in file_list]
saveDatInfo(datInfoFilePath, fileNames, datFileName)
# export dtt
export_dat.main(datFilePath, file_list)
export_dat.main(datFilePath, file_list, exportSteps.exportingForGame)
exportedFilesCount += 1
if exportSteps.useDttStep:
if len(context.scene.DttContents) == 0:
@@ -342,7 +350,7 @@ class ExportAll(bpy.types.Operator):
fileNames = [os.path.basename(file_path) for file_path in file_list]
saveDatInfo(datInfoFilePath, fileNames, dttFileName)
# export dtt
export_dat.main(dttFilePath, file_list)
export_dat.main(dttFilePath, file_list, exportSteps.exportingForGame)
exportedFilesCount += 1
tDiff = int(time.time() - t1)
+13 -1
View File
@@ -7,7 +7,7 @@ from ...utils.util import *
def to_string(bs, encoding = 'utf8'):
return bs.split(b'\x00')[0].decode(encoding)
def main(export_filepath, file_list):
def main(export_filepath, file_list, exportingForGame):
files = file_list
fileNumber = len(files)
from .datHashGenerator import generateHashData
@@ -46,11 +46,23 @@ def main(export_filepath, file_list):
#fileOffsets
fileOffsets = []
currentOffset = hashMapOffset + hashMapSize
# ASTRAL CHAIN's DTT files start at 0x8000 bytes.
if exportingForGame == "ASTRALCHAIN" and ".dtt" in export_filepath:
currentOffset = (math.ceil(currentOffset / 0x8000)) * 0x8000
# NieR Switch's DTT files start at intervals of 0x200.
if exportingForGame == "NIERSWITCH":
currentOffset = (math.ceil(currentOffset / 0x200)) * 0x200
for fp in files:
currentOffset = (math.ceil(currentOffset / 16)) * 16
fileOffsets.append(currentOffset)
currentOffset += os.path.getsize(fp)
# ASTRAL CHAIN's BNK files are padded out to 2048 bytes.
if exportingForGame == "ASTRALCHAIN" and ".bnk" in os.path.basename(fp):
currentOffset = (math.ceil(currentOffset / 2048)) * 2048
# fileSizes
fileSizes = []
for fp in files:
+20
View File
@@ -345,3 +345,23 @@ def saveDatInfo(filepath: str, files: List[str], filename: str):
"ext": ext[1:]
}
json.dump(jsonFiles, f, indent=4)
def resolveTextureDir(wmb_dir, wmbname):
texture_dir_dat = wmb_dir.replace(wmbname, "textures").replace(".dtt", ".dat")
texture_dir_dtt = wmb_dir.replace(wmbname, "textures").replace(".dat", ".dtt")
if os.path.exists(texture_dir_dat):
return texture_dir_dat
elif os.path.exists(texture_dir_dtt):
return texture_dir_dtt
else:
return ""
def resolveTexturePaths(texture_dir, textureID):
dds_path = "%s/%s.dds" % (texture_dir, textureID)
png_path = "%s/%s.png" % (texture_dir, textureID)
if os.path.exists(dds_path):
return dds_path
elif os.path.exists(png_path):
return png_path
else:
return ""
+22 -11
View File
@@ -5,7 +5,7 @@ import math
from typing import List, Tuple
from mathutils import Vector
from ...utils.util import ShowMessageBox, getPreferences, printTimings
from ...utils.util import ShowMessageBox, getPreferences, printTimings, resolveTexturePaths, resolveTextureDir
from .wmb import *
from ...wta_wtp.exporter.wta_wtp_ui_manager import isTextureTypeSupported, makeWtaMaterial
@@ -180,7 +180,7 @@ def addWtaExportMaterial(texture_dir, material):
material_name = material[0]
textures = material[1]
wtaTextures: List[Tuple[str, str, str]] = [
(mapType, id, os.path.join(texture_dir, f"{id}.dds"))
(mapType, id, resolveTexturePaths(texture_dir, id))
for mapType, id in textures.items()
if isTextureTypeSupported(mapType)
]
@@ -245,7 +245,7 @@ def construct_materials(texture_dir, material):
for texturesType in textures.keys():
textures_type = texturesType.lower()
material[texturesType] = textures.get(texturesType)
texture_file = "%s/%s.dds" % (texture_dir, textures[texturesType])
texture_file = resolveTexturePaths(texture_dir, textures[texturesType])
if os.path.exists(texture_file):
if textures_type.find('albedo') > -1:
albedo_maps[textures_type] = textures.get(texturesType)
@@ -258,9 +258,10 @@ def construct_materials(texture_dir, material):
# Albedo Nodes
albedo_nodes = []
albedo_uv_nodes = []
albedo_mixRGB_nodes = []
for i, textureID in enumerate(albedo_maps.values()):
texture_file = "%s/%s.dds" % (texture_dir, textureID)
texture_file = resolveTexturePaths(texture_dir, textureID)
if os.path.exists(texture_file):
albedo_image = nodes.new(type='ShaderNodeTexImage')
albedo_nodes.append(albedo_image)
@@ -277,6 +278,12 @@ def construct_materials(texture_dir, material):
albedo_mixRGB_nodes.append(mixRGB_shader)
mixRGB_shader.location = 300,(i-1)*-60
mixRGB_shader.hide = True
# UV map node for other textures
uv_shader = nodes.new(type='ShaderNodeUVMap')
albedo_uv_nodes.append(uv_shader)
uv_shader.uv_map = f"UVMap{i+1}"
uv_shader.location = -300,i*-60
uv_shader.hide = True
# Albedo Links
if len(albedo_nodes) == 1:
albedo_principled = links.new(albedo_nodes[0].outputs['Color'], principled.inputs['Base Color'])
@@ -287,8 +294,12 @@ def construct_materials(texture_dir, material):
for i in range(len(albedo_mixRGB_nodes)):
albedo_link = links.new(albedo_nodes[i+1].outputs['Color'], albedo_mixRGB_nodes[i].inputs['Color1'])
alpha_link = links.new(albedo_nodes[i].outputs['Alpha'], albedo_mixRGB_nodes[i].inputs['Fac'])
# UV is [usually] different for other AlbedoMaps
uv_link = links.new(albedo_uv_nodes[i].outputs['UV'], albedo_nodes[i+1].inputs['Vector'])
if i > 0:
mixRGB_link = links.new(albedo_mixRGB_nodes[i-1].outputs['Color'], albedo_mixRGB_nodes[i].inputs['Color2'])
# Preserve Alpha for last texture (ASTRAL CHAIN Harmony Square signage?)
links.new(albedo_nodes[-1].outputs['Alpha'], principled.inputs['Alpha'])
mixRGB_link = links.new(albedo_mixRGB_nodes[-1].outputs['Color'], principled.inputs['Base Color'])
# Mask Nodes
@@ -297,7 +308,7 @@ def construct_materials(texture_dir, material):
mask_sepRGB_nodes = []
mask_invert_nodes = []
for i, textureID in enumerate(mask_maps.values()):
texture_file = "%s/%s.dds" % (texture_dir, textureID)
texture_file = resolveTexturePaths(texture_dir, textureID)
if os.path.exists(texture_file):
mask_image = nodes.new(type='ShaderNodeTexImage')
mask_nodes.append(mask_image)
@@ -334,7 +345,7 @@ def construct_materials(texture_dir, material):
normal_nodes = []
normal_mixRGB_nodes = []
for i, textureID in enumerate(normal_maps.values()):
texture_file = "%s/%s.dds" % (texture_dir, textureID)
texture_file = resolveTexturePaths(texture_dir, textureID)
if os.path.exists(texture_file):
normal_image = nodes.new(type='ShaderNodeTexImage')
normal_nodes.append(normal_image)
@@ -374,7 +385,7 @@ def construct_materials(texture_dir, material):
curvature_sepRGB_nodes = []
curvature_mul_nodes = []
for i, textureID in enumerate(curvature_maps.values()):
texture_file = "%s/%s.dds" % (texture_dir, textureID)
texture_file = resolveTexturePaths(texture_dir, textureID)
if os.path.exists(texture_file):
curvature_image = nodes.new(type='ShaderNodeTexImage')
curvature_nodes.append(curvature_image)
@@ -574,14 +585,14 @@ def get_wmb_material(wmb, texture_dir):
try:
texture_stream = wmb.wta.getTextureByIdentifier(identifier,wmb.wtp_fp)
if texture_stream:
if not os.path.exists(os.path.join(texture_dir, identifier + '.dds')):
if not os.path.exists(resolveTexturePaths(texture_dir, identifier)):
create_dir(texture_dir)
texture_fp = open(os.path.join(texture_dir, identifier + '.dds'), "wb")
print('[+] could not find DDS texture, trying to find it in WTA; %s.dds'% identifier)
texture_fp.write(texture_stream)
texture_fp.close()
else:
print('[+] Found %s.dds'% identifier)
print('[+] Found ' + resolveTexturePaths(texture_dir, identifier).split("/")[-1])
except:
continue
materials.append([material_name,textures,uniforms,shader_name,technique_name,parameterGroups])
@@ -592,7 +603,7 @@ def get_wmb_material(wmb, texture_dir):
identifier = wmb.wta.wtaTextureIdentifier[textureIndex]
texture_stream = wmb.wta.getTextureByIdentifier(identifier,wmb.wtp_fp)
if texture_stream:
if not os.path.exists(os.path.join(texture_dir, identifier + '.dds')):
if not os.path.exists(resolveTexturePaths(texture_dir, identifier)):
create_dir(texture_dir)
texture_fp = open(os.path.join(texture_dir, identifier + '.dds'), "wb")
print('[+] dumping %s.dds'% identifier)
@@ -682,7 +693,7 @@ def main(only_extract = False, wmb_file = os.path.join(os.path.split(os.path.rea
wmbCollection.children.link(col)
#bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[-1]
texture_dir = wmb_file.replace(wmbname, 'textures')
texture_dir = resolveTextureDir(wmb_file, wmbname)
if wmb.hasBone:
boneArray = [[bone.boneIndex, "bone%d"%bone.boneIndex, bone.parentIndex,"bone%d"%bone.parentIndex, bone.world_position, bone.world_rotation, bone.boneNumber, bone.local_position, bone.local_rotation, bone.world_rotation, bone.world_position_tpose] for bone in wmb.boneArray]
armature_no_wmb = wmbname.replace('.wmb','')
Binary file not shown.
+540
View File
@@ -0,0 +1,540 @@
import subprocess
import asyncio
import math
import json
from ...utils.ioUtils import read_int32, write_Int32, write_uInt16, write_float16
from . import generate_wta_wtp_data
from .wta_wtp_utils import *
from ..tegrax1swizzle import compressImageData, getFormatByIndex
# Asynchronously encodes a PNG file to ASTC.
async def encode_astc(filePath, format: str):
script_file = os.path.realpath(__file__)
directory = os.path.dirname(script_file).replace("exporter", "")
appPath = os.path.join(directory, "astcenc-avx2.exe")
result = subprocess.run([appPath, "-cs", filePath, filePath.replace('.png', '.astc'), format.split("_")[1], "-medium"], cwd=directory)
def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame):
wta_fp = open(export_filepath_wta,'wb')
wtp_fp = open(export_filepath_wtp,'wb')
# Assign data and check if valid
identifiers_array, texture_paths_array, albedo_indexes, metadata_path = generate_wta_wtp_data.generate(context, exportingForGame)
if None in [identifiers_array, texture_paths_array, albedo_indexes]:
print("WTP Export Failed! :{")
return
# Assign some shit
unknown04 = 3
textureCount = len(texture_paths_array)
paddingAmount = ((textureCount + 7) // 8) * 8 #rounds up to the nearest 8th integer
textureOffsetArrayOffset = 32
textureSizeArrayOffset = textureOffsetArrayOffset + (paddingAmount * 4)
unknownArrayOffset1 = textureSizeArrayOffset + (paddingAmount * 4)
textureIdentifierArrayOffset = unknownArrayOffset1 + (paddingAmount * 4)
textureInfoArrayOffset = textureIdentifierArrayOffset + (paddingAmount * 4)
wtaTextureOffset = [0] * textureCount
wtaTextureSize = [0] * textureCount
wtaTextureIdentifier = [0] * textureCount
unknownArray1 = [0] * textureCount
textureInfoArray = []
paddingAmountArray = []
# Pad the DDS files
#pad_dds_files(texture_paths_array)
current_wtaTextureOffset = 0
if exportingForGame == "NIER":
# Open every DDS texture
for i in range(textureCount):
dds_fp = open(texture_paths_array[i], 'rb')
dds_paddedSize = os.stat(texture_paths_array[i]).st_size
#checks dds dxt and cube map info
dds_fp.seek(84)
dxt = dds_fp.read(4)
dds_fp.seek(112)
cube = dds_fp.read(4)
#finds how much padding bytes are added to a dds
dds_padding = 0
if i != len(texture_paths_array)-1:
dds_fp.seek(dds_paddedSize-4)
dds_padding = read_int32(dds_fp)
paddingAmountArray.append(dds_padding)
#wtaTextureOffset
if i+1 in range(len(wtaTextureSize)):
"""
if dds_paddedSize < 12289:
wtaTextureOffset[i+1] = wtaTextureOffset[i] + 12288
elif dds_paddedSize < 176129:
wtaTextureOffset[i+1] = wtaTextureOffset[i] + 176128
elif dds_paddedSize < 352257:
wtaTextureOffset[i+1] = wtaTextureOffset[i] + 352256
elif dds_paddedSize < 528385:
wtaTextureOffset[i+1] = wtaTextureOffset[i] + 528384
elif dds_paddedSize < 700417:
wtaTextureOffset[i+1] = wtaTextureOffset[i] + 700416
elif dds_paddedSize < 2797569:
wtaTextureOffset[i+1] = wtaTextureOffset[i] + 2797568
else:
wtaTextureOffset[i+1] = dds_paddedSize
"""
wtaTextureOffset[i+1] = current_wtaTextureOffset + dds_paddedSize
current_wtaTextureOffset += dds_paddedSize
#wtaTextureSize
wtaTextureSize[i] = dds_paddedSize# - dds_padding
#wtaTextureIdentifier
wtaTextureIdentifier[i] = identifiers_array[i]
#unknownArray1
if i in albedo_indexes:
unknownArray1[i] = 637534240
else:
unknownArray1[i] = 570425376
#unknownArray2
if dxt not in [b'DXT1', b'DXT3', b'DXT5']:
print("Unknown DXT format! Make sure you use DXT1, DXT3 or DXT5!")
dds_fp.close()
wta_fp.close()
return
if dxt == b'DXT1':
textureInfoArray.append(71)
textureInfoArray.append(3)
if cube == b'\x00\xfe\x00\x00':
textureInfoArray.append(4)
else:
textureInfoArray.append(0)
textureInfoArray.append(1)
textureInfoArray.append(0)
if dxt == b'DXT3':
textureInfoArray.append(74)
textureInfoArray.append(3)
if cube == b'\x00\xfe\x00\x00':
textureInfoArray.append(4)
else:
textureInfoArray.append(0)
textureInfoArray.append(1)
textureInfoArray.append(0)
if dxt == b'DXT5':
textureInfoArray.append(77)
textureInfoArray.append(3)
if cube == b'\x00\xfe\x00\x00':
textureInfoArray.append(4)
else:
textureInfoArray.append(0)
textureInfoArray.append(1)
textureInfoArray.append(0)
# Write WTP
dds_fp.seek(0)
content = dds_fp.read()
#print("-Writing dds: " + texture_paths_array[i] + " to file: " + export_filepath + " at position: " + str(i))
wtp_fp.write(content)
dds_fp.close()
dds_fp.close()
# Write everything
padding = b''
for i in range(paddingAmount - textureCount):
padding += b'\x00\x00\x00\x00'
wta_fp.write(b'WTB\x00')
wta_fp.write(to_bytes(unknown04))
wta_fp.write(to_bytes(textureCount))
wta_fp.write(to_bytes(textureOffsetArrayOffset))
wta_fp.write(to_bytes(textureSizeArrayOffset))
wta_fp.write(to_bytes(unknownArrayOffset1))
wta_fp.write(to_bytes(textureIdentifierArrayOffset))
wta_fp.write(to_bytes(textureInfoArrayOffset))
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureOffset[i]))
wta_fp.write(padding)
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureSize[i]))
wta_fp.write(padding)
for i in range(textureCount):
wta_fp.write(to_bytes(unknownArray1[i]))
wta_fp.write(padding)
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureIdentifier[i]))
wta_fp.write(padding)
for i in range(textureCount):
wta_fp.write(to_bytes(textureInfoArray[(i*5)]))
wta_fp.write(to_bytes(textureInfoArray[(i*5)+1]))
wta_fp.write(to_bytes(textureInfoArray[(i*5)+2]))
wta_fp.write(to_bytes(textureInfoArray[(i*5)+3]))
wta_fp.write(to_bytes(textureInfoArray[(i*5)+4]))
wta_fp.write(padding)
elif exportingForGame == "NIERSWITCH":
# NIER SWITCH
# Change offsets
textureSizeArrayOffset = math.ceil((textureOffsetArrayOffset + (textureCount * 4)) / 0x20) * 0x20
unknownArrayOffset1 = math.ceil((textureSizeArrayOffset + (textureCount * 4)) / 0x20) * 0x20
textureIdentifierArrayOffset = math.ceil((unknownArrayOffset1 + (textureCount * 4)) / 0x20) * 0x20
textureInfoArrayOffset = math.ceil((textureIdentifierArrayOffset + (textureCount * 4)) / 0x20) * 0x20
# Encode all PNG files to ASTC (asynchronously for speed)
tasks = []
for i in range(textureCount):
if ".png" in texture_paths_array[i].lower():
textureFormat = "ASTC_6x6_UNORM"
if identifiers_array[i].upper() in metadata.keys():
textureFormat = getFormatByIndex(metadata[identifiers_array[i].upper()]["format"])
tasks.append(asyncio.ensure_future(encode_astc(texture_paths_array[i], textureFormat)))
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
# Open every DDS texture
for i in range(textureCount):
path = texture_paths_array[i].replace(".png", ".astc")
#wtaTextureIdentifier
wtaTextureIdentifier[i] = identifiers_array[i]
wtaTextureOffset[i] = wtp_fp.tell()
# Default infos
info = {
"magic": b".tex",
"format": 0x7D,
"unk1": 1,
"width": 0,
"height": 0,
"depth": 1,
"mipCount": 1,
"unk2": 256,
"unk3": 0.25,
"unk4": 0,
# guesses used for swizzling (wtpImportOperator.py)
"type": 1,
"textureLayout": [4, 0],
"arrayCount": 1
}
if ".dds" in path.lower():
dds_fp = open(path, 'rb')
dds_paddedSize = os.stat(texture_paths_array[i]).st_size
dds_fp.seek(12)
info["width"] = int.from_bytes(dds_fp.read(4), "little")
info["height"] = int.from_bytes(dds_fp.read(4), "little")
#checks dds dxt and cube map info
dds_fp.seek(84)
dxt = dds_fp.read(4)
dds_fp.seek(112)
cube = dds_fp.read(4)
# DXT checking
if info["format"] == 0x7D: # If not prefilled
if dxt == b'DXT1':
info["format"] = 0x46 # BC1_UNORM_SRGB
elif dxt == b'DXT3':
info["format"] = 0x47 # BC2_UNORM_SRGB
elif dxt == b'DXT5':
info["format"] = 0x48 # BC3_UNORM_SRGB
else:
info["format"] = 0x50 # BC6H_UF16
#unknownArray1
unknownArray1[i] = 1677721632 # DDS textures are always SRGB
wtaTextureOffset[i] = wtp_fp.tell()
dds_fp.seek(0x80)
blockHeightLog2 = info["textureLayout"][0] & 7
wtp_fp.write(compressImageData(
getFormatByIndex(info['format']),
info['width'],
info['height'],
info['depth'],
info['arrayCount'],
info['mipCount'],
dds_fp.read(),
blockHeightLog2
))
wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i]
if wtaTextureSize[i] < 90112:
wtaTextureSize[i] = 90112
while wtp_fp.tell() < (wtaTextureSize[i] + wtaTextureOffset[i]):
wtp_fp.write(b'\x00')
info['imageSize'] = wtaTextureSize[i]
dds_fp.close()
else:
# ASTC
astc_fp = open(path, 'rb')
astc_fp.seek(7)
info["width"] = int.from_bytes(astc_fp.read(3), "little")
info["height"] = int.from_bytes(astc_fp.read(3), "little")
astc_fp.seek(16)
blockHeightLog2 = info["textureLayout"][0] & 7
wtp_fp.write(compressImageData(
getFormatByIndex(info['format']),
info['width'],
info['height'],
info['depth'],
info['arrayCount'],
info['mipCount'],
astc_fp.read(),
blockHeightLog2
))
wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i]
if wtaTextureSize[i] < 90112:
wtaTextureSize[i] = 90112
while wtp_fp.tell() < (wtaTextureSize[i] + wtaTextureOffset[i]):
wtp_fp.write(b'\x00')
info['imageSize'] = wtaTextureSize[i]
if "SRGB" in getFormatByIndex(info["format"]):
unknownArray1[i] = (1677721632)
else:
unknownArray1[i] = (1610612768)
astc_fp.close()
# Remove the temporary ASTC file
os.remove(path)
# MipCount needs to be set to 1 -- TODO: find what's causing this
#info["mipCount"] = int(math.log2(max(info["width"], info["height"]))) + 1
textureInfoArray.append(info)
wtp_fp.seek(math.ceil(wtp_fp.tell() / 16) * 16)
# Write everything
wta_fp.write(b'WTB\x00')
wta_fp.write(to_bytes(unknown04))
wta_fp.write(to_bytes(textureCount))
wta_fp.write(to_bytes(textureOffsetArrayOffset))
wta_fp.write(to_bytes(textureSizeArrayOffset))
wta_fp.write(to_bytes(unknownArrayOffset1))
wta_fp.write(to_bytes(textureIdentifierArrayOffset))
wta_fp.write(to_bytes(textureInfoArrayOffset))
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureOffset[i]))
wta_fp.seek(textureSizeArrayOffset)
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureSize[i]))
wta_fp.seek(unknownArrayOffset1)
for i in range(textureCount):
wta_fp.write(to_bytes(unknownArray1[i]))
wta_fp.seek(textureIdentifierArrayOffset)
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureIdentifier[i]))
for i in range(textureCount):
wta_fp.seek(textureInfoArrayOffset + i * 0x100)
wta_fp.write(textureInfoArray[i]["magic"])
write_Int32(wta_fp, textureInfoArray[i]["format"])
write_Int32(wta_fp, 1)
write_Int32(wta_fp, textureInfoArray[i]["width"])
write_Int32(wta_fp, textureInfoArray[i]["height"])
write_Int32(wta_fp, textureInfoArray[i]["depth"])
write_Int32(wta_fp, textureInfoArray[i]["mipCount"])
write_Int32(wta_fp, textureInfoArray[i]["unk2"])
write_float16(wta_fp, textureInfoArray[i]["unk3"])
write_uInt16(wta_fp, textureInfoArray[i]["unk4"])
while wta_fp.tell() % 16 != 0:
wta_fp.write(b'\x00')
elif exportingForGame == "ASTRALCHAIN":
# ASTRAL CHAIN
# Change offsets
textureSizeArrayOffset = math.ceil((textureOffsetArrayOffset + (textureCount * 4)) / 0x20) * 0x20
unknownArrayOffset1 = math.ceil((textureSizeArrayOffset + (textureCount * 4)) / 0x20) * 0x20
textureIdentifierArrayOffset = math.ceil((unknownArrayOffset1 + (textureCount * 4)) / 0x20) * 0x20
textureInfoArrayOffset = math.ceil((textureIdentifierArrayOffset + (textureCount * 4)) / 0x20) * 0x20
# Open metadata
if metadata_path:
with open(metadata_path, "r") as metadata_fp:
metadata = json.load(metadata_fp)
else:
metadata = {}
# Encode all PNG files to ASTC (asynchronously for speed)
tasks = []
for i in range(textureCount):
if ".png" in texture_paths_array[i].lower():
textureFormat = "ASTC_4x4_UNORM"
if identifiers_array[i].upper() in metadata.keys():
textureFormat = getFormatByIndex(metadata[identifiers_array[i].upper()]["format"])
tasks.append(asyncio.ensure_future(encode_astc(texture_paths_array[i], textureFormat)))
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
# Open every DDS texture
for i in range(textureCount):
path = texture_paths_array[i].replace(".png", ".astc")
#wtaTextureIdentifier
wtaTextureIdentifier[i] = identifiers_array[i]
wtaTextureOffset[i] = wtp_fp.tell()
# Default infos
info = {
"magic": b"XT1\x00",
"unk1": 16777473, # 0x 01 01 00 01
"imageSize": 0, # Uint64 identical to header (which is weird because the header is uint32, lol)
"headerSize": 56, # Always 56
"mipCount": 1,
"type": 1,
"format": 0x79,
"width": 0,
"height": 0,
"depth": 1,
"unk4": 32, # Always 32 (?)
"textureLayout": [1027, 65543],
"arrayCount": 1
}
if identifiers_array[i].upper() in metadata.keys():
info["type"] = metadata[identifiers_array[i].upper()]["type"]
info["format"] = metadata[identifiers_array[i].upper()]["format"]
info["textureLayout"] = metadata[identifiers_array[i].upper()]["textureLayout"]
if ".dds" in path.lower():
dds_fp = open(path, 'rb')
dds_paddedSize = os.stat(texture_paths_array[i]).st_size
dds_fp.seek(12)
info["width"] = int.from_bytes(dds_fp.read(4), "little")
info["height"] = int.from_bytes(dds_fp.read(4), "little")
#checks dds dxt and cube map info
dds_fp.seek(84)
dxt = dds_fp.read(4)
dds_fp.seek(112)
cube = dds_fp.read(4)
# DXT checking
if info["format"] == 0x79: # If not prefilled
if dxt == b'DXT1':
info["format"] = 0x46 # BC1_UNORM_SRGB
elif dxt == b'DXT3':
info["format"] = 0x47 # BC2_UNORM_SRGB
elif dxt == b'DXT5':
info["format"] = 0x48 # BC3_UNORM_SRGB
else:
info["format"] = 0x50 # BC6H_UF16
# Astral Chain uses no padding (i think); probably unnecessary
#unknownArray1
unknownArray1[i] = 1677721632 # DDS textures are always SRGB
wtaTextureOffset[i] = wtp_fp.tell()
dds_fp.seek(80)
blockHeightLog2 = info["textureLayout"][0] & 7
wtp_fp.write(compressImageData(
getFormatByIndex(info['format']),
info['width'],
info['height'],
info['depth'],
info['arrayCount'],
info['mipCount'],
dds_fp.read(),
blockHeightLog2
))
wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i]
if wtaTextureSize[i] < 90112:
wtaTextureSize[i] = 90112
while wtp_fp.tell() < (wtaTextureSize[i] + wtaTextureOffset[i]):
wtp_fp.write(b'\x00')
info['imageSize'] = wtaTextureSize[i]
dds_fp.close()
else:
# ASTC
astc_fp = open(path, 'rb')
astc_fp.seek(7)
info["width"] = int.from_bytes(astc_fp.read(3), "little")
info["height"] = int.from_bytes(astc_fp.read(3), "little")
astc_fp.seek(16)
blockHeightLog2 = info["textureLayout"][0] & 7
wtp_fp.write(compressImageData(
getFormatByIndex(info['format']),
info['width'],
info['height'],
info['depth'],
info['arrayCount'],
info['mipCount'],
astc_fp.read(),
blockHeightLog2
))
wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i]
if wtaTextureSize[i] < 90112:
wtaTextureSize[i] = 90112
while wtp_fp.tell() < (wtaTextureSize[i] + wtaTextureOffset[i]):
wtp_fp.write(b'\x00')
info['imageSize'] = wtaTextureSize[i]
if "SRGB" in getFormatByIndex(info["format"]):
unknownArray1[i] = (1677721632)
else:
unknownArray1[i] = (1610612768)
astc_fp.close()
# Remove the temporary ASTC file
os.remove(path)
# MipCount needs to be set to 1 -- TODO: find what's causing this
#info["mipCount"] = int(math.log2(max(info["width"], info["height"]))) + 1
textureInfoArray.append(info)
wtp_fp.seek(math.ceil(wtp_fp.tell() / 16) * 16)
# Write everything
wta_fp.write(b'WTB\x00')
wta_fp.write(to_bytes(unknown04))
wta_fp.write(to_bytes(textureCount))
wta_fp.write(to_bytes(textureOffsetArrayOffset))
wta_fp.write(to_bytes(textureSizeArrayOffset))
wta_fp.write(to_bytes(unknownArrayOffset1))
wta_fp.write(to_bytes(textureIdentifierArrayOffset))
wta_fp.write(to_bytes(textureInfoArrayOffset))
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureOffset[i]))
wta_fp.seek(textureSizeArrayOffset)
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureSize[i]))
wta_fp.seek(unknownArrayOffset1)
for i in range(textureCount):
wta_fp.write(to_bytes(unknownArray1[i]))
wta_fp.seek(textureIdentifierArrayOffset)
for i in range(textureCount):
wta_fp.write(to_bytes(wtaTextureIdentifier[i]))
wta_fp.seek(textureInfoArrayOffset)
for i in range(textureCount):
wta_fp.write(textureInfoArray[i]["magic"])
write_Int32(wta_fp, textureInfoArray[i]["unk1"])
write_Int32(wta_fp, 0)
write_Int32(wta_fp, textureInfoArray[i]["imageSize"])
write_Int32(wta_fp, textureInfoArray[i]["headerSize"])
write_Int32(wta_fp, textureInfoArray[i]["mipCount"])
write_Int32(wta_fp, textureInfoArray[i]["type"])
write_Int32(wta_fp, textureInfoArray[i]["format"])
write_Int32(wta_fp, textureInfoArray[i]["width"])
write_Int32(wta_fp, textureInfoArray[i]["height"])
write_Int32(wta_fp, textureInfoArray[i]["depth"])
write_Int32(wta_fp, textureInfoArray[i]["unk4"])
write_Int32(wta_fp, textureInfoArray[i]["textureLayout"][0])
write_Int32(wta_fp, textureInfoArray[i]["textureLayout"][1])
while wta_fp.tell() % 16 != 0:
wta_fp.write(b'\x00')
wta_fp.close()
wtp_fp.close()
print('WTA + WTP Export Complete. :]}')
+15 -8
View File
@@ -1,14 +1,16 @@
import string
import os
from ...utils.util import ShowMessageBox
def generate(context):
def generate(context, exportingForGame):
wta_data = context.scene.WTAMaterials
identifiers_array = []
texture_paths_array = []
albedo_indexes = []
metadata_path = None
true_index = 0
for index, texture in enumerate(wta_data):
@@ -18,28 +20,33 @@ def generate(context):
# Check if identifier is 8 chars long
if len( texture.texture_identifier) != 8:
print('[!] WTA/WTP Export Error: A texture identifier is not 8 characters long.')
ShowMessageBox('A texture identifier is not 8 characters long.', 'WTA/WTP Export Error', 'ERROR')
return None, None, None
print('[!] WTA/WTP Export Error: A texture identifier is not characters long.')
ShowMessageBox('A texture identifier is not characters long.', 'WTA/WTP Export Error', 'ERROR')
return None, None, None, None
# Check if identifier is valid hex
if not all(c in string.hexdigits for c in texture.texture_identifier):
print('[!] WTA/WTP Export Error: A texture identifier contains a non-hex character.')
ShowMessageBox('A texture identifier contains a non-hex character.', 'WTA/WTP Export Error', 'ERROR')
return None, None, None
return None, None, None, None
# Assign Identifier.
identifiers_array.append(texture.texture_identifier)
# Check if game metadata exists.
xt1MetadataPath = os.path.join(*texture.texture_path.split('/')[:-1], "xt1_info.json")
if exportingForGame == "ASTRALCHAIN" and not metadata_path and os.path.exists(xt1MetadataPath):
metadata_path = xt1MetadataPath
# Check if path is valid and assign.
if not texture.texture_path.lower().endswith('.dds'):
if not texture.texture_path.lower().endswith('.dds') and not texture.texture_path.lower().endswith('.png'):
if texture.parent_mat == "":
print('[!] WTA/WTP Export Error: A manual ' + texture.texture_map_type + ' texture does not have a valid texture assigned.')
ShowMessageBox('A manual ' + texture.texture_map_type + ' texture does not have a valid texture assigned.', 'WTA/WTP Export Error', 'ERROR')
else:
print('[!] WTA/WTP Export Error: A texture in material', texture.parent_mat, 'does not have a valid path assigned.')
ShowMessageBox(texture.parent_mat + ' does not have a valid texture assigned to ' + texture.texture_map_type, 'WTA/WTP Export Error', 'ERROR')
return None, None, None
return None, None, None, None
texture_paths_array.append(texture.texture_path)
@@ -49,4 +56,4 @@ def generate(context):
true_index += 1
return identifiers_array, texture_paths_array, albedo_indexes
return identifiers_array, texture_paths_array, albedo_indexes, metadata_path
+5 -5
View File
@@ -118,7 +118,7 @@ def handleAutoSetTextureWarnings(operatorSelf, warnings: List[str]):
print("\n".join(warnings))
def isTextureTypeSupported(textureType: str) -> bool:
for supportedTex in ['g_AlbedoMap', 'g_MaskMap', 'g_NormalMap', 'g_EnvMap', 'g_DetailNormalMap', 'g_IrradianceMap', 'g_CurvatureMap', 'g_SpreadPatternMap', 'g_LUT', 'g_LightMap', 'g_GradationMap', 'g_ParallaxMap']:
for supportedTex in ['g_AlbedoMap', 'g_ColorMask', 'g_MaskMap', 'g_NormalMap', 'g_EnvMap', 'g_DetailNormalMap', 'g_IrradianceMap', 'g_CurvatureMap', 'g_SpreadPatternMap', 'g_LUT', 'g_LightMap', 'g_GradationMap', 'g_ParallaxMap']:
if supportedTex in textureType:
return True
return False
@@ -286,8 +286,8 @@ class ExportWTAOperator(bpy.types.Operator, ExportHelper):
filter_glob: StringProperty(default="*.wta", options={'HIDDEN'})
def execute(self, context):
from . import export_wta
export_wta.main(context, self.filepath)
from . import export_wta_wtp
export_wta_wtp.main(context, self.filepath)
return{'FINISHED'}
class FilepathSelector(bpy.types.Operator, ImportHelper):
@@ -297,7 +297,7 @@ class FilepathSelector(bpy.types.Operator, ImportHelper):
bl_options = {"UNDO"}
filename_ext = ".dds"
filter_glob: StringProperty(default="*.dds", options={'HIDDEN'})
filter_glob: StringProperty(default="*.dds,*.png", options={'HIDDEN'})
id : bpy.props.IntProperty(options={'HIDDEN'})
@@ -602,7 +602,7 @@ class WTA_WTP_PT_Hints(bpy.types.Panel):
row = box.row()
row.label(text='- Texture identifier has to be 8 HEX characters long.')
row = box.row()
row.label(text='- Textures have to be in DDS format (DXT1, DXT3, DXT5).')
row.label(text='- Textures have to be in DDS format (DXT1, DXT3, DXT5) for NieR, or PNG (converting to ASTC) for Switch games.')
row = box.row()
row.label(text='- It is recommended to "Sync Identifiers in Materials" before WMB export.')
+177 -12
View File
@@ -2,7 +2,13 @@ import os
import bpy
from bpy.props import StringProperty
from bpy_extras.io_utils import ImportHelper
from struct import pack
import subprocess
import asyncio
import json
from ...utils import ioUtils as io
from ..tegrax1swizzle import getFormatByIndex, getFormatTable, loadImageData
class WTAData:
def __init__(self, f, wtpFile) -> None:
@@ -41,14 +47,54 @@ class WTAData:
self.idx.append(io.read_uint32(f))
# Texture Info
"""
f.seek(self.offsetTextureInfo)
self.infos = []
for i in range(self.num_files):
info = []
info.append(io.read_uint32(f))
info.append([io.read_uint32(f) for x in range(4)])
self.infos.append(info)
"""
infoFormat = f.read(4)
if infoFormat == b'XT1\x00': # Astral Chain, Bayonetta 3 WTA format
self.type = "XT1"
f.seek(f.tell() - 4)
for i in range(self.num_files):
info = {
"magic": f.read(4),
"unk1": io.read_uint32(f),
"imageSize": io.read_uint64(f),
"headerSize": io.read_uint32(f),
"mipCount": io.read_uint32(f),
"type": io.read_uint32(f),
"format": io.read_uint32(f),
"width": io.read_uint32(f),
"height": io.read_uint32(f),
"depth": io.read_uint32(f),
"unk4": io.read_uint32(f),
"textureLayout": [io.read_uint32(f), io.read_uint32(f)],
"arrayCount": 1
}
if info["type"] == 3 or info["type"] == 8: # T_Cube or T_Cube_Array
info["arrayCount"] = 6
self.infos.append(info)
elif infoFormat == b'.tex': # NieR Switch WTA format
self.type = "TEX"
for i in range(self.num_files):
f.seek(self.offsetTextureInfo + i * 0x100)
info = {
"magic": f.read(4),
"format": io.read_uint32(f),
"unk1": io.read_uint32(f),
"width": io.read_uint32(f),
"height": io.read_uint32(f),
"depth": io.read_uint32(f),
"mipCount": io.read_uint32(f),
"unk2": io.read_uint32(f),
"unk3": io.read_float16(f),
"unk4": io.read_uint16(f),
"type": 1,
"textureLayout": [4, 0],
"arrayCount": 1
}
self.infos.append(info)
else:
self.type = "PC"
self.data = wtpFile.read()
@@ -58,13 +104,132 @@ class WTAData:
count = 0
fileName = os.path.basename(self.wtaPath)
dir = os.path.dirname(self.wtaPath)
for i in range(self.num_files):
os.makedirs(extractionDir, exist_ok=True)
with open(os.path.join(extractionDir, f"{self.idx[i]:0>8X}.dds"), "wb") as f:
f.write(self.data[self.offsets[i]:self.offsets[i]+self.sizes[i]])
count += 1
if self.type == "PC":
for i in range(self.num_files):
os.makedirs(extractionDir, exist_ok=True)
with open(os.path.join(extractionDir, f"{self.idx[i]:0>8X}.dds"), "wb") as f:
f.write(self.data[self.offsets[i]:self.offsets[i]+self.sizes[i]])
count += 1
else:
# Switch games must construct the DDS/ASTC headers manually.
tasks = []
for i in range(self.num_files):
os.makedirs(extractionDir, exist_ok=True)
# Unswizzle
textureFormat = getFormatByIndex(self.infos[i]["format"])
blockHeightLog2 = self.infos[i]["textureLayout"][0] & 7
texture = loadImageData(
textureFormat,
self.infos[i]['width'],
self.infos[i]['height'],
self.infos[i]['depth'],
self.infos[i]['arrayCount'],
self.infos[i]['mipCount'],
self.data[self.offsets[i]:self.offsets[i]+self.sizes[i]],
blockHeightLog2
)
# Construct headers
if "ASTC" in textureFormat:
# ASTC
formatInfo = getFormatTable(textureFormat)
with open(os.path.join(extractionDir, f"{self.idx[i]:0>8X}.astc"), "wb") as f:
f.write(b''.join([
b'\x13\xAB\xA1\x5C', formatInfo[1].to_bytes(1, "little"),
formatInfo[2].to_bytes(1, "little"), b'\1',
self.infos[i]['width'].to_bytes(3, "little"),
self.infos[i]['height'].to_bytes(3, "little"), b'\1\0\0',
texture,
]))
tasks.append(asyncio.ensure_future(decode_astc(os.path.join(extractionDir, f"{self.idx[i]:0>8X}.astc"))))
else:
# DDS
headerDataObject = DDSHeader(textureFormat, self.infos[i]['width'], self.infos[i]['height'], self.infos[i]['depth'])
with open(os.path.join(extractionDir, f"{self.idx[i]:0>8X}.dds"), "wb") as f:
f.write(headerDataObject.save())
f.write(texture)
count += 1
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
# Write metadata file (need to store format and textureLayout for later)
metadata = {}
for i in range(self.num_files):
metadata[f"{self.idx[i]:0>8X}"] = {
"type": self.infos[i]["type"],
"format": self.infos[i]["format"],
"textureLayout": self.infos[i]["textureLayout"]
}
with open(os.path.join(extractionDir, "xt1_info.json"), "w") as f:
f.write(json.dumps(metadata, indent=4))
return count
# Asynchronously decodes an ASTC file to PNG.
async def decode_astc(filePath):
script_file = os.path.realpath(__file__)
directory = os.path.dirname(script_file).replace("importer", "")
appPath = os.path.join(directory, "astcenc-avx2.exe")
result = subprocess.run([appPath, "-ds", filePath, filePath.replace('.astc', '.png')], cwd=directory)
os.remove(filePath)
# WAY too lazy to reimplement this; remind me later i guess?
class DDSHeader(object):
# https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
class DDSPixelFormat(object):
def __init__(self, textureFormat):
self.size = 32
self.flags = 4 # contains fourcc
if textureFormat == "BC6H_UF16":
self.fourCC = b'DX10'
elif textureFormat.startswith("BC1"):
self.fourCC = b'DXT1'
elif textureFormat.startswith("BC2"):
self.fourCC = b'DXT3'
else:
self.fourCC = b'DXT5'
# BC1 = DXT1, BC2 = DXT3, above is DXT5 i think; BC6H is the only DX10 format
self.RGBBitCount = 0
self.RBitMask = 0x00000000
self.GBitMask = 0x00000000
self.BBitMask = 0x00000000
self.ABitMask = 0x00000000
def __init__(self, textureFormat, width, height, depth):
self.magic = b'DDS\x20'
self.size = 124
self.flags = 0x1 + 0x2 + 0x4 + 0x1000 + 0x20000 + 0x80000 # Defaults (caps, height, width, pixelformat) + mipmapcount and linearsize
self.height = height
self.width = width
self.format = textureFormat
if self.format == "R8G8B8A8_UNORM":
self.pitchOrLinearSize = ((self.width + 1) >> 1) * 4
else:
self.pitchOrLinearSize = int(max(1, ((self.width+3)/4) ) * getFormatTable(self.format)[0]) # https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
self.depth = depth
self.mipmapCount = 1#texture.mipCount # Setting this to the normal value breaks everything, don't do that
self.reserved1 = [0x00000000] * 11
self.ddspf = self.DDSPixelFormat(textureFormat)
self.caps = 4198408 # Defaults (DDSCAPS_TEXTURE) + mipmap and complex
self.caps2 = 0
self.caps3 = 0
self.caps4 = 0
self.reserved2 = 0
def save(self):
output = self.magic + pack("20I4s10I", self.size, self.flags, self.height, self.width, self.pitchOrLinearSize, self.depth,
self.mipmapCount, self.reserved1[0], self.reserved1[1], self.reserved1[2], self.reserved1[3], self.reserved1[4],
self.reserved1[5], self.reserved1[6], self.reserved1[7], self.reserved1[8], self.reserved1[9], self.reserved1[10],
self.ddspf.size, self.ddspf.flags, self.ddspf.fourCC, self.ddspf.RGBBitCount, self.ddspf.RBitMask, self.ddspf.GBitMask,
self.ddspf.BBitMask, self.ddspf.ABitMask, self.caps, self.caps2, self.caps3, self.caps4, self.reserved2)
if self.format == "BC6H_UF16":
output += bytearray(b"\x5F\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00")
return output
class ExtractNierWtaWtp(bpy.types.Operator, ImportHelper):
'''Extract textures from WTA/WTP files'''
bl_idname = "import_scene.nier_wta_wtp"
+255
View File
@@ -0,0 +1,255 @@
# TegraX1Swizzle.py - cabalex [Updated Dec 2022]
# Based on:
# KillzXGaming's Switch Toolbox texture decoding - https://github.com/KillzXGaming/Switch-Toolbox/blob/604f7b3d369bc97d9d05632da3211ed11b990ba7/Switch_Toolbox_Library/Texture%20Decoding/Switch/TegraX1Swizzle.cs
# aboood40091's BNTX-Extractor - https://github.com/aboood40091/BNTX-Extractor/blob/master/swizzle.py
# [Format table] Ryujinx's image table - https://github.com/Ryujinx/Ryujinx/blob/c86aacde76b5f8e503e2b412385c8491ecc86b3b/Ryujinx.Graphics/Graphics3d/Texture/ImageUtils.cs
formatTable = {
"R8G8B8A8_UNORM": [4, 1, 1, 1],
"BC1_UNORM": [8, 4, 4, 1],
"BC2_UNORM": [16, 4, 4, 1],
"BC3_UNORM": [16, 4, 4, 1],
"BC4_UNORM": [8, 4, 4, 1],
"BC1_UNORM_SRGB": [8, 4, 4, 1],
"BC2_UNORM_SRGB": [16, 4, 4, 1],
"BC3_UNORM_SRGB": [16, 4, 4, 1],
"BC4_SNORM": [8, 4, 4, 1],
"BC6H_UF16": [16, 4, 4, 1],
"ASTC_4x4_UNORM": [16, 4, 4, 1],
"ASTC_6x6_UNORM": [16, 6, 6, 1],
"ASTC_8x8_UNORM": [16, 8, 8, 1],
"ASTC_4x4_SRGB": [16, 4, 4, 1],
"ASTC_6x6_SRGB": [16, 6, 6, 1],
"ASTC_8x8_SRGB": [16, 8, 8, 1]
}
# each one: bytesPerPixel, blockWidth, blockHeight, blockDepth, targetBuffer (but i removed targetBuffer)
formats = {
# DDS
0x25: "R8G8B8A8_UNORM",
0x42: "BC1_UNORM",
0x43: "BC2_UNORM",
0x44: "BC3_UNORM",
0x45: "BC4_UNORM",
0x46: "BC1_UNORM_SRGB",
0x47: "BC2_UNORM_SRGB",
0x48: "BC3_UNORM_SRGB",
0x49: "BC4_SNORM",
0x50: "BC6H_UF16",
# ASTC (weird texture formats ??)
0x2D: "ASTC_4x4_UNORM",
0x38: "ASTC_8x8_UNORM",
0x3A: "ASTC_12x12_UNORM",
# ASTC
0x79: "ASTC_4x4_UNORM",
0x80: "ASTC_8x8_UNORM",
0x87: "ASTC_4x4_SRGB",
0x8E: "ASTC_8x8_SRGB",
# Unknown NieR switch formats
0x7D: "ASTC_6x6_UNORM",
0x8B: "ASTC_6x6_SRGB",
}
def getFormatTable(_format):
return formatTable[_format]
def getFormatByIndex(_format):
return formats[_format]
def pow2_round_up(x):
x -= 1
x |= x >> 1
x |= x >> 2
x |= x >> 4
x |= x >> 8
x |= x >> 16
return x + 1
def DIV_ROUND_UP(n, d):
return (n + d - 1) // d
def subArray(data, offset, length):
return data[offset:offset+length]
def round_up(x, y):
return ((x - 1) | (y - 1)) + 1
def _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, blockHeightLog2, data, toSwizzle):
block_height = 1 << blockHeightLog2
width = DIV_ROUND_UP(width, blkWidth)
height = DIV_ROUND_UP(height, blkHeight)
depth = DIV_ROUND_UP(depth, blkDepth)
if tileMode == 1:
if roundPitch == 1:
pitch = round_up(width * bpp, 32)
else:
pitch = width * bpp
surfSize = round_up(pitch * height, 32)
else:
pitch = round_up(width * bpp, 64)
surfSize = pitch * round_up(height, block_height * 8)
result = bytearray(surfSize)
for y in range(height):
for x in range(width):
if tileMode == 1:
pos = y * pitch + x * bpp
else:
pos = getAddrBlockLinear(x, y, width, bpp, 0, block_height)
pos_ = (y * width + x) * bpp
if pos + bpp <= surfSize:
if toSwizzle == 1:
result[pos:pos + bpp] = data[pos_:pos_ + bpp]
else:
result[pos_:pos_ + bpp] = data[pos:pos + bpp]
size = width * height * bpp
return result[:size]
#def deswizzle(width, height, blkWidth, blkHeight, bpp, tileMode, alignment, size_range, data):
def deswizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, data):
return _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, bytes(data), 0)
#return _swizzle(width, height, blkWidth, blkHeight, bpp, tileMode, alignment, size_range, bytes(data), 0)
def swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, data):
return _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, bytes(data), 1)
def getAddrBlockLinear(x, y, image_width, bytes_per_pixel, base_address, block_height):
"""
From the Tegra X1 TRM
"""
image_width_in_gobs = DIV_ROUND_UP(image_width * bytes_per_pixel, 64)
GOB_address = (base_address
+ (y // (8 * block_height)) * 512 * block_height * image_width_in_gobs
+ (x * bytes_per_pixel // 64) * 512 * block_height
+ (y % (8 * block_height) // 8) * 512)
x *= bytes_per_pixel
Address = (GOB_address + ((x % 64) // 32) * 256 + ((y % 8) // 2) * 64
+ ((x % 32) // 16) * 32 + (y % 2) * 16 + (x % 16))
return Address
def loadImageData(format: str, width: int, height: int, depth: int, arrayCount: int, mipCount: int, imageData, blockHeightLog2, target=1, linearTileMode=False):
[bpp, blkWidth, blkHeight, blkDepth] = getFormatTable(format)
blockHeight = DIV_ROUND_UP(height, blkHeight)
pitch = 0
dataAlignment = 512
if linearTileMode:
tileMode = 1
else:
tileMode = 0
if depth > 1:
numDepth = depth
else:
numDepth = 1
linesPerBlockHeight = (1 << int(blockHeightLog2)) * 8
arrayOffset = 0
for depthLevel in range(numDepth):
for arrayLevel in range(arrayCount):
surfaceSize = 0
blockHeightShift = 0
mipOffsets = []
for mipLevel in range(mipCount):
width = max(1, width >> mipLevel)
height = max(1, height >> mipLevel)
depth = max(1, depth >> mipLevel)
size = DIV_ROUND_UP(width, blkWidth) * DIV_ROUND_UP(height, blkHeight) * bpp
if pow2_round_up(DIV_ROUND_UP(height, blkWidth)) < linesPerBlockHeight:
blockHeightShift += 1
width__ = DIV_ROUND_UP(width, blkWidth)
height__ = DIV_ROUND_UP(height, blkHeight)
# calculate the mip size instead
alignedData = bytearray(round_up(surfaceSize, dataAlignment) - surfaceSize)
surfaceSize += len(alignedData)
mipOffsets.append(surfaceSize)
# get the first mip offset and current one and the total image size
msize = int((mipOffsets[0] + len(imageData) - mipOffsets[mipLevel]) / arrayCount)
data_ = subArray(imageData, arrayOffset + mipOffsets[mipLevel], msize)
try:
pitch = round_up(width__ * bpp, 64)
surfaceSize += pitch * round_up(height__, max(1, blockHeight >> blockHeightShift) * 8)
result = deswizzle(width, height, depth, blkWidth, blkHeight, blkDepth, target, bpp, tileMode, max(0, blockHeightLog2 - blockHeightShift), data_)
# the program creates a copy and uses that to remove unneeded data
# yeah, i'm not doing that
return result
except Exception as e:
raise e
print(f"Failed to swizzle texture! {e}")
return False
arrayOffset += len(imageData) / arrayCount
return False
def compressImageData(format: str, width: int, height: int, depth: int, arrayCount: int, mipCount: int, imageData, blockHeightLog2, target=1, linearTileMode=False):
bpp = formatTable[format][0]
blkWidth = formatTable[format][1]
blkHeight = formatTable[format][2]
blkDepth = formatTable[format][3]
blockHeight = DIV_ROUND_UP(height, blkHeight)
pitch = 0
dataAlignment = 512
if linearTileMode:
tileMode = 1
else:
tileMode = 0
if depth > 1:
numDepth = depth
else:
numDepth = 1
linesPerBlockHeight = (1 << int(blockHeightLog2)) * 8
arrayOffset = 0
for depthLevel in range(numDepth):
for arrayLevel in range(arrayCount):
surfaceSize = 0
blockHeightShift = 0
mipOffsets = []
for mipLevel in range(mipCount):
width = max(1, width >> mipLevel)
height = max(1, height >> mipLevel)
depth = max(1, depth >> mipLevel)
size = DIV_ROUND_UP(width, blkWidth) * DIV_ROUND_UP(height, blkHeight) * bpp
if pow2_round_up(DIV_ROUND_UP(height, blkWidth)) < linesPerBlockHeight:
blockHeightShift += 1
width__ = DIV_ROUND_UP(width, blkWidth)
height__ = DIV_ROUND_UP(height, blkHeight)
# calculate the mip size instead
alignedData = bytearray(round_up(surfaceSize, dataAlignment) - surfaceSize)
surfaceSize += len(alignedData)
mipOffsets.append(surfaceSize)
# get the first mip offset and current one and the total image size
msize = int((mipOffsets[0] + len(imageData) - mipOffsets[mipLevel]) / arrayCount)
data_ = subArray(imageData, arrayOffset + mipOffsets[mipLevel], msize)
try:
pitch = round_up(width__ * bpp, 64)
surfaceSize += pitch * round_up(height__, max(1, blockHeight >> blockHeightShift) * 8)
result = swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, target, bpp, tileMode, max(0, blockHeightLog2 - blockHeightShift), data_)
# the program creates a copy and uses that to remove unneeded data
# yeah, i'm not doing that
return result
except Exception as e:
raise e
print(f"Failed to swizzle texture! {e}")
return False
arrayOffset += len(imageData) / arrayCount
return False