Adobe Pixel Bender. Новый уровень в обработке изображений для flash

February 28, 2010

Компания adobe целенаправленно работает над превращением flash, изначально позиционировавшегося как средство быстрого создания анимации и баннеров, в платформу для создания “больших и серьезных” приложений. Так у всех на слуху такие технологии как flex и air, ориентированные, прежде всего, на создание бизнес-приложений с развитым пользовательским интерфейсом (кнопки, списки, поля и таблицы с данными). Помимо этого flex и air предлагают удобные средства общения приложения с веб-сервером и исполняющимся на нем приложением, что делает их отличным выбором для создания новых или миграции старых бизнес-приложений в веб-среду. Это все очень перспективно и можно только порадоваться за успешную деятельность adobe. Однако, акцент на flex и air вовсе не означает того, что adobe замедлила темпы развития flash как средства создания тех самых пресловутых баннеров и анимации. И эта работа не сводится к повышению удобства работы в среде flash ide, появлению новых инструментов анимации или улучшению интеграции flash cs и photoshop cs. Что более важно, adobe активировала работу над повышением быстродействия среды исполнения flash, в основном, за счет появления аппаратной поддержки рендеринга. Именно благодаря этому стал реальным быстрый прогресс 3d-движков, написанных на actionscript3. И, хотя flash 3d-движки были и раньше, но тогда они были слишком медленные и представляли в основном теоретический интерес. А теперь производительности flash player 10 хватает для того, чтобы “без тормозов” показывать настоящее 3d во flash-играх. Также adobe представила несколько интересных продуктов находящиеся на стыке flash и других языков и технологий. К примеру, проект alchemy (http://labs.adobe.com/wiki/index.php/Alchemy:FAQ) ставит целью дать возможность компилировать программы, написанных на языке “c”, в байт-код для последующего исполнения в среде flashplayer. Естественно, что поддержки “c++” нет, равно как и не все библиотеки и стандартные функции языка “c” могут быть подвергнуты такому преобразованию. Автор этой статьи не смог удержаться от того, чтобы не запустить у себя на компьютере старый добрый DOOM, адаптированный и портированный под flashplayer (http://www.newgrounds.com/portal/view/470460) – работает и очень быстро. Не меньший интерес представляет и технология Adobe Pixel Bender – тема сегодняшнего материала.

Трудно найти сейчас человека имеющего хоть какое-то отношение к IT-индустрии или просто домашнего пользователя, который не слышал об шейдерах. Даже не зная в деталях, что это и как оно устроены, мы знаем что шейдеры – это что нужно для современных 3d-игрушек. И, покупая для домашнего компьютера видеокарту, мы интересуемся тем, какую версию directx и какую версию шейдеров она поддерживает. Не вдаваясь в детали, шейдеры – это написанные на достаточно высокоуровневом языке программирования небольшие программки, которые говорят, что нужно делать с изображением перед тем как его показать на экране. Так, простейшая программа-шейдер, может изменить цветовую гамму изображения, реализовать красочный эффект взрыва, тумана, отражения и “почти как настоящую” воду. В зависимости от того над каким именно объектом оперирует программа-шейдер, различаются пиксельные, вершинные, геометрические и параллаксные шейдеры. Из названия понятно, что пиксельные шейдеры принимают на вход пиксель изображения перед его выводом на экран и по какому-то правилу на основе исходного цвета пикселя, его координат и глубины рассчитывают для пикселя новый цвет. Вершинный шейдер получает на вход координаты вершин треугольников и модифицирует их. Геометрический шейдер оперирует не над отдельными вершинами, а над геометрическими примитивами (треугольниками) и , в том числе, умеет создавать для этих примитивов новые вершины. Параллаксные шейдеры служат для реализации эффекта параллакса, когда изображение объекта меняется в зависимости от его расстояния до наблюдателя и расположения точки зрения. Именно на эффекте параллакса основано объемное (бинокулярное) зрение у человека. Когда-то на заре появления шейдеров в видеокарте существовали отдельные блоки, рассчитывающие шейдеры только определенных видов. Сейчас же используется унифицированная архитектура, когда один шейдерный блок видеокарты может просчитать любой вид шейдера. Теперь можно подвести важный итог: шейдеры позволяют написать небольшую программу, которая будет очень быстро выполняться на видеокарте (GPU). И происходит это за счет того, что шейдеры предполагают массированные однотипные вычисления. И это будет гораздо эффективнее, чем считать такие же эффекты на процессоре (пусть и более универсальном, но проигрывающем по скорости, если нужно просчитать большое число несложных и однотипных алгоритмов). С помощью шейдеров можно создавать красочные и фотореалистичные изображения, применяя к картинке различные эффекты. Поэтому появление поддержки шейдеров во flash можно только приветствовать, что и произошло с выпуском flash player 10. Причем сделано это было с “умной” аппаратной поддержкой, когда шейдер выполняется или на GPU или на CPU в зависимости от возможностей вашего компьютера, в том числе с поддержкой многоядерных процессоров.

Итак, Adobe Pixel Bender – это язык и технология для написания шейдерных программ и является, в какой-то мере, наследником другого известного “графического” языка программирования, а именно GLSL (OpenGL Shading Language). Созданные вами шейдерные программы могут быть внедрены или в Adobe Photoshop или Adobe After Effects, для последующего их применения к изображению как фильтр. Еще можно загрузить шейдер в flash-приложение (тот же баннер) и применить шейдерный эффект на тамошнюю картинку. Для удобной разработки этих самых шейдерных программ компания Adobe создала интегрированную среду разработки Adobe pixel bender toolkit. Конечно, не стоит рассчитывать на то, что flashplayer (pixel bender) умеет работать со всеми четырьмя видами шейдеров. Так в нашем распоряжении есть только самые простые “плоские” шейдеры – пиксельные, но и с их помощью можно много добиться (примеры можно посмотреть на сайтах http://www.adobe.com/cfusion/exchange/index.cfm?event=productHome&exc=26) или http://mrdoob.com/blog/post/586.

Теперь нужно разобраться с базовой терминологией pixel bender, т.е. я расскажу о том, что такое Kernel и Processing Graph. Воспринимайте Kernel как обычную функцию, получающую на вход один или несколько пикселей с одного или нескольких входных изображений, и затем выполняющую над этими пикселями какой-то расчет. В конце Kernel возвращает пиксель, который и нужно отрисовать на экране. Однако для создания по настоящему сложных эффектов этого не достаточно – гораздо лучше написать несколько kernel-программ, а затем объединить их в единое целое. Т.е. описать, как результаты работы одних Kernel-ов поступят как входные данные для других Kernel-ов, создав, таким образом, граф обработки изображения. В сегодняшней статье я не собираюсь углубляться в детали kernel-языка, равно как и в детали языка описания Processing Graph. Вся эта информация очень подробно изложена в справочной документации идущей вместе с Pixel Bender Toolkit.

Теперь перейдем к практике, и попробуем сделать своими руками небольшой эффект накладываемый на загруженную во flash-картинку. Для начала Pixel Bender Toolkit нужно скачать по следующей ссылке http://labs.adobe.com/technologies/pixelbender/ (продукт полностью бесплатен).

Программа для Pixel Bender сохраняется как файл с расширением “pbk”, затем эти файлы могут быть либо непосредственно загружены в среду Adobe Photoshop CS или Adobe After Effects CS. Либо вы можете выполнить компиляцию kernel-программы в файл “pbj”, который может быть загружен и выполнен средой исполнения flashplayer. Тут есть важный момент: если мы создаем приложение именно для выполнения в среде flashplayer, то нам будет доступна не вся функциональность языка Pixel Bender Kernel. А также будет полностью отсутствовать поддержка языка Graph Language (подробный перечень этих ограничений также можно найти в справке идущей вместе Pixel Bender Toolkit). Теперь я приведу заготовки программы на Pixel Bender Kernel. Эту заготовку вы можете получить в любой момент времени, если выберите пункт меню “File -> New Kernel”:
  1. <languageVersion : 1.0;>
  2. kernel NewFilter
  3. <   namespace : "Your Namespace";
  4.     vendor : "Your Vendor";
  5.     version : 1;
  6.     description : "your description";
  7. >
  8. {
  9.     input image4 src;
  10.     output pixel4 dst;
  11.  
  12.     void  evaluatePixel()  {
  13.         dst = sampleNearest(src,outCoord());
  14.     }
  15. }
В первых строках программы мы указываем в угловых скобках версию языка, на котором создан скрипт (1.0). Далее, указываем название нашей kernel программы. Потом, еще в одних угловых скобках, идет перечисление метахарактеристик программы. Т.е. задается название или имя разработчика скрипта, версия скрипта, краткое описание и пространство имен (смысл абсолютно идентичен тому, что подразумевается под namespace в других языка программирования, например, java или c#). Далее внутри фигурных скобок идет, собственно, kernel программа. Начинается она с того, что мы указываем какие данные поступают ей на вход, и что является результатом работы. Так запись “input image4 src” говорит, что на вход поступает картинка с именем ”src” и размерностью 4. В будущем мы будем часто встречаться с переменными, тип которых выглядит, например, так “int2”, “float4”, “pixel3” и т.д. Это означает, что переменные является векторами и хранят не одно, а несколько значений одновременно. Однако, понятие размерности не нужно воспринимать как синоним слова массив. Так объявление типа “int2” говорит, что у нас вектор из двух целых чисел, но объявление “image4” говорит о том, что у нас есть одно изображение, но состоящее из четырех каналов (RGBA), а не массив из четырех картинок. Итак, вот перечень доступных типов данных (здесь pixel1 – это пиксель, представленный только одним каналом, равно как и image1 – картинка, представленная только одним каналом):
float bool int pixel1 image1
float2 bool2 int2 pixel2 image2
float3 bool3 int3 pixel3 image3
float4 bool4 int4 pixel4 image4
Пользоваться такими векторными типами данных очень удобно, т.к. все арифметические операции “перегружены” и умеют перемножать, делить и складывать векторы друг с другом. Тут же стоит упомянуть об операции swizzling. Суть swizzling-а в том, что из вектора, например, из четырех элементов, мы хотим взять только три, например, первый, третий и четвертый элементы. И затем мы можем собрать новую переменную размерностью 3, причем сами элементы могут быть переставлены местами, например:
float4 vec4;
float4 red=vec4.rrrr
float3 rgb_only=vec4.rgb
Но, вернемся назад к описанию заготовки Kernel-скрипта. Перед объявлением функции evaluatePixel мы указали, что входными данными для всего kernel-скрипта является картинка, состоящая из четырех каналов, а на выходе должен быть пиксель, также представленный четырьмя цветовыми каналами. Название функции evaluatePixel() является предопределенной и эта функция должна присутствовать всегда. Однако помимо нее скрипт может содержать еще функции с предопределенным значением и именами, например, evaluateDependents, needed, changed, generated (для чего эти функции предназначены читайте в справке). А еще вы можете создавать собственные функции по правилу:
  1. returnType name([arguments]){
  2.    statements
  3. }
Важно помнить, что написанный нами код будет распараллелен для наиболее эффективного выполнения на видеокарте. А, следовательно, мы никак не можем писать так код, чтобы результаты вычисления одной функции являлись бы входными данными для другой (это нарушает идеологию шейдеров). Теперь внимание на строку:
  1. dst = sampleNearest(src, outCoord());
Здесь говорится, что переменная “dst” (результат вычисления шейдера) получается как результат вызова функции sampleNearest. Что делает функция sampleNearest? Можно было бы сказать, что функция sampleNearest просто берет из картинки (первого аргумента) пиксель в заданных координатах, однако это не совсем так. В компьютерной графике есть такое понятие как sampling, или выборка и вычисление какого-то значения на базе списка образцов (хотя термин sampling применим и к другим сферам деятельности). Предположим, что перед нами стоит задача “натянуть” текстуру изначально данную в форме картинки-образца (размером 200 на 200 пикселей) на поверхность большего или меньшего размера. Здесь мы не можем однозначно поставить в соответствие каждому выходному пикселю один и только один входной пиксель (пикселей может банально не хватать). Поэтому перед нами стоит задача “растянуть” или “сжать” изображение, но так, чтобы избежать вредных артефактов (размытия, неточности и т.д.). Существует несколько алгоритмов решения этой проблемы, но часто все сводится к тому, что мы берем по заданной координате не один пиксель, а несколько окружающих его пикселей, и вычисляем на основании их значений цвет пикселя расположенного посередине. Так, в Pixel Bender есть три функции sampling-а (также существуют версии функций, которые оперируют с картинками и пикселями размером в два, три и четыре канала):
  1. pixel4 sample( image4 im, float2 v )
  2. pixel4 sampleLinear( image4 im, float2 v )
  3. pixel4 sampleNearest( image4 im, float2 v )
Функции sample и sampleLinear делают одно и то же: вычисляют итоговый цвет на основании линейной интерполяции цвета соседних пикселей. Функция же sampleNearest просто берет цвет одного пикселя, расположенного наиболее близко к запрашиваемой выходной координате. Что же касается функции outCoord(), то она возвращает координату пикселя, цвет которого сейчас и рассчитывается. Это значит, что если запустить приведенный мною пример скрипта на выполнение, то мы получим как результат картинку, один в один совпадающую со входным изображением. Давайте это проверим. Для этого в меню “File” мы выбираем пункт “Load Image 1” и выберем какую-нибудь картинку. После того как мы нажмем на кнопку “F5”, то увидим, как и ожидалось, исходное изображение (наш код просто скопировал картинку). Для того, чтобы проверить все ли работает правильно можно модифицировать наш шейдер, например, так:
  1. void  evaluatePixel(){
  2.   dst = pixel4(0,0,1,1);
  3. }
После запуска шейдера на выполнение мы увидим, что вся область выходного изображения залита синим цветом. Т.к. я присвоил всем выходным пикселям цвет заданный его RGBA-компонентами (конструктор типа данных pixel4 принимает четыре числа в отрезке от нуля до единицы именно в порядке RGBA). Теперь попробуйте такой пример (то, что у меня получилось, показано на рис. 1):


  1. void evaluatePixel(){
  2.   if (outCoord().x < 200.0 && outCoord().y < 200.0)
  3.      dst = sampleNearest(src,outCoord());
  4.   else
  5.       dst = pixel4(1,0,0,1);
  6. }
Здесь я проверяю координаты каждого из входных пикселей. И, если они попадают в расположенный в верхнем левом углу квадрат со стороной 300px, то вывожу пиксель исходной картинки. В противном случае выходному пикселю назначается красный цвет. Следующий пример (см. рис. 2)



оперирует над двумя изображениями одновременно. Для того, чтобы его проверить, вам нужно выбрать в меню “File” пункт “Load Image 2”:
input image4 src1;
input image4 src2;
output pixel4 dst;
 
void  evaluatePixel(){
   if (outCoord().x < 300.0 && outCoord().y < 300.0)
      dst = sampleNearest(src1,outCoord());
   else
      dst = sampleNearest(src2,outCoord());
}
А следующая формула может осветлить изображение:
  1. dst = 2.0*sampleNearest(src1,outCoord());
  2. // или наоборот сделать его более темным:
  3. dst = sampleNearest(src1,outCoord()) / 2.0;
Я уверен, что вы и сами сможете придумать достаточно эффектов с Pixel Bender, а мы перейдем к более важной теме и рассмотрим как можно созданный нами шейдер загрузить в среду flashplayer и применить к какому-то изображению. Первым шагом нужно скомпилировать код шейдера в pbj-файл, для чего выбираем пункт меню “File -> Export File For Plash Player”. Затем нужно создать новый actionscript3 проект в предпочитаемой вами среде разработке (intellij idea, flashdevelop, flex builder или adobe flash cs 4). Важно, что классы Shader и ShaderFilter, которыми мы будем оперировать в следующем примере, появились только в версии flex sdk 3 или 4 (http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4). Также нужно указать в свойствах проекта, что код будет генерироваться для десятой версии flashplayer. В моем случае я создал проект в среде flashdevelop, затем скопировал в каталог с проектом произвольную png-картинку. Теперь осталось только привести пример кода, внедряющего картинку в приложение, а затем, применяющего к ней шейдер:
  1. package {
  2.  import flash.display.*;
  3.  import flash.events.*;
  4.  import flash.net.*;
  5.  import flash.filters.*;
  6.  
  7.  [SWF(width="600", height="400", backgroundColor="#aa0000", framerate="30")]
  8.  public class Main extends Sprite {
  9.  
  10.   [Embed(source='/lisa.png')]
  11.   private var fotoClass:Class;
  12.   private var fotoImage:Bitmap = new fotoClass ()
  13.   private var urlLoader:URLLoader = null;
  14.  
  15.   public function Main():void {
  16.     addChild (fotoImage);
  17.     var urlRequest: URLRequest = new URLRequest("shader.pbj");
  18.     urlLoader= new URLLoader();
  19.     urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
  20.     urlLoader.addEventListener( Event.COMPLETE, applyFilter );
  21.     urlLoader.load( urlRequest );
  22.   }
  23.  
  24.   private function applyFilter( event:Event ):void{
  25.      urlLoader.removeEventListener( Event.COMPLETE, applyFilter );
  26.      var shader:Shader = new Shader( event.target.data );
  27.      var shaderFilter:ShaderFilter = new ShaderFilter( shader );
  28.      fotoImage.filters = [ shaderFilter ];
  29.   }
  30.  }
  31. }
Если после компиляции положить рядом с swf-файлом полученный на прошлом этапе pbj-файл с шейдером, то после запуска swf-ролика мы увидим, что к изображению был применен шейдер и оно поменялось (см. рис. 3).



Как видите, здесь нет ничего сложного: для того, чтобы присоединить шейдер к любому “видимому” компоненту нужно поместить его внутрь свойства filters. Естественно, что перед этим необходимо загрузить файл с шейдером (за это отвечают классы URLRequest и URL), а затем нужно сконструировать объект Shader на основании загруженного из файла массива байтов. Более того, мы можем применить шейдер не только к статическому изображению, но и видео-файлу, показываемому с помощью компонента Video, ведь и у компонента Video есть свойство filters.

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

Categories: Flash & Flex