Merge pull request #536 from GriffinRichards/4bpp

Support 8BPP tileset tile images
This commit is contained in:
GriffinR 2023-08-13 22:33:07 -04:00 committed by GitHub
commit d28849a533
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 41 additions and 26 deletions

View file

@ -7,6 +7,9 @@ and this project somewhat adheres to [Semantic Versioning](https://semver.org/sp
The **"Breaking Changes"** listed below are changes that have been made in the decompilation projects (e.g. pokeemerald), which porymap requires in order to work properly. It also includes changes to the scripting API that may change the behavior of existing porymap scripts. If porymap is used with a project or API script that is not up-to-date with the breaking changes, then porymap will likely break or behave improperly. The **"Breaking Changes"** listed below are changes that have been made in the decompilation projects (e.g. pokeemerald), which porymap requires in order to work properly. It also includes changes to the scripting API that may change the behavior of existing porymap scripts. If porymap is used with a project or API script that is not up-to-date with the breaking changes, then porymap will likely break or behave improperly.
## [Unreleased] ## [Unreleased]
### Added
- Support for 8BPP tileset tile images.
### Changed ### Changed
- The Palette Editor now remembers the Bit Depth setting. - The Palette Editor now remembers the Bit Depth setting.
- The min/max levels on the Wild Pokémon tab will now adjust automatically if they invalidate each other. - The min/max levels on the Wild Pokémon tab will now adjust automatically if they invalidate each other.

View file

@ -38,6 +38,8 @@ public:
QList<QList<QRgb>> palettes; QList<QList<QRgb>> palettes;
QList<QList<QRgb>> palettePreviews; QList<QList<QRgb>> palettePreviews;
bool hasUnsavedTilesImage;
static Tileset* getMetatileTileset(int, Tileset*, Tileset*); static Tileset* getMetatileTileset(int, Tileset*, Tileset*);
static Tileset* getTileTileset(int, Tileset*, Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*);
static Metatile* getMetatile(int, Tileset*, Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*);

View file

@ -13,6 +13,7 @@ QImage getMetatileImage(Metatile*, Tileset*, Tileset*, QList<int>, QList<float>,
QImage getTileImage(uint16_t, Tileset*, Tileset*); QImage getTileImage(uint16_t, Tileset*, Tileset*);
QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false); QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false);
QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset); QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset);
void flattenTo4bppImage(QImage * image);
static QList<QRgb> greyscalePalette({ static QList<QRgb> greyscalePalette({
qRgb(0, 0, 0), qRgb(0, 0, 0),

View file

@ -4,6 +4,7 @@
#include "selectablepixmapitem.h" #include "selectablepixmapitem.h"
#include "paletteutil.h" #include "paletteutil.h"
#include "imageproviders.h"
#include <memory> #include <memory>
using std::shared_ptr; using std::shared_ptr;
@ -127,10 +128,7 @@ public:
this->tileset = QImage(tilesetFilepath); this->tileset = QImage(tilesetFilepath);
this->format = format; this->format = format;
if (this->tileset.format() == QImage::Format::Format_Indexed8 && this->format == TilemapFormat::BPP_4) { if (this->tileset.format() == QImage::Format::Format_Indexed8 && this->format == TilemapFormat::BPP_4) {
// Squash pixel data to fit 4BPP. Allows project repo to use 8BPP images for 4BPP tilemaps flattenTo4bppImage(&this->tileset);
uchar * pixel = this->tileset.bits();
for (int i = 0; i < this->tileset.sizeInBytes(); i++, pixel++)
*pixel %= 16;
} }
bool err; bool err;
if (!palFilepath.isEmpty()) { if (!palFilepath.isEmpty()) {

View file

@ -22,7 +22,8 @@ Tileset::Tileset(const Tileset &other)
palettePaths(other.palettePaths), palettePaths(other.palettePaths),
metatileLabels(other.metatileLabels), metatileLabels(other.metatileLabels),
palettes(other.palettes), palettes(other.palettes),
palettePreviews(other.palettePreviews) palettePreviews(other.palettePreviews),
hasUnsavedTilesImage(false)
{ {
for (auto tile : other.tiles) { for (auto tile : other.tiles) {
tiles.append(tile.copy()); tiles.append(tile.copy());

View file

@ -18,6 +18,7 @@
#include "mapparser.h" #include "mapparser.h"
#include "prefab.h" #include "prefab.h"
#include "montabwidget.h" #include "montabwidget.h"
#include "imageexport.h"
#include <QFileDialog> #include <QFileDialog>
#include <QClipboard> #include <QClipboard>
@ -1280,7 +1281,7 @@ void MainWindow::on_actionNew_Tileset_triggered() {
} }
newSet.palettes[0][1] = qRgb(255,0,255); newSet.palettes[0][1] = qRgb(255,0,255);
newSet.palettePreviews[0][1] = qRgb(255,0,255); newSet.palettePreviews[0][1] = qRgb(255,0,255);
editor->project->saveTilesetTilesImage(&newSet); exportIndexed4BPPPng(newSet.tilesImage, newSet.tilesImagePath);
editor->project->saveTilesetMetatiles(&newSet); editor->project->saveTilesetMetatiles(&newSet);
editor->project->saveTilesetMetatileAttributes(&newSet); editor->project->saveTilesetMetatileAttributes(&newSet);
editor->project->saveTilesetPalettes(&newSet); editor->project->saveTilesetPalettes(&newSet);

View file

@ -6,7 +6,6 @@
#include "paletteutil.h" #include "paletteutil.h"
#include "tile.h" #include "tile.h"
#include "tileset.h" #include "tileset.h"
#include "imageexport.h"
#include "map.h" #include "map.h"
#include "orderedjson.h" #include "orderedjson.h"
@ -1008,7 +1007,15 @@ void Project::saveTilesetMetatiles(Tileset *tileset) {
} }
void Project::saveTilesetTilesImage(Tileset *tileset) { void Project::saveTilesetTilesImage(Tileset *tileset) {
exportIndexed4BPPPng(tileset->tilesImage, tileset->tilesImagePath); // Only write the tiles image if it was changed.
// Porymap will only ever change an existing tiles image by importing a new one.
if (tileset->hasUnsavedTilesImage) {
if (!tileset->tilesImage.save(tileset->tilesImagePath, "PNG")) {
logError(QString("Failed to save tiles image '%1'").arg(tileset->tilesImagePath));
return;
}
tileset->hasUnsavedTilesImage = false;
}
} }
void Project::saveTilesetPalettes(Tileset *tileset) { void Project::saveTilesetPalettes(Tileset *tileset) {
@ -1350,6 +1357,7 @@ void Project::loadTilesetAssets(Tileset* tileset) {
QImage image; QImage image;
if (QFile::exists(tileset->tilesImagePath)) { if (QFile::exists(tileset->tilesImagePath)) {
image = QImage(tileset->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); image = QImage(tileset->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither);
flattenTo4bppImage(&image);
} else { } else {
image = QImage(8, 8, QImage::Format_Indexed8); image = QImage(8, 8, QImage::Format_Indexed8);
} }

View file

@ -169,3 +169,12 @@ QImage getPalettedTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *s
QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
return getColoredTileImage(tileId, primaryTileset, secondaryTileset, greyscalePalette); return getColoredTileImage(tileId, primaryTileset, secondaryTileset, greyscalePalette);
} }
// gbagfx allows 4bpp image data to be represented with 8bpp .png files by considering only the lower 4 bits of each pixel.
// Reproduce that here to support this type of image use.
void flattenTo4bppImage(QImage * image) {
if (!image) return;
uchar * pixel = image->bits();
for (int i = 0; i < image->sizeInBytes(); i++, pixel++)
*pixel %= 16;
}

View file

@ -701,15 +701,6 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) {
msgBox.setIcon(QMessageBox::Icon::Critical); msgBox.setIcon(QMessageBox::Icon::Critical);
msgBox.exec(); msgBox.exec();
return; return;
} else if (palette.length() != 16) {
QMessageBox msgBox(this);
msgBox.setText("Failed to import palette.");
QString message = QString("The palette must have exactly 16 colors, but it has %1.").arg(palette.length());
msgBox.setInformativeText(message);
msgBox.setDefaultButton(QMessageBox::Ok);
msgBox.setIcon(QMessageBox::Icon::Critical);
msgBox.exec();
return;
} }
QVector<QRgb> colorTable = palette.toVector(); QVector<QRgb> colorTable = palette.toVector();
@ -717,20 +708,21 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) {
} }
// Validate image is properly indexed to 16 colors. // Validate image is properly indexed to 16 colors.
if (image.colorCount() != 16) { int colorCount = image.colorCount();
QMessageBox msgBox(this); if (colorCount > 16) {
msgBox.setText("Failed to import tiles."); flattenTo4bppImage(&image);
msgBox.setInformativeText(QString("The image must be indexed and contain 16 total colors, or it must be un-indexed. The provided image has %1 indexed colors.") } else if (colorCount < 16) {
.arg(image.colorCount())); QVector<QRgb> colorTable = image.colorTable();
msgBox.setDefaultButton(QMessageBox::Ok); for (int i = colorTable.length(); i < 16; i++) {
msgBox.setIcon(QMessageBox::Icon::Critical); colorTable.append(Qt::black);
msgBox.exec(); }
return; image.setColorTable(colorTable);
} }
this->project->loadTilesetTiles(tileset, image); this->project->loadTilesetTiles(tileset, image);
this->refresh(); this->refresh();
this->hasUnsavedChanges = true; this->hasUnsavedChanges = true;
tileset->hasUnsavedTilesImage = true;
} }
void TilesetEditor::closeEvent(QCloseEvent *event) void TilesetEditor::closeEvent(QCloseEvent *event)