Добавляем эффект 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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

      0
      Интересно, а можно так переместить объект из точки А в точку Б, чтоб всё блурилось… Тогда перемещение по ключевым кадрам круто бы смотрелось :)
        0
        Если речь о контролах внутри окна, не вижу ничего сложного — в том же редакторе Shazzam есть пример шейдера DirectionalBlur, который создаёт эффект быстрого движения. Правда, в нём задаётся только направление и скорость движения. Идеальный шейдер должен, наверно, уметь искривлять размытый след по трём точкам, чтобы можно было двигать объекты по нелинейным траекториям.
        А если хотите анимировать таким образом движение окон, тут всё сложнее. WPF не умеет рисовать за пределами окна, поэтому нужно позиционировать окно так, чтобы оно вмещало не только собственно элементы окна, но и след от движения, что уже не так тривиально.
        0
        Как мне кажется если ваше изображение имело бы прозрачную границу в несколько пикселей, то Motion Blur по краю затемнял бы изображение, так как цвет смешивался с прозрачными (обычно чёрными) пикселями изображения.

        Для исправления этого может понадобится добавить проверку на прозрачность и находить средние значение только для непрозрачных пикселей на прямой.
          0
          Проверил, затемнения нет. Равно как нет и засвета на тёмных картинках и обрезанной рамкой. Так что всё ок. Не могу объяснить такое поведение шейдеров — не спец :)
          0
          Левая картинка выглядит тормозящей, правая искусственно смазанной, при прочих равных правая приемлимее. Но ведь в реальных условиях слева fps должен быть выше (или оно уже учтено?). // Впрочем, к сути статьи это не имеет никакого отношения.
            0
            К сожалению, это проблема софта, который я использовал для записи gif-анимации. На сколько я понял, там было выставлено ограничение на минимальную задержку между кадрами равную 0.06 секунд. В описании программы было написано, что это минимально возможная задержка для современных браузеров. В демо-программе всё работает быстрее, и смазанность практически не видна.
            +1
            Основная ценность статьи для меня, это не motion blur, а работа с шейдерными эффектами.

            Да и на гифке не очень хорошо заметно, может лучше было бы сделать видео? Побольше кадров.
              0
              А есть ли на платформе WP ограничение на частоту кадров?
              Если нет, то лучше в 2 раза увеличить частоту отрисовки анимации и глаз сам будет видеть размытие, самое естественное и правильное
                0
                Оконная подсистема Windows основана на очереди сообщений и разгребает их с такой скоростью, которая только возможна на данном железе. Я бы с радостью увеличил частоту отрисовки в 2, 10, 100 раз, да только кто ж мне позволит? :)
                  0
                  У WPF своя очередь. По умолчанию FPS равен 60, к слову.

                  Этот FPS иногда наоборот приходится снижать, чтобы CPU не проседал из-за графических ненужностей.
                  0
                  FPS у WPF по умолчанию равен 60. Если FPS меньше, значит, отрисовка тормозит.
                    0
                    Насчёт своей очереди знал, а вот насчёт 60FPS нет, спасибо. Да, отрисовка из примера на моём железе происходит медленнее, чем 60 раз в секунду, не зависимо от того, включен Motion Blur или нет. Видимо, дело в RenderTransform.
                  +2
                  На мой взгляд, повсеместно такой эффект лучше не использовать (мне кажется, меня бы затошнило, если бы на экране постоянно была такая муть). А вот для привлечения внимания и усиления эффекта — вполне. Сразу создается впечатление, что окно не просто появилось, а прямо так «БАБАХ!» появилось =) И не нужно спрашивать мнение людей напрямую, роль у подобных эффектов — воздействовать на подсознательном уровне.
                    +3
                    Извиняюсь, если уведу разговор в иное русло.
                    Я, как специалист в компьютерной графике, анализирую движение и анимацию.

                    Реалистичность, или привычность движения очень зависит от анимации. В вашем примере у вас нет причинно следственной связи в анимации обьекта. А именно — он двигается постоянно равномерно. Это очень скучно почти всегда. Тогда как — он должен выскочить, затормозить и затухать. Это почти всегда 3 разные фазы.

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

                    Нет под рукoй инструментов, чтобы сконвертить в gif, извините.
                    С мошн Блюром Без мошн Блюра.
                      0
                      Спасибо за комментарий. Собственно, функция ElasticEase, которую я использовал для анимации, ведет себя именно так, как в ваших примерах. Она параметризуется количеством осцилляций, и в моём примере оно равно единице. Если его увеличить, то получится как раз как у вас. Я пробовал ставить большие значения, но мне показалось, что анимация становится слишком навязчивой, хотя это дело вкуса, конечно.
                      –2
                      А я как человек, который ещё и ПОЛЬЗУЕТСЯ программами (в отличие от некоторых, которые их только пишут), говорю, что мне науй не нужно, чтоб каждая мулька грузила комп своими окошками с моушнблуром.

                      Only users with full accounts can post comments. Log in, please.