Hatred's Log Place

DON'T PANIC!

Nov 18, 2013 - 7 minute read - programming c++

Эффект плазмы

Во многих системах есть хранитель экрана который показывает примерно следующее:
-PLASMA-ColorCycling.Gif

Разбираясь на работе с функционалом дисплея, особенно в динамическом режиме (при смене картинок), решил использовать алгоритм для генерации данной красоты.

Сразу сниму все лавры со своей головы - алгоритм полностью взят отсюда: 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 цветной палитры, базовой картинки (паттерна) и сдвига для генерации цвета в нужном пикселе. Если генерацией палитры мы сможем только поменять составные цвета результирующей картинки, то генерация базовой картинки очень существенно сказывается на внешнем виде конечного результата:

plasma2.jpg plasma3.jpg plasma5.jpg

(картинки взяты с сайта с описанием реализации алгоритма)

В текущей реализации я решил, что палитра будет генерироваться постоянно одна и та же((не совсем правда)), а вот алгоритм генерации паттерна можно переключать: три предопределённых и пользовательский.

При задании пользовательского алгоритма необходимо задать функцию, в которой будет произведён расчёт паттерна. Кроме того, в случае пользовательского алгоритма, можно заполнить и палитру своими значениями цвета. Обратите внимание: по умолчанию размер палитры - 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)