Browse Source

Calculate average and dominant colors per texture, and use those to precalculate combined color * light values per face vertex, for cheap vertex colored drawing.

master
Nico de Poel 3 years ago
parent
commit
dc78b9700d
  1. 79
      main.cpp
  2. 87
      texture.cpp
  3. 4
      texture.h

79
main.cpp

@ -136,33 +136,84 @@ int process_faces(const world_t* world, const TextureList& textures)
const miptex_t* miptex = &world->miptexes[texinfo->texture_id]; const miptex_t* miptex = &world->miptexes[texinfo->texture_id];
const auto& texture = textures[texinfo->texture_id]; const auto& texture = textures[texinfo->texture_id];
outFace->firstPolygon = (unsigned short)outPolygons.size();
// Skip sky surfaces for now
if (!strncmp(miptex->name, "sky", 3))
// Skip over invisible collision volumes entirely
if (!strcmp(miptex->name, "clip") || !strcmp(miptex->name, "trigger"))
{ {
outFace->flags |= SURF_DRAWSKY;
outFace->firstPolygon = 0;
outFace->numPolygons = 0; outFace->numPolygons = 0;
continue; continue;
} }
// Invisible collision volumes don't have to be tessellated
if (!strcmp(miptex->name, "clip") || !strcmp(miptex->name, "trigger"))
// Detect special surface types
if (!strncmp(miptex->name, "sky", 3))
{ {
outFace->numPolygons = 0;
continue;
outFace->flags |= SURF_DRAWSKY;
} }
// Draw water as fullbright transparent surfaces
if (miptex->name[0] == '*')
else if (miptex->name[0] == '*')
{ {
// Detect special liquid types that should not be transparent
// Certain liquid types should not be transparent
if (strncmp(miptex->name, "*lava", 5) && strncmp(miptex->name, "*slime", 6) && strncmp(miptex->name, "*tele", 5)) if (strncmp(miptex->name, "*lava", 5) && strncmp(miptex->name, "*slime", 6) && strncmp(miptex->name, "*tele", 5))
outFace->flags |= SURF_DRAWWATER; outFace->flags |= SURF_DRAWWATER;
// Draw liquids as fullbright transparent surfaces
outFace->flags |= SURF_DRAWLIQUID; outFace->flags |= SURF_DRAWLIQUID;
} }
// Calculate average face lighting * color from texture data
for (int faceVertIdx = 0; faceVertIdx < outFace->numFaceVertices; ++faceVertIdx)
{
auto& faceVert = outFaceVertices[outFace->firstFaceVertex + faceVertIdx];
Vec3 point = tesselator.getVertices()[faceVert.index];
unsigned char light;
if (outFace->flags & SURF_DRAWSKY || outFace->flags & SURF_DRAWLIQUID || face->lightmap < 0)
{
light = 255;
}
else
{
light = compute_faceVertex_light5(world, face, faceBounds, point);
light = (int)((float)light * 1.5f); // Compromise between overbright and non-overbright lighting. Looks good in practice.
if (light > 255)
light = 255;
}
// Apply lighting to each color channel, and keep track of any overflows
int col[3], maxCol = 0;
for (int i = 0; i < 3; ++i)
{
col[i] = texture.dominantColor.channel[i] * light / 255;
if (col[i] > maxCol)
maxCol = col[i];
}
if (maxCol > 255)
{
// Saturate color to the highest channel value
for (int i = 0; i < 3; ++i)
{
col[i] = 255 * col[i] / maxCol;
}
}
// Boost the color value by 2, to simulate the PS1's overbright vertex color modulation
faceVert.r = (unsigned char)(col[0] >> 2);
faceVert.g = (unsigned char)(col[1] >> 2);
faceVert.b = (unsigned char)(col[2] >> 2);
faceVert.a = 0;
}
// Sky surfaces do not need to be tesselated
if (outFace->flags & SURF_DRAWSKY)
{
outFace->firstPolygon = 0;
outFace->numPolygons = 0;
continue;
}
// Tesselate the face into smaller polygons based on repeating textures
outFace->firstPolygon = (unsigned short)outPolygons.size();
auto polygons = tesselator.tesselateFace(face); auto polygons = tesselator.tesselateFace(face);
for (auto polyIter = polygons.begin(); polyIter != polygons.end(); ++polyIter) for (auto polyIter = polygons.begin(); polyIter != polygons.end(); ++polyIter)
{ {
@ -195,8 +246,6 @@ int process_faces(const world_t* world, const TextureList& textures)
outFace->totalQuads += (outPoly.numPolyVertices - 1) / 2; outFace->totalQuads += (outPoly.numPolyVertices - 1) / 2;
} }
// TODO: calculate average face lighting * color from texture data
outFace->numPolygons = (unsigned char)(outPolygons.size() - outFace->firstPolygon); outFace->numPolygons = (unsigned char)(outPolygons.size() - outFace->firstPolygon);
} }

87
texture.cpp

@ -70,7 +70,7 @@ static bool generate_clut(const Color palette[PALETTE_SIZE], tim::PARAM* outTim)
for (int c = 0; c < PALETTE_SIZE - 1; ++c) // Final palette entry is for alpha masking for (int c = 0; c < PALETTE_SIZE - 1; ++c) // Final palette entry is for alpha masking
{ {
unsigned char color[3]; unsigned char color[3];
desaturate(&palette[c].rgb.r, color);
desaturate(palette[c].channel, color);
clut[c].r = color[0] >> 3; clut[c].r = color[0] >> 3;
clut[c].g = color[1] >> 3; clut[c].g = color[1] >> 3;
@ -95,6 +95,79 @@ static bool generate_clut(const Color palette[PALETTE_SIZE], tim::PARAM* outTim)
return true; 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 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 spaces_type = rectpack2D::empty_spaces<false>;
@ -137,6 +210,8 @@ bool process_textures(const world_t* world, TextureList& outTextures) // TODO: r
ps1mip++; 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. // 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")) if (!strcmp(miptex->name, "quake"))
ps1mip--; ps1mip--;
@ -192,6 +267,8 @@ bool process_textures(const world_t* world, TextureList& outTextures) // TODO: r
if (*outName == '*' || *outName == '+') if (*outName == '*' || *outName == '+')
outName++; outName++;
TextureDescriptor tex = { 0 };
for (int mipLevel = 0; mipLevel < 4; ++mipLevel) for (int mipLevel = 0; mipLevel < 4; ++mipLevel)
{ {
unsigned char* texBytes = world->textures[texNum * 4 + mipLevel]; unsigned char* texBytes = world->textures[texNum * 4 + mipLevel];
@ -205,7 +282,6 @@ bool process_textures(const world_t* world, TextureList& outTextures) // TODO: r
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)); 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.w = (u_char)rectangle.w;
tex.h = (u_char)rectangle.h; tex.h = (u_char)rectangle.h;
@ -216,10 +292,13 @@ bool process_textures(const world_t* world, TextureList& outTextures) // TODO: r
tex.uoffs = (u_char)((x % 64) << (2 - outTim.format)); tex.uoffs = (u_char)((x % 64) << (2 - outTim.format));
tex.voffs = (u_char)(y & 0xFF); tex.voffs = (u_char)(y & 0xFF);
// TODO: animated textures // TODO: animated textures
outTextures.push_back(tex);
} }
} }
// 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); sprintf_s(path, _MAX_PATH, "atlas-%s.tim", world->name);

4
texture.h

@ -6,7 +6,7 @@ struct Color
{ {
struct struct
{ {
unsigned char a, r, g, b;
unsigned char r, g, b, a;
} rgb; } rgb;
unsigned int value; unsigned int value;
unsigned char channel[4]; unsigned char channel[4];
@ -19,7 +19,7 @@ struct TextureDescriptor
unsigned char w, h; // Width and height of the selected texture mip level unsigned char w, h; // Width and height of the selected texture mip level
unsigned char uoffs, voffs; // Texture coordinate offset within the texture page unsigned char uoffs, voffs; // Texture coordinate offset within the texture page
Color averageColor; Color averageColor;
Color medianColor;
Color dominantColor;
}; };
typedef std::vector<TextureDescriptor> TextureList; typedef std::vector<TextureDescriptor> TextureList;

Loading…
Cancel
Save