Эффект кофты на шейдерах для мобильных устройств

Пролог


Доброго времени суток! После опубликовании статьи о визуализации квадратичного дерева (Quad-tree), меня попросили написать статью, показывающую работу шейдера, переводящего изображение в «кофту».

Так что, давай рассмотрим данную методику.

Реализация


Для реализации алгоритма потребуется текстура со стежками кофты. Она выглядит следующим образом.

9dc13fc15a4b4f2b96557edd18ad9b8c.png

Текстура состоит из 30 стежков одинаковой ширины в 30 пикселей. В итоге, мы должны выводить эту текстуру на экран, меня цвет стежка под нужный цвет на экране, и перемешивать сами стежки на экране, чтобы изображение не казалось искусственным.

Изначально нужно подготовить параметры для шейдера, языком описанным в статье будет C# и GLSL соответсвенно.

override public void OnScaleUpdated(float scale) {
        Vector2 screenSize = ScreenTool.GetViewportSize ();
        Texture2D stitchTexture = textureLoader.texture;

        float resolutionH = Mathf.Clamp((Mathf.RoundToInt(screenSize.x/(float)itemWidth * scale) * 2), 1, 2048);
        float resolutionV = Mathf.Clamp((Mathf.RoundToInt(screenSize.y/(float)stitchTexture.height * scale) * 2), 1, 2048);
        resolution = new Vector4 (resolutionH, resolutionV, 0f, 0f);

        float amountH = (float)(resolutionH * itemWidth) / (2.0f * (float)stitchTexture.width);
        float amountV = (float)resolutionV / 2.0f;
        Vector4 amount = new Vector4 (amountH, amountV, 0f, 0f);

        material.SetTexture ("_ItemTex",      stitchTexture);
        material.SetVector ("_Resolution",    resolution);
        material.SetVector ("_ItemsAmount", amount);
}

Функция вызывается при обновлении коэффициента скалирования, в ней рассчитывается количество текстур со стежками.

  • Amount — это количество текстур, которое влезет на экран;
  • itemWidth — ширина одного стяжка;

В переменной amountH происходит деление на 2, это сделано для того, чтобы разделить один стежок по полам и менять цвет половинки стежка.

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

162827389f1948f29397a14d411357ec.png

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

float2 colorBlock = floor(input.tex * _Resolution);

Следующий этап для меня оказался самым сложным, необходимо на определённый блок, выбрать относительно случайный стежок из текстуры.

float2 stichUV = float2(frac(_ItemsAmount.x * (input.tex.x + pow(colorBlock.y, 2.0) / _Resolution.x*2)), frac(_ItemsAmount.y * input.tex.y));

Далее берём цвет блока, цвет стежка и цвет предыдущего блока. Цвет предыдущего блока необходимо брать, так как стежки выходят за пределы своих блоков.

fixed4 color = tex2D(_MainTex, input.tex);
fixed4 newColor = tex2D(_ItemTex, stichUV);
fixed4 prevColor = tex2D(_MainTex, float2(input.tex.x, input.tex.y + 1.0/_Resolution.y));

83d50213e43e4c2282c15255b4783750.png

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

if (stichUV.y > 0.5)
 {
        newColor *= color;
        float2 topStichUV = frac(float2(_ItemsAmount.x * (input.tex.x + pow(colorBlock.y + 1, 2) / _Resolution.x*2), _ItemsAmount.y*input.tex.y - 0.5));
        fixed4 otherStich = tex2D(_ItemTex, topStichUV) * prevColor;
                                        
        if (otherStich.a > 0.05)
        {
                newColor = lerp(newColor, otherStich, 1 - newColor.a);
        }
}

Изначально смешиваем цвет стежка с цветом блока, после чего находим стежок, который должен отрисовываться над красной частью и потом смешиваем две части стежка.

Нижняя часть отрисовывает аналогично (зелёная):

else
{
        stichUV = float2(frac(_ItemsAmount.x * (input.tex.x + pow(colorBlock.y + 1, 2) / _Resolution.x*2)), frac(_ItemsAmount.y*input.tex.y));
        newColor = tex2D(_ItemTex, stichUV);
        newColor *= prevColor;
        float2 bottomStichUV = float2(frac(_ItemsAmount.x * (input.tex.x + pow(colorBlock.y, 2) / _Resolution.x*2)), frac(_ItemsAmount.y*input.tex.y + 0.5));
        fixed4 otherStich = tex2D(_ItemTex, bottomStichUV) * color;
                                        
        if (otherStich.a > 0.7)
        {
                newColor = lerp(otherStich, newColor, 1 - prevColor.a);
        }
}

Эпилог


В результате получаем довольно шустрый шейдер, который при желании можно заставить хорошо работать на старых Android (на iOS он уже будет работать нормально). При этом можно использовать различные текстуры для изменения эффекта, допустим вышивка крестиком.

Спасибо за внимание!

© Habrahabr.ru