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.
310 lines
10 KiB
310 lines
10 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].channel, 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;
|
|
}
|
|
|
|
static int color_hash(const Color& color)
|
|
{
|
|
const int divider = 24;
|
|
int r = (int)roundf((float)color.rgb.r / divider);
|
|
int g = (int)roundf((float)color.rgb.g / divider);
|
|
int b = (int)roundf((float)color.rgb.b / divider);
|
|
return (b << 16) | (g << 8) | r;
|
|
}
|
|
|
|
struct ColorAccumulate
|
|
{
|
|
uint32_t r, g, b;
|
|
size_t count;
|
|
|
|
ColorAccumulate() : r(0), g(0), b(0), count(0) { }
|
|
|
|
ColorAccumulate(const Color& color) : r(color.rgb.r), g(color.rgb.g), b(color.rgb.b), count(1)
|
|
{
|
|
}
|
|
|
|
Color getAverage()
|
|
{
|
|
Color result;
|
|
result.rgb.r = (unsigned char)(r / count);
|
|
result.rgb.g = (unsigned char)(g / count);
|
|
result.rgb.b = (unsigned char)(b / count);
|
|
return result;
|
|
}
|
|
};
|
|
|
|
static void analyze_texture(unsigned char* texBytes, int numBytes, const Color palette[PALETTE_SIZE], TextureDescriptor& outTexture)
|
|
{
|
|
uint64_t rSum = 0, gSum = 0, bSum = 0;
|
|
std::unordered_map<int, ColorAccumulate> colorCount;
|
|
|
|
ColorAccumulate dominant;
|
|
for (int i = 0; i < numBytes; ++i)
|
|
{
|
|
unsigned char b = texBytes[i];
|
|
Color color = palette[b];
|
|
|
|
rSum += color.rgb.r;
|
|
gSum += color.rgb.g;
|
|
bSum += color.rgb.b;
|
|
|
|
int key = color_hash(color);
|
|
const auto iter = colorCount.find(key);
|
|
if (iter != colorCount.end())
|
|
{
|
|
iter->second.r += color.rgb.r;
|
|
iter->second.g += color.rgb.g;
|
|
iter->second.b += color.rgb.b;
|
|
iter->second.count++;
|
|
}
|
|
else
|
|
{
|
|
colorCount[key] = ColorAccumulate(color);
|
|
}
|
|
|
|
if (colorCount[key].count > dominant.count)
|
|
dominant = colorCount[key];
|
|
}
|
|
|
|
outTexture.averageColor.rgb.r = (unsigned char)(rSum / numBytes);
|
|
outTexture.averageColor.rgb.g = (unsigned char)(gSum / numBytes);
|
|
outTexture.averageColor.rgb.b = (unsigned char)(bSum / numBytes);
|
|
|
|
outTexture.dominantColor = dominant.getAverage();
|
|
|
|
desaturate(outTexture.averageColor.channel, outTexture.averageColor.channel);
|
|
desaturate(outTexture.dominantColor.channel, outTexture.dominantColor.channel);
|
|
}
|
|
|
|
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.
|
|
// The brush that displays this texture is actually broken up into two parts: one that's 224 texels wide, and one that's 64 texels wide. We can take advantage of that.
|
|
// We just need to turn that second part into a separate texture entry, and patch the texture ID and UVs on the face when we encounter it.
|
|
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++;
|
|
|
|
TextureDescriptor tex = { 0 };
|
|
|
|
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));
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Use the smallest mip level for analysis, should be good enough
|
|
analyze_texture(world->textures[texNum * 4 + 3], (miptex->width >> 3) * (miptex->height >> 3), palette, tex);
|
|
|
|
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;
|
|
}
|