Привет, Хабр!
Все мы много раз слышали фразу о том, что 24 кадра в секунду — это максимально возможное значение, которое способно воспринимать человеческое зрение. Поэтому, если ускорить видеоряд всего на один лишний кадр, можно внедрить в подсознание зрителя любую информацию. И все мы, конечно, знаем, что это неправда. Так же, как и фотодиоды матрицы цифровых фотоаппаратов, нейроны сетчатки фиксируют не мгновенную освещённость в данной точке, а суммарный световой поток за некоторый короткий интервал времени, в результате чего быстродвижущиеся объекты кажутся нам «смазанными». Более того, наш мозг привык к такой особенности зрения, поэтому видео, скомпонованное из отдельных фотографий объекта, нам кажется неестественным. То же самое касается и компьютерной анимации. Художники-мультипликаторы уже давно научились дорисовывать размытые шлейфы за своими персонажами — такой приём называется «Motion blur» и доступен во всех современных пакетах 2d- и 3d-анимации. Но как быть обычным desktop-программистам? В этой статье я попытаюсь рассказать о том, как я прикручивал Motion Blur к WPF-приложению для придания эффекта отзывчивости при появлении всплывающих окон.
Для начала, предлагаю взглянуть на две картинки. Количество кадров в них одинаковое.
Обычная анимация | 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
(На самом деле, Shazzam Shader Editor сам умеет генерировать подобные классы-обёртки для шейдеров, чем я и воспользовался.) /// <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); }
}
}
Анимация при появлении окна
У любого визуального элемента имеется свойство 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