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

от автора


Привет, Хабр!
Все мы много раз слышали фразу о том, что 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

ссылка на оригинал статьи http://habrahabr.ru/post/194734/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *