Во многих системах есть хранитель экрана который показывает примерно следующее:
Разбираясь на работе с функционалом дисплея, особенно в динамическом режиме (при смене картинок), решил использовать алгоритм для генерации данной красоты.
Сразу сниму все лавры со своей головы - алгоритм полностью взят отсюда: https://lodev.org/cgtutor/plasma.html (вообще интересный сайт, в частности: https://lodev.org/cgtutor/). Более подробно про него можно почитать тут: Plasma_effect . Кроме того, будет полезно ознакомиться со схожим эффектом: https://www.cubic.org/docs/marble.htm.
Реализация на C++ (примешали совсем чуточку шаблонов :))
Для начала определим вспомогательные классы: RgbColor
, YuvColor
что бы хранить составляющие в цветовых простанствах RGB24 и YUV444 соответственно (что бы хранить составляющие YUV420/411/422 и т.п. после достаточно произвести необходимые расчёты с UV компонентами):
struct RgbColor {
RgbColor()
: R(0), G(0), B(0)
{}
RgbColor(uint8_t r, uint8_t g, uint8_t b)
: R(r),
G(g),
B(b)
{}
uint8_t R;
uint8_t G;
uint8_t B;
};
struct YuvColor {
YuvColor()
: Y(0), U(0), V(0)
{}
YuvColor(uint8_t y, uint8_t u, uint8_t v)
: Y(y),
U(u),
V(v)
{}
uint8_t Y;
uint8_t U;
uint8_t V;
};
Сразу определю функцию для перехода от RGB24 к YUV:
inline
void rgb2yuv(const RgbColor &rgb, YuvColor &yuv)
{
yuv.Y = uint8_t((( 66 * rgb.R + 129 * rgb.G + 25 * rgb.B + 128) >> 8) + 16);
yuv.U = uint8_t((( -38 * rgb.R - 74 * rgb.G + 112 * rgb.B + 128) >> 8) + 128);
yuv.V = uint8_t((( 112 * rgb.R - 94 * rgb.G - 18 * rgb.B + 128) >> 8) + 128);
}
Теоретически, можно было бы сделать симметричную функцию для YUV->RGB, но мне она не требовалась. Кроме того, можно было бы сделать соответствующие методы и конструкторы в классах RgbColor/YuvColor для перехода между цветовыми пространствами, но переход, особенно целочисленный, привносит ошибки, поэтому оставил так.
Теперь короткое лирическое отступление: данная реализация алгоритма основывается на использовании 256 цветной палитры, базовой картинки (паттерна) и сдвига для генерации цвета в нужном пикселе. Если генерацией палитры мы сможем только поменять составные цвета результирующей картинки, то генерация базовой картинки очень существенно сказывается на внешнем виде конечного результата:
(картинки взяты с сайта с описанием реализации алгоритма)
В текущей реализации я решил, что палитра будет генерироваться постоянно одна и та же((не совсем правда)), а вот алгоритм генерации паттерна можно переключать: три предопределённых и пользовательский.
При задании пользовательского алгоритма необходимо задать функцию, в которой будет произведён расчёт паттерна. Кроме того, в случае пользовательского алгоритма, можно заполнить и палитру своими значениями цвета. Обратите внимание: по умолчанию размер палитры - 256. Размер не ограничен (в разумных пределах, конечно), но если нужно больше нужно будет установить новый размер вектору с палитрой.
Кроме того, сама генерация паттерна вынесена в свободные функции, поэтому можно в пользовательском алгоритме поменять только палитру и далее вызвать предопределённую функцию генерации паттерна.
Вернёмся к коду. Что бы для стандартных алгоритмов не обращаться к указателям ввёл вспомогательное перечисление:
enum PlasmaAlgorithm
{
PLASMA_UNDEFINED,
PLASMA_CIRCLE,
PLASMA_ALGO1,
PLASMA_ALGO2,
PLASMA_USER,
PLASMA_DEFAULT = PLASMA_ALGO1
};
Далее определим три стандартных алгоритма генерации паттерна:
template<typename Color>
void PlasmaCircleGenerator(uint32_t width, uint32_t height, vector<Color> &/*pallete*/, vector<int> &pattern)
{
assert(pattern.size() >= width * height);
for (uint32_t y = 0; y < height; ++y)
{
for (uint32_t x = 0; x < width; ++x)
{
int color = int(
128.0 + (128.0 * sin(x / 16.0)) +
128.0 + (128.0 * sin(y / 16.0))
) / 2;
pattern[y * width + x] = color;
}
}
}
template<typename Color>
void PlasmaAlgo1Generator(uint32_t width, uint32_t height, vector<Color> &/*pallete*/, vector<int> &pattern)
{
assert(pattern.size() >= width * height);
for (uint32_t y = 0; y < height; ++y)
{
for (uint32_t x = 0; x < width; ++x)
{
int color = int
(
128.0 + (128.0 * sin(x / 16.0))
+ 128.0 + (128.0 * sin(y / 8.0))
+ 128.0 + (128.0 * sin((x + y) / 16.0))
+ 128.0 + (128.0 * sin(sqrt(double(x * x + y * y)) / 8.0))
) / 4;
pattern[y * width + x] = color;
}
}
}
template<typename Color>
void PlasmaAlgo2Generator(uint32_t width, uint32_t height, vector<Color> &/*pallete*/, vector<int> &pattern)
{
assert(pattern.size() >= width * height);
for (uint32_t y = 0; y < height; ++y)
{
for (uint32_t x = 0; x < width; ++x)
{
int color = int
(
128.0 + (128.0 * sin(x / 16.0))
+ 128.0 + (128.0 * sin(y / 32.0))
+ 128.0 + (128.0 * sin(sqrt(double((x - width / 2.0)* (x - width / 2.0) + (y - height / 2.0) * (y - height / 2.0))) / 8.0))
+ 128.0 + (128.0 * sin(sqrt(double(x * x + y * y)) / 8.0))
) / 4;
pattern[y * width + x] = color;
}
}
}
Тут начинает шаблонная магия самого низкого (в смысле сложности) уровня: я хочу видеть плазму одинаковой для RGB и YUV и, возможно, для другого цветового пространства (HSV, к примеру), но не сейчас.
Далее определяем функции генерации палитры:
template<typename Color>
void CreateDefaultPlasmaPallete(vector<Color> &plasmaPallete);
template<>
void CreateDefaultPlasmaPallete<RgbColor>(vector<RgbColor> &plasmaPallete)
{
for (uint32_t i = 0; i < plasmaPallete.size(); ++i)
{
RgbColor &rgb = plasmaPallete[i];
rgb.R = uint8_t(128.0 + 128 * sin(3.1415 * i / 32.0));
rgb.G = uint8_t(128.0 + 128 * sin(3.1415 * i / 64.0));
rgb.B = uint8_t(128.0 + 128 * sin(3.1415 * i / 128.0));
}
}
template<>
void CreateDefaultPlasmaPallete<YuvColor>(vector<YuvColor> &plasmaPallete)
{
for (uint32_t i = 0; i < plasmaPallete.size(); ++i)
{
YuvColor &yuv = plasmaPallete[i];
RgbColor rgb;
rgb.R = uint8(128.0 + 128 * sin(3.1415 * i / 32.0));
rgb.G = uint8(128.0 + 128 * sin(3.1415 * i / 64.0));
rgb.B = uint8(128.0 + 128 * sin(3.1415 * i / 128.0));
rgb2yuv(rgb, yuv);
}
}
Обратите внимание, что CreateDefaultPlasmaPallete
определена только для RgbColor
и YuvColor
. Если нужна поддержка других цветовых пространств вам нужно будет определить функцию генерации палитры для него.
Ну и собираем все это в классе PlasmaGenerator
:
template<typename Color = RgbColor>
class PlasmaGenerator
{
public:
typedef void (*PlasmaUserGenerator)(uint32_t width, uint32_t height, vector<Color> &pallete, vector<int> &pattern);
public:
PlasmaGenerator()
: m_plasmaPallete(256),
m_width(0),
m_height(0),
m_algo(PLASMA_UNDEFINED),
m_generator(0)
{}
PlasmaGenerator(uint32_t width, uint32_t height, PlasmaAlgorithm algo = PLASMA_DEFAULT)
: m_plasmaPallete(256),
m_generator(0)
{
init(width, height, algo);
}
void setUserGenerator(PlasmaUserGenerator generator)
{
m_generator = generator;
}
void init(uint32_t width, uint32_t height, PlasmaAlgorithm algo = PLASMA_DEFAULT)
{
assert(algo != PLASMA_UNDEFINED);
m_width = width;
m_height = height;
m_algo = algo;
if (m_plasmaPattern.size() != (width * height))
m_plasmaPattern.resize(width * height);
switch(algo)
{
case PLASMA_CIRCLE:
m_generator = &PlasmaCircleGenerator<Color>;
break;
case PLASMA_ALGO1:
m_generator = &PlasmaAlgo1Generator<Color>;
break;
case PLASMA_ALGO2:
m_generator = &PlasmaAlgo2Generator<Color>;
break;
}
commonPlasmaInit();
}
bool isInited() const
{
return (m_plasmaPattern.size() > 0);
}
const Color& getColor(uint32_t x, uint32_t y, uint32_t shift) const
{
assert(m_plasmaPattern.size() > 0);
const uint32_t patternIndex = (y * m_width + x) % m_plasmaPattern.size();
return m_plasmaPallete[(m_plasmaPattern[patternIndex] + shift) % m_plasmaPallete.size()];
}
private:
void commonPlasmaInit()
{
assert(m_generator != 0);
if (m_algo != PLASMA_USER)
{
CreateDefaultPlasmaPallete(m_plasmaPallete);
}
m_generator(m_width, m_height, m_plasmaPallete, m_plasmaPattern);
}
private:
vector<Color> m_plasmaPallete;
vector<int> m_plasmaPattern;
uint32_t m_width;
uint32_t m_height;
PlasmaAlgorithm m_algo;
PlasmaUserGenerator m_generator;
};
Пользоваться достаточно просто:
#include <iostream>
#include <ctime>
#include "plasmagenerator.h"
int main()
{
PlasmaGenerator<RgbColor> plasma; // создаст непроинициализированный генератор
// plasma.isInited() возвращает false
// plasma.getColor(x, y, shift) вызывает assert
// Инициализируем генератор для изображения размером 320x240 пикселей, используя генератор по умолчанию
plasma.init(320, 240);
// Переинициализируем генератор для нового размера изображения и нового алгоритма
// Для алгоритма PLASMA_USER нужно предварительно установить генератор:
// plasma.setUserGenerator(userGenerator);
plasma.init(640, 480, PLASMA_CIRCLE);
while (true) {
// Если считать сдвиг от времени, по получим изменяющуюся по времени картинку
uint32_t shift = int(std::time(0) / 10.0);
for (uint32_t y = 0; y < 480; ++y) {
for (uint32_t x = 0; x < 640; ++x) {
const RgbColor& rgb = plasma.getColor(x, y, shift);
// Записываем в картинку, буффер или ещё куда
}
}
// Отрисовываем буффер
// Тут хорошо бы поспать
}
}
Отмечу, что доступ к PlasmaGenerator::getColor()
из разных потоков безопасен, поэтому заполнение целевой картинки, теоретически, можно параллелить. Но вот генерацию плазмы (PlasmaGenerator::init()
) нужно обязательно синхронизировать и работает она в один поток.
PS вспомогательный функционал:
- Целочисленное преобразование RGB в YUV (псевдокод):
Y = ( ( 66 * R + 129 * G + 25 * B + 128) >> 8) + 16 U = ( ( -38 * R - 74 * G + 112 * B + 128) >> 8) + 128 V = ( ( 112 * R - 94 * G - 18 * B + 128) >> 8) + 128
- Целочисленное преобразование YUV в RGB (псевдокод):
C = Y - 16 D = U - 128 E = V - 128 R = uint8_t(( 298 * C + 409 * E + 128) >> 8) G = uint8_t(( 298 * C - 100 * D - 208 * E + 128) >> 8) B = uint8_t(( 298 * C + 516 * D + 128) >> 8)