You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
6.2 KiB
182 lines
6.2 KiB
#include "common.h"
|
|
#include "bsp.h"
|
|
#include "ps1types.h"
|
|
#include "ps1bsp.h"
|
|
#include "texture.h"
|
|
|
|
#include "rectpack/finders_interface.h"
|
|
#include "tim.h"
|
|
|
|
#define PALETTE_SIZE 256
|
|
|
|
static char path[_MAX_PATH];
|
|
|
|
/**
|
|
tp specifies the color depth for the texture page in the range of 0 to 2 (0:4-bit, 1:8-bit, 2:16-bit).
|
|
abr specifies the blend operator for both non-textured and textured semi-transparent primitives which can be ignored for now and lastly,
|
|
x,y specifies the X,Y coordinates of the VRAM in 16-bit pixel units.
|
|
Keep in mind that the coordinates will be rounded down to the next lowest texture page.
|
|
*/
|
|
#define getTPage(tp, abr, x, y) ( \
|
|
(((x) / 64) & 15) | \
|
|
((((y) / 256) & 1) << 4) | \
|
|
(((abr) & 3) << 5) | \
|
|
(((tp) & 3) << 7) | \
|
|
((((y) / 512) & 1) << 11) \
|
|
)
|
|
|
|
static bool generate_clut(const char* paletteFile, tim::PARAM* outTim)
|
|
{
|
|
unsigned char palette[PALETTE_SIZE * 3];
|
|
|
|
FILE* fp;
|
|
fopen_s(&fp, paletteFile, "rb");
|
|
if (fp == NULL)
|
|
return false;
|
|
|
|
fread(palette, sizeof(unsigned char) * 3, PALETTE_SIZE, fp);
|
|
fclose(fp);
|
|
|
|
tim::PIX_RGB5* clut = (tim::PIX_RGB5*)malloc(PALETTE_SIZE * sizeof(tim::PIX_RGB5));
|
|
if (clut == NULL)
|
|
return false;
|
|
|
|
for (int c = 0; c < PALETTE_SIZE; ++c)
|
|
{
|
|
clut[c].r = palette[3 * c + 0] >> 3;
|
|
clut[c].g = palette[3 * c + 1] >> 3;
|
|
clut[c].b = palette[3 * c + 2] >> 3;
|
|
clut[c].i = (c == 255); // Final palette entry is for transparencies
|
|
|
|
// Completely black pixels are regarded as transparent by the PS1, so prevent that from happening by making those palette entries *nearly* black
|
|
if (clut[c].r == 0 && clut[c].g == 0 && clut[c].b == 0)
|
|
clut[c].r = clut[c].g = clut[c].b = 1;
|
|
}
|
|
|
|
outTim->clutData = clut;
|
|
outTim->clutWidth = PALETTE_SIZE;
|
|
outTim->clutHeight = 1;
|
|
return true;
|
|
}
|
|
|
|
bool process_textures(const world_t* world, std::vector<ps1bsp_texture_t>& outTextures)
|
|
{
|
|
using spaces_type = rectpack2D::empty_spaces<false>;
|
|
using rect_type = rectpack2D::output_rect_t<spaces_type>;
|
|
|
|
auto report_successful = [](rect_type&) {
|
|
return rectpack2D::callback_result::CONTINUE_PACKING;
|
|
};
|
|
|
|
auto report_unsuccessful = [](rect_type&) {
|
|
printf("Failed to fit all textures into atlas!\n");
|
|
return rectpack2D::callback_result::ABORT_PACKING;
|
|
};
|
|
|
|
const auto max_bin = rectpack2D::rect_wh(1024, 256); // 8-bit textures take up half the horizontal space so this is 512x256 in practice, or a quarter of the PS1's VRAM allocation.
|
|
const auto discard_step = -4;
|
|
|
|
std::vector<rect_type> rectangles;
|
|
|
|
// Try some texture packing and see if we fit inside the PS1's VRAM
|
|
for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum)
|
|
{
|
|
miptex_t* miptex = &world->miptexes[texNum];
|
|
if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps?
|
|
miptex->width = miptex->height = 0;
|
|
|
|
//printf("Texture %d (%dx%d): %.16s\n", texNum, miptex->width, miptex->height, miptex->name);
|
|
|
|
// Shrink the larger textures, but keep smaller ones at their original size
|
|
int ps1mip = miptex->width > 64 || miptex->height > 64 ? 1 : 0;
|
|
|
|
// Make an exception for the difficulty selection teleporters
|
|
if (!strncmp(miptex->name, "skill", 5))// || !strcmp(miptex->name, "quake"))
|
|
ps1mip = 0;
|
|
|
|
if (strcmp(miptex->name, "clip") && strcmp(miptex->name, "trigger"))
|
|
rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> ps1mip, miptex->height >> ps1mip));
|
|
else
|
|
rectangles.emplace_back(rectpack2D::rect_xywh(0, 0, miptex->width >> 3, miptex->width >> 3)); // Add the lowest mip level so that it at least gets included in the final texture list, and we don't mess up the texture IDs
|
|
}
|
|
|
|
const auto result_size = rectpack2D::find_best_packing<spaces_type>(
|
|
rectangles,
|
|
rectpack2D::make_finder_input(
|
|
max_bin,
|
|
discard_step,
|
|
report_successful,
|
|
report_unsuccessful,
|
|
rectpack2D::flipping_option::DISABLED
|
|
)
|
|
);
|
|
|
|
printf("%d textures. Packed texture atlas size: %d x %d\n", world->mipheader.numtex, result_size.w, result_size.h);
|
|
|
|
tim::PARAM outTim = { 0 };
|
|
outTim.format = 1; // 8-bit per pixel, all Quake textures use this
|
|
outTim.imgXoffs = 512;
|
|
outTim.imgYoffs = 256;
|
|
outTim.clutXoffs = 512;
|
|
outTim.clutYoffs = 0;
|
|
generate_clut("palette.lmp", &outTim);
|
|
|
|
outTim.imgWidth = result_size.w;
|
|
outTim.imgHeight = result_size.h;
|
|
outTim.imgData = malloc(result_size.w * result_size.h * sizeof(unsigned char));
|
|
if (outTim.imgData == NULL)
|
|
return false;
|
|
|
|
memset(outTim.imgData, 0, result_size.w * result_size.h * sizeof(unsigned char));
|
|
|
|
// Try to construct the texture atlas, see what we get
|
|
for (int texNum = 0; texNum < world->mipheader.numtex; ++texNum)
|
|
{
|
|
miptex_t* miptex = &world->miptexes[texNum];
|
|
if (miptex->name[0] == '\0') // Weird edge case on N64START.bsp, corrupt data perhaps?
|
|
{
|
|
outTextures.push_back(ps1bsp_texture_t{ 0 }); // We have to add something, otherwise the texture IDs get messed up
|
|
continue;
|
|
}
|
|
|
|
char* outName = miptex->name;
|
|
if (*outName == '*' || *outName == '+')
|
|
outName++;
|
|
|
|
for (int mipLevel = 0; mipLevel < 4; ++mipLevel)
|
|
{
|
|
unsigned char* texBytes = world->textures[texNum * 4 + mipLevel];
|
|
|
|
const auto& rectangle = rectangles[texNum];
|
|
if (miptex->width >> mipLevel == rectangle.w) // This is the mip level we've previously decided we want for our PS1 atlas
|
|
{
|
|
// Copy the source texture line by line into the atlas at the desired position
|
|
for (int y = 0; y < rectangle.h; ++y)
|
|
{
|
|
memcpy_s((unsigned char*)outTim.imgData + ((rectangle.y + y) * result_size.w + rectangle.x), rectangle.w * sizeof(unsigned char), texBytes + (y * rectangle.w), rectangle.w * sizeof(unsigned char));
|
|
}
|
|
|
|
ps1bsp_texture_t ps1tex = { 0 };
|
|
ps1tex.w = (u_char)rectangle.w;
|
|
ps1tex.h = (u_char)rectangle.h;
|
|
|
|
u_short x = (rectangle.x / 2) + outTim.imgXoffs; // Divide by 2 to get the coordinate in 16-bit pixel units
|
|
u_short y = rectangle.y + outTim.imgYoffs;
|
|
|
|
ps1tex.tpage = getTPage(outTim.format, 0, x, y);
|
|
ps1tex.uoffs = (u_char)((x % 64) << (2 - outTim.format));
|
|
ps1tex.voffs = (u_char)(y & 0xFF);
|
|
// TODO: animated textures
|
|
|
|
outTextures.push_back(ps1tex);
|
|
}
|
|
}
|
|
}
|
|
|
|
sprintf_s(path, _MAX_PATH, "atlas-%s.tim", world->name);
|
|
tim::ExportFile(path, &outTim);
|
|
|
|
free(outTim.imgData);
|
|
free(outTim.clutData);
|
|
return true;
|
|
}
|