среда, 22 декабря 2010 г.

Pixel Bender

Всем привет, поздравляю всех с наступающими новогодними праздниками и представляю вашему вниманию очередной перевод из книги AdvancED ActionScript 3.0 Animation, автор Keith Peters.

Сегодня речь пойдет о Pixel Bender



Что такое Pixel Bender?



Pixel Bender Toolkit это среда разработки, которая используется для создания, компилирования и тестирования пиксельных шейдеров для использования их в различных продуктах семейства CS, включая Flash. Это предельно простая среда разработки, которая состоит из редактора кода, области предварительного просмотра и области для задания параметров и просмотра ошибок и предупреждений (см рисунок 1).
Pixel Bender это язык программирования, который вы используете для написания пиксельных шейдеров в среде Pixel Bender Toolkit.

Pixel Bender Toolkit


Рисунок 1 – Pixel Bender Toolkit



Хорошо, но что такое пиксельный шейдер? Если вкратце, то это маленькая программа, которая рассчитывает значение отдельно взятого пикселя. Это звучит довольно просто, но это действительно то, чем занимается шейдер. Он берет различные входные данные, производит сложные вычисления и в конце дает значение для каждого пикселя.

Есть несколько причин того, почему разработчики считают, что Pixel Bender это здорово. Во-первых Pixel Bender может взять битмап, заливку или другой визуальный объект и запустить пиксельный шейдер для каждого пикселя этого изображения. Не один за другим, а параллельно. Да, да, значения пикселей вычисляются одновременно. Шейдеры предварительно откомпилированы и запускаются самостоятельно отдельным процессом от Flash Player. Все это делает шейдеры невероятно быстрым средством обработки графики, по сравнению с любыми другими средствами Flash Player.

Один из недостатков – то, что шейдеры должны быть созданы вне Flash с использованием инструмента Pixel Bender Toolkit и языка Pixel Bender, который основан на C. Они должны быть откомпилированы, сохранены, и, наконец, загружены или присоединены с помощью тега embed в ваш ролик перед их использованием. Да, предстоит маленько потрудиться и немного изучить С. Не волнуйтесь – от этого вы только выиграете.

Шейдеры основаны на совсем другой парадигме в отличие от программ на Action Script. Т.к. шейдеру интересен только один пиксель и он выполняется тысячи раз, то все что ему надо знать это координаты x, y пикселя над которым он будет выполняться. Вы можете создавать переменные и передавать их в качестве параметров (включая одно или несколько изображений), но когда вы будете делать это в первый раз, вам может показаться, что вы несколько ограничены в своих действиях. Но скоро вы почувствуете язык и поймете, как использовать те возможности Pixel Bender, которые он вам дает.

Во Flash вы можете использовать шейдеры Pixel Bender в четырех ситуациях:
- Пользовательские фильтры: Также как и нативные фильтры плеера, вы можете присвоить ShaderFilter к любому отображаемому объекту. Фильтр связан с шейдером, который вы написали на Pixel Bender.
- Заливки: при использовании API рисования вы присваиваете заливку сплошным цветом, градиентом или битмапой. Теперь вы можете использовать шейдеры с помощью метода beginShaderFill.
- Режимы смешивания (сопряжения): Шейдер может использоваться в режимах наложения, определяя как отображаемый объект должен влиять ни вид другого объекта, который находится под ним. Для этого используется свойство blendShader отображаемого объекта.
- «Generic number crunching». Использование шейдеров для ускорения математических операций над данными. Тема выходит за пределы этой книги, но довольно интересна для самостоятельного изучения.


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



Итак, открываем Pixel Bender Toolkit, идем в File-->New Kernel Filter. В области редактирования появляется следующий код:



kernel NewFilter
< namespace : "Your Namespace";
vendor : "Your Vendor";
version : 1;
description : "your description";
>
{
input image4 src;
output pixel4 dst;

void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
}
}


Чтобы увидеть результат работы шейдера вам нужно загрузить изображение. Вы можете это сделать с помощью меню File. Выберите изображение и вы его увидите в области предварительного просмотра. Нажмите кнопку Run и вы увидите результат работы шейдера применительно к этому изображению.
И что же вы видите? Совершенно ничего – изображение не изменилось т.к. дефолтный шейдер не делает ничего. Он просто выводит каждый пиксель на выходе в ту же самую позицию, что и на входе. Таким образом, вывод в этой ситуации это то же самое, что и ввод.
Изменим код следующим образом:



kernel NewFilter
< namespace : "Your Namespace";
vendor : "Your Vendor";
version : 1;
description : "your description";
>
{
input image4 src;
output pixel4 dst;

void
evaluatePixel()
{
dst = pixel4(1,0,0,1);
}
}


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

Этот шейдер самый простейший и он отлично подходит для изучения его структуры. Каждый шейдер должен содержать следующие элементы:
- Метаданные, хранящие номер версии языка.
- Ядро определения, которое содержит остальную часть необходимых элементов.
- Метаданные содержащие пространство имен (namespace), ссылка на создателя или распространителя (vendor), версию шейдера (version) и опционально описание шейдера (description).
- Свойство output, которое представляет собой пиксель с которым работает этот шейдер.
- Функция evaulatePixel, эта главная функция шейдера. Фактически, когда пишется шейдер для использования его во Flash, то это единственная функция шейдера. Таким образом, это то место, где находится весь код для вычисления значения пикселя.
- Вы можете определить (хотя это не обязательно) свойство input, которое представляет собой исходное растровое изображение. Если вы используете шейдер в качестве заливки в API рисования, то никакого исходного изображения не нужно. Однако, чтобы увидеть результат работы шейдера при работе в среде Pixel Bender Toolkit, всегда необходимо входное изоброжение.

Первое, метаданные, хранящие версию языка. Это самая первая строчка и выглядит она следующим образом:



Конечно, в будущих версиях инструмента будут использоваться новые версии языка. Это позволяет компилятору и Flash узнать какие свойства ожидаются в шейдере.
Второе ядро определения. Ядро это базовая часть шейдера. Ядро можно представить как класс, который однозначно определяет объект шейдера. Оно может содержать функции, свойства и константы. Функции могут вызывать друг друга и получать доступ к переменным и константам ядра, и вызывать другие встроенные функции или функции из библиотек.

Есть довольно много ограничений на создание шейдеров для использования во Flash: использование библиотек и вызов других функций за пределами главной функции evaulatePixel запрещен.

Также как и класс, ядро определяется с помощью ключевого слова – в данном случае это слово kernel, за которым идет наименование ядра. Тело ядра ограничено фигурными скобками:

kernel NewFilter
{
//kernel code
}


Как видите перед телом ядра располагается блок метаданных:

kernel NewFilter
< namespace : "Your Namespace";
vendor : "Your Vendor";
version : 1;
description : "your description";
>
{
//kernel code
}


К этим метаданным можно обратиться из Flash, после загрузки или встраивания в него шейдера.

В ядре сначала определяются две специальные переменные. Эти переменные задаются с помощью ключевых слов input и output. Переменная input ссылается на исходное изображение, output ссылается на пиксель, значение которого рассчитывается.

Если вы используете только ActionScript, то синтакс определения этих переменных может показаться странным. В ActionScript мы объявляем переменные с помощью ключевого слова var и тип переменной пишем после ее наименования:

var variableName:Type


В языке C, на котором базируется Pixel Bender, вы начинаете объявление переменной с указания ее типа:

Type variableName;


Для этих специальных переменных мы добавляем ключевые слова input и output спереди, как видите у нас есть переменная scr типа image4 и переменная dst типа pixel4:

input image4 scr;
output pixel4 dst;


В дальнейшем мы сделаем небольшой обзор типов данных, а пока скажу, что тип image4 определяет четырехканальное растровое изображение (RGB + альфа канал), а pixel4 представляет четырехканальный пиксель.

Как было сказано ранее, вам почти всегда нужно определить переменную с исходным изображением, по крайней мере при работе в Pixel Bender Toolkit. У вас также должна быть определена переменная output для выходного пикселя.

Теперь функция evaluatePixel. Также как и с переменными, объявление функций начинается с указания типа возвращаемого значения, далее идет имя функции, тело функции ограничивается фигурными скобками:

void
evaluatePixel()
{
dst = pixel4(1,0,0,1);
}


В фильтре сгенерированном автоматически, тип возвращаемого значения и имя функции расположены на различных строчках. Это не принципиально и эквивалентно следующей записи:

void evaluatePixel()
{
dst = pixel4(1,0,0,1);
}


Тип возвращаемого значения void, поэтому функция ничего не возвращает. Независимо от того, что мы делаем в этой функции, мы должны в ней присвоить переменной dst определенное значение. В нашем случае мы в этой функции присваиваем этой переменной в качестве значения экземпляр pixel4. Конструктор pixel4 принимает аргументы-значения для каждого канала: красного, зеленого, синего и альфа. Каждому из этих каналов можно присвоить значение от 0.0 до 1.0. Фактически, вы можете ввести любые числа, но они автоматически будут ограничены указанным диапазоном. Например, следующее присваивание pixel4(100,0,0,1) эквивалентно pixel4(1,0,0,1). В этом примере мы устанавливаем красному каналу значение 1, зеленому и синему каналу значение 0, альфа каналу значение 1. Это даст нам непрозрачный пиксель залитый сплошным красным цветом. Этот шейдер выполняется для каждого пикселя изображения одновременно, в итоге все пиксели будут закрашены в красный цвет.


Типы данных



Перед тем, как идти дальше, давайте посмотрим какие типы данных нам доступны в Pixel Bender. Основные доступные типы это bool, int и float, которые соответствуют типам Boolean, int и Number в ActionScript. Тип bool определяет булевы значения true/false, тип int – целые числа, тип float – числа с плавающей запятой.

Важно запомнить, что если вы присваиваете переменной типа float число, то это число должно быть указано в подобном формате float num = 1.0, если вы укажете так float num = 1, то будет ошибка компиляции. Возможно, потребуется некоторое время, чтобы к этому привыкнуть. Вы также должны быть осторожны при использовании int вместо float в аргументах и переменных функций или математических операциях.

Есть векторные типы, которые соответствуют векторам в ActionScript в которых хранятся значения одного и того же типа. Например, переменная типа float2 содержит две переменных типа float, переменная типа float3 – три типа float, и переменная типа float4 – четыре переменных типа float. Подобные типы существуют и для типов bool и int. Скорее всего, из всех этих типов наиболее часто вы будете использовать тип float2, аналогично использованию класса Point в ActionScript.
Теперь типы для хранения отдельных пикселей и целых изображений. Тип pixel1 содержит единственный канал для пикселя. Типы pixel2, pixel3 и pixel4 – векторные типы для хранения двух, трех и четырех каналов соответственно. Типы для хранения изображений – image1, image2, image3, image4. Типы pixel4 и image4 мы будем использовать наиболее часто, т.к. они хранят четыре канала.

В ActionScript при использовании переменной типа Vector, чтобы получить его элемент мы используем подобное выражение:

myVector[2]


Векторные типы Pixel Bender более дружественны по отношению к пользователю. Например для получения первого элемента (значения красного канала) переменной типа pixel4, вместо того, чтобы писать myPixel[0], мы пишем myPixel.r. Подобно этому значения зеленого, синего и альфа каналов мы получаем с помощью переменных g, b и a.
Значение типа float2 имеет два свойства х и у.

Фактически все векторные типы, хранящие четыре элемента имеют следующие свойства, независимо от типа данных переменных, содержащихся в них:

r, g, b, a
x, y, z, w
s, t, p, q


Эти свойства представляют собой наборы из четырех элементов вектора. Так myPixel.x имеет то же значение что и myPixel.r и myPixel.s.
Вы можете переписать функцию evaluatePixel() следующим образом:

void
evaluatePixel()
{
dst.r = 1.0;
dst.g = 0.0;
dst.b = 0.0;
dst.a = 1.0;
}


Кроме того, мы можем получить два и более элемента с помощью следующих выражений:

myPixel.rgb // возвращает вектор pixel3, содержащий красный, зеленый и синий каналы
myPixel.ra //возвращает вектор pixel2, содержащий красный и альфа каналы


Это позволяет делать следующее:

evaluatePixel()
{
pixel3 pxl = pixel3(1, 0, 1);
dst.rgb = pxl;
dst.a = 1.0;
}


Здесь мы создаем переменную типа pixel3 содержащую значения 1, 0 и 1 соответственно для красного, зеленого и синего канала. Замет эту переменную мы присваиваем dst.rgb. А затем отдельно присваиваем значение для альфа канала.

Более того, при присваивании вы можете поменять значения каналов местами. Например, в следующем примере мы поменяли местами синий и зелёный каналы в присваивании pxl.

evaluatePixel()
{
pixel3 pxl = pixel3(1, 0, 1);
dst.rbg = pxl;
dst.a = 1.0;
}


В результате мы получим в переменной dst значения 1, 1, 0 для rgb. Вы можете сделать и следующее:

evaluatePixel()
{
pixel3 pxl = pixel3(1, 0, 1);
dst.rgb = pxl.grb;
dst.a = 1.0;
}


Этот код меняет местам значения каналов переменной pxl при присваивании значений каналам dst. Эта перестановка элементов вектора известна как swizzing и может быть использована для быстрой обработки изображений.


Получение текущих координат пикселя



Следующее, что мы посмотрим, это получение координат оцениваемого пикселя. Вы можете использовать для этого встроенную функцию outCoord, которая возвращает значение типа float2, содержащее координаты х и у. Т.к. каждый пиксель имеет свои координаты, то вы можете использовать их для задания каждому пикселю цвета, зависящего от координаты этого пикселя.



kernel SineWave1
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "draws vertical bands";
>
{
input image4 src;
output pixel4 dst;

void
evaluatePixel()
{
dst = pixel4(0, 0, 0, 1);
float2 pos = outCoord();
dst.r = sin(pos.x * .2) * .5 + .5;
}
}


Сначала мы закрашиваем пиксель в непрозрачный черный цвет, затем мы получаем координаты и сохраняем их в переменной pos. В последней строчке мы умножаем pos.x на 0.2 и вычисляем синус от этого значения. Это дает нам значение от – 1.0 до + 1.0. Умножаем его на 0.5 для получения диапазона от – 0.5 до + 0.5. Затем добавляем 0.5 для получения диапазона от 0 до 1.0, которое послужит нам корректным значение для канала. Наконец, мы присваиваем это значение переменной dst.r. Теперь значение красного канала каждого пикселя зависит от его координаты x, что дает нам следующую картинку (см рисунок 2):

Рисунок 2. – Градиентные полосы


Рисунок 2. – Градиентные полосы



Точно такую же вещь мы можем сделать для координаты у и синего канала:



kernel SineWave2
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "draws vertical and horizontal bands";
>
{
input image4 src;
output pixel4 dst;

void
evaluatePixel()
{
dst = pixel4(0, 0, 0, 1);
float2 pos = outCoord();
dst.r = sin(pos.x * .2) * .5 + .5;
dst.b = sin(pos.y * .2) * .5 + .5;
}
}


Это даст нам следующий рисунок (см рисунок 3):

Рисунок 3. – Двойные полосы


Рисунок 3. – Двойные полосы



Теперь предположим, что вы хотите менять несколько каналов одновременно. Например, вы хотите чередовать черные и белые полосы, для этого вам нужно менять красный, зеленый и синий каналы одинаково и одновременно. Это будет похоже на следующее:

    void
evaluatePixel()
{
dst = pixel4(0, 0, 0, 1);
float2 pos = outCoord();
dst.r = sin(pos.x * .2) * .5 + .5;
dst.g = sin(pos.x * .2) * .5 + .5;
dst.b = sin(pos.x * .2) * .5 + .5;
}


Это эквивалентно следующей записи:

    void
evaluatePixel()
{
dst = pixel4(0, 0, 0, 1);
float2 pos = outCoord();
pixel1 pxl = sin() * .5 + .5;
dst.r = pxl;
dst.g = pxl;
dst.b = pxl;
}


Это позволит произвести вычисление лишь однажды и затем присвоить один и тот же результат всем каналам. Но есть еще более простой путь:



kernel SineWave3
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "draws vertical and horizontal bands";
>
{
input image4 src;
output pixel4 dst;

void
evaluatePixel()
{
dst = pixel4(0, 0, 0, 1);
float2 pos = outCoord();
dst.rgb = pixel3(sin(pos.x * .2) * .5 + .5);
dst.a = 1.0;
}
}


Если вы при создании векторной переменной передадите в конструктор единственное значение, то Pixel Bender присвоит это значение всем свойствам этой векторной переменной. Сейчас все каналы получают одно и тоже значение, в результате чего получаем вертикальные черно-белые полосы, которые можно увидеть на рисунке 4:

Рисунок 4. – Черно-белые полосы


Рисунок 4. – Черно-белые полосы



Руководство по языку Pixel Bender, которое устанавливается вместе со средой разработки, описывает все доступные встроенные математические и геометрические функции. Поиграйтесь с ними и посмотрите какие интересные шаблоны они могут вам предложить. Теперь посмотрим как изменять различные значения в рантайме.


Параметры



Pixel Bender позволяет изменять заданные значение в рантайме. Такие значения называют параметрами. В простейшем случае они объявляются также как и другие переменные, но с добавление впереди ключевого слово parameter:

parameter float myParameter;


Когда вы добавляете числовой параметр и компилируете шейдер в Pixel Bender Toolkit то на панели слева вы увидите ползунок для изменения этого параметра. Если вы добавите параметр типа bool то появится чекбокс.



kernel SineWaveParam
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "draws vertical bands";
>
{
input image4 src;
output pixel4 dst;

parameter float mult;

void
evaluatePixel()
{
dst = pixel4(0, 0, 0, 1);
float2 pos = outCoord();
dst.r = sin(pos.x * mult) * .5 + .5;
}
}


Запустим этот пример и увидим следующую картину (см рисунок 5):

Рисунок 5


Рисунок 5



Здесь мы создали параметр и назвали его mult. Мы умножаем pos.x на это значение и затем вычисляем синус. Теперь вы можете изменять размер полос, перемещая ползунок вправо и влево.
Если вы сделаете параметр значением векторного типа, то получите группу ползунков, как в следующем примере:



kernel ColorChooser
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "shows a pixel4 parameter in action";
>
{
input image4 src;
output pixel4 dst;

parameter pixel4 color;

void
evaluatePixel()
{
dst = color;
}
}


Т.к. параметр color имеет тип pixel4 мы получаем четыре ползунка (см рисунок 6):

Рисунок 6


Рисунок 6



Перемещайте ползунки для изменения цвета изображения. Конечно, нужно переместить и ползунок [3], который изменяет значения альфа-канала, чтобы увидеть изменения других каналов.


Расширенные параметры



Когда вы создаете параметр, то по умолчанию ему задается диапазон от 0.0 до 1.0 и начальное значение равное 0.0. Для некоторых параметров это хорошо, но иногда эти значения нужно изменить. Для этого вы можете использовать следующие метаданные параметра:

parameter float myValue
<
minValue:0.0;
maxValue:100.0;
defaultValue:50.0;
>;


Значение от имени свойства параметра отделяется двоеточием. В предыдущем примере мы создаем слайдер с диапазоном от 0.0 до 100.0, с начальным значением 50.0.
Следующий пример иллюстрирует создание двух параметров типа int:



kernel Checkerboard
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "creates a checkerboard pattern";
>
{
input image4 src;
output pixel4 dst;

parameter int xres
<
minValue:1;
maxValue:200;
defaultValue:50;
>;
parameter int yres
<
minValue:1;
maxValue:200;
defaultValue:50;
>;

void
evaluatePixel()
{
float2 pos = outCoord();
float xpos = floor(pos.x / float(xres));
float ypos = floor(pos.y / float(yres));
if(mod(xpos, 2.0) > 0.0 ^^ mod(ypos, 2.0) > 0.0)
{
dst = pixel4(1, 1, 1, 1);
}
else
{
dst = pixel4(0, 0, 0, 1);
}
}
}


Сначала мы получаем текущие координаты с помощью outCoord. Мы делим их на значения параметров xres и yres и округляем полученные значения.
Затем мы берем остаток от деления xpos на 2.0 и сравниваем его с 0.0, если остаток больше, то каналам присваивается значение 1, если меньше – значение 0. В результате мы получим расцветку шахматной доски. Изменяя параметры, мы можем изменять размеры прямоугольников.


Выборка входного изображения



Прежде мы использовали входное изображение просто как рабочую область, набор пикселей, значения которых мы изменяли не читая их предыдущего значения. Но Pixel Bender может служить и для обработки изображений. Это делается с помощью нескольких функций выборки - получения значения пикселя в определенном местоположении.
Самая простая функция выборки – sampleNearest. Эта функция принимает в качестве параметров исходное изображение и переменную типа float2 в которой хранятся координаты интересующей нас точки, функция находит по координатам эту точку и возвращает значение этого пикселя.

Теперь давайте вернемся к шейдеру, который генерируется при создании нового шейдера по умолчанию и вставим в код следующее:

    void
evaluatePixel()
{
dst = sampleNearest(scr, outCoord());
}


Мы передаем в качестве первого параметра переменную scr, которая представляет собой исходное изображение, а в качестве второго параметра мы передаем результат вызова функции outCoord() – т.е. значение типа float2 – координаты текущей точки. Например, мы оцениваем пиксель с координатами 100, 100. Эта конструкция вернет значения пикселя с координатами 100, 100. Так, пиксель за пикселем, выходное изображение построится из входного. Конечно, мы можем осуществить выборку пикселей по определенному закону, задавать пикселям на выходном изображении новое изображение и создавать различные искажения. В следующем примере мы этим и займемся:



kernel GlassTile
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "creates a glass tile refraction effect";
>
{
input image4 src;
output pixel4 dst;

parameter float mult
<
minValue:0.0;
maxValue:10.0;
defaultValue:1.0;
>;

parameter float wave
<
minValue:0.0;
maxValue:1.0;
defaultValue:0.1;
>;

void
evaluatePixel()
{
float2 pos = outCoord();
pos += float2(sin(pos.x * wave) * mult, sin(pos.y * wave) * mult);
dst = sampleNearest(src, pos);
}
}


Вместо простой выборки текущего пикселя, этот шейдер смещает координаты исходного пикселя путем вычисления синуса от произведения координаты на параметр wave и умножения результата на параметр mult. Результат представлен на рисунке 7:

Рисунок 7


Рисунок 7




Линейная выборка



Существует несколько различных типов выборки. Пока мы имели дело только с функцией sampleNearest. Она берет координаты х и у и возвращает пиксель с этими координатами. Посмотрим на рисунок 8:

Рисунок 8. – Все три выбранные точки дают значение среднего пикселя


Рисунок 8. – Все три выбранные точки дают значение среднего пикселя



На рисунке показана область из девяти пекселей. Предположим, что средний пиксель имеет координаты 100, 100. Три крестика показывают три результата вызова функции sampleNearest. Например верхний левый – 99,7 и 99,6, правый - 100,4 и 99,9 и нижний – 99,8 и 100,5. Все эти результаты дают выборку одного и того же пикселя с координатами 100 и 100, т.е. одно и тоже значение. Хотя это возможно самый простой и быстрый способ, но в результате мы будем наблюдать некоторую блочность, как на рисунке 9 на котором представлен увеличенный результат работы шейдера GlassTile:

Рисунок 9


Рисунок 9



Если вы сталкиваетесь в своих шейдерах с подобным, то вы можете попробовать другой метод выборки: sampleLinear. Фактически, если мы посмотрим документацию, нам доступны три метода: sample, samleLinear и sampleNearest. Однако sample это то же, что и sampleLinear, но с другим названием.

Суть sampleLinear в том, что она берет своего рода среднее значение от четырех ближайших пикселей. Например, на рисунке 10 вы можете увидеть крестик, представляющий среднюю точку.

Рисунок 10


Рисунок 10



Четыре серых пикселя представляют четыре пикселя, которые будут выбраны. Крестик – результат билинейной интерполяции. Другими словами, пиксель находящийся ближе к точке выборки, будет учитываться в конечном результате больше, чем пиксель, который находится дальше. Таким образом достигается определенное сглаживание выбранной точки.

Чтобы увидеть это в действии поменяйте следующую строчку в шейдере GlassTile.pbk

dst = sampleNearest(scr, pos);


на такую:

dst = sampleLinear(scr, pos);


или такую:

dst = sample(scr, pos);


Это избавит от распада выходного изображения на блоки и даст сглаженное изображение (см рисунок 11).

Рисунок 11. – результат выборки функцией sampleLinear


Рисунок 11. – результат выборки функцией sampleLinear




Шейдер вращения



В простых шейдерах, включенных в среду Pixel Bender Toolkit, есть фильтр вращения, который поворачивает пиксели изображения вокруг определенной точки. К сожалению, его написали так, что его невозможно экспортировать для Flash. Давайте сделаем такой же шейдер, с которым можно будет работать во Flash.
Откроем файл TwirlFlash.pbk и увидим следующее:



kernel TwirlFlash
< namespace : "com.friendsofed";
vendor : "Advanced ActionScript 3.0 Animation";
version : 1;
description : "spins an image";
>
{
input image4 src;
output pixel4 dst;

parameter float2 center
<
minValue:float2(0.0);
maxValue:float2(1000.0);
defaultValue:float2(200.0);
>;

parameter float twist
<
minValue:-10.0;
maxValue:10.0;
defaultValue:0.0;
>;

parameter float radius
<
minValue:0.0;
maxValue:500.0;
defaultValue:100.0;
>;

void
evaluatePixel()
{
float2 pos = outCoord();
float dist = distance(center, pos);

float PI = 3.14159;

if(dist < radius)
{
float dx = pos.x - center.x;
float dy = pos.y - center.y;
float angle = atan(dy, dx);
angle += sin(dist / radius * PI) * twist;
float2 newpos = center + float2(cos(angle) * dist, sin(angle) * dist);
dst = sampleNearest(src, newpos);
}
else
{
dst = sampleNearest(src, pos);
}
}
}


Здесь у нас три параметра:
- center – параметр, определяющий точку, вокруг которой будет располагаться эффект вращения. Запомним, что у Pixel Bender нет такой информации, как ширина и высота исходного изображения, т.е. нет такого понятия как stageWidth и stageHight, как во Flash.
- twist – параметр, определяющий степень скручивания изображения.
- radius – параметр, определяющий размер области изображения, которая будет затронута эффектом. Эффект будет виден только на круглой области указанного радиуса.

В функции evaluatePixel мы получаем позицию текущего пикселя и расстояние от этого пикселя до центра эффекта, который определен параметром center. Расстояние находим с помощью встроенной функции distance. Также мы определяем значение PI.

       float2 pos = outCoord();
float dist = distance(center, pos);
float PI = 3.14159;


У нас есть оператор if, который является единственной управляющей структурой, которая будет доступна при использовании шейдера во Flash. Нет никаких массивов, циклов и конструкций switch. Ограничения в средствах сделают вас более творческими :) Надеюсь. Так или иначе, если расстояние меньше радиуса, то мы вычисляем вращение, если нет, то мы берем следующий пиксель для оценки.

Теперь нужно сделать несколько математических действий. Мы раскладываем расстояние на проекции на оси х и у и находим угол наклона вектора расстояния точки к оси х:

            float dx = pos.x - center.x;
float dy = pos.y - center.y;
float angle = atan(dy, dx);


Теперь нам нужно к углу добавить определенное значение, чтобы получить некоторое искажение.

             angle += sin(dist / radius * PI) * twist;


Сначала бы вычисляем значение dist/radius * PI. Значение dist будет между 0.0 и значением radius, таким образом, результат выражения dist/radius всегда будет находиться между 0.0 и 1.0. Умножая его на PI, поучим угол в радианах от 0.0 до 3.14. И, наконец, умножаем все на значение twist и прибавляем к текущему значению угла. Таки образом, максимальное искажение будет в точке максимально удаленной от радиуса.
Теперь у нас есть расстояние и угол. Мы находим точку, расположенную под этим углом и на этом расстоянии и приравниваем ее переменной dst. Результат можно увидеть на рисунке 12:

Рисунок 12 – Шейдер TwirlFlash в действии


Рисунок 12 – Шейдер TwirlFlash в действии



Теперь посмотрим, как это применять во Flash.


Использование Pixel Bender во Flash



Использование шейдеров во Flash включает три основных шага:
- Экспортирование шейдера в формат, который может прочитать Flash.
- Загрузка или внедрение шейдера в Flash-ролик.
- Создание экземпляра шейдера и использование его в качестве фильтра, заливки и т.д.

Итак, пройдемся по всем этим шагам. Сначала нам надо экспортировать шейдер из Pixel Bender Toolkit. Откройте шейдер Checkerboard.pbk созданный ранее. Экспортируйте шейдер с помощью команды меню Export Kernel Filter for Flash Player. Используйте имя по умолчанию, которое должно совпадать с именем исходного шейдера. Сохраните в место, куда может получить доступ Flash-ролик. Расширение файла станет .pbj.

Теперь нам нужно создать ролик для использования этого шейдера.


Загрузка и внедрение шейдеров



Для использования шейдера во Flash его нужно туда загрузить в рантайме или встроить на этапе компиляции. Я предпочитаю встраивание по нескольким причинам. Во-первых, вам не нужно отслеживать состояние шейдера, т.к. он уже встроен. Во-вторых, не нужно ждать его загрузки. Все это дает более простой код. Конечно, загрузка шейдера позволяет избежать повторной компиляции проекта, если изменяется шейдер.

Мы рассмотрим использование двух этих способов.

Начнем с создания структуры, которая используется для загрузки любого контента, будь-то XML или что-то другое.

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;

public class ShaderFillDemo extends Sprite
{

private var loader:URLLoader;
private var shaderURL:String = "Checkerboard.pbj";

public function ShaderFillDemo()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

loader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onLoadComplete);
loader.dataFormat = URLLoaderDataFormat.BINARY;
loader.load(new URLRequest(shaderURL));
}

private function onLoadComplete(event:Event):void
{
}
}
}


Заметим, что мы устанавливаем данным loader бинарный формат. Т.к. мы загружаем бинарные данные. Итак, убеждаемся, что шейдер с расширением .pbj лежит там же, где и ролик и начинаем загрузку.

Теперь, когда данные загружены, нам нужно из них создать объект Shader. Это будет экземпляром класса flash.display.Shader. Свойство data загрузчика содержит двоичный байткод шейдера. Мы помещаем его в конструктор класса Shader следующим образом:

		private function onLoadComplete(event:Event):void
{
var shader:Shader = new Shader(loader.data);
}



Использования шейдера для заливки



Мы сделаем это с помощью метода beginShaderFill класса Graphics. Фигура будет залита шаблоном, созданным вашим шейдером. Итак, весь код целиком:

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;

public class ShaderFillDemo extends Sprite
{

private var loader:URLLoader;
private var shaderURL:String = "Checkerboard.pbj";

public function ShaderFillDemo()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

loader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onLoadComplete);
loader.dataFormat = URLLoaderDataFormat.BINARY;
loader.load(new URLRequest(shaderURL));
}

private function onLoadComplete(event:Event):void
{
var shader:Shader = new Shader(loader.data);
graphics.beginShaderFill(shader);
graphics.drawRect(0, 0, 400, 400);
graphics.endFill();
}
}
}


Если все сделано правильно, вы получите прямоугольник, напоминающий шахматную доску.

Хорошо. Теперь посмотрим, как встраивать шейдеры. Для этого используется тег метаданных Embed. Смотрим код класса ShaderFillEmbed:

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.geom.Matrix;

public class ShaderFillEmbed extends Sprite
{
[Embed (source="Checkerboard.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

public function ShaderFillEmbed()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
}
}
}


Вы должны установить параметру mymeType значение «application/octet-stream».
Теперь вы готовы для того, чтобы создать шейдер, но теперь в конструктор класса Shader вы передаете байткод, содержащийся в экземпляре класса ShaderClass:

var shader:Shader = new Shader(new ShaderClass());


Теперь этот экземпляр вы можете использовать в методе beginShaderFill, как и в предыдущем примере. Теперь весь класс целиком:

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.geom.Matrix;

public class ShaderFillEmbed extends Sprite
{
[Embed (source="Checkerboard.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

public function ShaderFillEmbed()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

var shader:Shader = new Shader(new ShaderClass());
graphics.beginShaderFill(shader);
graphics.drawRect(0, 0, 400, 400);
graphics.endFill();
}
}
}



Доступ к метаданным шейдера во Flash



Как только вы создали экземпляр шейдера вы можете получить доступ к его метаданным. Это делается с помощью свойства data шейдера. Это свойство имеет тип flash.display.ShaderData. С его помощью вы можете получить доступ к namespace, vendor, version, description, которые вы создали при написании шейдера в Pixel Bender Toolkit. Все эти данные имеют тип строки. Просто добавьте строчки с trace в последний пример, чтобы увидеть, как это работает:

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.geom.Matrix;

public class ShaderFillEmbed extends Sprite
{
[Embed (source="Checkerboard.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

public function ShaderFillEmbed()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

var shader:Shader = new Shader(new ShaderClass());
trace(shader.data.name);
trace(shader.data.namespace);
trace(shader.data.vendor);
trace(shader.data.version);
trace(shader.data.description);

graphics.beginShaderFill(shader);
graphics.drawRect(0, 0, 400, 400);
graphics.endFill();
}
}
}


Вывод будет следующим:

Checkerboard
com.frindsofed
Advanced ActionScript 3.0 Animation
1
creates a checkerboard pattern


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


Установка параметров шейдера во Flash



Доступ к параметрам шейдера осуществляется с помощью все того же свойства data. Но не так же прямо как с метаданными.
Каждый параметр, который вы создали в шейдере, является свойством объекта data. Так, для нашего шейдера, генерирующего шахматную доску, мы имеем следующие параметры:

shader.data.xres


и

shader.data.yres


Вы можете подумать, что присваивание должно осуществляться следующим образом:

shader.data.xres = 50;


Но это не так, изменять параметры необходимо с помощью свойства value этого параметра:

shader.data.xres.value


Но есть еще кое-что. Т.к. значение value может быть векторным типом, то само значение мы должны передавать как элемент массива:

shader.data.xres.value = [20];


Наконец, изменив параметр, мы получаем следующее (см рисунок 13):

Рисунок 13


Рисунок 13



Теперь попробуйте изменить параметр yres.


Трансформация заливки шейдера



Метод beginShaderFill, так же как и другие методы заливки, имеет второй аргумент типа flash.display.Matrix. Вы можете использовать масштабирование, поворот и перенос шейдера при отрисовке. Если вы посмотрите описание класса Matrix в документации, то вы увидите, что вы можете осуществлять поворот изменением угла в следующих параметрах, где q угол поворота в радианах:

new Matrix(cos(q), sin(q), -sin(q), cos(q), 0, 0);


Конечно, можно делать и так:

var m:Matrix = new Matrix();
m.rotate(q);


Теперь повернем заливку шейдера:

		public function ShaderFillEmbed()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

var shader:Shader = new Shader(new ShaderClass());

var angle:Number = Math.PI / 4;
var cos:Number = Math.cos(angle);
var sin:Number = Math.sin(angle);
graphics.beginShaderFill(shader, new Matrix(cos, sin, -sin, cos));
graphics.drawRect(20, 20, 400, 400);
graphics.endFill();
}


Этот код поворачивает заливку на 45 градусов и отрисовывает ее (см рисунок 14).

Рисунок 14


Рисунок 14




Анимация заливки шейдера



Теперь мы увидим реальную мощь параметров для создания динамического шейдера. Идея проста: отрисовываем заливку шейдера, меняем ее параметры и в следующем кадре снова ее отрисовываем. Следующий пример очень прост и снова использует шейдер, генерирующий шахматную доску:

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class ShaderFillAnim extends Sprite
{
[Embed (source="Checkerboard.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

private var shader:Shader;
private var xAngle:Number = 0;
private var yAngle:Number = 0;
private var xSpeed:Number = .09;
private var ySpeed:Number = .07;

public function ShaderFillAnim()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

shader = new Shader(new ShaderClass());

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
var xres:Number = Math.sin(xAngle += xSpeed) * 50 + 55;
var yres:Number = Math.sin(yAngle += ySpeed) * 50 + 55;

shader.data.xres.value = [xres];
shader.data.yres.value = [yres];
graphics.clear();
graphics.beginShaderFill(shader);
graphics.drawRect(20, 20, 400, 400);
graphics.endFill();
}
}
}


Здесь мы создаем параметры для углов и скоростей по осям х и у. Мы добавляем их к xres и yres в каждом кадре, тем самым уменьшая размер прямоугольников шахматной доски. Затем мы очищаем graphics и отрисовываем прямоугольник с заливкой шейдера.

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/12/22/shaderFillAnim.swf, 450, 450[/SWF]




Изменение входного изображения



В следующем примере мы изменим входное изображение на любое другое и применим шейдер к нему.

Для этого мы внедряем любое изображение, создаем новый экземпляр класса Bitmap из этого изображения и присваиваем свойству scr.input шейдера свойство BitmapData экземпляра класса Bitmap.

package {
import flash.display.Bitmap;
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.filters.ShaderFilter;

public class ShaderFillImage extends Sprite
{
[Embed (source="TwirlFlash.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

[Embed (source="john_davey.jpg")]
private var JohnDavey:Class;

public function ShaderFillImage()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

var shader:Shader = new Shader(new ShaderClass());
shader.data.center.value = [400, 300];
shader.data.twist.value = [-6];
shader.data.radius.value = [300];

var image:Bitmap = new JohnDavey();
image.filters = [new ShaderFilter(shader)];
shader.data.src.input = image.bitmapData;

graphics.beginShaderFill(shader);
graphics.drawRect(20, 20, 800, 600);
graphics.endFill();
}
}
}


Результат показан на рисунке 15:

Рисунок 15


Рисунок 15




Использование шейдера в качестве фильтра



Использовать шейдер в качестве фильтра еще легче, чем использовать его в качестве заливки. Давайте используем снова шейдер TwirlShader, но не на картинке, а на любом отображаемом объекте, например текстовом поле.

package {
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.filters.ShaderFilter;
import flash.text.TextField;
import flash.text.TextFormat;

[SWF(backgroundColor=0xFFFFFF)]
public class ShaderAsFilter extends Sprite
{
[Embed (source="TwirlFlash.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

public function ShaderAsFilter()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

var shader:Shader = new Shader(new ShaderClass());
shader.data.center.value = [200, 200];
shader.data.twist.value = [-1];
shader.data.radius.value = [200];

var tf:TextField = new TextField();
tf.width = 400;
tf.height = 400;
tf.wordWrap = true;
tf.multiline = true;
tf.border = true;
tf.defaultTextFormat = new TextFormat("Arial", 24);
addChild(tf);

for(var i:int = 0; i < 340; i++)
{
tf.appendText(String.fromCharCode(65 + Math.random() * 25));
}
tf.filters = [new ShaderFilter(shader)];
}
}
}


Здесь мы создаем текстовое поле и заполняем его случайным текстом. Затем в качестве фильтра этого поля мы указываем наш шейдер. Все предельно просто!

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/12/22/ShaderAsFilter.swf, 400, 400[/SWF]




Использование шейдера в режиме смешивания



Прежде всего, для использования шейдера в режиме смешивания нам понадобятся два изображение. Одно на задний фон, другое – на передний. Первым делом мы должны их определить в ядре шейдера. Затем во Flash мы должны будем создать две картинки, которые будем накладывать друг на друга.

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



kernel ChannelBlend
< namespace : "com.friendsofed";
vendor : "Advamced ActionScript 3.0 Animation";
version : 1;
description : "blends the channels of two images";
>
{
input image4 back;
input image4 fore;

output pixel4 dst;

parameter float3 amt;

void
evaluatePixel()
{
pixel4 bg = sampleNearest(back, outCoord());
pixel4 fg = sampleNearest(fore, outCoord());
dst.r = fg.r * amt.r + bg.r * (1.0 - amt.r);
dst.g = fg.g * amt.g + bg.g * (1.0 - amt.g);
dst.b = fg.b * amt.b + bg.b * (1.0 - amt.b);
dst.a = 1.0;
}
}


Использование шейдера во Flash выглядит следующим образом:

package {
import flash.display.Bitmap;
import flash.display.Shader;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;

public class ShaderBlendMode extends Sprite
{
[Embed (source="ChannelBlend.pbj",
mimeType="application/octet-stream")]
private var ShaderClass:Class;

[Embed (source="input1.jpg")]
private var input1:Class;

[Embed (source="input2.jpg")]
private var input2:Class;

public function ShaderBlendMode()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

var back:Bitmap = new input2();
addChild(back);

var fore:Bitmap = new input1();
addChild(fore);

var shader:Shader = new Shader(new ShaderClass());
shader.data.amt.value = [1.0, 0.9, 0.0];
fore.blendShader = shader;
}
}
}


Здесь мы создаем два изображения. Шейдер применяется к изображению переднего плана. Результат представлен на рисунке 16.

Рисунок 16


Рисунок 16



На этом у меня все :). Исходники

Комментариев нет:

Отправить комментарий