Pull to refresh

Добавляем эффект Motion Blur в WPF-приложения

Reading time6 min
Views19K

Привет, Хабр!
Все мы много раз слышали фразу о том, что 24 кадра в секунду — это максимально возможное значение, которое способно воспринимать человеческое зрение. Поэтому, если ускорить видеоряд всего на один лишний кадр, можно внедрить в подсознание зрителя любую информацию. И все мы, конечно, знаем, что это неправда. Так же, как и фотодиоды матрицы цифровых фотоаппаратов, нейроны сетчатки фиксируют не мгновенную освещённость в данной точке, а суммарный световой поток за некоторый короткий интервал времени, в результате чего быстродвижущиеся объекты кажутся нам «смазанными». Более того, наш мозг привык к такой особенности зрения, поэтому видео, скомпонованное из отдельных фотографий объекта, нам кажется неестественным. То же самое касается и компьютерной анимации. Художники-мультипликаторы уже давно научились дорисовывать размытые шлейфы за своими персонажами — такой приём называется «Motion blur» и доступен во всех современных пакетах 2d- и 3d-анимации. Но как быть обычным desktop-программистам? В этой статье я попытаюсь рассказать о том, как я прикручивал Motion Blur к WPF-приложению для придания эффекта отзывчивости при появлении всплывающих окон.

Для начала, предлагаю взглянуть на две картинки. Количество кадров в них одинаковое.
Обычная анимация Motion Blur
Окно без Motion Blur
Окно с Motion Blur

Если вы не видите разницы, можно считать, что я потратил несколько вечеров впустую. Но хочется верить, что разница всё же заметна :)

А вот как это выглядит в покадровой развёртке:
Обычная анимация
Motion Blur

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

Заглянем под капот?

Пиксельные шейдеры

Реализовать нетормозящий Motion Blur эффект вряд ли возможно даже на самых мощных современных центральных процессорах, поэтому ключевую роль в отрисовке размытого «следа» в моём примере играет GPU. Благодаря поддержке в WPF пиксельных шейдеров, мы можем применять к визуальным элементам различные эффекты, в том числе тени, distortion-эффекты (лупа, скручивание, рябь), изменение цветового баланса, размытие и т.п.
Если вам кажется, что шейдеры — это что-то страшное и сложное, я с вами полностью согласен. До недавнего времени я был уверен, что никогда в жизни с ними не столкнусь, если только не пойду в game-dev. Но оказалось, что и в прикладном программировании они тоже могут пригодиться. При этом совсем не обязательно знать специализированные языки для написания шейдеров, такие как GLSL, HLSL и т.д. На просторах интернета уже давно существует множество готовых примеров шейдеров, один из которых я и использовал. Называется он ZoomBlurEffect и входит в поставку демо-шейдеров бесплатного редактора «Shazzam Shader Editor». Вот его код:

ZoomBlurEffect.fx
/// <class>ZoomBlurEffect</class>
/// <description>An effect that applies a radial blur to the input.</description>

sampler2D  inputSource : register(S0);

/// <summary>The center of the blur.</summary>
float2 Center : register(C0);

/// <summary>The amount of blur.</summary>
float BlurAmount : register(C1);


float4 main(float2 uv : TEXCOORD) : COLOR
{
	float4 c = 0;    
	uv -= Center;

	for (int i = 0; i < 15; i++)  {
		float scale = 1.0 + BlurAmount * (i / 14.0);
		c += tex2D(inputSource, uv * scale + Center);
	}
   
	c /= 15;
	return c;
}


Даже не зная языка HLSL, на котором написан данный шейдер, можно легко понять алгоритм его работы: для каждой точки конечного изображения вычисляется усредненное значение цвета 15-ти точек, расположенных на прямой, проходящей между данной точкой и центром размытия, хранящимся в регистре C0. Удалённость усредняемых точек от данной точки регулируется параметром BlurAmount, хранящемся в регистре C1. В нашем примере размытие происходит из центра изображения, поэтому C0 равен (0.5;0.5), а значение параметра BlurAmount зависит от того, насколько сильно текущий кадр отличается от предыдущего, но об этом чуть позже.
Конечно, в таком виде шейдер использовать не получится — его необходимо скомпилировать с помощью утилиты fxc.exe, входящей в состав DirectX SDK. Результатом компиляции пиксельного шейдера является файл с расширением ".ps", который может быть использован в нашем WPF-приложении. Для этого добавим его в наш проект в качестве ресурса и создадим класс ZoomBlurEffect:

ZoomBlurEffect.cs
    /// <summary>An effect that applies a radial blur to the input.</summary>
    public class ZoomBlurEffect : ShaderEffect
    {
        public static readonly DependencyProperty InputProperty =
            RegisterPixelShaderSamplerProperty("Input", typeof (ZoomBlurEffect), 0);

        public static readonly DependencyProperty CenterProperty =
            DependencyProperty.Register("Center", typeof (Point), typeof (ZoomBlurEffect),
            new UIPropertyMetadata(new Point(0.9D, 0.6D), PixelShaderConstantCallback(0)));

        public static readonly DependencyProperty BlurAmountProperty =
            DependencyProperty.Register("BlurAmount", typeof (double), typeof (ZoomBlurEffect),
            new UIPropertyMetadata(0.1D, PixelShaderConstantCallback(1)));

        public ZoomBlurEffect()
        {
            PixelShader = new PixelShader
            {
                UriSource = new Uri(@"pack://application:,,,/ZoomBlurEffect.ps", UriKind.Absolute)
            };

            UpdateShaderValue(InputProperty);
            UpdateShaderValue(CenterProperty);
            UpdateShaderValue(BlurAmountProperty);
        }

        public Brush Input
        {
            get { return ((Brush) (GetValue(InputProperty))); }
            set { SetValue(InputProperty, value); }
        }

        /// <summary>The center of the blur.</summary>
        public Point Center
        {
            get { return ((Point) (GetValue(CenterProperty))); }
            set { SetValue(CenterProperty, value); }
        }
        
        /// <summary>The amount of blur.</summary>
        public double BlurAmount
        {
            get { return ((double) (GetValue(BlurAmountProperty))); }
            set { SetValue(BlurAmountProperty, value); }
        }
    }

(На самом деле, Shazzam Shader Editor сам умеет генерировать подобные классы-обёртки для шейдеров, чем я и воспользовался.)

Анимация при появлении окна

У любого визуального элемента имеется свойство RenderTransform, которое используется графической подсистемой для трансформации элемента во время его отрисовки. К таким трансформациям относятся масштабирование, вращение и наклон. В нашем примере мы будем изменять масштаб контента окна от нуля (контент «свёрнут» в точку) до единицы (контент растянут на всё окно). Само окно при этом имеет прозрачный фон, а отрисовка хрома (рамки с заголовком) у него отключена.
Для анимации в WPF традиционно используются так называемые «функции плавности». Мы можем использовать предопределённые функции или написать свои, унаследовавшись от класса EasingFunctionBase.
В примере из данной статьи я использовал функцию ElasticEase, которая придаёт окну эффект «отпущенной пружины» — вначале оно резко расширяется до размеров, немного превосходящих установленные, а затем плавно уменьшается.

Псевдокод анимации появления окна без эффекта Motion Blur
double t = 0.0;
int ВремяНачалаАнимации = ТекущееСистемноеВремя;
while (t < 1.0)
{
  УстановитьМасштабКонтента(ElasticEase(t));
  t = (ТекущееСистемноеВремя - ВремяНачалаАнимации) / ПродолжительностьАнимации;
}
УстановитьМасштабКонтента(1.0);


Здесь t изменяется в пределах от 0 до 1, где 0 — момент начала анимации, а 1 — момент её окончания. Значение функции ElasticEase(t) изменяется примерно по такому закону:


Добавим motion-blur к нашей анимации. Для этого используем свойство Effect у дочернего контрола окна:
content.Effect = new ZoomBlurEffect { Center = new Point(0.5, 0.5) };

Псевдокод анимации с эффектом Motion Blur
double t = 0.0;
double prevEase = 0.0;
int ВремяНачалаАнимации = ТекущееСистемноеВремя;
УстановитьЭффектКонтента(new ZoomBlurEffect { Center = new Point(0.5, 0.5) });
while (t < 1.0)
{
  var ease = ElasticEase(t);
  УстановитьМасштабКонтента(ease);
  content.Effect.BlurAmount = ease - prevEase;
  prevEase = ease;
  t = (ТекущееСистемноеВремя - ВремяНачалаАнимации) / ПродолжительностьАнимации;
}
УстановитьМасштабКонтента(1.0);
УстановитьЭффектКонтента(null);


Отличие данного кода от предыдущего в том, что на каждом шаге мы изменяем значение BlurAmount в зависимости от того, на сколько сильно текущее значение функции ElasticEase отличается от значения на предыдущем шаге. В начале анимации функция растёт быстро, BlurAmount имеет довольно большое значение, поэтому и «смазанность» окна большая. В конце — BlurAmount практически равен нулю, а значит и «смазанность» почти не заметна.

О недостатках

К сожалению, применение эффекта Motion Blur в WPF-приложениях вызывает некоторые проблемы. Вот некоторые из них:
  • Производительность. Как показала практика, даже GPU не всесилен. По результатам тестов, добавление zoom-эффекта к окну замедляет рендеринг примерно в 1.5-2 раза (на моей видео-карте). Однако, поскольку визуально кажется, что FPS существенно возрос, это не кажется мне большой проблемой.
  • Не ясно, зачем это вообще нужно :) Я проводил опрос среди друзей, видят ли они разницу между поведением с включенным эффектом и без него. И все сначала сказали, что разницы нет. После указания, на что конкретно нужно смотреть, большая часть выразила wow-эффект, но другая часть сказала, что без эффекта гораздо лучше и чётче. Их мнение тоже нужно учитывать.


Заключение

К сожалению, я так и не довёл идею использования эффекта Motion Blur до состояния production-кода, поскольку она едва ли применима в тех приложениях, которыми мне приходится заниматься. Делал, так сказать, для души. Надеюсь, что данный материал окажется кому-то полезен в его работе.

Скачать демонстрационный проект можно отсюда: github.com/misupov/motion-blur
Tags:
Hubs:
Total votes 40: ↑38 and ↓2+36
Comments16

Articles