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.

150 lines
5.5 KiB

#include "common.h"
#include "bsp.h"
#include "ps1types.h"
#include "ps1bsp.h"
#include "texture.h"
#include "rectpack/finders_interface.h"
static char path[_MAX_PATH];
#define getTPage(tp, abr, x, y) ( \
(((x) / 64) & 15) | \
((((y) / 256) & 1) << 4) | \
(((abr) & 3) << 5) | \
(((tp) & 3) << 7) | \
((((y) / 512) & 1) << 11) \
)
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&) {
return rectpack2D::callback_result::ABORT_PACKING;
};
const auto max_side = 512; // Max height of PS1 VRAM. 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;
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, 0, 0));
}
// Automatic atlas packing. Nice but it tries to make a square atlas which is not what we want. (This is solved by hacking the header itself)
const auto result_size = rectpack2D::find_best_packing<spaces_type>(
rectangles,
rectpack2D::make_finder_input(
max_side,
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);
unsigned char* atlas = (unsigned char*)malloc(result_size.w * result_size.h * sizeof(unsigned char));
if (atlas == NULL)
return false;
memset(atlas, 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];
// Dump each individual texture
//FILE* fraw;
//sprintf_s(path, _MAX_PATH, "textures/%s-%s-mip%d-%dx%d.raw", world->name, outName, mipLevel, miptex->width >> mipLevel, miptex->height >> mipLevel);
//fopen_s(&fraw, path, "wb");
//if (fraw != NULL)
//{
// size_t numBytes = (miptex->width * miptex->height) >> mipLevel;
// fwrite(texBytes, sizeof(unsigned char), numBytes, fraw);
// fclose(fraw);
//}
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
{
//printf("Writing texture %s mip %d to position: (%d, %d) w = %d, h = %d\n", miptex->name, mipLevel, rectangle.x, rectangle.y, rectangle.w, rectangle.h);
for (int y = 0; y < rectangle.h; ++y)
{
memcpy_s(atlas + ((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)miptex->width;
ps1tex.h = (u_char)miptex->height;
u_short x = rectangle.x + 512;
u_short y = rectangle.y + 256;
// prect is derived from the texture's position inside the atlas (rectangle.x/y) and the planned position of the atlas in VRAM (512, 256)
// mode is always 1 (8-bit palletized)
//texture->uoffs = (texture->prect.x % 64) << (2 - (texture->mode & 0x3));
//texture->voffs = (texture->prect.y & 0xFF);
/*
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. */
ps1tex.tpage = getTPage(1, 0, x, y);
ps1tex.uoffs = (u_char)((x % 64) << 1);
ps1tex.voffs = (u_char)(y & 0xFF);
// TODO: animated textures
outTextures.push_back(ps1tex);
}
}
}
FILE* fatlas;
sprintf_s(path, _MAX_PATH, "%s-atlas-%dx%d.raw", world->name, result_size.w, result_size.h);
fopen_s(&fatlas, path, "wb");
if (fatlas != NULL)
{
fwrite(atlas, sizeof(unsigned char), result_size.w * result_size.h, fatlas);
fclose(fatlas);
}
free(atlas);
return true;
}