
Пишем шейдер на AGAL
Ни для кого уже не секрет, что Flash Player 11 имеет поддержку GPU ускорения графики. Новая версия вводит Molehill API, позволяя работать с видеокартой на достаточно низком уровне, что с одной стороны даёт полную волю фантазии, с другой требует более глубокого понимания принципов работы современной 3D графики.
В данной статье речь пойдёт о языке написания шейдеров — AGAL (Adobe Graphics Assembly Language). Предполагается, что читатель знаком с базовыми основами современной realtime 3D графики, а в идеале — имеет опыт работы с OpenGL или Direct3D. Для остальных же проведу небольшой экскурс:
В текущей реализации AGAL используется обрезок Shader Model 2.0, т.е. фитчелист железа ограничен 2005 годом. Но стоит помнить, что это ограничение лишь возможностей шейдерной программы, но никак не производительности железки. Возможно, в будущих версиях Flash Player планка будет поднята до SM 3.0, и мы сможем рендерить сразу в несколько текстур и делать текстурную выборку прямо из вершинного шейдера, но учитывая политику Adobe, случится это не раньше выхода следующего поколения мобильных устройств.
Любая программа на AGAL является по сути низкоуровневым языком ассемблера. Сам по себе язык очень простой, но требует изрядной доли внимательности. Код шейдера представлен набором инструкций вида:
Рассмотрим каждый из типов регистров подробнее.
Регистр-вывода
В результате расчёта вершинный шейдер обязан записать значение оконной позиции вершины в регистр op (output position), а фрагментный – в oc (output color) значение итогового цвета пикселя. В случае с фрагментным шейдером существует возможность отмены обработки инструкцией kil, которая будет описана ниже.
Регистр-атрибут
Вершина может содержать в себе до 8 атрибутов-векторов, обращение к которым из шейдера осуществляется через регистры va, положение которых в вершинном буфере задаётся функцией Context3D.setVertexBufferAt. Данные атрибута могут быть формата FLOAT_1, FLOAT_2, FLOAT_3, FLOAT_4 и BYTES_4. Число в названии обозначает количество компонент вектора. Стоит отметить, что в случае с BYTES_4 значения компонентов нормализуются, т.е. делятся на 255.
Регистр-интерполятор
Помимо записи в регистр op, вершинный шейдер может передать до 8 векторов в фрагментный шейдер через регистры v. Значения этих векторов будут линейно интерполированы по всей площади полигона во время растеризации. Проиллюстрируем работу интерполяторов на примере треугольника, в вершинах которого хранится атрибут, выводимый фрагментным шейдером:

Регистр-переменная
В вершинном и фрагментном шейдерах доступно до 8 регистров vt и ft для хранения промежуточных результатов расчёта. Например, в фрагментном шейдере необходимо посчитать сумму четырёх векторов, принятых из вершинной программы (v0..v3 регистры):
В результате ft0 будет хранить нужную нам сумму, и всё вроде бы здорово, но существует неочевидная на первый взгляд возможность оптимизации, которая напрямую связана с архитектурой работы программного конвейера видеокарты и отчасти является причиной её высокой производительности.
В основу шейдеров заложена концепция ILP (Instruction-level parallelism), которая уже, судя из названия, позволяет выполнять несколько инструкций одновременно. Основным условием для задействования этого механизма, является независимость инструкций друг от друга. Применительно к примеру выше:
Первые две инструкции выполнятся одновременно, т.к. работают с независимыми регистрами. Отсюда следует вывод, что ключевую роль в производительности вашего шейдера играет не столько количество инструкций, сколько их независимость друг от друга.
Регистр-константа
Хранение численных констант прямо в коде шейдера не допускается, т.е. все необходимые для работы константы должны быть переданы в шейдер до вызова Context3D.drawTriangles, и будут доступны в регистрах vc (128 векторов) и fc (28 векторов). Существует возможность обращения к регистру по его индексу используя квадратные скобки, что весьма удобно при реализации скелетной анимации или индексирования материалов. Важно помнить, что операция задания шейдерных констант относительно дорогая, и её следует по возможности избегать. Так например, нет смысла передавать в шейдер матрицу проекции перед рендером каждого объекта, если она не меняется в текущем кадре.
Регистр-семплер
В фрагментный шейдер можно передать до 8 текстур функцией Context3D.setTextureAt, обращение к которым осуществляется через соответствующие регистры fs, которые используются исключительно в операторе tex. Немного изменим пример с треугольником, и в качестве второго атрибута вершины передадим текстурные координаты, а в фрагментном шейдере сделаем текстурную выборку по этим уже интерполированным координатам:

Операторы
На данный момент (октябрь 2011), AGAL реализует следующие операторы:
Остальные операторы, включая условные переходы и циклы планируются реализовать в последующих версиях Flash Player. Но это не означает, что сейчас нельзя использовать даже обычный if, инструкции slt и sge вполне подходят для этих задач.
Эффекты
С основами ознакомились, теперь самая интересная часть статьи – практическое применение новых знаний. Как говорилось в самом начале, возможность писать шейдера полностью развязывает руки программисту графики, т.е. фактические ограничения лишь в фантазии и математической смекалке разработчика. Ранее можно было убедиться, что сам по себе ассемблерный язык прост, но за простотой скрывается сложность “вкуривания” в уже забытый код. Поэтому крайне рекомендую комментировать ключевые участки кода шейдера, дабы быстро в нём ориентироваться в случае необходимости.
Заготовка
Отправной точкой для всех последующих примеров будет небольшая “болванка” в виде чайника. В отличие от примера с треугольником, нам понадобится матрица проекции и трансформации камеры, для создания эффекта перспективы и вращения вокруг объекта. Её мы передадим в константные регистры. Тут важно помнить, что матрица 4х4 занимает ровно 4 регистра, и при записи её в регистр vc0, занятыми окажутся v0..v3. Также нам пригодится константный вектор из часто используемых в шейдере чисел (0.0, 0.5, 1.0, 2.0).
Итого, базовый код шейдера будет выглядеть так:

Texture mapping
В шейдере возможно наложение до 8 текстур, при практически неограниченном числе выборок. Это означает, что данный лимит не имеет особого значения при использовании атласов или кубических текстур. Усовершенствуем наш пример и, вместо задания цвета в фрагментном шейдере, будем получать его из текстуры по текстурным координатам-интерполяторам, принятым из вершинного шейдера:

Lambert shading
Самая примитивная модель освещения, имитирующая реальное. Основана на положении, что интенсивность света, упавшего на поверхность, линейно зависит от косинуса угла между векторами падения и нормали к поверхности. Из школьного курса математики вспомним, что скалярное произведение единичных векторов даёт косинус угла между ними, следовательно, наша формула освещения по Ламберту будет иметь вид:
Lambert = Diffuse * ( Ambient + max( 0, dot( LightVec, Normal ) ) )
Color = Lambert
где Diffuse – цвет объекта в точке (взятый из текстуры например),
Ambient – цвет фонового освещения
LightVec – единичный вектор из точки на источник света
Normal – перпендикуляр к поверхности
Color – итоговый цвет пикселя
Шейдер будет принимать два новых константных параметра: позицию источника и значение фонового света:

Phong shading
Вводит понятие блика от источника света в модель освещения по Ламберту. Подразумевает, что интенсивность блика определяется степенной функцией по косинусу угла между вектором на источник и направления, получившегося в результате отражения вектора наблюдателя относительно нормали к поверхности.
Phong = pow( max( 0, dot( LightVec, reflect(-ViewVec, Normal) ) ), SpecularPower ) * SpecularLevel
Color = Lamber + Phong
где ViewVec – вектор взгляда наблюдателя
SpecularPower – степень, определяющая размер блика
SpecularLevel – уровень интенсивности блика или его цвет
reflect – функция вычисления отражения f(v, n) = 2 * n * dot(n, v) – v
Для сложных моделей принято использовать Specular и Gloss карты, которые определяют цвет/интенсивность (SpecularLevel), а также размер блика (SpecularPower) на разных участках текстурного пространства модели. В нашем случае, обойдёмся константными значениями степени и интенсивности. В вершинный шейдер передадим новый параметр – позицию наблюдателя для последующего вычисления ViewVec:

Normal mapping
Относительно простой метод для имитации рельефа поверхности посредством использования текстуры нормалей. Направление нормали в такой текстуре принято задавать в виде RGB значения, полученного из приведения её координат к диапазону 0..1 (xyz * 0.5 + 0.5). Нормали могут быть представлены как в пространстве объекта (Object Space), так и в относительном пространстве (Tangent Space), построенном на базисе текстурных координат и нормали к вершине. Первый имеет ряд порой значительных недостатков в виде большого расхода памяти под текстуры из-за невозможности тайлинга и mirror-текстурирования, но позволяет сэкономить на количестве инструкций. В примере будем использовать более гибкий и общий вариант с Tangent Space, для которого помимо нормали потребуется ещё два дополнительных вектора базиса Tangent и Binormal. Реализация сводится к переводу векторов viewVec и lightVec к TBN (Tangent, Binormal, Normal) базису, и дальнейшей выборке относительной нормали из текстуры в фрагментном шейдере.

Toon Shading
Разновидность нефотореалистичной модели освещения, имитирующая мультипликационную рисовку затенения. Реализуется множеством способов, самым простым из которых является выборка цвета из 1D текстуры по косинусу угла из модели Ламберта. В нашем случае, для примера используем текстуру 16x1:


Sphere mapping
Самый простой вариант для имитации отражения, чаще используемый для эффекта хромирования металла. Представляет окружение в виде текстуры со сферическим искажением по типу “рыбий глаз”, как показано ниже:

Основная задача сводится к преобразованию координат вектора отражения в соответствующие текстурные координаты:
uv = ( xy / sqrt(x^2 + y^2 + (z + 1)^2) ) * 0.5 + 0.5
Умножение и сдвиг на 0.5 нужны для приведения нормированного результата к пространству текстурных координат 0..1. В простом случае для идеально отражающей поверхности, влияние карты аддитивное, а для более сложных случаев когда требуется диффузная составляющая, принято использовать приближение формул Френеля. Также для комплексных моделей часто используются Reflection карты, указывающие интенсивность отражения разных частей текстуры модели.

На этом пожалуй закончу. Представленные здесь примеры, по большей части, описывают свойства материала объекта, но шейдера находят своё применение и в других задачах, таких как скелетная анимация, тени, вода и других относительно сложных задачах (в том числе невизуальных). А при должной прокачке навыков позволяют за короткие сроки реализовывать достаточно комплексные вещи по типу:
Заключение
Игры на флеше – это просто! пример к статье.
В данной статье речь пойдёт о языке написания шейдеров — AGAL (Adobe Graphics Assembly Language). Предполагается, что читатель знаком с базовыми основами современной realtime 3D графики, а в идеале — имеет опыт работы с OpenGL или Direct3D. Для остальных же проведу небольшой экскурс:
- в каждом кадре всё рендерится заново, подходы с частичной перерисовкой экрана крайне нежелательны
- 2D – частный случай 3D
- видеокарта способна растеризовать треугольники и ничего кроме
- треугольники строятся на вершинах
- каждая вершина содержит в себе атрибуты (координата, нормаль, вес и др.)
- порядок задания вершин в треугольнике определяется индексами
- данные вершин и индексов хранятся в вершинном и индексном буферах соответственно
- шейдер – программа выполняемая видеокартой
- каждая вершина проходит через вершинный шейдер, а каждый пиксель при растеризации через фрагментный (пиксельный)
- видеокарта не умеет работать с целыми числами, но отлично работает с 4D векторами
В текущей реализации AGAL используется обрезок Shader Model 2.0, т.е. фитчелист железа ограничен 2005 годом. Но стоит помнить, что это ограничение лишь возможностей шейдерной программы, но никак не производительности железки. Возможно, в будущих версиях Flash Player планка будет поднята до SM 3.0, и мы сможем рендерить сразу в несколько текстур и делать текстурную выборку прямо из вершинного шейдера, но учитывая политику Adobe, случится это не раньше выхода следующего поколения мобильных устройств.
Любая программа на AGAL является по сути низкоуровневым языком ассемблера. Сам по себе язык очень простой, но требует изрядной доли внимательности. Код шейдера представлен набором инструкций вида:
opcode [dst], [src1], [src2]
что в вольной трактовке означает «выполнить команду opcode с параметрами src1 и src2, вернув значение в dst». Шейдер может содержать до 256 инструкций. В качестве dst, src1 и src2 выступают имена регистров: va, vc, fc, vt, ft, op, oc, v, fs. Каждый из этих регистров, за исключением fs, является четырёхмерным (xyzw или rgba) вектором. Существует возможность работы с отдельными компонентами вектора, в том числе и swizzling (иной порядок):dp4 ft0.x, v0.xyzw, v0.yxww
Рассмотрим каждый из типов регистров подробнее.
Регистр-вывода
В результате расчёта вершинный шейдер обязан записать значение оконной позиции вершины в регистр op (output position), а фрагментный – в oc (output color) значение итогового цвета пикселя. В случае с фрагментным шейдером существует возможность отмены обработки инструкцией kil, которая будет описана ниже.
Регистр-атрибут
Вершина может содержать в себе до 8 атрибутов-векторов, обращение к которым из шейдера осуществляется через регистры va, положение которых в вершинном буфере задаётся функцией Context3D.setVertexBufferAt. Данные атрибута могут быть формата FLOAT_1, FLOAT_2, FLOAT_3, FLOAT_4 и BYTES_4. Число в названии обозначает количество компонент вектора. Стоит отметить, что в случае с BYTES_4 значения компонентов нормализуются, т.е. делятся на 255.
Регистр-интерполятор
Помимо записи в регистр op, вершинный шейдер может передать до 8 векторов в фрагментный шейдер через регистры v. Значения этих векторов будут линейно интерполированы по всей площади полигона во время растеризации. Проиллюстрируем работу интерполяторов на примере треугольника, в вершинах которого хранится атрибут, выводимый фрагментным шейдером:
// vertex
mov op, va0 // первый атрибут - позиция
mov v0, va1 // второй атрибут передаём в шейдер как интерполятор
// fragment
mov oc, v0 // возвращаем полученный интерполятор в качестве цвета

Регистр-переменная
В вершинном и фрагментном шейдерах доступно до 8 регистров vt и ft для хранения промежуточных результатов расчёта. Например, в фрагментном шейдере необходимо посчитать сумму четырёх векторов, принятых из вершинной программы (v0..v3 регистры):
add ft0, v0, v1 // ft0 = v0 + v1
add ft0, ft0, v2 // ft0 = ft0 + v2
add ft0, ft0, v3 // ft0 = ft0 + v3
В результате ft0 будет хранить нужную нам сумму, и всё вроде бы здорово, но существует неочевидная на первый взгляд возможность оптимизации, которая напрямую связана с архитектурой работы программного конвейера видеокарты и отчасти является причиной её высокой производительности.
В основу шейдеров заложена концепция ILP (Instruction-level parallelism), которая уже, судя из названия, позволяет выполнять несколько инструкций одновременно. Основным условием для задействования этого механизма, является независимость инструкций друг от друга. Применительно к примеру выше:
add ft0, v0, v1 // ft0 = v0 + v1
add ft1, v2, v3 // ft1 = v2 + v3
add ft0, ft0, ft1 // ft0 = ft0 + ft1
Первые две инструкции выполнятся одновременно, т.к. работают с независимыми регистрами. Отсюда следует вывод, что ключевую роль в производительности вашего шейдера играет не столько количество инструкций, сколько их независимость друг от друга.
Регистр-константа
Хранение численных констант прямо в коде шейдера не допускается, т.е. все необходимые для работы константы должны быть переданы в шейдер до вызова Context3D.drawTriangles, и будут доступны в регистрах vc (128 векторов) и fc (28 векторов). Существует возможность обращения к регистру по его индексу используя квадратные скобки, что весьма удобно при реализации скелетной анимации или индексирования материалов. Важно помнить, что операция задания шейдерных констант относительно дорогая, и её следует по возможности избегать. Так например, нет смысла передавать в шейдер матрицу проекции перед рендером каждого объекта, если она не меняется в текущем кадре.
Регистр-семплер
В фрагментный шейдер можно передать до 8 текстур функцией Context3D.setTextureAt, обращение к которым осуществляется через соответствующие регистры fs, которые используются исключительно в операторе tex. Немного изменим пример с треугольником, и в качестве второго атрибута вершины передадим текстурные координаты, а в фрагментном шейдере сделаем текстурную выборку по этим уже интерполированным координатам:
// vertex
mov op, va0 // позиция
mov v0, va1 // второй атрибут - текстурная координата
// fragment
tex oc, v0, fs0 <2d,linear> // выборка из текстуры

Операторы
На данный момент (октябрь 2011), AGAL реализует следующие операторы:
mov dst = src1
neg dst = -src1
abs dst = abs(src1)
add dst = src1 + src2
sub dst = src1 – src2
mul dst = src1 * src2
div dst = src1 / src2
rcp dst = 1 / src1
min dst = min(src1, src2)
max dst = max(src1, src2)
sat dst = max(min(src1, 1), 0)
frc dst = src1 – floor(src1)
sqt dst = src1^0.5
rsq dst = 1 / (src1^0.5)
pow dst = src1^src2
log dst = log2(src1)
exp dst = 2^src1
nrm dst = normalize(src1)
sin dst = sine(src1)
cos dst = cosine(src1)
slt dst = (src1 < src2) ? 1 : 0
sge dst = (src1 >= src2) ? 1 : 0
dp3 скалярное произведение
dst = src1.x*src2.x + src1.y*src2.y + src1.z*src2.z
dp4 скалярное произведение всех четырёх компонент вектора
dst = src1.x*src2.x + src1.y*src2.y + src1.z*src2.z + src1.w*src2.w
crs векторное произведение
dst.x = src1.y * src2.z – src1.z * src2.y
dst.y = src1.z * src2.x – src1.x * src2.z
dst.z = src1.x * src2.y – src1.y * src2.x
m33 умножение вектора на матрицу 3х3
dst.x = dp3(src1, src2[0])
dst.y = dp3(src1, src2[1])
dst.z = dp3(src1, src2[2])
m34 умножение вектора на матрицу 3х4
dst.x = dp4(src1, src2[0])
dst.y = dp4(src1, src2[1])
dst.z = dp4(src1, src2[2])
m44 умножение вектора на матрицу 4х4
dst.x = dp4(src1, src2[0])
dst.y = dp4(src1, src2[1])
dst.z = dp4(src1, src2[2])
dst.w = dp4(src1, src2[3])
kil отмена обработки фрагмента
прекращает выполнение фрагментного шейдера, если значение src1
меньше нуля, обычно используется для реализации alpha-test,
когда нет возможности сортировки порядка полупрозрачных объектов.
tex выборка значения из текстуры
заносит в dst значение цвета в координатах src1 из текстуры src2
также принимает дополнительные параметры, перечисленные
через запятую, например:
tex ft0, v0, fs0 <2d,repeat,linear,miplinear>
данные параметры нужны для обозначения:
формата текстуры 2d, cube
фильтрации nearest, linear
мипмаппинга nomip, miplinear, mipnearest
тайлинга clamp, repeat
Остальные операторы, включая условные переходы и циклы планируются реализовать в последующих версиях Flash Player. Но это не означает, что сейчас нельзя использовать даже обычный if, инструкции slt и sge вполне подходят для этих задач.
Эффекты
С основами ознакомились, теперь самая интересная часть статьи – практическое применение новых знаний. Как говорилось в самом начале, возможность писать шейдера полностью развязывает руки программисту графики, т.е. фактические ограничения лишь в фантазии и математической смекалке разработчика. Ранее можно было убедиться, что сам по себе ассемблерный язык прост, но за простотой скрывается сложность “вкуривания” в уже забытый код. Поэтому крайне рекомендую комментировать ключевые участки кода шейдера, дабы быстро в нём ориентироваться в случае необходимости.
Заготовка
Отправной точкой для всех последующих примеров будет небольшая “болванка” в виде чайника. В отличие от примера с треугольником, нам понадобится матрица проекции и трансформации камеры, для создания эффекта перспективы и вращения вокруг объекта. Её мы передадим в константные регистры. Тут важно помнить, что матрица 4х4 занимает ровно 4 регистра, и при записи её в регистр vc0, занятыми окажутся v0..v3. Также нам пригодится константный вектор из часто используемых в шейдере чисел (0.0, 0.5, 1.0, 2.0).
Итого, базовый код шейдера будет выглядеть так:
// vertex
m44 op, va0, vc0 // применяем viewProj матрицу
// fragment
mov ft0, fc0.xxxz // занесём в ft0 чёрный непрозрачный цвет
mov oc, ft0 // вернём ft0 в качестве цвета пикселя

Texture mapping
В шейдере возможно наложение до 8 текстур, при практически неограниченном числе выборок. Это означает, что данный лимит не имеет особого значения при использовании атласов или кубических текстур. Усовершенствуем наш пример и, вместо задания цвета в фрагментном шейдере, будем получать его из текстуры по текстурным координатам-интерполяторам, принятым из вершинного шейдера:
// vertex
...
mov v0, va1 // передаём в фрагментный шейдер текстурную координату
// fragment
tex ft0, v0, fs0 <2d,repeat,linear,miplinear>

Lambert shading
Самая примитивная модель освещения, имитирующая реальное. Основана на положении, что интенсивность света, упавшего на поверхность, линейно зависит от косинуса угла между векторами падения и нормали к поверхности. Из школьного курса математики вспомним, что скалярное произведение единичных векторов даёт косинус угла между ними, следовательно, наша формула освещения по Ламберту будет иметь вид:
Lambert = Diffuse * ( Ambient + max( 0, dot( LightVec, Normal ) ) )
Color = Lambert
где Diffuse – цвет объекта в точке (взятый из текстуры например),
Ambient – цвет фонового освещения
LightVec – единичный вектор из точки на источник света
Normal – перпендикуляр к поверхности
Color – итоговый цвет пикселя
Шейдер будет принимать два новых константных параметра: позицию источника и значение фонового света:
// vertex
...
mov v1, va2 // v1 = normal
sub v2, vc4, va0 // v2 = lightPos - vertex (lightVec)
// fragment
...
nrm ft1.xyz, v1 // normal ft1 = normalize(lerp_normal)
nrm ft2.xyz, v2 // lightVec ft2 = normalize(lerp_lightVec)
dp3 ft5.x, ft1.xyz, ft2.xyz // ft5 = dot(normal, lightVec)
max ft5.x, ft5.x, fc0.x // ft5 = max(ft5, 0.0)
add ft5, fc1, ft5.x // ft5 = ambient + ft5
mul ft0, ft0, ft5 // color *= ft5

Phong shading
Вводит понятие блика от источника света в модель освещения по Ламберту. Подразумевает, что интенсивность блика определяется степенной функцией по косинусу угла между вектором на источник и направления, получившегося в результате отражения вектора наблюдателя относительно нормали к поверхности.
Phong = pow( max( 0, dot( LightVec, reflect(-ViewVec, Normal) ) ), SpecularPower ) * SpecularLevel
Color = Lamber + Phong
где ViewVec – вектор взгляда наблюдателя
SpecularPower – степень, определяющая размер блика
SpecularLevel – уровень интенсивности блика или его цвет
reflect – функция вычисления отражения f(v, n) = 2 * n * dot(n, v) – v
Для сложных моделей принято использовать Specular и Gloss карты, которые определяют цвет/интенсивность (SpecularLevel), а также размер блика (SpecularPower) на разных участках текстурного пространства модели. В нашем случае, обойдёмся константными значениями степени и интенсивности. В вершинный шейдер передадим новый параметр – позицию наблюдателя для последующего вычисления ViewVec:
// vertex
...
sub v3, va0, vc5 // v3 = vertex - viewPos (viewVec)
// fragment
...
nrm ft3.xyz, v3 // viewVec ft3 = normalize(lerp_viewVec)
// расчёт вектора отражения reflect(-viewVec, normal)
dp3 ft4.x, ft1.xyz ft3.xyz // ft4 = dot(normal, viewVec)
mul ft4, ft1.xyz, ft4.x // ft4 *= normal
add ft4, ft4, ft4 // ft4 *= 2
sub ft4, ft3.xyz, ft4 // reflect ft4 = viewVec - ft4
// phong
dp3 ft6.x, ft2.xyz, ft4.xyz // ft6 = dot(lightVec, reflect)
max ft6.x, ft6.x, fc0.x // ft6 = max(ft6, 0.0)
pow ft6.x, ft6.x, fc2.w // ft6 = pow(ft6, specularPower)
mul ft6, ft6.x, fc2.xyz // ft6 *= specularLevel
add ft0, ft0, ft6 // color += ft6

Normal mapping
Относительно простой метод для имитации рельефа поверхности посредством использования текстуры нормалей. Направление нормали в такой текстуре принято задавать в виде RGB значения, полученного из приведения её координат к диапазону 0..1 (xyz * 0.5 + 0.5). Нормали могут быть представлены как в пространстве объекта (Object Space), так и в относительном пространстве (Tangent Space), построенном на базисе текстурных координат и нормали к вершине. Первый имеет ряд порой значительных недостатков в виде большого расхода памяти под текстуры из-за невозможности тайлинга и mirror-текстурирования, но позволяет сэкономить на количестве инструкций. В примере будем использовать более гибкий и общий вариант с Tangent Space, для которого помимо нормали потребуется ещё два дополнительных вектора базиса Tangent и Binormal. Реализация сводится к переводу векторов viewVec и lightVec к TBN (Tangent, Binormal, Normal) базису, и дальнейшей выборке относительной нормали из текстуры в фрагментном шейдере.
// vertex
...
// transform lightVec
sub vt1, vc4, va0 // vt1 = lightPos - vertex (lightVec)
dp3 vt3.x, vt1, va4
dp3 vt3.y, vt1, va3
dp3 vt3.z, vt1, va2
mov v2, vt3.xyzx // v2 = lightVec
// transform viewVec
sub vt2, va0, vc5 // vt2 = vertex - viewPos (viewVec)
dp3 vt4.x, vt2, va4
dp3 vt4.y, vt2, va3
dp3 vt4.z, vt2, va2
mov v3, vt4.xyzx // v3 = viewVec
// fragment
tex ft1, v0, fs1 <2d,repeat,linear,miplinear> // ft1 = normalMap(v0)
// 0..1 to -1..1
add ft1, ft1, ft1 // ft1 *= 2
sub ft1, ft1, fc0.z // ft1 -= 1
nrm ft1.xyz, ft1 // normal ft1 = normalize(normal)
...

Toon Shading
Разновидность нефотореалистичной модели освещения, имитирующая мультипликационную рисовку затенения. Реализуется множеством способов, самым простым из которых является выборка цвета из 1D текстуры по косинусу угла из модели Ламберта. В нашем случае, для примера используем текстуру 16x1:

// fragment
...
dp3 ft5.x, ft1.xyz, ft2.xyz // ft5 = dot(normal, lightVec)
tex ft0, ft5.xx, fs3 <2d,nearest> // color = toonMap(ft5)

Sphere mapping
Самый простой вариант для имитации отражения, чаще используемый для эффекта хромирования металла. Представляет окружение в виде текстуры со сферическим искажением по типу “рыбий глаз”, как показано ниже:

Основная задача сводится к преобразованию координат вектора отражения в соответствующие текстурные координаты:
uv = ( xy / sqrt(x^2 + y^2 + (z + 1)^2) ) * 0.5 + 0.5
Умножение и сдвиг на 0.5 нужны для приведения нормированного результата к пространству текстурных координат 0..1. В простом случае для идеально отражающей поверхности, влияние карты аддитивное, а для более сложных случаев когда требуется диффузная составляющая, принято использовать приближение формул Френеля. Также для комплексных моделей часто используются Reflection карты, указывающие интенсивность отражения разных частей текстуры модели.
// fragment
...
add ft6, ft4, fc0.xxz // ft6 = reflect (x, y, z + 1)
dp3 ft6.x, ft6, ft6 // ft6 = ft6^2
rsq ft6.x, ft6.x // ft6 = 1 / sqrt(ft6)
mul ft6, ft4, ft6.x // ft6 = reflect / ft6
mul ft6, ft6, fc0.y // ft6 *= 0.5
add ft6, ft6, fc0.y // ft6 += 0.5
tex ft0, ft6, fs2 <2d,nearest> // color = reflect(ft6)

На этом пожалуй закончу. Представленные здесь примеры, по большей части, описывают свойства материала объекта, но шейдера находят своё применение и в других задачах, таких как скелетная анимация, тени, вода и других относительно сложных задачах (в том числе невизуальных). А при должной прокачке навыков позволяют за короткие сроки реализовывать достаточно комплексные вещи по типу:
Заключение
Игры на флеше – это просто! пример к статье.
- +21
- XProger
Комментарии (17)
Спасибо, за статью, очень познавательно.
Вряд ли мои мозги когда нибудь смогут такое переварить, но автору респект...:)
Не так и сложно — короткие команды с 2-3 параметрами.
В институтах часто Ассемблер преподают, Нужно было специальность тщательней выбирать:) Хотя мне кажется, в Ассемблере более менее у нас разобралось не более 1%. Я, например, особых успехов в нём не достиг. Больше привлекают высокоуровневые языки. AS3, кстати, чересчур выосокоуровневый (т.к. на виртуальной машине работает):)
Тут для меня, конечно, совсем неведомая хрень описана; и только из-за того, что мне оно не нужно.
Я как не программист люблю as3. Из тех немногих языков программирования, с которыми я сталкивался, as3 мне ближе, удобнее и понятнее.
Плюсую за гаечку =)
Ато на HLSL я когда то много писал, а вот этот «ассамблер» меня пугает.
Но все работает через одно место.
1. Менять текстовый файл
2. Запускать pbutil
3. Перекомпилировать флэшку
Как-то не очень радует.
Ради каждого изменения в шейдере AGAL:
1. Менять текстовый файл (исходный код)
2. Перекомпилировать флэшку
Ради каждого изменения в шейдере в Pixel Bender 3D:
1. Менять текстовый файл
2. Перекомпилировать флэшку с включенным pbutil в Pre-Build запуск.
Вполне радует.
О Г Р О М Н О Е С П А С И Б О.
очень интересно и полезно.
скачал. запустил. все работает. начал разбираться.
1-й вопрос. а что это за формат модели чайника?