From 6f8bebf9d6e07d07b464edabd9e22aec316445a4 Mon Sep 17 00:00:00 2001 From: Cainan Date: Fri, 5 Jul 2024 20:00:26 +0100 Subject: [PATCH] Update Wii U 3 Texture Tool --- TaikoWiiUSongTextureTool/README.md | 18 +- TaikoWiiUSongTextureTool/generate.py | 195 +++++++++++++++--- TaikoWiiUSongTextureTool/generate_nut.py | 249 +++++++++++++++-------- 3 files changed, 338 insertions(+), 124 deletions(-) diff --git a/TaikoWiiUSongTextureTool/README.md b/TaikoWiiUSongTextureTool/README.md index 91923e2..f4ae733 100644 --- a/TaikoWiiUSongTextureTool/README.md +++ b/TaikoWiiUSongTextureTool/README.md @@ -3,9 +3,23 @@ Tool to generate song textures for Taiko no Tatsujin Wii U 1-3, using a modern wordlist.json file. Only supports Japanese text for now. -Usage: python generate.py song_id genreNo +Usage: generate.py [-ura] There is also an additional script in here to convert the folder of textures to a .nut texture. The code in this was partially based on the NUT code found in [Smash Forge](https://github.com/jam1garner/Smash-Forge) -Usage: generate_nut.py input_folder output_file \ No newline at end of file +Usage: generate_nut.py [-h] [--format {dxt1,dxt3,dxt5}] input_folder output_file + +Generate NUT file from PNG files. + +positional arguments: + input_folder Input folder containing PNG files. + output_file Output NUT file. + +options: + -h, --help show this help message and exit + --format {dxt1,dxt3,dxt5} + Texture compression format. + +Requirements: +[NVIDIA Texture Tools Exporter](https://developer.nvidia.com/texture-tools-exporter) installed and added to PATH \ No newline at end of file diff --git a/TaikoWiiUSongTextureTool/generate.py b/TaikoWiiUSongTextureTool/generate.py index fc9148b..3599d64 100644 --- a/TaikoWiiUSongTextureTool/generate.py +++ b/TaikoWiiUSongTextureTool/generate.py @@ -26,7 +26,9 @@ rotated_chars = { '《': '︽', '》': '︾', '【': '︻', '】': '︼', '〔': '︹', '〕': '︺', - '~': '|', '~': '|' + '~': '|', '~': '|', + '(': '︵', ')': '︶', + '-': 'l' } rotated_letters = { @@ -94,7 +96,7 @@ def generate_image(draw, text, font, rotated_font, size, position, alignment, st char = rotated_chars.get(char, char) char = rotated_letters.get(char, char) text_bbox = get_text_bbox(draw, char, char_font) - char_height = 40 + char_height = (text_bbox[3] + text_bbox[1]) char_width = text_bbox[2] - text_bbox[0] draw.text((text_position[0] - char_width / 2, y_offset), char, font=char_font, fill=fill, stroke_width=stroke_width, stroke_fill=stroke_fill) y_offset += char_height @@ -102,11 +104,10 @@ def generate_image(draw, text, font, rotated_font, size, position, alignment, st y_offset = 5 for char in text: char_font = rotated_font if char in rotated_chars else font - char = rotated_letters.get(char, char) char = rotated_chars.get(char, char) char = rotated_letters.get(char, char) text_bbox = get_text_bbox(draw, char, char_font) - char_height = 27 + char_height = (text_bbox[3] + text_bbox[1]) char_width = text_bbox[2] - text_bbox[0] draw.text((text_position[0] - char_width / 2, y_offset), char, font=char_font, fill=fill, stroke_width=stroke_width, stroke_fill=stroke_fill) y_offset += char_height @@ -158,7 +159,12 @@ def create_images(data, id, genreNo, font_path, rotated_font_path, append_ura=Fa if append_ura: japanese_text += "─" - japanese_text += " " + #japanese_text = " " + japanese_text + #japanese_text += " " + + padded_japanese_text = japanese_text + " " + even_more_padded_japanese_text = " " + japanese_text + padded_japanese_sub_text = japanese_sub_text + " " # Check if texts were found if not japanese_text: @@ -174,24 +180,149 @@ def create_images(data, id, genreNo, font_path, rotated_font_path, append_ura=Fa rotated_font = ImageFont.truetype(rotated_font_path, int(font_size_medium)) # Image 0.png - img0 = Image.new('RGBA', (720, 64), color=(0, 0, 0, 0)) + img0_width = 720 + + img0 = Image.new('RGBA', (img0_width, 64), color=(0, 0, 0, 0)) draw0 = ImageDraw.Draw(img0) - generate_image(draw0, japanese_text, font_large, rotated_font, (720, 64), (17, 10), 'right', 5, 'black', 'white') - img0.save(os.path.join(folder_name, '0.png')) + + temp_img0 = Image.new('RGBA', (2880, 64), (0, 0, 0, 0)) # Temporary image with 2880px width + temp_draw0 = ImageDraw.Draw(temp_img0) + + # Generate the image with the Japanese text + generate_image(temp_draw0, even_more_padded_japanese_text, font_large, rotated_font, (2880, 64), (0, 10), 'right', 5, 'black', 'white') + + # Calculate the bounding box of the entire text + text_bbox = get_text_bbox(temp_draw0, japanese_text, font_large) + text_width = (text_bbox[2] - text_bbox[0]) + 5 + + # Resize the image if it exceeds the specified height + if text_width > img0_width: + cropped_img0 = temp_img0.crop((2880 - text_width, 0, 2880, 64)) + + scaled_img0 = cropped_img0.resize((img0_width, 64), Image.Resampling.LANCZOS) + + final_img0 = Image.new('RGBA', (img0_width, 64), (0, 0, 0, 0)) + final_img0.paste(scaled_img0) + else: + # Crop the temporary image to the actual width of the text + cropped_img0 = temp_img0.crop((2880 - text_width, 0, 2880, 64)) + final_img0 = Image.new('RGBA', (img0_width, 64), (0, 0, 0, 0)) + final_img0.paste(cropped_img0, (img0_width - text_width, 0)) + + # Create a new image with the specified width and right-align the text + #final_img0 = Image.new('RGBA', (img0_width, 64), (0, 0, 0, 0)) + #final_img0.paste(cropped_img, (img0_width - text_width, 0)) + + # Save the final image + final_img0.save(os.path.join(folder_name, '0.png')) # Image 1.png - img1 = Image.new('RGBA', (720, 104), color=(0, 0, 0, 0)) + img1_width = 720 + + img1 = Image.new('RGBA', (img1_width, 104), color=(0, 0, 0, 0)) draw1 = ImageDraw.Draw(img1) - generate_image(draw1, japanese_text, font_extra_large, rotated_font, (720, 104), (0, 13), 'center', 5, 'black', 'white') - generate_image(draw1, japanese_sub_text, font_medium, rotated_font, (720, 104), (0, 68), 'center', 4, 'black', 'white') - img1.save(os.path.join(folder_name, '1.png')) + + temp_img1 = Image.new('RGBA', (2880, 104), (0, 0, 0, 0)) # Temporary image with 2880px width + temp_draw1 = ImageDraw.Draw(temp_img1) + + temp_sub_img1 = Image.new('RGBA', (2880, 104), (0, 0, 0, 0)) # Temporary image with 2880px width + temp_sub_draw1 = ImageDraw.Draw(temp_sub_img1) + + # Generate the image with the Japanese text + generate_image(temp_draw1, japanese_text, font_extra_large, rotated_font, (2880, 104), (0, 13), 'center', 5, 'black', 'white') + + # Calculate the bounding box of the entire text + text_bbox = get_text_bbox(temp_draw1, japanese_text, font_extra_large) + text_width = (text_bbox[2] - text_bbox[0]) + 5 + + # Resize the image if it exceeds the specified width + if text_width > img1_width: + # Calculate the crop box to crop equally from both sides + left_crop = (2880 - text_width) // 2 + right_crop = 2880 - left_crop + cropped_img1 = temp_img1.crop((left_crop, 0, right_crop, 104)) + scaled_img1 = cropped_img1.resize((img1_width, 104), Image.Resampling.LANCZOS) + img1_1 = Image.new('RGBA', (img1_width, 104), (0, 0, 0, 0)) + img1_1.paste(scaled_img1) + else: + # Crop the temporary image to the actual width of the text + left_crop = (2880 - text_width) // 2 + right_crop = 2880 - left_crop + cropped_img1 = temp_img1.crop((left_crop, 0, right_crop, 104)) + img1_1 = Image.new('RGBA', (img1_width, 104), (0, 0, 0, 0)) + offset = (img1_width - text_width) // 2 + img1_1.paste(cropped_img1, (offset, 0)) + + # Generate the image with the Japanese sub-text + generate_image(temp_sub_draw1, japanese_sub_text, font_medium, rotated_font, (2880, 104), (0, 68), 'center', 4, 'black', 'white') + + # Calculate the bounding box of the entire sub-text + text_bbox_sub = get_text_bbox(temp_sub_draw1, japanese_sub_text, font_medium) + text_width_sub = (text_bbox_sub[2] - text_bbox_sub[0]) + 5 + + # Resize the sub-image if it exceeds the specified width + if text_width_sub > img1_width: + # Calculate the crop box to crop equally from both sides + left_crop_sub = (2880 - text_width_sub) // 2 + right_crop_sub = 2880 - left_crop_sub + cropped_img1_sub = temp_sub_img1.crop((left_crop_sub, 0, right_crop_sub, 104)) + scaled_img1_sub = cropped_img1_sub.resize((img1_width, 104), Image.Resampling.LANCZOS) + img1_2 = Image.new('RGBA', (img1_width, 104), (0, 0, 0, 0)) + img1_2.paste(scaled_img1_sub) + else: + # Crop the temporary sub-image to the actual width of the sub-text + left_crop_sub = (2880 - text_width_sub) // 2 + right_crop_sub = 2880 - left_crop_sub + cropped_img1_sub = temp_sub_img1.crop((left_crop_sub, 0, right_crop_sub, 104)) + img1_2 = Image.new('RGBA', (img1_width, 104), (0, 0, 0, 0)) + offset_sub = (img1_width - text_width_sub) // 2 + img1_2.paste(cropped_img1_sub, (offset_sub, 0)) + + final_img1 = Image.new('RGBA', (img1_width, 104), (0, 0, 0, 0)) + final_img1.paste(img1_1, (0, 0)) + final_img1.paste(img1_2, (0, 0), img1_2) + final_img1.save(os.path.join(folder_name, '1.png')) + # Image 2.png - img2 = Image.new('RGBA', (720, 64), color=(0, 0, 0, 0)) - draw2 = ImageDraw.Draw(img2) - generate_image(draw2, japanese_text, font_large, rotated_font, (720, 64), (0, 4), 'center', 5, 'black', 'white') - img2.save(os.path.join(folder_name, '2.png')) + img2_width = 720 + img2 = Image.new('RGBA', (img2_width, 64), color=(0, 0, 0, 0)) + draw2 = ImageDraw.Draw(img2) + + temp_img2 = Image.new('RGBA', (2880, 64), (0, 0, 0, 0)) # Temporary image with 2880px width + temp_draw2 = ImageDraw.Draw(temp_img2) + + # Generate the image with the Japanese text + generate_image(temp_draw2, japanese_text, font_large, rotated_font, (2880, 64), (0, 4), 'center', 5, 'black', 'white') + + + # Calculate the bounding box of the entire text + text_bbox = get_text_bbox(temp_draw2, japanese_text, font_large) + text_width = (text_bbox[2] - text_bbox[0]) + 5 + + # Resize the image if it exceeds the specified height + if text_width > img2_width: + # Calculate the crop box to crop equally from both sides + left_crop = (2880 - text_width) // 2 + right_crop = 2880 - left_crop + cropped_img2 = temp_img2.crop((left_crop, 0, right_crop, 64)) + + scaled_img2 = cropped_img2.resize((img2_width, 64), Image.Resampling.LANCZOS) + + final_img2 = Image.new('RGBA', (img2_width, 64), (0, 0, 0, 0)) + final_img2.paste(scaled_img2) + else: + # Crop the temporary image to the actual width of the text + left_crop = (2880 - text_width) // 2 + right_crop = 2880 - left_crop + cropped_img2 = temp_img2.crop((left_crop, 0, right_crop, 64)) + final_img2 = Image.new('RGBA', (img2_width, 64), (0, 0, 0, 0)) + offset = (img2_width - text_width) // 2 + final_img2.paste(cropped_img2, (offset, 0)) + + final_img2.save(os.path.join(folder_name, '2.png')) + # Image 3.png img3_height = 400 @@ -201,22 +332,22 @@ def create_images(data, id, genreNo, font_path, rotated_font_path, append_ura=Fa img3_2 = Image.new('RGBA', (96, 400), color=(0, 0, 0, 0)) draw3 = ImageDraw.Draw(img3) - temp_img3 = Image.new('RGBA', (96, 1000), (0, 0, 0, 0)) # Temporary image with 1000px height + temp_img3 = Image.new('RGBA', (96, 3000), (0, 0, 0, 0)) # Temporary image with 1000px height temp_draw3 = ImageDraw.Draw(temp_img3) - temp_sub_img3 = Image.new('RGBA', (96, 1000), (0, 0, 0, 0)) # Temporary image with 1000px height + temp_sub_img3 = Image.new('RGBA', (96, 3000), (0, 0, 0, 0)) # Temporary image with 1000px height temp_sub_draw3 = ImageDraw.Draw(temp_sub_img3) - generate_image(temp_draw3, japanese_text, font_large, rotated_font, (96, 1000), (89, 0), 'center', 5, 'black', 'white', vertical=True) + generate_image(temp_draw3, padded_japanese_text, font_large, rotated_font, (96, 3000), (89, 0), 'center', 5, 'black', 'white', vertical=True) # Crop the temporary image to the actual height of the text y_offset = 0 - for char in japanese_text: + for char in padded_japanese_text: char_font = rotated_font if char in rotated_chars else font_large char = rotated_chars.get(char, char) char = rotated_letters.get(char, char) text_bbox = get_text_bbox(temp_draw3, char, char_font) - char_height = 42 + char_height = (text_bbox[3] + text_bbox[1]) y_offset += char_height # Crop the temporary image to the actual height of the text @@ -228,16 +359,16 @@ def create_images(data, id, genreNo, font_path, rotated_font_path, append_ura=Fa else: img3_1 = temp_img3.crop((0, 0, 96, img3_height)) - generate_image(temp_sub_draw3, japanese_sub_text, font_medium, rotated_font, (96, 1000), (32, 156), 'center', 4, 'black', 'white', vertical_small=True) + generate_image(temp_sub_draw3, japanese_sub_text, font_medium, rotated_font, (96, 3000), (32, 156), 'center', 4, 'black', 'white', vertical_small=True) # Crop the temporary image to the actual height of the text y_offset = 0 for char in japanese_sub_text: - char_font = rotated_font if char in rotated_chars else font_large + char_font = rotated_font if char in rotated_chars else font_medium char = rotated_chars.get(char, char) char = rotated_letters.get(char, char) text_bbox = get_text_bbox(temp_sub_draw3, char, char_font) - char_height = 28 + char_height = round((text_bbox[3] + text_bbox[1]) * 1.1) y_offset += char_height # Crop the temporary image to the actual height of the text @@ -259,19 +390,19 @@ def create_images(data, id, genreNo, font_path, rotated_font_path, append_ura=Fa img4 = Image.new('RGBA', (56, 400), color=(0, 0, 0, 0)) draw4 = ImageDraw.Draw(img4) - temp_img4 = Image.new('RGBA', (56, 1000), (0, 0, 0, 0)) # Temporary image with 1000px height + temp_img4 = Image.new('RGBA', (56, 3000), (0, 0, 0, 0)) # Temporary image with 3000px height temp_draw4 = ImageDraw.Draw(temp_img4) - generate_image(temp_draw4, japanese_text, font_large, rotated_font, (56, 400), (48, 0), 'center', 5, genre_color, 'white', vertical=True) + generate_image(temp_draw4, padded_japanese_text, font_large, rotated_font, (56, 400), (48, 0), 'center', 5, genre_color, 'white', vertical=True) # Crop the temporary image to the actual height of the text y_offset = 0 - for char in japanese_text: + for char in padded_japanese_text: char_font = rotated_font if char in rotated_chars else font_large char = rotated_chars.get(char, char) char = rotated_letters.get(char, char) text_bbox = get_text_bbox(temp_draw4, char, char_font) - char_height = 42 + char_height = (text_bbox[3] + text_bbox[1]) y_offset += char_height # Crop the temporary image to the actual height of the text @@ -291,19 +422,19 @@ def create_images(data, id, genreNo, font_path, rotated_font_path, append_ura=Fa img5 = Image.new('RGBA', (56, 400), color=(0, 0, 0, 0)) draw5 = ImageDraw.Draw(img5) - temp_img5 = Image.new('RGBA', (56, 1000), (0, 0, 0, 0)) # Temporary image with 1000px height + temp_img5 = Image.new('RGBA', (56, 3000), (0, 0, 0, 0)) # Temporary image with 1000px height temp_draw5 = ImageDraw.Draw(temp_img5) - generate_image(temp_draw5, japanese_text, font_large, rotated_font, (56, 400), (48, 0), 'center', 5, 'black', 'white', vertical=True) + generate_image(temp_draw5, padded_japanese_text, font_large, rotated_font, (56, 400), (48, 0), 'center', 5, 'black', 'white', vertical=True) # Crop the temporary image to the actual height of the text y_offset = 0 - for char in japanese_text: + for char in padded_japanese_text: char_font = rotated_font if char in rotated_chars else font_large char = rotated_chars.get(char, char) char = rotated_letters.get(char, char) text_bbox = get_text_bbox(temp_draw5, char, char_font) - char_height = 42 + char_height = (text_bbox[3] + text_bbox[1]) y_offset += char_height # Crop the temporary image to the actual height of the text diff --git a/TaikoWiiUSongTextureTool/generate_nut.py b/TaikoWiiUSongTextureTool/generate_nut.py index 9a5dfd3..384da73 100644 --- a/TaikoWiiUSongTextureTool/generate_nut.py +++ b/TaikoWiiUSongTextureTool/generate_nut.py @@ -1,7 +1,8 @@ import argparse import os -from PIL import Image import struct +import subprocess +#from PIL import Image class TextureSurface: def __init__(self): @@ -14,6 +15,7 @@ class NutTexture: self.Height = height self.pixelInternalFormat = pixel_format self.pixelFormat = pixel_type + self.HashId = 0 def add_mipmap(self, mipmap_data): self.surfaces[0].mipmaps.append(mipmap_data) @@ -23,13 +25,27 @@ class NutTexture: return len(self.surfaces[0].mipmaps) def getNutFormat(self): - if self.pixelInternalFormat == 'RGBA': - return 14 - raise NotImplementedError("Only RGBA format is implemented") + if self.pixelInternalFormat == 'CompressedRgbaS3tcDxt1Ext': + return 0 + elif self.pixelInternalFormat == 'CompressedRgbaS3tcDxt3Ext': + return 1 + elif self.pixelInternalFormat == 'CompressedRgbaS3tcDxt5Ext': + return 2 + elif self.pixelInternalFormat == 'RGBA': + if self.pixelFormat == 'RGBA': + return 14 + elif self.pixelFormat == 'ABGR': + return 16 + else: + return 17 + else: + raise NotImplementedError(f"Unknown pixel format {self.pixelInternalFormat}") class NUT: def __init__(self): self.textures = [] + self.endian = 'big' + self.version = 0x0200 def add_texture(self, texture): self.textures.append(texture) @@ -39,108 +55,161 @@ class NUT: f.write(self.build()) def build(self): + o = bytearray() data = bytearray() + + if self.endian == 'big': + o.extend(struct.pack('>I', 0x4E545033)) # NTP3 + else: + o.extend(struct.pack('>I', 0x4E545744)) # NTWD + + if self.version > 0x0200: + self.version = 0x0200 + + o.extend(struct.pack('>H', self.version)) num_textures = len(self.textures) - # File header - header = struct.pack(">IHH", 0x4E545033, 0x0200, num_textures) - data.extend(header) + if num_textures != 1 and num_textures != 6: + raise ValueError("The number of images must be either 1 or 6.") + o.extend(struct.pack('>H', num_textures)) + o.extend(b'\x00' * 8) # Reserved space - # Initial offset (0x18 bytes for the header, then 0x4 bytes per texture offset) - texture_offset_base = 0x18 + (0x4 * num_textures) - texture_headers_offset = texture_offset_base - texture_data_offset = texture_headers_offset + (0x50 * num_textures) - - # Ensure texture data starts at the correct offset (0x42E0) - texture_data_offset = max(texture_data_offset, 0x4000) - - # Offset table - texture_offsets = [] + header_length = 0 for texture in self.textures: - texture_offsets.append(texture_data_offset) - texture_data_offset += 0x50 + sum(len(mipmap) for mipmap in texture.surfaces[0].mipmaps) + surface_count = len(texture.surfaces) + is_cubemap = surface_count == 6 + if surface_count < 1 or surface_count > 6: + raise NotImplementedError(f"Unsupported surface amount {surface_count} for texture. 1 to 6 faces are required.") + if surface_count > 1 and surface_count < 6: + raise NotImplementedError(f"Unsupported cubemap face amount for texture. Six faces are required.") + mipmap_count = len(texture.surfaces[0].mipmaps) + header_size = 0x50 + (0x10 if is_cubemap else 0) + (mipmap_count * 4 if mipmap_count > 1 else 0) + header_size = (header_size + 0xF) & ~0xF # Align to 16 bytes + header_length += header_size - for offset in texture_offsets: - data.extend(struct.pack(">I", offset)) - - # Texture headers and mipmaps - for texture, offset in zip(self.textures, texture_offsets): - data.extend(self.build_texture_header(texture, offset)) - for texture in self.textures: - for mipmap in texture.surfaces[0].mipmaps: - data.extend(mipmap) + surface_count = len(texture.surfaces) + is_cubemap = surface_count == 6 + mipmap_count = len(texture.surfaces[0].mipmaps) - return data + data_size = sum((len(mipmap) + 0xF) & ~0xF for mipmap in texture.surfaces[0].mipmaps) + header_size = 0x50 + (0x10 if is_cubemap else 0) + (mipmap_count * 4 if mipmap_count > 1 else 0) + header_size = (header_size + 0xF) & ~0xF - def build_texture_header(self, texture, offset): - mipmap_count = texture.MipMapsPerSurface - size = texture.Width * texture.Height * 4 # Texture size - header = struct.pack(">IIIIHHIIII", - size, texture.Width, texture.Height, 0, 0, - mipmap_count, texture.getNutFormat(), - texture.Width, texture.Height, 0) - additional_data = b'\x65\x58\x74\x00\x00\x00\x00\x20\x00\x00\x00\x10\x00\x00\x00\x00' \ - b'\x47\x49\x44\x58\x00\x00\x00\x10\x00\x00\x00\x05\x00\x00\x00\x00' - return header + additional_data.ljust(0x50 - len(header), b'\x00') + o.extend(struct.pack('>I', data_size + header_size)) + o.extend(b'\x00' * 4) # Padding + o.extend(struct.pack('>I', data_size)) + o.extend(struct.pack('>H', header_size)) + o.extend(b'\x00' * 2) # Padding - def modify_nut_file(self, file_path, output_path): - # Set replacement bytes to 00 + o.extend(b'\x00') + o.extend(struct.pack('B', mipmap_count)) + o.extend(b'\x00') + o.extend(struct.pack('B', texture.getNutFormat())) + o.extend(struct.pack('>HH', texture.Width, texture.Height)) + o.extend(b'\x00' * 4) # Padding + o.extend(struct.pack('>I', 0)) # DDS Caps2 placeholder - with open(file_path, 'rb') as f: - data = bytearray(f.read()) - - # Replace bytes from 0x00 to 0x1F0 - #data[0x00:0x1EF] = replacement_bytes - # Delete bytes from 0x42E0 to 0x42F3 (0x42E0 to 0x42F4 inclusive) - del data[0x42E0:0x42F3] - del data[0x0040:0x0044] - data[0x1F0:0x1F0] = b'\x00\x00\x00\x00' - data[0x008:0x010] = b'\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x010:0x040] = b'\x00\x02\xd0P\x00\x00\x00\x00\x00\x02\xd0\x00\x00P\x00\x00\x00\x01\x00\x0e\x02\xd0\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x060:0x090] = b'\x00\x04\x92P\x00\x00\x00\x00\x00\x04\x92\x00\x00P\x00\x00\x00\x01\x00\x0e\x02\xd0\x00h\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xd1\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x0B0:0x0E0] = b'\x00\x02\xd0P\x00\x00\x00\x00\x00\x02\xd0\x00\x00P\x00\x00\x00\x01\x00\x0e\x02\xd0\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07c@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x100:0x130] = b'\x00\x02X\x50\x00\x00\x00\x00\x00\x02X\x00\x00P\x00\x00\x00\x01\x00\x0e\x00`\x01\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\n2\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x150:0x180] = b'\x00\x01^P\x00\x00\x00\x00\x00\x01^\x00\x00P\x00\x00\x00\x01\x00\x0e\x00\x38\x01\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x8a\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x1A0:0x1D0] = b'\x00\x01^P\x00\x00\x00\x00\x00\x01^\x00\x00P\x00\x00\x00\x01\x00\x0e\x00\x38\x01\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\xe8P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - data[0x5B:0x5C] = b'\x00' - data[0xAB:0xAC] = b'\x01' - data[0xFB:0xFC] = b'\x02' - data[0x14B:0x14C] = b'\x03' - data[0x19B:0x19C] = b'\x04' - # Add three 0x00 bytes to the end of the file - data.extend(b'\x00\x00\x00') + if self.version >= 0x0200: + o.extend(struct.pack('>I', header_length + len(data))) + else: + o.extend(b'\x00' * 4) # Padding - with open(output_path, 'wb') as f: - f.write(data) + header_length -= header_size + o.extend(b'\x00' * 12) # Reserved space -def load_png_to_texture(filepath): - with Image.open(filepath) as img: - img = img.convert("RGBA") - width, height = img.size - mipmap_data = img.tobytes() - texture = NutTexture(width, height, "RGBA", "RGBA") - texture.add_mipmap(mipmap_data) + if is_cubemap: + o.extend(struct.pack('>II', len(texture.surfaces[0].mipmaps[0]), len(texture.surfaces[0].mipmaps[0]))) + o.extend(b'\x00' * 8) # Padding + + if texture.getNutFormat() == 14 or texture.getNutFormat() == 17: + self.swap_channel_order_down(texture) + + for surface in texture.surfaces: + for mipmap in surface.mipmaps: + mip_start = len(data) + data.extend(mipmap) + while len(data) % 0x10 != 0: + data.extend(b'\x00') + if mipmap_count > 1: + mip_end = len(data) + o.extend(struct.pack('>I', mip_end - mip_start)) + + while len(o) % 0x10 != 0: + o.extend(b'\x00') + + if texture.getNutFormat() == 14 or texture.getNutFormat() == 17: + self.swap_channel_order_up(texture) + + o.extend(b'\x65\x58\x74\x00') # "eXt\0" + o.extend(struct.pack('>II', 0x20, 0x10)) + o.extend(b'\x00' * 4) + + o.extend(b'\x47\x49\x44\x58') # "GIDX" + o.extend(struct.pack('>I', 0x10)) + o.extend(struct.pack('>I', texture.HashId)) # Texture ID + o.extend(b'\x00' * 4) + + o.extend(data) + + return o + + def swap_channel_order_down(self, texture): + for surface in texture.surfaces: + for i, mipmap in enumerate(surface.mipmaps): + mipmap = bytearray(mipmap) + for j in range(0, len(mipmap), 4): + mipmap[j], mipmap[j + 2] = mipmap[j + 2], mipmap[j] + surface.mipmaps[i] = bytes(mipmap) + + def swap_channel_order_up(self, texture): + for surface in texture.surfaces: + for i, mipmap in enumerate(surface.mipmaps): + mipmap = bytearray(mipmap) + for j in range(0, len(mipmap), 4): + mipmap[j], mipmap[j + 2] = mipmap[j + 2], mipmap[j] + surface.mipmaps[i] = bytes(mipmap) + +def nvcompress_png_to_dds(png_filepath, dds_filepath, format_option): + format_map = { + 'dxt1': '-bc1', + 'dxt3': '-bc2', + 'dxt5': '-bc3', + } + format_arg = format_map.get(format_option.lower(), '-bc1') + command = f"nvcompress {format_arg} {png_filepath} {dds_filepath}" + subprocess.run(command, shell=True, check=True) + +def load_dds_to_texture(dds_filepath, index, pixel_format): + DDS_HEADER_SIZE = 128 # DDS header size in bytes + with open(dds_filepath, 'rb') as dds_file: + dds_data = dds_file.read() + print(f"Length of dds_data: {len(dds_data)}") + print(f"Bytes from 12 to 20: {dds_data[12:20]}") + width, height = struct.unpack_from('