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.
173 lines
5.2 KiB
173 lines
5.2 KiB
#pragma once |
|
|
|
#include <array> |
|
#include <cstddef> |
|
#include <cstdint> |
|
#include <limits> |
|
#include <span> |
|
#include <string> |
|
#include <utility> |
|
|
|
#ifdef USE_SDL3 |
|
#include <SDL3/SDL_pixels.h> |
|
#elif defined(USE_SDL1) |
|
#include <SDL_video.h> |
|
#else |
|
#include <SDL_pixels.h> |
|
#endif |
|
|
|
#include "utils/str_cat.hpp" |
|
|
|
#define DEVILUTIONX_PRINT_PALETTE_BLENDING_TREE_GRAPHVIZ 0 // NOLINT(modernize-macro-to-enum) |
|
|
|
namespace devilution { |
|
|
|
/** |
|
* @brief Depth (number of levels) of the tree. |
|
*/ |
|
constexpr size_t PaletteKdTreeDepth = 5; |
|
|
|
/** |
|
* @brief A node in the k-d tree. |
|
* |
|
* @tparam RemainingDepth distance to the leaf nodes. |
|
*/ |
|
template <size_t RemainingDepth> |
|
struct PaletteKdTreeNode { |
|
using RGB = std::array<uint8_t, 3>; |
|
|
|
static constexpr unsigned Coord = (PaletteKdTreeDepth - RemainingDepth) % 3; |
|
|
|
PaletteKdTreeNode<RemainingDepth - 1> left; |
|
PaletteKdTreeNode<RemainingDepth - 1> right; |
|
uint8_t pivot; |
|
|
|
[[nodiscard]] const PaletteKdTreeNode<RemainingDepth - 1> &child(bool isLeft) const |
|
{ |
|
return isLeft ? left : right; |
|
} |
|
[[nodiscard]] PaletteKdTreeNode<RemainingDepth - 1> &child(bool isLeft) |
|
{ |
|
return isLeft ? left : right; |
|
} |
|
|
|
[[maybe_unused]] void toGraphvizDot(size_t id, std::span<const std::pair<RGB, uint8_t>, 256> values, std::string &dot) const |
|
{ |
|
StrAppend(dot, " node_", id, " [label=\"", "rgb"[Coord], ": ", pivot, "\"]\n"); |
|
const size_t leftId = (2 * id) + 1; |
|
const size_t rightId = (2 * id) + 2; |
|
left.toGraphvizDot(leftId, values, dot); |
|
right.toGraphvizDot(rightId, values, dot); |
|
StrAppend(dot, " node_", id, " -- node_", leftId, |
|
"\n node_", id, " -- node_", rightId, "\n"); |
|
} |
|
}; |
|
|
|
/** |
|
* @brief A leaf node in the k-d tree. |
|
*/ |
|
template <> |
|
struct PaletteKdTreeNode</*RemainingDepth=*/0> { |
|
using RGB = std::array<uint8_t, 3>; |
|
|
|
// We use inclusive indices to allow for representing the full [0, 255] range. |
|
// An empty node is represented as [1, 0]. |
|
uint8_t valuesBegin; |
|
uint8_t valuesEndInclusive; |
|
|
|
void toGraphvizDot(size_t id, std::span<const std::pair<RGB, uint8_t>, 256> values, std::string &dot) const; |
|
}; |
|
|
|
/** |
|
* @brief A kd-tree used to find the nearest neighbor in the color space. |
|
* |
|
* Each level splits the space in half by red, green, and blue respectively. |
|
*/ |
|
class PaletteKdTree { |
|
private: |
|
using RGB = std::array<uint8_t, 3>; |
|
static constexpr unsigned NumLeaves = 1U << PaletteKdTreeDepth; |
|
|
|
public: |
|
PaletteKdTree() = default; |
|
|
|
/** |
|
* @brief Constructs a PaletteKdTree |
|
* |
|
* The palette is used as points in the tree. |
|
* Colors between skipFrom and skipTo (inclusive) are skipped. |
|
*/ |
|
PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo); |
|
|
|
struct VisitState { |
|
uint8_t best; |
|
uint32_t bestDiff; |
|
}; |
|
|
|
[[nodiscard]] uint8_t findNearestNeighbor(const RGB &rgb) const |
|
{ |
|
VisitState visitState; |
|
visitState.bestDiff = std::numeric_limits<uint32_t>::max(); |
|
findNearestNeighborVisit(tree_, rgb, visitState); |
|
return visitState.best; |
|
} |
|
|
|
[[maybe_unused]] [[nodiscard]] std::string toGraphvizDot() const; |
|
|
|
private: |
|
[[nodiscard]] static constexpr uint32_t getColorDistance(const std::array<uint8_t, 3> &a, const std::array<uint8_t, 3> &b) |
|
{ |
|
const int diffr = a[0] - b[0]; |
|
const int diffg = a[1] - b[1]; |
|
const int diffb = a[2] - b[2]; |
|
return (diffr * diffr) + (diffg * diffg) + (diffb * diffb); |
|
} |
|
|
|
[[nodiscard]] static constexpr uint32_t getColorDistanceToPlane(int x1, int x2) |
|
{ |
|
// Our planes are axis-aligned, so a distance from a point to a plane |
|
// can be calculated based on just the axis coordinate. |
|
const int delta = x1 - x2; |
|
return static_cast<uint32_t>(delta * delta); |
|
} |
|
|
|
template <size_t RemainingDepth> |
|
void findNearestNeighborVisit(const PaletteKdTreeNode<RemainingDepth> &node, const RGB &rgb, |
|
VisitState &visitState) const |
|
{ |
|
const uint8_t coord = rgb[PaletteKdTreeNode<RemainingDepth>::Coord]; |
|
findNearestNeighborVisit(node.child(coord < node.pivot), rgb, visitState); |
|
|
|
// To see if we need to check a node's subtree, we compare the distance from the query |
|
// to the current best candidate vs the distance to the edge of the half-space represented |
|
// by the node. |
|
if (getColorDistanceToPlane(node.pivot, coord) < visitState.bestDiff) { |
|
findNearestNeighborVisit(node.child(coord >= node.pivot), rgb, visitState); |
|
} |
|
} |
|
|
|
void findNearestNeighborVisit(const PaletteKdTreeNode<0> &node, const RGB &rgb, |
|
VisitState &visitState) const |
|
{ |
|
// Nodes are almost never empty. |
|
// Separating the empty check from the loop makes this faster, |
|
// probaly because of better branch prediction. |
|
if (node.valuesBegin > node.valuesEndInclusive) return; |
|
|
|
const std::pair<RGB, uint8_t> *it = values_.data() + node.valuesBegin; |
|
const std::pair<RGB, uint8_t> *const end = values_.data() + node.valuesEndInclusive; |
|
do { |
|
const auto &[paletteColor, paletteIndex] = *it++; |
|
const uint32_t diff = getColorDistance(paletteColor, rgb); |
|
if (diff < visitState.bestDiff) { |
|
visitState.best = paletteIndex; |
|
visitState.bestDiff = diff; |
|
} |
|
} while (it <= end); |
|
} |
|
|
|
PaletteKdTreeNode<PaletteKdTreeDepth> tree_; |
|
std::array<std::pair<RGB, uint8_t>, 256> values_; |
|
}; |
|
|
|
} // namespace devilution
|
|
|