diff --git a/asset_dumper.py b/asset_dumper.py index 811ff69..d360f36 100644 --- a/asset_dumper.py +++ b/asset_dumper.py @@ -52,7 +52,7 @@ def dump_all_textures_in_gcm(self, gcm: GCM, out_dir): for _ in self.dump_all_textures_in_rarc(rarc, out_path, display_path_prefix=file_path): continue elif file_ext == ".bti": - out_path = os.path.join(out_dir, rel_dir, base_name + ".png") + out_path = os.path.join(out_dir, rel_dir, base_name) bti = BTI(gcm.get_changed_file_data(file_path)) self.dump_texture(bti, out_path) elif file_ext in [".bmd", ".bdl", ".bmt"]: @@ -85,7 +85,7 @@ def dump_all_textures_in_rarc(self, rarc: RARC, out_dir, display_path_prefix=Non try: if file_ext == ".bti": - out_path = os.path.join(out_dir, rel_dir, base_name + ".png") + out_path = os.path.join(out_dir, rel_dir, base_name) bti = rarc.get_file(file_entry.name, BTI) self.dump_texture(bti, out_path) elif file_ext in [".bmd", ".bdl", ".bmt"]: @@ -112,7 +112,7 @@ def dump_all_textures_in_tex1(self, tex1: TEX1, out_dir): if len(textures) == 0: continue - images: list[Image.Image] = [] + unique_image_count = 0 for i, texture in enumerate(textures): is_duplicate = False for prev_texture in textures[:i]: @@ -122,34 +122,31 @@ def dump_all_textures_in_tex1(self, tex1: TEX1, out_dir): break if is_duplicate: continue - - image = texture.render() - - if image in images: - # A duplicate not detected by is_visually_equal_to(). - # For example, if one version uses C8 format with RGB565 palette and the other uses RGB565 format, they can be identical, but is_visually_equal_to() will not detect this. - continue - - images.append(image) + unique_image_count += 1 - has_different_duplicates = len(images) > 1 - for i, image in enumerate(images): + has_different_duplicates = unique_image_count > 1 + for i, texture in enumerate(textures): + dupe_tex_name = texture_name if has_different_duplicates: # If there's more than one unique version of this texture, append _dupe0 _dupe1 etc to all the images. - out_path = os.path.join(out_dir, texture_name + "_dupe%d.png" % i) - else: - out_path = os.path.join(out_dir, texture_name + ".png") + dupe_tex_name += ".dupe%d" % i - image.save(out_path) + out_path = os.path.join(out_dir, dupe_tex_name) - self.succeeded_file_count += 1 + self.dump_texture(texture, out_path) def dump_texture(self, bti: BTI, out_path): out_dir = os.path.dirname(out_path) - if not os.path.isdir(out_dir): - os.makedirs(out_dir) + os.makedirs(out_dir, exist_ok=True) - image = bti.render() - image.save(out_path) + for i in range(bti.mipmap_count): + mip_out_path = out_path + if (bti.mipmap_count) > 1: + mip_out_path += ".mip%d" % i + mip_out_path += ".png" + # if os.path.isfile(mip_out_path): + # continue + image = bti.render_mipmap(i) + image.save(mip_out_path) self.succeeded_file_count += 1 diff --git a/gcft_ui/bti_tab.py b/gcft_ui/bti_tab.py index a778c77..be0466a 100644 --- a/gcft_ui/bti_tab.py +++ b/gcft_ui/bti_tab.py @@ -28,6 +28,7 @@ ("min_lod", 1), ("max_lod", 1), ("lod_bias", 2), + ("mipmap_count", 1), ] class BTITab(QWidget): @@ -41,6 +42,8 @@ def __init__(self): self.ui.export_bti.setDisabled(True) self.ui.export_bti_image.setDisabled(True) + self.ui.bti_curr_mipmap.setDisabled(True) + self.ui.replace_bti_mipmap.setDisabled(True) self.ui.bti_file_size.setText("") self.ui.bti_resolution.setText("") @@ -57,6 +60,8 @@ def __init__(self): self.ui.import_bti_image.clicked.connect(self.import_bti_image) self.ui.export_bti_image.clicked.connect(self.export_bti_image) self.ui.import_bti_from_bnr.clicked.connect(self.import_bti_from_bnr) + self.ui.bti_curr_mipmap.currentIndexChanged.connect(self.reload_bti_image) + self.ui.replace_bti_mipmap.clicked.connect(self.replace_bti_mipmap_image) for field_name, field_enum in BTI_ENUM_FIELDS: widget_name = "bti_" + field_name @@ -116,6 +121,13 @@ def import_bti_from_bnr(self): file_type="BNR", file_filter="All files (*.*)" ) + def replace_bti_mipmap_image(self): + self.window().generic_do_gui_file_operation( + op_callback=self.replace_bti_mipmap_image_by_path, + is_opening=True, is_saving=False, is_folder=False, + file_type="image", file_filter="PNG Files (*.png)" + ) + def import_bti_by_path(self, bti_path): with open(bti_path, "rb") as f: @@ -126,6 +138,8 @@ def import_bti_by_path(self, bti_path): self.import_bti_by_data(data, bti_name) def import_bti_by_data(self, data, bti_name): + self.ui.bti_curr_mipmap.setCurrentIndex(0) + prev_bti = self.bti self.bti = BTI(data) @@ -187,9 +201,15 @@ def import_bti_by_data(self, data, bti_name): self.ui.export_bti.setDisabled(False) self.ui.export_bti_image.setDisabled(False) + self.ui.replace_bti_mipmap.setDisabled(False) + self.ui.bti_curr_mipmap.setDisabled(False) def reload_bti_image(self): - self.bti_image = self.bti.render() + selected_mipmap_index = self.ui.bti_curr_mipmap.currentIndex() + selected_mipmap_index = max(0, selected_mipmap_index) + selected_mipmap_index = min(self.bti.mipmap_count-1, selected_mipmap_index) + + self.bti_image = self.bti.render_mipmap(selected_mipmap_index) image_bytes = self.bti_image.tobytes('raw', 'BGRA') qimage = QImage(image_bytes, self.bti_image.width, self.bti_image.height, QImage.Format_ARGB32) @@ -210,6 +230,14 @@ def reload_bti_image(self): self.ui.bti_image_label.setFixedWidth(self.bti_image.width) self.ui.bti_image_label.setFixedHeight(self.bti_image.height) self.ui.bti_image_label.show() + + self.ui.bti_curr_mipmap.blockSignals(True) + self.ui.bti_curr_mipmap.clear() + for i in range(self.bti.mipmap_count): + _, _, mipmap_width, mipmap_height = self.bti.get_mipmap_offset_and_size(i) + self.ui.bti_curr_mipmap.addItem(f"{i}: {mipmap_width}x{mipmap_height}") + self.ui.bti_curr_mipmap.setCurrentIndex(selected_mipmap_index) + self.ui.bti_curr_mipmap.blockSignals(False) def export_bti_by_path(self, bti_path): self.bti.save_changes() @@ -290,6 +318,26 @@ def import_bti_from_bnr_by_data(self, data, bti_name): self.import_bti_by_data(data, bti_name) + def replace_bti_mipmap_image_by_path(self, image_path): + _, _, width, height = self.bti.get_mipmap_offset_and_size(self.ui.bti_curr_mipmap.currentIndex()) + image = Image.open(image_path) + if image.width != width or image.height != height: + QMessageBox.warning(self, + "Invalid mipmap size", + "The image you selected has the wrong size for this mipmap.\n" + + "Each mipmap must be exactly half the size of the previous one.\n" + + f"Expected size: {width}x{height}\n" + + f"Instead got: {image.width}x{image.height}\n\n" + + "If you want to completely replace all mipmaps with an image of a different size, select \"Import Image\" instead." + ) + + self.bti.replace_mipmap(self.ui.bti_curr_mipmap.currentIndex(), image) + + self.bti.save_changes() + print(self.bti.num_colors) + + self.reload_bti_image() + def bti_header_field_changed(self): for field_name, field_enum in BTI_ENUM_FIELDS: widget_name = "bti_" + field_name @@ -324,13 +372,20 @@ def bti_header_field_changed(self): QMessageBox.warning(self, "Invalid value", "\"%s\" is not a valid decimal number." % new_str_value) new_value = old_value - if new_value < 0: - QMessageBox.warning(self, "Invalid value", "Value cannot be negative.") + if field_name == "mipmap_count": + min_value = 1 + max_value = self.bti.get_max_valid_mipmap_count() + else: + min_value = 0 + max_value = max_value = (2**(byte_size*8)) - 1 + + if new_value < min_value: + QMessageBox.warning(self, "Invalid value", f"Value is too small (minimum value: 0x{min_value:X})") new_value = old_value - if new_value >= 2**(byte_size*8): + if new_value > max_value: QMessageBox.warning( self, "Invalid value", - "Value is too large to fit in field %s (maximum value: 0x%X)" % (field_name, (2**(byte_size*8))-1) + "Value is too large to fit in field %s (maximum value: 0x%X)" % (field_name, max_value) ) new_value = old_value diff --git a/gcft_ui/bti_tab.ui b/gcft_ui/bti_tab.ui index 6cdeff3..cfbf2a9 100644 --- a/gcft_ui/bti_tab.ui +++ b/gcft_ui/bti_tab.ui @@ -30,10 +30,17 @@ + + + + Replace Mipmap + + + - Import From GameCube Banner (.bnr) + Import From Banner (.bnr) @@ -58,14 +65,17 @@ + + + - + - + 0 @@ -145,8 +155,8 @@ 0 0 - 591 - 433 + 570 + 431 @@ -393,6 +403,40 @@ + + + + Num Mipmaps + + + + + + + + 35 + 16777215 + + + + + + + + Selected Mipmap + + + + + + + + 80 + 0 + + + + diff --git a/gcft_ui/uic/ui_bti_tab.py b/gcft_ui/uic/ui_bti_tab.py index d7ade99..1815b5a 100644 --- a/gcft_ui/uic/ui_bti_tab.py +++ b/gcft_ui/uic/ui_bti_tab.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'bti_tab.ui' ## -## Created by: Qt User Interface Compiler version 6.4.0 +## Created by: Qt User Interface Compiler version 6.5.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -39,6 +39,11 @@ def setupUi(self, BTITab): self.horizontalLayout_5.addWidget(self.import_bti_image) + self.replace_bti_mipmap = QPushButton(BTITab) + self.replace_bti_mipmap.setObjectName(u"replace_bti_mipmap") + + self.horizontalLayout_5.addWidget(self.replace_bti_mipmap) + self.import_bti_from_bnr = QPushButton(BTITab) self.import_bti_from_bnr.setObjectName(u"import_bti_from_bnr") @@ -64,6 +69,11 @@ def setupUi(self, BTITab): self.horizontalLayout.addWidget(self.widget) + self.widget_2 = QWidget(BTITab) + self.widget_2.setObjectName(u"widget_2") + + self.horizontalLayout.addWidget(self.widget_2) + self.verticalLayout.addLayout(self.horizontalLayout) @@ -92,7 +102,7 @@ def setupUi(self, BTITab): self.bti_image_scroll_area.setWidgetResizable(True) self.scrollAreaWidgetContents = QWidget() self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") - self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 591, 433)) + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 570, 431)) self.gridLayout = QGridLayout(self.scrollAreaWidgetContents) self.gridLayout.setObjectName(u"gridLayout") self.bti_image_label = QLabel(self.scrollAreaWidgetContents) @@ -272,9 +282,32 @@ def setupUi(self, BTITab): self.formLayout.setWidget(13, QFormLayout.FieldRole, self.bti_lod_bias) + self.label_14 = QLabel(BTITab) + self.label_14.setObjectName(u"label_14") + + self.formLayout.setWidget(14, QFormLayout.LabelRole, self.label_14) + + self.bti_mipmap_count = QLineEdit(BTITab) + self.bti_mipmap_count.setObjectName(u"bti_mipmap_count") + self.bti_mipmap_count.setMaximumSize(QSize(35, 16777215)) + + self.formLayout.setWidget(14, QFormLayout.FieldRole, self.bti_mipmap_count) + + self.label_16 = QLabel(BTITab) + self.label_16.setObjectName(u"label_16") + + self.formLayout.setWidget(15, QFormLayout.LabelRole, self.label_16) + + self.bti_curr_mipmap = QComboBox(BTITab) + self.bti_curr_mipmap.setObjectName(u"bti_curr_mipmap") + self.bti_curr_mipmap.setMinimumSize(QSize(80, 0)) + + self.formLayout.setWidget(15, QFormLayout.FieldRole, self.bti_curr_mipmap) + self.horizontalLayout_8.addLayout(self.formLayout) + self.horizontalLayout_8.setStretch(0, 1) self.verticalLayout.addLayout(self.horizontalLayout_8) @@ -288,7 +321,8 @@ def retranslateUi(self, BTITab): BTITab.setWindowTitle(QCoreApplication.translate("BTITab", u"Form", None)) self.import_bti.setText(QCoreApplication.translate("BTITab", u"Import BTI", None)) self.import_bti_image.setText(QCoreApplication.translate("BTITab", u"Import Image", None)) - self.import_bti_from_bnr.setText(QCoreApplication.translate("BTITab", u"Import From GameCube Banner (.bnr)", None)) + self.replace_bti_mipmap.setText(QCoreApplication.translate("BTITab", u"Replace Mipmap", None)) + self.import_bti_from_bnr.setText(QCoreApplication.translate("BTITab", u"Import From Banner (.bnr)", None)) self.export_bti.setText(QCoreApplication.translate("BTITab", u"Export BTI", None)) self.export_bti_image.setText(QCoreApplication.translate("BTITab", u"Export Image", None)) self.bti_image_label.setText("") @@ -310,5 +344,7 @@ def retranslateUi(self, BTITab): self.label_8.setText(QCoreApplication.translate("BTITab", u"Min LOD", None)) self.label_9.setText(QCoreApplication.translate("BTITab", u"Max LOD", None)) self.label_10.setText(QCoreApplication.translate("BTITab", u"LOD Bias", None)) + self.label_14.setText(QCoreApplication.translate("BTITab", u"Num Mipmaps", None)) + self.label_16.setText(QCoreApplication.translate("BTITab", u"Selected Mipmap", None)) # retranslateUi diff --git a/gclib b/gclib index 59aed74..7e6e587 160000 --- a/gclib +++ b/gclib @@ -1 +1 @@ -Subproject commit 59aed743a5768001c848822994eb0bd65f7e27f1 +Subproject commit 7e6e587a8e747dab439c112c25370a9146f12539