diff --git a/main.cpp b/main.cpp index 7964651..ce6f71b 100644 --- a/main.cpp +++ b/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 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; 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)) outFace->flags |= SURF_DRAWWATER; + // Draw liquids as fullbright transparent surfaces 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); 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; } - // TODO: calculate average face lighting * color from texture data - outFace->numPolygons = (unsigned char)(outPolygons.size() - outFace->firstPolygon); } diff --git a/texture.cpp b/texture.cpp index 61d98bf..a63c490 100644 --- a/texture.cpp +++ b/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 { unsigned char color[3]; - desaturate(&palette[c].rgb.r, color); + desaturate(palette[c].channel, color); clut[c].r = color[0] >> 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; } +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 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; @@ -137,6 +210,8 @@ bool process_textures(const world_t* world, TextureList& outTextures) // TODO: r 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--; @@ -192,6 +267,8 @@ bool process_textures(const world_t* world, TextureList& outTextures) // TODO: r if (*outName == '*' || *outName == '+') outName++; + TextureDescriptor tex = { 0 }; + for (int mipLevel = 0; mipLevel < 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)); } - TextureDescriptor tex = { 0 }; tex.w = (u_char)rectangle.w; 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.voffs = (u_char)(y & 0xFF); // 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); diff --git a/texture.h b/texture.h index 59bf337..dce9fd1 100644 --- a/texture.h +++ b/texture.h @@ -6,7 +6,7 @@ struct Color { struct { - unsigned char a, r, g, b; + unsigned char r, g, b, a; } rgb; unsigned int value; 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 uoffs, voffs; // Texture coordinate offset within the texture page Color averageColor; - Color medianColor; + Color dominantColor; }; typedef std::vector TextureList;