Tools for preprocessing data files from Quake to make them suitable for use on PS1 hardware
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.

231 lines
8.1 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) \
)
// A straight conversion of the Quake palette colors comes out looking rather over-saturated.
// So we desaturate the palette ahead of time to more closely match the original Quake's look.
static void desaturate(const unsigned char inColor[3], unsigned char outColor[3])
{
double f = 0.15; // desaturate by 15%
double L = 0.3 * inColor[0] + 0.6 * inColor[1] + 0.1 * inColor[2];
outColor[0] = (unsigned char)((double)inColor[0] + f * (L - inColor[0]));
outColor[1] = (unsigned char)((double)inColor[1] + f * (L - inColor[1]));
outColor[2] = (unsigned char)((double)inColor[2] + f * (L - inColor[2]));
}
static bool load_palette(const char* paletteFile, Color outPalette[PALETTE_SIZE])
{
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);
for (int c = 0; c < PALETTE_SIZE; ++c)
{
outPalette[c].rgb.r = palette[3 * c + 0];
outPalette[c].rgb.g = palette[3 * c + 1];
outPalette[c].rgb.b = palette[3 * c + 2];
outPalette[c].rgb.a = 0;
}
return true;
}
static bool generate_clut(const Color palette[PALETTE_SIZE], tim::PARAM* outTim)
{
tim::PIX_RGB5* clut = (tim::PIX_RGB5*)malloc(PALETTE_SIZE * sizeof(tim::PIX_RGB5) * 2);
if (clut == NULL)
return false;
memset(clut, 0, PALETTE_SIZE * sizeof(tim::PIX_RGB5) * 2);
for (int c = 0; c < PALETTE_SIZE - 1; ++c) // Final palette entry is for alpha masking
{
unsigned char color[3];
desaturate(&palette[c].rgb.r, color);
clut[c].r = color[0] >> 3;
clut[c].g = color[1] >> 3;
clut[c].b = color[2] >> 3;
clut[c].i = 0;
// 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;
}
// Create a second palette with the transparency flag set. Used for water surfaces.
for (int c = 0; c < PALETTE_SIZE; ++c)
{
clut[c + PALETTE_SIZE] = clut[c];
clut[c + PALETTE_SIZE].i = 1;
}
outTim->clutData = clut;
outTim->clutWidth = PALETTE_SIZE;
outTim->clutHeight = 2;
return true;
}
bool process_textures(const world_t* world, TextureList& outTextures) // TODO: return TextureDescriptor structs, including average texture color
{
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))
ps1mip = 0;
// Ensure we don't include any textures that are larger than the PS1 can address
// Texture pages are 64 pixels wide, effectively 128 texels in 8-bit mode, and with U offset added no U value may exceed 255
// We could solve this more elegantly by page-aligning larger textures, but I'm not sure if that's actually necessary
while ((miptex->width >> ps1mip) > 128 || (miptex->height >> ps1mip) > 256)
ps1mip++;
// TODO: make an exception for the QUAKE sign that's displayed on the start map. It needs to be bold and detailed, but it's too wide for the PS1 to address at full resolution, so it'll need to be broken up.
if (!strcmp(miptex->name, "quake"))
ps1mip--;
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);
Color palette[PALETTE_SIZE];
load_palette("palette.lmp", palette);
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, &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(TextureDescriptor{ 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));
}
TextureDescriptor tex = { 0 };
tex.w = (u_char)rectangle.w;
tex.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;
tex.ps1tex.tpage = getTPage(outTim.format, 0, x, y);
tex.uoffs = (u_char)((x % 64) << (2 - outTim.format));
tex.voffs = (u_char)(y & 0xFF);
// TODO: animated textures
outTextures.push_back(tex);
}
}
}
sprintf_s(path, _MAX_PATH, "atlas-%s.tim", world->name);
tim::ExportFile(path, &outTim);
free(outTim.imgData);
free(outTim.clutData);
return true;
}