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.
 
 
 
 
 
 

395 lines
10 KiB

/**
* @file palette.cpp
*
* Implementation of functions for handling the engines color palette.
*/
#include "engine/palette.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <span>
#include <fmt/core.h>
#include "engine/backbuffer_state.hpp"
#include "engine/demomode.h"
#include "engine/dx.h"
#include "engine/load_file.hpp"
#include "engine/random.hpp"
#include "headless_mode.hpp"
#include "hwcursor.hpp"
#include "options.h"
#include "utils/display.h"
#include "utils/palette_blending.hpp"
#include "utils/sdl_compat.h"
namespace devilution {
std::array<SDL_Color, 256> logical_palette;
std::array<SDL_Color, 256> system_palette;
namespace {
/** Specifies whether the palette has max brightness. */
bool sgbFadedIn = true;
void LoadBrightness()
{
int brightnessValue = *GetOptions().Graphics.brightness;
brightnessValue = std::clamp(brightnessValue, 0, 100);
GetOptions().Graphics.brightness.SetValue(brightnessValue - brightnessValue % 5);
}
/**
* @brief Cycle the given range of colors in the palette
* @param from First color index of the range
* @param to First color index of the range
*/
void CycleColors(int from, int to)
{
std::rotate(logical_palette.begin() + from, logical_palette.begin() + from + 1, logical_palette.begin() + to + 1);
std::rotate(system_palette.begin() + from, system_palette.begin() + from + 1, system_palette.begin() + to + 1);
for (auto &palette : paletteTransparencyLookup) {
std::rotate(std::begin(palette) + from, std::begin(palette) + from + 1, std::begin(palette) + to + 1);
}
std::rotate(&paletteTransparencyLookup[from][0], &paletteTransparencyLookup[from + 1][0], &paletteTransparencyLookup[to + 1][0]);
#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT
UpdateTransparencyLookupBlack16(from, to);
#endif
}
/**
* @brief Cycle the given range of colors in the palette in reverse direction
* @param from First color index of the range
* @param to Last color index of the range
*/
void CycleColorsReverse(int from, int to)
{
std::rotate(logical_palette.begin() + from, logical_palette.begin() + to, logical_palette.begin() + to + 1);
std::rotate(system_palette.begin() + from, system_palette.begin() + to, system_palette.begin() + to + 1);
for (auto &palette : paletteTransparencyLookup) {
std::rotate(std::begin(palette) + from, std::begin(palette) + to, std::begin(palette) + to + 1);
}
std::rotate(&paletteTransparencyLookup[from][0], &paletteTransparencyLookup[to][0], &paletteTransparencyLookup[to + 1][0]);
#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT
UpdateTransparencyLookupBlack16(from, to);
#endif
}
// When brightness==0, then a==0 (identity mapping)
// When brightness==100, then a==-MaxAdjustment (maximum brightening)
constexpr float CalculateToneMappingParameter(int brightness)
{
// Maximum adjustment factor (tweak this constant to change the effect strength)
constexpr float MaxAdjustment = 2.0F;
return -(brightness / 100.0f) * MaxAdjustment;
}
constexpr uint8_t MapTone(float a, uint8_t color)
{
const auto x = static_cast<float>(color / 255.0f);
// Our quadratic tone mapping: f(x) = a*x^2 + (1-a)*x.
const float y = std::clamp(a * x * x + (1.0f - a) * x, 0.0f, 1.0f);
return static_cast<uint8_t>(y * 255.0f + 0.5f);
}
void ApplyGlobalBrightnessSingleColor(SDL_Color &dst, const SDL_Color &src)
{
const float a = CalculateToneMappingParameter(*GetOptions().Graphics.brightness);
dst.r = MapTone(a, src.r);
dst.g = MapTone(a, src.g);
dst.b = MapTone(a, src.b);
}
} // namespace
void ApplyGlobalBrightness(SDL_Color *dst, const SDL_Color *src)
{
// Get the brightness slider value (0 = neutral, 100 = max brightening)
const int brightnessSlider = *GetOptions().Graphics.brightness;
// Precompute a lookup table for speed.
const float a = CalculateToneMappingParameter(brightnessSlider);
uint8_t toneMap[256];
for (int i = 0; i < 256; i++) {
toneMap[i] = MapTone(a, i);
}
// Apply the lookup table to each color channel in the palette.
for (int i = 0; i < 256; i++) {
dst[i].r = toneMap[src[i].r];
dst[i].g = toneMap[src[i].g];
dst[i].b = toneMap[src[i].b];
}
}
void ApplyFadeLevel(unsigned fadeval, SDL_Color *dst, const SDL_Color *src)
{
for (int i = 0; i < 256; i++) {
dst[i].r = (fadeval * src[i].r) / 256;
dst[i].g = (fadeval * src[i].g) / 256;
dst[i].b = (fadeval * src[i].b) / 256;
}
}
// Applies a tone mapping curve based on the brightness slider value.
// The brightness value is in the range [0, 100] where 0 is neutral (no change)
// and 100 produces maximum brightening.
void UpdateSystemPalette(std::span<const SDL_Color, 256> src)
{
ApplyGlobalBrightness(system_palette.data(), src.data());
SystemPaletteUpdated();
RedrawEverything();
}
void SystemPaletteUpdated(int first, int ncolor)
{
if (HeadlessMode)
return;
assert(Palette);
if (SDLC_SetSurfaceAndPaletteColors(PalSurface, Palette.get(), system_palette.data() + first, first, ncolor) < 0) {
ErrSdl();
}
}
void palette_init()
{
LoadBrightness();
}
void LoadPalette(const char *path)
{
assert(path != nullptr);
if (HeadlessMode) return;
LogVerbose("Loading palette from {}", path);
std::array<Color, 256> palData;
LoadFileInMem(path, palData);
for (unsigned i = 0; i < palData.size(); i++) {
logical_palette[i] = palData[i].toSDL();
}
}
void LoadPaletteAndInitBlending(const char *path)
{
assert(path != nullptr);
if (HeadlessMode) return;
LoadPalette(path);
if (leveltype == DTYPE_CAVES || leveltype == DTYPE_CRYPT) {
GenerateBlendedLookupTable(/*skipFrom=*/1, /*skipTo=*/31);
} else if (leveltype == DTYPE_NEST) {
GenerateBlendedLookupTable(/*skipFrom=*/1, /*skipTo=*/15);
} else {
GenerateBlendedLookupTable();
}
}
void LoadRndLvlPal(dungeon_type l)
{
if (HeadlessMode)
return;
if (l == DTYPE_TOWN) {
LoadPaletteAndInitBlending("levels\\towndata\\town.pal");
return;
}
if (l == DTYPE_CRYPT) {
LoadPaletteAndInitBlending("nlevels\\l5data\\l5base.pal");
return;
}
int rv = RandomIntBetween(1, 4);
char szFileName[27];
if (l == DTYPE_NEST) {
if (!*GetOptions().Graphics.alternateNestArt) {
rv++;
}
*fmt::format_to(szFileName, R"(nlevels\l{0}data\l{0}base{1}.pal)", 6, rv) = '\0';
} else {
*fmt::format_to(szFileName, R"(levels\l{0}data\l{0}_{1}.pal)", static_cast<int>(l), rv) = '\0';
}
LoadPaletteAndInitBlending(szFileName);
}
void IncreaseBrightness()
{
int brightnessValue = *GetOptions().Graphics.brightness;
if (brightnessValue < 100) {
int newBrightness = std::min(brightnessValue + 5, 100);
GetOptions().Graphics.brightness.SetValue(newBrightness);
UpdateSystemPalette(logical_palette);
}
}
void DecreaseBrightness()
{
int brightnessValue = *GetOptions().Graphics.brightness;
if (brightnessValue > 0) {
int newBrightness = std::max(brightnessValue - 5, 0);
GetOptions().Graphics.brightness.SetValue(newBrightness);
UpdateSystemPalette(logical_palette);
}
}
int UpdateBrightness(int brightness)
{
if (brightness >= 0) {
GetOptions().Graphics.brightness.SetValue(brightness);
UpdateSystemPalette(logical_palette);
}
return *GetOptions().Graphics.brightness;
}
void BlackPalette()
{
for (SDL_Color &c : system_palette) {
c.r = c.g = c.b = 0;
}
SystemPaletteUpdated();
}
void PaletteFadeIn(int fr, const std::array<SDL_Color, 256> &srcPalette)
{
if (HeadlessMode)
return;
if (demo::IsRunning())
fr = 0;
SDL_Color palette[256];
ApplyGlobalBrightness(palette, srcPalette.data());
if (fr > 0) {
const uint32_t tc = SDL_GetTicks();
fr *= 3;
uint32_t prevFadeValue = 255;
for (uint32_t i = 0; i < 256; i = fr * (SDL_GetTicks() - tc) / 50) {
if (i == prevFadeValue) {
SDL_Delay(1);
continue;
}
ApplyFadeLevel(i, system_palette.data(), palette);
SystemPaletteUpdated();
// We can skip hardware cursor update for fade level 0 (everything is black).
if (i != 0 && IsHardwareCursor()) {
ReinitializeHardwareCursor();
}
prevFadeValue = i;
BltFast(nullptr, nullptr);
RenderPresent();
}
}
UpdateSystemPalette(palette);
if (IsHardwareCursor()) ReinitializeHardwareCursor();
if (fr <= 0) {
BltFast(nullptr, nullptr);
RenderPresent();
}
sgbFadedIn = true;
}
void PaletteFadeOut(int fr, const std::array<SDL_Color, 256> &srcPalette)
{
if (!sgbFadedIn || HeadlessMode)
return;
if (demo::IsRunning())
fr = 0;
if (fr > 0) {
SDL_Color palette[256];
ApplyGlobalBrightness(palette, srcPalette.data());
const uint32_t tc = SDL_GetTicks();
fr *= 3;
uint32_t prevFadeValue = 0;
for (uint32_t i = 0; i < 256; i = fr * (SDL_GetTicks() - tc) / 50) {
if (i == prevFadeValue) {
SDL_Delay(1);
continue;
}
ApplyFadeLevel(256 - i, system_palette.data(), palette);
SystemPaletteUpdated();
prevFadeValue = i;
BltFast(nullptr, nullptr);
RenderPresent();
}
}
BlackPalette();
if (IsHardwareCursor()) ReinitializeHardwareCursor();
if (fr <= 0) {
BltFast(nullptr, nullptr);
RenderPresent();
}
sgbFadedIn = false;
}
void palette_update_caves()
{
CycleColors(1, 31);
SystemPaletteUpdated(1, 31);
}
/**
* @brief Cycle the lava every other frame, and glow every frame
* Lava has 15 colors and the glow 16, so the full animation has 240 frames before it loops
*/
void palette_update_crypt()
{
static bool delayLava = false;
if (!delayLava) {
CycleColorsReverse(1, 15);
delayLava = false;
}
CycleColorsReverse(16, 31);
SystemPaletteUpdated(1, 31);
delayLava = !delayLava;
}
/**
* @brief Cycle the pond waves and bubles colors every 3rd frame
* Bubles have 8 colors and waves 7, so the full animation has 56 frames before it loops
*/
void palette_update_hive()
{
static uint8_t delay = 0;
if (delay != 2) {
delay++;
return;
}
CycleColorsReverse(1, 8);
CycleColorsReverse(9, 15);
SystemPaletteUpdated(1, 15);
delay = 0;
}
void SetLogicalPaletteColor(unsigned i, const SDL_Color &color)
{
logical_palette[i] = color;
ApplyGlobalBrightnessSingleColor(system_palette[i], logical_palette[i]);
SystemPaletteUpdated(i, 1);
UpdateBlendedLookupTableSingleColor(i);
}
} // namespace devilution