Туториал по шейдерам

Материал из Вики по GameMaker
Перейти к: навигация, поиск
Forumlogo.pngЭту статью можно обсудить на форуме:
http://gmakers.ru/index.php?topic=6828

Сразу оговорюсь, что урок не для нубов и нужно уметь как минимум программировать на GML без помощи кнопок.

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

Туториал получился настолько большой, что заталкивать всё в один форумный пост просто нецелесообразно, а контент представляет из себя слишком цельный поток информации, чтобы можно было его логически разбить на части. Поэтому сам туториал выложен на вики, а обсуждение и комментарии - в треде на форуме: http://gmakers.ru/index.php?topic=6828

Шейдеры появились в GameMaker уже довольно давно, но вот о них по прежнему очень мало говорят - видмо, мало кто использует, хоть про-версия GameMaker: Studio и не требуется. Оно и понятно - в справке ничего вразумительного не написано, из немногочисленных туториалов часто ничего не понятно, а чтение спеков (сокращение от "спецификации") по GL ES требует не только большой усидчивости, так как текст весьма длинный, большого напряжения мозгов для вникания, так как текст сугубо технический и научно-популярно там никто ничего не объясняет, ну и для полного счастья - весь текст спеков целиком на английском. Хотя на самом деле эта фича очень полезна и каждому следует уметь работать с шейдерами, чтобы создавать красивые графические эффекты. Поэтому здесь был составлен этот большой туториал по шейдерам в GameMaker. Поскольку только OpenGL ES поддерживается на всех платформах, обсуждаться будет именно он.

Для тех, кто в танке: шейдер (shader) - это программа для графического процессора (того, который на видеокарте), согласно которой она будет отрисовывать изображение на экране. Название связано с историей его появления: ранние 3д-видеокарты имели строго вшитый алгоритм отрисовки и ничего особого сделать было нельзя, но со временем появилась поддержка пользовательских алгоритмов, которые указывают, как придавать оттенок (shade) треугольникам - с тех пор название закрепилось, хотя поменялось всё чуть ли не в корне. Современные шейдеры состоят из двух частей - вертекс-шейдер (vertex shader) и фрагмент-шейдер (fragment shader), которые работают вместе. Название вертекс-шейдера происходит от того, что он вычисляет позицию вершин полигонов (vertex) на экране исходя из их позиции в игровом мире. Второй так называется потому, что при растеризаци полигона, он разбивается на большое количество фрагментов (fragment), соответствующих пикселям на экране, и этот шейдер вычисляет для каждого отдельного фрагмента какого цвета должен быть соответствующий пиксель.

Содержание

[править] Основы шейдеров.

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

/// passthrough vertex shader
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    vec4 vertex_position = vec4 ( in_Position.x, in_Position.y, in_Position.z, 1.0 );
    gl_Position = gm_Matrices[ MATRIX_WORLD_VIEW_PROJECTION ] * vertex_position;
    
    v_vColour = in_Colour;
    v_vTexcoord = in_TextureCoord;
}
/// passthrough fragment shader
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord ) * v_vColour;
}

Чтобы понять в чём суть, давайте разберём по частям тут написанное. Вникать не нужно, по ходу туториала всё и так станет понятно. В вертекс-шейдере сначала указываются входные данные (attribute) - тут это позиция вертекса в мире, его цвет и координата текстуры. При отрисовке стандартными функциями GameMaker все эти значения заполнены соответствующими данными автоматически. Далее определяются значения, которые будут переданы в фрагмент-шейдер (varying) - координата текстуры и цвет. В фрагмент-шедере указаны эти же величины, как видно - это важно. Эти величины будут линейно интерполированы между вертексами полигонов для каждого фрагмента, так что если, например, на левом вертексе будет красный цвет, а на правом - синий, то фрагмент где-то между ними будет филоетовый, ближе к левой стороне - краснее, а к правой - синее. То же самое происходит и с координатами текстур. В главной программе шейдера (main) преобразуется позиция входной вершины - позиция вертекса в мире умножается на матрицу вида-мира-проекции (об этом далее, но вкратце - это "главная" матрица) и эта величина записывается в переменную, которая отвечает за позицию вертекса на экране gl_Position. А далее - указываются величины, которые нужно передать в фрагмент-шейдер. Это можно делать в любом порядке, в общем-то, это ни на что не влияет - главное чтобы к концу главной программы все нужные переменные были выставлены (иначе там будут значения по-умолчанию, которые ничем не гарантируются - но обычно это нули и единицы). Этот вертекс-шейдер ничего интересного не делает и просто отправляет цвет и координаты текстур как есть, в результате на экране мы получим стандартный текстурированный полигон.

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

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

Перед тем, как сделать что-то интересное с этим шейдером, нужно сначала уяснить ряд важных деталей.

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

Во-вторых, в шейдерах строгая типизация данных - это значит, что для каждой переменной нужно указать тип, и если где-то в ходе вычислений типы будут не совпадать, то шейдер вывалится с ошибкой (не скомпилируется). Это первое большое отличие от скриптов в GameMaker, у которых тип данных меняется автоматически в зависимости от того, какую величину (число, строку, этц.) туда записать и поэтому указывать его не надо, равно как и соблюдать (хотя таки нельзя, например, умножить число на строку). На векторах можно указывать маски (например myvector.xyz), тогда тип вектора считаться таким, который соответствует количеству компонент вектора, указанных в маске - если указано четыре компоненты то четырёхмерный, три - трёхмерный, две - двухмерный; если указать только одну компоненту, то вектор будет считаться как обычная числовая переменная.

В-третьих, функциии в шейдерах объявляются прямо там же, а не в отдельных файлах скриптов. Собственно, главная функция шейдера (main) так и объявлена, и все остальные функции нужно объявлять аналогичным образом. В объявлении функции нужно указать, какой тип данных она возвращает, и в скобках - какие типы данных принимает (если никаких то можно оставить пустыми) вместе с названиями соответствующих аргументов (вместо argument0, argument1 и т.д.).

Чтобы в вертекс-шейдере установить результаты вычислений, нужно выставить переменной gl_Position соответствующее значение. В фрагмент-шейдере эта переменная - gl_FragColor. Главная текстура, установленная GameMaker для отрисовки - gm_BaseTexture. Список матриц для умножения вертексов - gm_Matrices.

Чтобы шейдер начал работать, его надо активировать через функцию shader_set. Чтобы отменить шейдер и использовать отрисовку по-умолчанию, нужно использовать функцию shader_reset.

[править] Полезные шейдеры

[править] Предумноженная альфа

Самый простой но при этом полезный шейдер, который можно сделать, это шейдер, выполняющий предумножение альфы. Графика с предумноженной альфой лучше отображается и корректно отрисовывается на сурфейсах. Но графические редакторы по-умолчанию делают только "обычную" графику. Можно провести операцию предумножения в редакторе (в редакторе гамака есть такая кнопка), и иметь нужную графику изначлаьно. Однако при отрисовке спрайтов с полупрозрачностью уже начинаются занимательные пляски с перевычислением цвета спрайта на лету и прочие радости, что сказывается на производительности. Но можно сделать шейдер, который домножит цвет на альфу прямо перед выводом - эта операция будет занимать очень мало времени по сравнению со временем сэмплинга собственно текстуры, поэтому можно применять такой шейдер совершенно не опасаясь за производительность. А в качестве бонуса, в исходных текстурах будет "нормальная" графика, если она вдруг потребуется именно в таком виде - достаточно просто будет отменить шейдер с предумножением.

Чтобы обычную графику сделать "предумноженной", нужно все три цветовые компоненты пикселя умножить на его альфу. Для этого надо в конец фрагмент-шейдера добавить оответствующую операцию умножения:

/// glsl_premultiplied_from_normal
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord ) * v_vColour;
    
    gl_FragColor.rgb *= gl_FragColor.a;
}
Исходный спрайт
.

Вычисляем цвет пикселя как обычно, и в конце - три цветовые компоненты (r - красный, g - зелёный, b - синий) полученного пикселя домножаем на его же альфу (a). Вуаля! Теперь вся графика, отрисованная с этим шейдером, будет иметь предумноженную альфу, и предумножение будет корректно работать всегда и везде. В принципе, любой шейдер можно сделать "предумноженным", поместив в конце это умножение. Здесь отчётливо видно, что в шейдерах цвет представлен четырёхмерными векторами, в которых компоненты r, g, b, a (в таком порядке) соответствуют красному, зелёному, синему и альфа-каналу. Если будете рисовать предумноженную графику в сурфейс, то имейте ввиду, что теперь сурфейс тоже предумноженный - на нём шейдер применять не нужно, но отрисовывать надо с таким же бленд-модом. Также надо учитывать, что "нормальный" бленд-мод будет некорректно работать с предумноженной альфой, вместо него надо использовать bm_one, bm_inv_src_alpha.

[править] Инверсия

Инверсия цвета.

Аналогичным способом можно сделать инверсию цвета:

/// glsl_invert
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord ) * v_vColour;
    
    gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;
}

Здесь каждому цветовому каналу задаётся его "противоположное" значение. Это соответствует функции редактора спрайтов "Invert". Обратите внимание, что в числе "1.0" указан десятичный разряд через точку - в шейдерах это указывает на то, что это число с плавающей запятой, а не целое число. Это важно, так как целые числа и числа с плавающей запятой несовместимы. Все обычные векторы в шейдерах имеют числа с плавающей запятой. Обратите внимание, что если совершить операцию между числом и вектором, то операция совершится сразу надо всеми элементами вектора, причём одинаково. Поэтому можно векторы умножать на числа, прибавлять и вычитать, и так далее (но присваивать число вектору через знак равенства - нельзя). В предыдущем примере шейдера это тоже используется, но там это менее очевидно. Поскольку операция совершается над вектором, то и результат операции тоже вектор - причём по метрике (количестве измерений) совпадает с тем вектором (rgb), значение которого тут устанавливается.

[править] Черно-белый

Можно сделать спрайт чёрно-белым. Для этого нужно будет вычислить яркость пикселя и записать эту величину во все три цветовых канала, чтобы получить оттенок серого цвета:

/// glsl_black_and_white
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord ) * v_vColour;
    
    float luminance = ( gl_FragColor.r + gl_FragColor.g + gl_FragColor.b ) / 3.0;
    
    gl_FragColor.rgb = vec3 ( luminance );
}

Здесь яркость вычисляется путём суммирования всех трёх каналов, а чтобы величина лежала в пределах между 0 и 1 - далее делится на три. Затем это число устанавливается во все три канала. Переменная, которая будет содержать яркость, объявляется в начале строки ключевым словом float, которое обозначает число с плавающей запятой - в шейдерах вместо слова var используются названия типов данных; для целых чисел существует int, а для булевых - bool, но в силу особенностей процессоров, используемых на видеокартах, их полезность в шейдерах невелика. Полученную величину нужно записать во все три цветовые канала, что можно сделать по отдельности, но можно выполнить за одноу операцию, если использовать совместимый тип. Для этого из числа создаётся трёхмерный вектор - это указывается цифрой "3" в ключевом слове vec3, аналогичным способом создаются четырёхмерные и двухмерные векторы. При операции только над первыми тремя компонентами, четвёртая компонента вектора (альфа) не будет затронута - останется то значение, которое было до этого. Числа как правило легко преобразуются в вектор соответствующей операцией, причём можно туда передавать числа как по одному на каждую компоненту (в вертекс-шейдере создаётся 4-мерный вектор, в который компоненты передаются по одному), из смеси векторов и чисел (будет показано далее), вектором превышающим метрику нужного вектора (лишние компоненты будут отброшены) и одним числом (оно окажется во всех компонентах вектора).

В спрайтах, окрашенных через чёрно-белый шейдер, нет характерного "зачернения" цветов.

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

/// glsl_black_and_white
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

const vec3 c_LuminanceWeight = vec3 ( 0.2126, 0.7152, 0.072 );

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    vec3 color_weighted = gl_FragColor.rgb * c_LuminanceWeight;
    float luminance = color_weighted.r + color_weighted.g + color_weighted.b;
    vec4 luminance_color = vec4 ( luminance, luminance, luminance, gl_FragColor.a );
    
    gl_FragColor = luminance_color * v_vColour;
}

Можно было умножать цветовые компоненты по одному, но опять же проще сразу сделать операцию над целым вектором, да и графический процессор это вычислит быстрее ибо он как раз под это заточен (операция выполняется одновременно надо всеми элементами вектора сразу, а не по одному). А чтобы не вставлять этот вектор посреди кода, то он объявляется в виде именованой константы - результат тот же самый, но так удобней поскольку не нужно копипастить эту величину по всему коду. Так как операция эта векторная, то и результат тоже вектор - чтобы получить собственно значение яркости одним числом, надо компоненты сложить по одному. В этом шейдере вектор яркости создаётся четырёхмерным, чтобы его можно было без затруднений умножить на аналогичный четырёхмерный вектор цвета окрашивания; четвёртой величиной берётся альфа исходного изображения, так как над ней не проводится никаких операций и нужно пропустить как есть. Операция умножения цвета пикселя на цвет отрисовки перенесена в конец. Если не указывать маску вычислений вектора, то это будет равносильно тому, чтобы указать в маске сразу все компоненты.

Этот шейдер аналогичен функции редактора спрайтов "Black and White", если рисовать с белым цветом. Теперь окрашивание спрайта работает как надо, да и спрайт будет не обязательно чёрно-белым, можно делать и чёрно-зелёный, и чёрно-красный, и чёрно-серо-буро-малиновый. Если рисовать окрашенный спрайт с этим шейдером, то "противоположные" цвета не будут становиться чёрными, а будут иметь цвет - этот шейдер работает почти как перекрашивание спрайта. Хотя это всё-таки не перекрашивание, а только полное окрашивание - если выставить, например, красный цвет, то самые белые участки спрайта будут красными, серые-синие-красные станут буро-коричневыми, и вообще все цвета станут темнее.

[править] Перекрашивание

При перекраске не потерялась яркость и насыщенность цвета.

Шейдер можно далее переделать в "перекрашивающий", если при окраске принимать во внимание насыщенность исходного цвета:

/// glsl_colorize
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
 
    float maxcolor = max ( max ( gl_FragColor.r, gl_FragColor.g ), gl_FragColor.b );
    float mincolor = min ( min ( gl_FragColor.r, gl_FragColor.g ), gl_FragColor.b );
    vec3 luminance = vec3 ( maxcolor );
    float saturation = ( maxcolor - mincolor ) / maxcolor;
    
    gl_FragColor.rgb = mix ( luminance, v_vColour.rgb * luminance, saturation );
    gl_FragColor.a *= v_vColour.a;
}

Чтобы вычислить насыщенность (saturation), нужно узнать максимальную компоненту цвета и минимальную. Для этого используются встроенные функции для нахождения минимума и максимума из двух величин min и max, аналогичные функциям GameMaker. Функций нахождения минимума и максимума из компонентов вектора не предусмотрено, равно как и для более чем двух элементов, поэтому придётся городить скобки (либо не придётся - об этом далее). Значение яркости (luminance) вычисляется как максимальная из цветовых компонент, чтобы при перекрашивании цвета равномерно влияли на конечную яркость - поскольку взвешенная яркость сильно отличается в зависимости от конкретного цвета, красных и особенно синих цветов она была бы гораздо ниже реальной. Также, вместо одной переменной яркость преобразуется в вектор - он далее понадобится именно в такой форме, чтобы выполнить векторное умножение. Далее вычисляется насыщенность цвета - это простая операция, нужно вычислить разницу между самым ярким и самым тусклым цветовым компонентом относительно собственно самого яркого. В конце используется функция mix - она собственно предназначена для смешивания цветов: вычисляет среднее между двумя векторами (или числами), со смещением, указанным в третьем числе - если оно ближе к нулю, то результат на выходе будет ближе к первому аргументу, если ближе к единице - ближе ко второму. Здесь это используется, чтобы смешивать цвет от чёрно-белого к цветному в зависимости от насыщенности цвета в исходном пикселе - серые, чёрные и белые так и останутся бесцветными, а цветные станут тем ближе к указанному цвету, чем больше была исходная насыщенность. Чтобы не было ситуаций, когда околочёрные (и поэтому ярконасыщенные) цвета окрашивались яркими красками, создавая уродливые артефакты, цвет окрашивания домножается на яркость - тёмные цвета так и останутся тёмными, но при этом будут иметь высокую насыщенность. Поскольку альфа пикселя из текстуры нигде не домножается на альфу отрисовки, то в конце эту операцию надо выполнить отдельно.

Этим шейдером можно перекрашивать спрайты аналогично функции "Colorize" редактора спрайтов, и менять насыщенность спрайтов аналогично функции "Intensity". Говоря по иному, перекрашивает все цветные участки в указанный цвет. Таким шейдером довольно удобно перекрашивать спрайты противников на ходу. Теперь неплохо бы какой-то определённый цвет перекрасить, чтобы не затрагивать весь цветной спрайт. Можно пойти сложным путём и создать шейдер-аналог функции "Colorize Partial", но можно и пойти более прямолинейным путём, заодно сделав кой-что весьма интересное.

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

/// glsl_recolor
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3 u_Color1;
uniform vec3 u_Color2;
uniform vec3 u_Color3;

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    gl_FragColor.rgb = u_Color1 * gl_FragColor.r + 
                       u_Color2 * gl_FragColor.g + 
                       u_Color3 * gl_FragColor.b;
    
    gl_FragColor *= v_vColour;
}
/// obj_car draw event
var u1, u2, u3;
u1 = shader_get_uniform ( glsl_recolor, "u_Color1" );
u2 = shader_get_uniform ( glsl_recolor, "u_Color1" );
u3 = shader_get_uniform ( glsl_recolor, "u_Color3" );

shader_set ( glsl_recolor );
shader_set_uniform_f ( u1, 0.95, 0.91, 0.73 );
shader_set_uniform_f ( u2, 0.11, 0.65, 0.21 );
shader_set_uniform_f ( u3, 0.84, 0.23, 0.10 );
draw_sprite ( spr_car, 0, x, y );

Здесь используются внешние переменные - юниформы (uniform). Эти переменные задаются из скриптов GameMaker во время работы шейдера, и сохраняют своё значение до тех пор, пока не поменять его на иное - достаточно установить один раз, если не нужно постоянно менять. Чтобы установить юниформ, сначала нужно активировать соответствующий шейдер. Собственно устанавливают юниформы через функции семейства shader_set_uniform_* - есть несколько вариантов для конкретных типов юниформов. В данном случае используется shader_set_uniform_f - буква "f" в конце указывает на то, что имеется ввиду число с плавающей запятой, это должно совпадать с реальным типом юниформа. Устанавливают юниформы по индексу, который получают через shader_get_uniform. Индексы юниформов не меняются, поэтом их можно, например, записать в переменные, чтобы не вызывать соответствующую функцию каждый раз. Чтобы получить индекс юниформа, шейдер активировать не надо. Значение юниформа в шейдере тоже не меняется, если его не трогать.

[править] Перекрашивание 3-х цветов

Для перекрашивания спрайта доступны три отдельные зоны, которым можно задать произвольный цвет. Серые участки не будут затронуты.

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

/// glsl_recolor_with_grayscale
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3 u_Color1;
uniform vec3 u_Color2;
uniform vec3 u_Color3;

float max ( float a, float b, float c )
{
    return max ( max ( a, b ), c );
}
float min ( float a, float b, float c )
{
    return min ( min ( a, b ), c );
}

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    float maxcolor = max ( gl_FragColor.r, gl_FragColor.g, gl_FragColor.b );
    float mincolor = min ( gl_FragColor.r, gl_FragColor.g, gl_FragColor.b );
    float saturation = ( maxcolor - mincolor ) / maxcolor;
    vec3 luminance = vec3 ( maxcolor );
    vec3 newcolor = u_Color1 * gl_FragColor.r + 
                    u_Color2 * gl_FragColor.g + 
                    u_Color3 * gl_FragColor.b;
    gl_FragColor.rgb = mix ( luminance, newcolor, saturation );
    
    gl_FragColor *= v_vColour;
}

Для простоты работы, здесь мы объявили собственные фукнции min и max, которые принимают по три аргумента - как раз чтобы не городить скобки. Перед названием функции указывается тип возвращаемых данных (здесь float, но может быть и vec2 и vec3 и т.п.), потом указано название функции, а далее в скобках перечислены типы аргументов и их названия, по которым код внутри функции будет к ним обращаться - вместо argument0, argument1 и т.д. в обычных скриптах гамака. Здесь из функции смешивания исключено умножение цвета на яркость, так как яркость и так уже будет изначально установлена на нужном уровне. В качестве измерения яркости тоже используется максимум из трёх каналов, так как они в любом случае будут перекрашены, и их абсолютный цвет не имеет значения. Правда цвет должен быть либо полностью насыщенным, либо полностью бесцветным (серым) - попытка смешать вместе два разных цвета приведёт к тому, на выходе получится невразумительного оттенка засвеченный учасотк.

За счёт использования насыщенности цвета можно достигнуть лучших результатов.

Если проявить смекалку, то можно сделать так, чтобы работала не только яркость, но и насыщенность цвета. Насыщенность и яркость и так вычисляются как надо, но из-за наложения каналов, их нельзя использовать вместе. Если домножить сумму каналов на их соотношение (вес) в общем цвете относительно самого тусклого, то вес двух "слабых" каналов будут умножен на ноль, а вес "сильного" канала вырастет до единицы.

/// glsl_recolor_with_grayscale_saturation
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3 u_Color1;
uniform vec3 u_Color2;
uniform vec3 u_Color3;

float max ( vec3 a )
{
    return max ( max ( a.x, a.y ), a.z );
}
float min ( vec3 a )
{
    return min ( max ( a.x, a.y ), a.z );
}

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    float maxcolor = max ( gl_FragColor );
    float mincolor = min ( gl_FragColor );
    float colordelta = maxcolor - mincolor;
    float saturation = colordelta / maxcolor;
    vec3 luminance = vec3 ( maxcolor );
    vec3 weights = ( gl_FragColor.rgb - mincolor ) / colordelta;
    
    vec3 newcolor = u_Color1 * gl_FragColor.r * weights.r + 
                    u_Color2 * gl_FragColor.g * weights.g + 
                    u_Color3 * gl_FragColor.b * weights.b;
                    
    gl_FragColor.rgb = mix ( luminance, newcolor, saturation );
    
    gl_FragColor *= v_vColour;
}

Разница между максимальным и минимальным цветом вынесена в отдельную переменную - она далее понадобится. Относительный вес каждого канала высчитывается как величина этого канала относительно "нижней планки", и делится на коэффицент разницы каналов. В этом случае самый "сильный" канал домножается до единицы, самый "слабый - до нуля, а "средний" (если такй есть) до некоего промежуточного значения. Полученная развесовка умножает величину цветовых каналов, чтобы усилить "главный" канал и ослабить "вторичные", которые не должны участвовать в формировании цвета.

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

Такие "трёхцветные" спрайты очень полезны, если нужно, например, отобразить на экране очень большое количество разноцветных противников, так как на все случаи жизни спрайтов в редакотре не напасёшься, да и динамически создавать такое количество отдельных спрайтов может быть проблематично. Но есть и особые требования к самому спрайту - цвет должен быть либо конкретно зелёный/синий/красный с оттенками (или равномерное линейное "накладывание" одного на другое на границах), либо серый/белый/чёрный.

[править] Замена определённого цвета

Артефакты при недостаточно высоком пороге и рассеивании (сверху).
Схематичное изображение принципа работы.

Иногда требуется перекрасить какой-то один определённый цвет на цветном спрайте, и вот тогда ничего не остаётся, кроме как использовать нечто вроде "Colorize Partial". Есть два варианта работы этой функции - заменить определённый цвет на другой определённый цвет, и заменить определённый тон на другой тон сохраняя насыщенность и яркость. В первом случае получить разные оттенки цвета не получится и на "мягких" краях будут заметны артефакты. Во втором случае придётся отдать целый тон, например красный и его производные, такие как розовый, коричневый и т.п. - в спрайте их нельзя будет использовать, нужно будет обходиться другими тонами. В любом случае, как уже понятно, нужно иметь специально подготовленные под перекраску спрайты. Первый вариант шейдера гораздо проще, с него и начнём:

/// glsl_replace_color
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3 u_SourceColor;
uniform vec3 u_TargetColor;
uniform float u_Tolerance;
uniform float u_Falloff;

float manhattan_length ( vec3 a )
{
    return abs( a.x ) + abs ( a.y ) + abs ( a.z );
}

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    float overlap = manhattan_length ( gl_FragColor.rgb - u_SourceColor );
    float mix_factor = clamp ( ( u_Tolerance - overlap ) / u_Falloff, 0.0, 1.0 );
    vec4 newcolor = vec4 ( u_TargetColor, gl_FragColor.a );
    
    gl_FragColor = mix ( gl_FragColor, newcolor, mix_factor );
    gl_FragColor *= v_vColour;
}

Здесь вычисляется, насколько цвет пикселя спрайта отличается от искомой величины - функция manhattan_length определяет длину вектора. Если вычесть один цвет из другого, то на выходе будет вектор, в каждо компоненте которого будет разница по каждому соответствующему каналу. У этого вектора есть длина, и длина этого вектора соответствует разнице по всем трём каналам вместе. Но если вычислять геометрическую длину, то если разница будет больше, чем по одному каналу, то будет считаться, что абсолютная разница несколько меньше, чем сумма всех разниц. Поэтому используется "манхеттенская длина" (в Манхеттене улицы расположены под прямым углом, так что расстояние от точки А до точки Б это сумма расстояний по вертикали и горизонтали) - сумма длин всех компонент вектора. Далее, функция clamp заталкивает величину в указанные границы - это аналогично комбинации функций max и min или функции гамака clamp, которая делает то же самое. Реальная разница вычитается из максимально допустимой (ширины окна), таким образом пока цвет в нужных пределах, результат будет положительным (обрезается до 1.0), и эта величина дополнительно делится на расхождение (поскольку эта степень меньше единицы, то результат деления будет больше исходного числа), таким образом делая "разрыв" более жёстким - если оставить единицей, то перекрашивание будет плавно простираться на весь цветовой спектр.

[править] Замена всех оттенков определённого цвета

На спектре охватывается целая полоса по вертикали. Сверху показаны артефакты неоптимальных настроек.

Шейдер с заменой цвета по тону с сохранением насыщенности потребует вычисления величины тона (hue), и потом вычисления нового цвета с учётом яркости и насыщенности, поэтому он много сложнее:

/// glsl_colorize_partial
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_SourceHue;
uniform vec3 u_TargetColor;
uniform float u_Tolerance;
uniform float u_Falloff;

vec3 rgb2hsv ( vec3 color )
{
    vec4 K = vec4 ( 0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0 );
    vec4 p = mix ( vec4 ( color.bg, K.wz ), vec4 ( color.gb, K.xy ), step ( color.b, color.g ) );
    vec4 q = mix ( vec4 ( p.xyw, color.r ), vec4 ( color.r, p.yzx ), step ( p.x, color.r ) );

    float d = q.x - min ( q.w, q.y );
    float e = 1.0e-10;
    return vec3 ( abs ( q.z + ( q.w - q.y ) / ( 6.0 * d + e ) ), d / ( q.x + e ), q.x );
}

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    vec3 hsv = rgb2hsv ( gl_FragColor.rgb );
    float saturation = hsv.y;
    vec3 luminance = vec3 ( hsv.z );
    
    float overlap = abs ( hsv.x - u_SourceHue );
    float mix_factor = clamp ( ( u_Tolerance - overlap ) / u_Falloff, 0.0, 1.0 );
    vec4 newcolor = vec4 ( mix ( luminance, u_TargetColor * luminance, mix_factor ), gl_FragColor.a );
    
    gl_FragColor = mix ( gl_FragColor, newcolor, mix_factor );
    gl_FragColor *= v_vColour;
}

Функции преобразования цвета из RGB в HSV (показано далее) и обратно по-честному стырены из интернета, ибо все велосипеды давно изобретены и изобретать с нуля еще один ни к чему (хоть это и полезно). В этом случае функциями попроще (как в предыдущих шейдерах) не обойтись, и использование этих больших и запутанных функций вполне оправдано. Параметры смешивания вычисляются аналогичным образом с предыдущим шейдером; новый цвет, который есть перекрашенный старый также вычисляется аналогично прошлым соответствующим шейдерам. В конце, перекрашенный цвет смешивается с основным в зависимости от того, насколько совпадали исходные тона.

Это аналогично функции редактора спрайтов "Colorize Partial". Это, наверное, наиболее полезный шейдер для перекрашивания. Хотя, как уже было сказано, графику под перекрашивание всё равно надо специально готовить заранее. На спрайте нужно будет найти неиспользуемый тон, и часть для перекрашивания выкрасить разными оттенками этого тона, таким образом, чтобы hue лежал максимально "ровно" по всему диапазону оттенков - чем уже требуется ширина обхвата, тем меньше лишних пикселей будет затронуто. Поскольку шейдер работает на определённых тонах, то в нём можно выделить под перекраску гораздо больше цветов, чем в ранее описанном "трёхцветном" шейдере - как минимум шесть (три основных и три промежуточных) будут хорошо работать, но при этом использовать пересечение цветов перекрашивания нельзя, так как это чревато довольно непредсказуемыми результатами.

[править] Сдвиг тона

Сдвиг тона.

Менее полезный шейдер, но создающий забавный эффект - сдвиг спектра, "Shift Hue".

/// glsl_shift_hue
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_Shift;

vec3 rgb2hsv ( vec3 color )
{
    vec4 K = vec4 ( 0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0 );
    vec4 p = mix ( vec4 ( color.bg, K.wz ), vec4 ( color.gb, K.xy ), step ( color.b, color.g ) );
    vec4 q = mix ( vec4 ( p.xyw, color.r ), vec4 ( color.r, p.yzx ), step ( p.x, color.r ) );

    float d = q.x - min ( q.w, q.y );
    float e = 1.0e-10;
    return vec3 ( abs ( q.z + ( q.w - q.y ) / ( 6.0 * d + e ) ), d / ( q.x + e ), q.x );
}

vec3 hsv2rgb ( vec3 hsv )
{
    vec4 K = vec4 ( 1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0 );
    vec3 p = abs ( fract ( hsv.xxx + K.xyz ) * 6.0 - K.www );
    return hsv.z * mix ( K.xxx, clamp ( p - K.xxx, 0.0, 1.0 ), hsv.y );
}

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    vec3 hsv = rgb2hsv ( gl_FragColor.rgb );
    vec3 shifted_color = vec3 ( hsv.x + u_Shift, hsv.y, hsv.z );
    gl_FragColor.rgb = hsv2rgb ( shifted_color );
    
    gl_FragColor *= v_vColour;
}

Здесь цвет преобразуется в формат HSV, его цвет сдвигается по палитре на указанную величину, и затем полученный цвет преобразуется обратно в RGB.

[править] Сдвиг тона по цвету

Сдвиг тона по цвету.

Эффект этого шейдера главным образом состоит в том, что объекты будут переливаться радугой. А еще можно сделать так, чтобы радугой переливался один из цветов, так же как с перекраской спрайта. Хотя это по сути то же самое что и просто перекраска, но со сдвигом спектра цвета вместо выбора какого-то определённого тона.

/// glsl_shift_hue_partial
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_SourceHue;
uniform float u_Shift;
uniform float u_Tolerance;
uniform float u_Falloff;

vec3 rgb2hsv ( vec3 color )
{
    vec4 K = vec4 ( 0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0 );
    vec4 p = mix ( vec4 ( color.bg, K.wz ), vec4 ( color.gb, K.xy ), step ( color.b, color.g ) );
    vec4 q = mix ( vec4 ( p.xyw, color.r ), vec4 ( color.r, p.yzx ), step ( p.x, color.r ) );

    float d = q.x - min ( q.w, q.y );
    float e = 1.0e-10;
    return vec3 ( abs ( q.z + ( q.w - q.y ) / ( 6.0 * d + e ) ), d / ( q.x + e ), q.x );
}

vec3 hsv2rgb ( vec3 hsv )
{
    vec4 K = vec4 ( 1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0 );
    vec3 p = abs ( fract ( hsv.xxx + K.xyz ) * 6.0 - K.www );
    return hsv.z * mix ( K.xxx, clamp ( p - K.xxx, 0.0, 1.0 ), hsv.y );
}

void main ( )
{
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    
    vec3 hsv = rgb2hsv ( gl_FragColor.rgb );
    vec3 shifted_color = hsv2rgb ( vec3 ( hsv.x + u_Shift, hsv.y, hsv.z ) );
    
    float overlap = abs ( hsv.x - u_SourceHue );
    float mix_factor = clamp ( ( u_Tolerance - overlap ) / u_Falloff, 0.0, 1.0 );
    
    gl_FragColor.rgb = mix ( gl_FragColor.rgb, shifted_color, mix_factor );
    gl_FragColor *= v_vColour;
}

Этот шейдер отличается от предыдущего только одной операцией (на несклько строк) - той, в которой определяется нужный цвет. В исходном шейдере он берётся из юниформа, а в последующем - преобразуется из исходного. А далее подогнан код из ранее использованного шейдера с пересечением цветов.

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

Во многих шейдерах, как видно, от цвета отрисовки спрайта по большому счёту нужна только альфа, а остальные три канала не используются так как установлены на белый цвет. Так как перекладывание юниформов может быть длительной операцией, можно сэкономить это время путём использования доступных трёх каналов цвета отрисовки вместо них. Правда, скорее всего пострадает точность обсчёта цвета, но она и так невелика изначально. Зато снизится время простоя кода пока юниформы уходят на видеокарту.

[править] Программное рендерение.

Во всех предыдущих шейдерах, исходный цвет получался путём семплинга заранее нарисованной текстуры через функцию texture2D, которая возвращает цвет пикселя в указанной координате. Но ведь цвет пикселя задавать и другими споосбами, математической формулой например. При таком рендерении сэмплинг текстуры не проиводится и оно в целом быстрее, чем рендерение спрайтов - если не переусердствовать с рассчётами и не налегать на передачу новых данных в шейдер через юниформы. Для программного рендерения всё равно потребуется отрисовать полигон, и самый простой способ это сделать - отрисовать спрайт. Для этого можно использовать спрайт в форме квадрата размером 2х2, который для начала лучше вынести в отдельную текстуру - пометить "Used for 3D", это очень упростит доступ к координатам текстуры. При этом цвет и рисунок спрайта не будут играть никакой роли, поскольку текстура спрайта не будет сэмплиться.

[править] Квадрат

Самое простое, что можно рендерить - это квадрат.

/// glsl_square_plain
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main ( )
{
    gl_FragColor = v_vColour;
}

Это выставит всем фрагментам указанный при рендерении цвет. Хоть это отрендерит не обязательно именно квадрат, а скорее просто белый (цветной) полигон произвольной формы, но поскольку для отрисовки спрайтов используются квадраты, то и результатом будет квадрат. Текстура, как видно, не используется. Координаты текстур конкретно в этом шейдере будут лишними, и компилятор их вырежет, для простоты они оставлены, но в реальном коде их лучше вырезать. Правда края квадрата будут жёсткие, если не выставить антиалиасинг. Чтобы с этим справиться без прибегания к таким мерам, нужно добавить плавную границу. Поскольку спрайт будет при отрисовке искажаться горизонтально и вертикально, потребуется указать степень искажения в шейдере.

/// glsl_square
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec2 u_Border;

float min ( vec4 a )
{
    return min ( min ( a.x, a.y ), min ( a.z, a.w ) );
}

void main ( )
{
    gl_FragColor = v_vColour;
    vec4 q = vec4 ( v_vTexcoord, 1.0 - v_vTexcoord );
    q = min ( q, u_Border.xyxy ) / u_Border.xyxy;
    gl_FragColor.a *= min ( q );
}

В юниформ u_Border записывается ширина границы, по горизонтали и вертикали, для компенсации искажения спрайта при масштабировании, но можно и реально назначить разную ширину границы. Альфу цвета нужно будет домножить на коэффицент "приграничности", чтобы получить мягкую границу. Таких коэффицентов будет четыре - по количеству сторон квадрата. Чтобы проще было считать, они объявляются в виде четырёхмерного вектора, созданного из двух двухмерных векторов. Первая половина - координата текстуры, ограниченная шириной границы. Вторая - инвертированная координата, тоже ограниченная шириной границы. Таким образом, в каждом элементе четырёхмерного вектора будет расстояние пикселя от границы спрайта. Поскольку число нужно получить в диапазоне между 0 и 1, а не 0 и u_Border.xy, то числа делятся на этот юниформ, таким образом домножаясь до нужного диапазона. Здесь видно применение неоднородных масок вычислений - во-первых компоненты указаны в нестандартной последовательности, а во-вторых из двухмерного вектора получен четырёхмерный. Кастомная функция min вычисления минимальной компоненты из четырёхмерного вектора выявляет коэффицент, на который нужно домножить альфу. Далее альфа домножается на коэффицент. Если для выбора коэффицента умножения использовать минимум, то получается характерный "заострённый" квадрат. Если же все коэффиценты перемножить между собой, получится эдакое "скругление":

float mul ( vec4 a )
{
    return a.x * a.y * a.z * a.w;
}

Этим же шейдером можно рисовать линии, если прайт отрисовывать очень длинным и тонким.

[править] Круг

Отрисовать круг несколько сложнее. Для отрисовки квадрата по сути только требуется заполнить весь квадратный спрайт белым цветом и убрать немного пикселей с границ. Для отрисовки пикселя круга нужно будет определить, принадлежит ли точка кругу или нет. Функция |A - C| < R описывает зависимость принадлежности произвольной точки к множеству точек круга. То есть нужно вычислить дистанцию от пикселя до центра спрайта. Чтобы упростить вычисления, нужно координаты текстур изменить так, чтобы в центре спрайта было 0х0, слева сверху отрицательные координаты, а справа снизу - положительные. Можно это сделать в фрагмент-шейдере, но это будет дополнительная операция на каждый фрагмент, а их может быть десятки и сотни тысяч даже для не супер-больших кругов (для них счёт идёт на миллионы). А можно сделать это и в вертекс-шейдере - результат будет такой же, но вот фрагментов может получиться очень много, а вертексов всего по четыре на спрайт.

/// glsl_circle_plain
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

const vec2 c_Centre = vec2 ( 0.5, 0.5 );

void main ( )
{
    vec4 vertex_position = vec4 ( in_Position.x, in_Position.y, in_Position.z, 1.0 );
    gl_Position = gm_Matrices[ MATRIX_WORLD_VIEW_PROJECTION ] * vertex_position;
    
    v_vColour = in_Colour;
    v_vTexcoord = in_TextureCoord - c_Centre;
}
/// glsl_circle
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_Border;

const vec2 c_VecZero = vec2 ( 0.0, 0.0 );

void main ( )
{
    gl_FragColor = v_vColour;
    float d = 0.5 - distance ( c_VecZero, v_vTexcoord );
    float q = clamp ( d, 0.0, u_Border ) / u_Border;
    gl_FragColor.a *= q;
}

По аналогии с предыдущим шейдером вычисляется коэффицент умножения альфы. Для подсчёта дистанции от центра используется функция distance, которая возвращает дистанцию между двумя векторами, которые в данном случае служат координатами. Поскольку дистанция от центра до края спрайта будет 0.5, то именно из этого числа нужно вычитать дистанцию. Так как в углах спрайта дистанция будет больше, чем 0.5, то результат будет ниже нуля, таким образом нужно использовать функцию clamp. Этим шейдером можно и эллипсы рисовать, отрисовывая спрайт сплюснутым - хоть у эллипсов и другая функция ( |A - (C + E)| + |B - (C - E)| < R ). Однако при этом ширина границы тоже будет сплюснута. При отрисовке эллипса через функцию-эллипсоид граница будет точно так же сплюснута. Так что на этом поприще для совершенствования алгоритма отрисовки бескрайние просторы.

[править] Пустотелый квадрат

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

/// glsl_square_hollow
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec2 u_Border;
uniform vec2 u_Thickness;

float min ( vec4 a )
{
    return min ( min ( a.x, a.y ), min ( a.z, a.w ) );
}

void main ( )
{
    gl_FragColor = v_vColour;
    
    vec4 q = vec4 ( v_vTexcoord, 1.0 - v_vTexcoord );
    q = min ( q, u_Border.xyxy ) / u_Border.xyxy;
    float k = min ( q );
    q = vec4 ( v_vTexcoord - u_Thickness, ( 1.0 - v_vTexcoord ) - u_Thickness );
    q = clamp ( q, vec4 ( 0.0 ), u_Border.xyxy ) / u_Border.xyxy;
    k -= min ( q );
    
    gl_FragColor.a *= k;
}

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

[править] Пустотелый и градиентный круг

Полый круг отрисовывается аналогичным образом:

/// glsl_circle_hollow
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_Border;
uniform float u_Thickness;

void main ( )
{
    gl_FragColor = v_vColour;
    float d = 0.5 - distance ( c_VecZero, v_vTexcoord );
    float q = clamp ( d, 0.0, u_Border ) / u_Border;
    q -= clamp ( d - u_Thickness, 0.0, u_Border ) / u_Border;
    gl_FragColor.a *= q;
}

Для отрисовки градиентного круга нужно вычислить соотношение смешивания первого и второго цвета:

/// glsl_circle_gradient
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_Border;
uniform vec4 u_Color2;
uniform vec2 u_Anchors;

const vec2 c_VecZero = vec2 ( 0.0, 0.0 );

void main ( )
{
    float d = distance ( c_VecZero, v_vTexcoord );
    float q = clamp ( 0.5 - d, 0.0, u_Border ) / u_Border;
    float c = clamp ( d - u_Anchors.x, 0.0, u_Anchors.y ) / u_Anchors.y;
    
    gl_FragColor = mix ( u_Color2, v_vColour, c );
    gl_FragColor.a *= q;
}

Коэффицент смешения вычисляется аналогично коэффиценту ширины границы, но здесь вместо альфы он управляет соотношением цветов. В юниформе u_Anchors будут записаны радиус внутреннего цвета (х) ширина градиента (y). Для экономии на передаче данных в шейдер, первым цветом будет использоваться цвет окрашивания, и только второй цвет будет передан через юниформ.

[править] Линия

Чтобы отрисовать реальную линию (точнее сегмент линии - отрезок), нужно посчитать расстояние от пикселя до этой линии. Формулы опять же берутся из интернета или учебников по математике.

/// glsl_line
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec2 u_Coords;
uniform float u_Border;
uniform float u_Width;

float dist_to_line ( vec2 a, vec2 b, vec2 p )
{
    float t = dot ( p - a, b - a ) / pow ( distance ( a, b ), 2.0 );
    float d = distance ( a + t * ( b - a ), p );
    d = mix ( d, distance ( a, p ), step ( t, 0.0 ) );
    d = mix ( d, distance ( b, p ), step ( 1.0, t ) );
    return d;
}

void main ( )
{
    gl_FragColor = v_vColour;
    float d = dist_to_line ( u_Coords, 1.0 - u_Coords, v_vTexcoord );
    float q = clamp ( u_Width - d, 0.0, u_Border ) / u_Border;
    gl_FragColor.a *= q;
}

Линия рисуется из точки A в точку B, здесь считается что это две точки с противоположных концов спрайта, хотя можно вводить произвольные параметры. Функция вычисления дистанции до линии dist_to_line будет возвращать дистанцию в любой точке до указанного отрезка, поэтому работает аналогично функции дистанции между двумя точками. В этой функции для выбора точки отсчёта используется функция step - она возвращает 0 если A меньше B, и 1 в противном случае (чтобы проверить A больше B, нужно поменять местами аргументы). Это в сочетании с функцией mix используется вместо операции if-then-else, поскольку они имеют плохую производительность в шейдерах и их нужно избегать. Лучше провести несколько лишних вычислений и отбросить ненужные, чем пытаться не вычислять их изначально через проверки условий.

[править] Примеры фракталов

Поскольку программное вычисление цвета по сути есть пропускание координаты текстуры через некую математическую функцию, то таким путём можно рисовать даже фракталы:

Бассейны Ньютона.
Множество Мандельброта.
///glsl_newton_fractal
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

const float sigma = 0.2;

const vec2 r1 = vec2 ( 1.0, 0.0 );
const vec2 r2 = vec2 ( -0.5, 0.86602540378443 );
const vec2 r3 = vec2 ( -0.5, -0.86602540378443 );

const vec3 c_1 = vec3 ( 1.3, 0.5, 0.0 );
const vec3 c_2 = vec3 ( 0.6, 1.4, 0.2 );
const vec3 c_3 = vec3 ( 0.7, 0.0, 1.6 );

vec2 cdiv ( vec2 a, vec2 b )
{
    float d = b.x * b.x + b.y * b.y;
    return vec2 ( a.x * b.x + a.y * b.y, a.y * b.x - a.x * b.y ) / d;
}

vec2 cmul ( vec2 a, vec2 b )
{ return vec2 ( ( a.x * b.x - a.y * b.y ), ( a.y * b.x + a.x * b.y ) ); }

vec2 fmain ( vec2 z )
{ return cmul ( cmul ( z, z ), z ) - vec2 ( 1.0, 0.0 ); }

vec2 fderived ( vec2 z )
{ return cmul ( cmul ( z, z ), vec2 ( 3.0, 0.0 ) ); }

void main ( )
{
    vec2 z = v_vTexcoord * 2.0 - 1.0;
    float i = 0.0;
    float ii = 0.0;
    vec3 c = c_1;
    
    while ( i < 1.0 )
    {
        if ( distance ( z, r1 ) < sigma ) { ii = i; i = 2.0; }
        if ( distance ( z, r2 ) < sigma ) { c = c_2; ii = i; i = 2.0; }
        if ( distance ( z, r3 ) < sigma ) { c = c_3; ii = i; i = 2.0; }
        z = z - cdiv ( fmain ( z ), fderived ( z ) );
        i += 0.01;
    }
    
    c = max ( vec3 ( 0.0 ), c - max ( 1.0 - ii * 5.0, 0.0 ) );
    c = mix ( c, vec3 ( 1.0 ), max ( ii - 0.2, 0.0 ) / 0.2 );
    gl_FragColor = vec4 ( c, 1.0 );
}
/// glsl_mandelbrot_fractal
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

vec3 c_1 = vec3 ( 0.0, 0.0, 0.0 );
vec3 c_2 = vec3 ( 0.2, 0.0, 1.0 );
vec3 c_3 = vec3 ( 1.0, 0.0, 0.3 );
vec3 c_4 = vec3 ( 1.0, 1.0, 0.3 ); 
vec3 c_5 = vec3 ( 1.0, 1.0, 1.0 );

void main()
{
    vec2 p = vec2 ( v_vTexcoord.x * 3.5 - 2.5, v_vTexcoord.y * 2.0 - 1.0 );
    vec2 n = vec2 ( 0.0, 0.0 );
    float tx = 0.0;
    float i = 0.0;
    float r = 1.4;
    
    while ( i < 1.0 )
    {
        tx = n.x * n.x - n.y * n.y + p.x;
        n.y = 2.0 * n.x * n.y + p.y;
        n.x = tx;
        i += 0.01;
        if ( length ( n ) > 2.0 )
        {
            r = i;
            i = 2.0;
        }
    }
    
    vec3 с = mix ( c_1, c_2, clamp ( r, 0.0, 0.25 ) / 0.25 );
    c = mix ( c, c_3, clamp ( r - 0.25, 0.0, 0.25 ) / 0.25 );
    c = mix ( c, c_4, clamp ( r - 0.75, 0.0, 0.25 ) / 0.25 );
    c = mix ( c, c_5, clamp ( r - 0.9, 0.0, 0.4 ) / 0.5 );
    
    gl_FragColor = vec4 ( c, 1.0 );
}

Если увеличивать масштаб спрайта, то функция-фрактал будет бесконечно (или пока точность не кончится) углубляться внутрь без потери чёткости картинки, открывая всё новые глубины фрактала. Максимальная глубина фрактала ограничена количеством итераций алгоритма - здесь их всего 100, что довольно мало, но учитывая точность переменных в шейдере, этого предостаточно. Чтобы рендерить очень глубокие фракталы, всё таки нужно использовать 128-битные числа с плавающей запятой, которые недоступны на видеокартах, и рендерить такие фракталы на центральном процессоре.

[править] Полноэкранные эффекты.

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

[править] Блюр

Мыло 72%, ГОСТ 30266-95
.

Первое, что пришло в голову при мысле о полноэкранных эффектах это блюр. Всем известный фильтр в консольных играх для смазывания лесенок и мыльных текстур. В 2д-играх на гамаке врядли его потребуется использовать по прямому назначению, но в ряде ситуаций он может иметь художественную ценность, да и он достаточно прост, поэтому он тут и приведён. Вообще, чтобы выполнить заблюривание шейдер не нужен - достаточно отрисовать главный сурфейс несколько раз со смещением с неполной альфой и режимом добавления. Но может потребоваться отрисовывать его очень много раз, чтобы получить качественный блюр с большим радиусом, что просадит производительность, так что делать это шейдером может быть даже выгодней.

/// glsl_gaussian_blur
varying vec2 v_vTexcoord;

uniform vec2 u_Scale;

vec4 sample2points ( vec2 basecoord, vec2 shift )
{
    return texture2D( gm_BaseTexture, basecoord + shift ) + 
           texture2D( gm_BaseTexture, basecoord - shift ); 
}

void main()
{
    gl_FragColor = ( sample2points ( v_vTexcoord, u_Scale * 1.0 ) +
                     sample2points ( v_vTexcoord, u_Scale * 0.8 ) +
                     sample2points ( v_vTexcoord, u_Scale * 0.6 ) +
                     sample2points ( v_vTexcoord, u_Scale * 0.4 ) +
                     sample2points ( v_vTexcoord, u_Scale * 0.2 ) +
                     texture2D ( gm_BaseTexture, v_vTexcoord ) ) / 11.0;
}

Этот шейдер сэмплит 11 точек ( 5*2 + 1 ), но можно сделать побольше или поменьше, на своё усмотрение - от этого будет зависеть качество блюра, но и производительность тоже. Чтобы поменьше копипастить кода, вынесена отдельно функция сэмплинга двух точек на противоположных сторонах от центра сэмплинга. Таким образом, каждый пиксель на выходе будет составлен из цвета 11 соседних пикселей. Циклов в шейдерах лучше избегать, поэтому вместо их использования здесь накопипащено одних и тех же функций, где со смещением сэмплится текстура. Впрочем, если цикл имеет фиксированную длину, то он автоматически будет развёрнут в такую же копипасту компилятором на этапе оптимизации. Этим шейдером главный сурфейс рендерится в два прохода - сначала горизонтальным блюром, а потом вертикальным (или наоборот - это неважно).

Для применения эффекта, нужно вручную отрендерить через шейдер главный сурфейс. Для этого главный сурфейс рендерится прямо на экран через указанный шейдер. При этом автоматическое рендерение главного сурфейса становится ненужным и его нужно выключить. Можно и без этого обойтись, рисуя эффект в собственно главный сурфейс, но поскольку нельзя рендерить сурфейс в самого себя, то придётся задействовать дополнительные промежуточные сурфейсы. Для управления автоматическим рендерением сурфейса служит функция application_surface_draw_enable. Игра по прежнему будет рисовать всё в этот сурфейс, но теперь он не будет автоматически выводиться на экран, нужно будет это делать вручную через функцию draw_surface. Чтобы создать новый сурфейс, нужно вызвать функцию surface_create - по размеру он не должен совпадать с главным сурфейсом, но качество эффекта будет лучше, если совпадение будет 1:1. Как известно из справки, в начале Draw эвента таргетом устанавливается главный сурфейс, после этого эвента и перед Post-Draw эвентом таргетом устанавливается экран. После Post-Draw и перед Draw GUI (по-умолчанию) на экран отрисовывается главный сурфейс. Далее происходит Draw GUI эвент. Это значит, что отрисовывать главный сурфейс через шейдер нужно в Post-Draw.

/// create
surf_blur_pass = surface_create ( 800, 600 );
application_surface_draw_enable ( false );
uscale = shader_get_uniform ( glsl_gaussian_blur, "u_Scale" );

/// post_draw shader_set ( glsl_gaussian_blur ); shader_set_uniform_f ( uscale, 0.003, 0.0 ); surface_set_target ( surf_blur_pass ); draw_surface ( application_surface, 0, 0 ); surface_reset_target ( ); shader_set_uniform_f ( uscale, 0.0, 0.004 ); draw_surface ( surf_blur_pass, 0, 0 ); shader_reset ( );

В коде применения эффекта сначала включается шейдер и в него передаётся сила блюра по горизонтали, но не по вертикали, так как блюрриться будет по диагонали. Чтобы не потерять результаты первого прохода, таргетом устанавливается промежуточный сурфейс, после чего главный сурфейс отрисовывается с шейдером. Следующую прорисовку нужно сразу выводить на экран, поэтому таргет отменяется. Вторым проходом в шейдер передаётся вертикальный блюр. Здесь он больше в 4/3 раза, чем горизонтальный, потому что соотношение сторон сурфейса - 4/3 (800/600). Эти две величины компенсируют друг друга, и на экране оказывается "ровный" блюр. Можно увеличить производительность эффекта за счёт понижения размера промежуточного сурфейса и соответственно снижением количества сэмплов в шейдере без особой потери в качестве - это каждый для себя может поэкспериментировать.

[править] Блум

Блум.

Похожий эффект, но несколько более сложный - блум. Тоже часто встречается в играх, но в отличии от блюра, он действительно улучшает качество картинки - повышает реалистичность ярких объектов за счёт отрисовывания характерного сияния вокруг них. Также, в нём используется предыдущий шейдер для создания собственно сияния. Суть заключается в том, что из основной картинки извлекаются наиболее ярко освещённые участки в отдельную текстуру, там домножаются и размазываются по большей площади, и отрисовываются наложением поверх исходной текстуры.

/// glsl_threshold
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float u_Threshold;

void main()
{
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    gl_FragColor.rgb = max ( vec3 ( 0.0 ), gl_FragColor.rgb - u_Threshold );
    gl_FragColor.rgb /= 1.0 - u_Threshold;
}

Блум не имеет собственного шейдера - это комбинация нескольких эффектов. Для выделения наиболее ярких участков используется шейдер порогового отсечения. Из цветовых каналов вычитается минимальный порог, таким образом остаются только достаточно яркие участки. Далее, чтобы привести цвета к полному диапазону, оставшиеся цвета делятся на величину, обратную к порогу отсечения. Таким образом, весь цветовой диапазон обрезается до самой верхней его части, которая растягивается на весь диапазон.

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

/// create 
surf_blur_pass = surface_create ( 800, 600 );
surf_bloom = surface_create ( 800, 600 );
application_surface_draw_enable ( false );
uscale = shader_get_uniform ( glsl_gaussian_blur, "u_Scale" );
uthresh = shader_get_uniform ( glsl_threshold, "u_Threshold" );
/// post_draw
shader_set ( glsl_threshold );
shader_set_uniform_f ( uthresh, 0.8 );
surface_set_target ( surf_bloom );
draw_surface ( application_surface, 0, 0 );
surface_reset_target ( );

shader_set ( glsl_gaussian_blur );
shader_set_uniform_f ( uscale, 0.012, 0.0 );
surface_set_target ( surf_blur_pass );
draw_surface ( surf_bloom, 0, 0 );
surface_reset_target ( );

surface_set_target ( surf_bloom );
shader_set_uniform_f ( uscale, 0.0, 0.016 );
draw_surface ( surf_blur_pass, 0, 0 );
surface_reset_target ( );
shader_reset ( );

draw_surface ( application_surface, 0, 0 );
draw_set_blend_mode ( bm_add );
draw_surface ( surf_bloom, 0, 0 );
draw_set_blend_mode ( bm_normal );

В Post Draw эвенте сначала отрисовывается сурфейс блума - главная текстура пропускается через шейдер порога. Сначала устанавливается шейдер, в который передаётся величина порога отсечения, и рендер-таргетом устанавливается сурфейс блума. В него рендерится главный сурфейс. Далее, сурфейс блума пропускается через блюр аналогично предыдущему эффекту. В конце, рендерится главный сурфейс, а затем с аддитивным режимом смешивания рендерится полученный сурфейс блума.

[править] Дизеринг

Дизеринг.
Паттерн Байера (8-кратное увеличение).

Не очень полезный но красивый эффект - дизеринг, "крапинка" в 16-битной графике. Теория достаточно заумная, но на практике достаточно просто пропустить цвет через специальный паттерн Байера.

/// glsl_dither
const vec3 c_LuminanceWeight = vec3 ( 0.2126, 0.7152, 0.072 );
const vec3 c1 = vec3 ( 0.0, 0.0, 0.0 );
const vec3 c2 = vec3 ( 0.45, 0.4, 0.5 );
const vec3 c3 = vec3 ( 0.95, 0.9, 1.0 );

uniform vec2 u_texturesize;
uniform sampler2D u_bayer;
 
vec3 dither ( vec3 v1, vec3 v2, float bias, float mask  )
{
    return mix ( v1, v2, step ( mask, bias ) );
}
 
void main()
{
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    
    vec3 color_weighted = gl_FragColor.rgb * c_LuminanceWeight;
    float luminance = color_weighted.r + color_weighted.g + color_weighted.b;
    
    float mask = texture2D ( u_bayer, v_vTexcoord * u_texturesize ).r;
    
    vec3 c = dither ( c1, c2, clamp ( luminance, 0.0, 0.5 ) / 0.5, mask );
    c = dither ( c, c3, clamp ( luminance - 0.5, 0.0, 0.5 ) / 0.5, mask );
    
    gl_FragColor.rgb = c;
}

Шейдер принимает два юниформа - один из них это коэффицент разницы размера текстуры (паттерна и текущего атласа отрисовки), второй - сэмплер собственно паттерна. Если в игре все атласы одинакового размера, то достаточно выполнить установку всего один раз. Если нет - при каждой смене атласа необходимо вычислить и передать новые коэффиценты, так что лучше как-нибудь всё-таки этого избежать. Тут используется тот же приём вычисления яркости, что и для шейдера чёрно-белой окраски. Далее, выбирается сэмпл из паттерна Байера с домножением на коэффицент разницы размера текстур. Функция dither играет роль "сокращённой нотации" функций mix и step с соответствующими параметрами. В ней выбирается цвет между первым и вторым переданным в неё, исходя из "сдвига" цвета, аналогично функции mix, но не плавно смешивая цвета, а выбирая один или второй с некоторой определённой вероятностью - по паттерну Байера.

// create
shader_set ( glsl_dither );

maintexture = sprite_get_texture ( sprite, 0 );
bayertexture = sprite_get_texture ( spr_bayer, 0 );
bayersampler = shader_get_sampler_index ( glsl_dither, "u_bayer" );

h = 0.125 / texture_get_texel_height ( maintexture );
w = 0.125 / texture_get_texel_width ( maintexture );

shader_set_uniform_f ( shader_get_uniform ( glsl_dither, "u_texturesize" ), w, h );
texture_set_stage ( bayersampler, bayertexture );
texture_set_repeat_ext ( bayersampler, true );
texture_set_interpolation_ext ( bayersampler, false );

shader_reset ( );

Необходима настройка шейдера - в него нужно передать коэффицент размера текстур и отправить туда собственно текстуру паттерна. Здесь вместо sprite нужно передать тот спрайт, который находится на текущем атласе, который игра собирается отрисовывать. Коэффиценты 0.125 соответствуют размеру паттерна в 8 пикселей - его нужно вынести в отдельный спрайт и пометить "Used for 3D", чтобы гамак выделил ему отдельную текстуру-атлас. Также устанавливается повторение текстуры паттерна и убирается сглаживание. Если все атласы одинакового размера, то на этом вся настройка шейдера закончена и теперь его можно просто использовать. Но если в игре есть атласы разного размера, то настройку коэффицента размера текстуры нужно проводить при каждой смене текущего атласа.

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

[править] Сдвиг сэмплинга

Сдвиг сэмплинга.
Спрайт для дисплейса ударной волны.

Интерес представляет дисплейс-мап - карта смещения. Суть заключается в том, что главный сурфейс игры перерисовывается на экран, и при сэмплинге пикселей текстурные координаты берутся не напрямую, а со смещением, в зависимости от данных в другой текстуре - дисплейсмент-мапе. При отрисовке графики, в эту текстуру (сурфейс) отрисовываются участки, которые надо сместить на готовом изображении. С помощью этого эффекта можно делать ударные волны, капли дождя на камере, преломление света на воде и в стеклянных предметах, эффект горячего воздуха, ну и конечно эффект "невидимости" а-ля старкрафт.

Сам шейдер эффекта довольно простой:

/// glsl_displace
varying vec2 v_vTexcoord;

uniform sampler2D u_DisplaceSampler;
uniform vec2 u_Scale;

void main()
{
    vec3 displace = texture2D ( u_DisplaceSampler, v_vTexcoord ).rba;
    displace.xy -= 0.5;
    displace.xy *= displace.z * u_Scale;
    gl_FragColor = texture2D( gm_BaseTexture, v_vTexcoord + displace.xy );
}

Пример спрайта для дисплейс-шейдера - ударная волна. Красный цвет указывает на смещение вправо, синий - вниз. Серый (или любой другой, в котором красный и синий = 0.5) указывает на отсутствие смещения. Более чёрные цвета указывают смещение в противоположную сторону - влево и вверх соответственно.

Как видно, даже не используется цвет окрашивания (этот varying из вертекс-шейдера тоже нужно удалить, заодно можно удалить attribute in_Colour). Дополнительной текстурой объявляется юниформ u_DisplaceSampler, для чего указывается тип данных sampler2D. В дополнительную переменную сэмплится эта текстура, точнее красный и синий каналы, домножаемые на альфа-канал - для удобства. Впрочем, если в сурфейсе дисплейса используется предумноженная альфа, то это дополнительное умножение будет лишним. Полученная величина домножается на коэффицент силы эффекта, который также скомпенсирует неквадратность текстуры. В связи с этим текстуру можно было бы изначально создать двухканальной чтобы сэкономить памяти, но гамак такую фичу не поддерживает. А далее основная текстура (которая будет главным сурфейсом игры) сэмплится со смещением, сохранённым в предыдущей переменной.

/// create
surf_displace = surface_create ( 800, 600 );
application_surface_draw_enable ( false );
usampler = shader_get_sampler_index ( glsl_displace, "u_DisplaceSampler" );
uscale = shader_get_uniform ( glsl_displace, "u_Scale" );
/// draw
surface_set_target ( surf_displace );
draw_clear_alpha ( make_color_rgb ( 128, 128, 128 ), 0.0 );
draw_sprite_ext ( spr_displace, 0, x, y, 1, 1, 0, c_white, 1 );
surface_reset_target ( );
/// post_draw
texture_set_stage ( usampler, surface_get_texture ( surf_displace ) );
shader_set ( glsl_displace );
shader_set_uniform_f ( uscale, 0.04, 0.03 );
draw_surface ( application_surface, 0, 0 );
shader_reset ( );

В Create эвенте создаётся текстура для дисплейса и отключается автоматическая отрисовка главного сурфейса. Этот сурфейс потом отрисовывается в Draw эвенте. Поскольку ID юниформов не меняется, он там же вычисляется и записывается в переменную. Сэмплер является юниформом, но для него есть отдельная функция - shader_get_sampler_index. Чтобы дисплейс заработал, надо в текстуру дисплейса что-нибудь нарисовать. В Draw эвенте выставляется нужный сурфейс и очищается до серого цвета с нулевой альфой. Далее туда отрисовываются заранее подготовленные красно-синие спрайты смещения. Когда работа с этой текстурой закончена, рисование в неё отменяется. Для передачи второй текстуры в шейдер используется функция texture_set_stage. Здесь масштабом выставляется вектор 0.04х0.03 поскольку разрешение сурфейса 800х600, и его соотношение сторон равняется 4/3 - в результирующей картинке смещение по горизонтали и вертикали будет равномерно. После окончания работы, этот шейдер тоже отменяется.

[править] Преломление

Преломление света через 3д-объект. Сам объект бесцветен, сверху дополнительно отрисована модель.

Одна занятная возможность эффекта - трёхмерные объекты, преломляющие свет. Нужно отрисовать 3д-объект со специальным шейдером, который вернёт угол преломления вместо цвета полигона.

/// glsl_refract
attribute vec3 in_Position;
attribute vec3 in_Normal;

varying vec4 v_vPosition;
varying vec4 v_vNormal;

void main()
{
    vec4 objpos = vec4( in_Position, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * objpos;
    v_vPosition = gl_Position;
    
    v_vNormal = gm_Matrices[MATRIX_WORLD_VIEW] * ( objpos + vec4 ( in_Normal, 1.0 ) );
    v_vNormal -= gm_Matrices[MATRIX_WORLD_VIEW] * objpos;
}
/// glsl_refract
varying vec4 v_vPosition;
varying vec4 v_vNormal;

uniform float u_Eta;

void main()
{
    vec4 ref = refract ( normalize ( v_vPosition ), normalize ( v_vNormal ), u_Eta );
    
    gl_FragColor.rb = ref.xy * vec2 ( -0.5, 0.5 ) + 0.5;
    gl_FragColor.ga = vec2 ( 0.0, 1.0 );
}

[править] Матрицы

Итак, вот и настало время обсудить матрицы. "Матрица" в математике это термин, который обозначает просто квадратные сетки чисел. В GL ES бывают матрицы 2х2, 3х3 и 4х4, на которые можно умножать векторы с такой же метрикой (в 3д используются 4х4 и четырёхмерные векторы соответственно). Принцип матрицы трансформации заключается в том, чтобы преобразовать одни координаты вертекса в другие, что в итоге по всем вертексам даёт вращение, растягивание, перемещение модели и прочие эффекты. Преобразование в классической схеме рендерения 3д заключается в преобразовании координат модели в координаты экрана, чтобы прямолинейным образом растеризировать полигоны. При этом камера как бы всегда находится в нулевой точке, направлена вдоль оси Z, её верхняя сторона это ось Y а правая - ось X, и весь мир вращается вокруг неё, имитируя таким образом движение камеры. Сам OpenGL не предоставляет никаких матриц и препологается, что в gl_Position передаются уже готовые координаты вертекса на экране, поэтому матрицы объявляет программа (гамак) а трансформации выполняет шейдер. Матрица MATRIX_WORLD содержит трансформацию, которая преобразует координаты модели к координатам мира (комнаты), то есть эта матрица содержит преобразования функций d3d_transform. Матрица MATRIX_VIEW содержит трансформацию, которая далее преобразует координаты мира в координаты относительно неподвижной камеры, то есть d3d_set_projection. Матрица MATRIX_PROJECTION содержит финальный элемент - трансформация координат камеры в координты экрана с учётом соотношения сторон, угла обзора камеры и т.п. Это тоже управляется через d3d_set_projection - матрица вида и матрица камеры логически взаимосвязаны и поэтому управляются вместе. В шейдер-аттрибут in_Position передаются координаты в пространстве модели, те самые, которые выставлены при создании модели или объявлении полигона. Таким образом, чтобы преобразовать вертекс из координат модели в координаты экрана нужно умножить вертекс последовательно на матрицу мира, потом вида, и затем проекции. Чтобы сэкономить на вычислениях, эти матрицы заранее умножаются друг на друга (при этом трансформации "складываются" вместе) и объявляется матрица MATRIX_WORLD_VIEW_PROJECTION - при умножении вертекса на эту матрицу вертекс преобразуется к экранным координатам в одну операцию. Для удобства объявлена и MATRIX_WORLD_VIEW без MATRIX_PROJECTION, однако матрицы MATRIX_VIEW_PROJECTION без MATRIX_WORLD не предусмотрено. Нужно отметить, что в отличии от скаляров и векторов, матрицы "умножаются" по особым правилам линейной алгебры (поэтому эта операция умножением в буквальном смысле как бы не является), так что важен и порядок операндов: при умножении матрицы на вектор результатом будет трансформированный вектор, при умножении вектора на матрицу результатом будет вектор, элементами которого будут скалярное умножение этого вектора на соответствующие строки матрицы по горизонтали как если бы это были векторами, а при умножении матрицы на матрицу трансформация левой матрицы (слева от знака умножения) добавляется к правой, а не наоборот, как можно было ожидать.

Тут отредактирован вертекс-шейдер - добавлено умножение нормали на матрицу вида-мира. Так как для преломления луча о поверхность, нужно знать нормаль этой поверхности - разкомментирован attribute in_Normal (при отрисовке спрайтов гамак автоматически передаёт все нужные данные), а чтобы нормаль передать в вертекс-шейдер, добавлен varying v_vNormal. Также во фрагмент-шейдер передаются преобразованные к пространству вида-мира координаты. Нормали объявлены в пространстве объекта, так что после трансформации объекта на матрицу вида-мира-проекции, они уже не будут соответствовать объекту. Для этого нормали тоже нужно умножить на матрицу трансформации. Однако главная матрица также содержит трансформацию проекции, которая переместит нормали туда же, куда и вертексы, а также добавит искажения соотношения сторон экрана. Есть и другая матрица, которая не включает матрицу проекции - MATRIX_WORLD_VIEW. Если умножить нормали на эту матрицу, то тогда нормали будут трансформированы так же, как объект, но не будут перемещены согласно матрице проекции. Нормали вместе с вертексами трансформируются по матрице вида-мира, а затем из полученной точки вычитается преобразованный вертекс. Обратите внимание, что это не то же самое, что просто преобразовать нормаль по матрице вида-мира. Во фрагмент-шейдере нормаль скармливается функции refract, которая преломляет исходный луч о нормаль с некоторой степенью преломления. Поскольку в экранном пространстве камера всегда имеет нулевую позицию, то вектор позиции фрагмента в экранном пространстве по сути равен лучу из камеры к этому фрагменту. Этот вектор (нормализованный) принимается за исходный луч. Вектор нормали тоже нормализуется, так как из-за линейной интерполяции между вертексами его длина может "потеряться" и он будет меньше, чем есть на самом деле. Функция refract требует, чтобы оба вектора были нормализованы. Цвет составляется из направления преломлённого луча по осям X и Y. Так как это число может быть между -1 и 1, то оно дополнительно домножается, чтобы соответствовать диапазону 0-1. Поскольку в 3д-режиме координата Y указывает вверх а не вниз, красный канал (смещение по вертикали) инвертируется.

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

[править] Размытие движением

Размытие движением.

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

/// glsl_motionblur
varying vec2 v_vTexcoord;

uniform sampler2D u_MotionSampler;
uniform vec2 u_Scale;

vec4 sample2points ( vec2 basecoord, vec2 shift )
{
    return texture2D( gm_BaseTexture, basecoord + shift ) +
           texture2D( gm_BaseTexture, basecoord - shift ) ; 
}

void main()
{
    vec3 motionmap = texture2D ( u_MotionSampler, v_vTexcoord ).rba;
    motionmap.xy -= 0.5;
    vec2 motion = motionmap.xy * u_Scale * motionmap.z;
    gl_FragColor = ( sample2points ( v_vTexcoord, m * 1.0 ) +
                     sample2points ( v_vTexcoord, m * 0.8 ) +
                     sample2points ( v_vTexcoord, m * 0.6 ) +
                     sample2points ( v_vTexcoord, m * 0.4 ) +
                     sample2points ( v_vTexcoord, m * 0.2 ) +
                     texture2D ( gm_BaseTexture, v_vTexcoord ) ) / 11.0;

}

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

Один довольно важный полноэкранный эффект - динамические тени. Есть множество способов сделать их, но шейдерами - наиболее эффективный.

[править] Динамические тени

Шейдером можно без затруднений рендерить огромное количество теней.

Суть динамических теней в 2д чем-то похожа на отложенное освещение в 3д - сцена рендерится без освещения, и только в дополнительном проходе к ней добавляется свет. То есть вся игровая графика рендерится, как если бы освещения не было. Затем сурфейс под маску теней рендерятся тени, и через эту маску в сурфейс освещения рендерятся спрайты источников света. На конечном этапе, главный сурфейс рендерится на экран через полученный лайтмап.

Шейдер наложения лайтмапа на главный сурфейс достаточно простой:

/// glsl_shadows_render
varying vec2 v_vTexcoord;

uniform sampler2D u_LightMapSampler;

void main()
{
    vec4 light = texture2D ( u_LightMapSampler, v_vTexcoord );
    gl_FragColor = light * texture2D( gm_BaseTexture, v_vTexcoord );
}

Здесь не используется цвет окрашивания, и этот юниформ удалён. Зато добавлен сэмплер лайтмапа u_LightMapSampler. Каждый пиксель главного сурфейса домножается на цвет, находящийся в лайтмапе на соответствущем пикселе, подразумевая, что белые и цветные участки лайтмапа это свет, а чёрные - тень.

Шейдер отрисовки спрайтов источников света несколько сложнее, но в целом суть та же:

/// glsl_shadows_lightmap
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenCoord;

void main()
{
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    
    v_vColour = in_Colour;
    v_vTexcoord = in_TextureCoord;
    v_vScreenCoord = gl_Position.xy / vec2 ( 2.0, -2.0 ) + 0.5;
}
/// glsl_shadows_lightmap
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenCoord;

uniform sampler2D u_ShadowMaskSampler;
uniform vec4 u_LightMask;

void main()
{
    vec4 shadow = texture2D ( u_ShadowMaskSampler, v_vScreenCoord );
    float lit = length ( shadow * u_LightMask );
    
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord ) * lit;
}

В вертекс-шейдере добавлен юниформ u_ScreenCoord, который будет передавать во фрагмент-шейдер координаты пикселей на главной текстуре. Нельзя использовать координаты текстуры спрайта при сэмплинге сурфейса теней, так как это внутренние координаты текстуры спрайта, и они никак не связаны с координатами текстур сурфейса теней. Зато им соответствуют координаты пикселя на экране. В юниформ u_LightMask записывается цветовая маска данного источника света. Если в сурфейсе теней имеется соответствующий цветовой канал, то тогда множитель цвета спрайта будет равен единице, а если нет (тень) - нулю. Для этого цвет пикселя текстуры теней умножается на цветовую маску - остаётся только тот цвет, который был в обоих векторах. Функцией length в одну операцию выбирается длиннейший из элементов (так как они все либо равны нулю либо единице), вместо перебора каждого из них.

Модель тени.

И вот самая важная часть это заполнение шейдера теней собственно тенями. Тут уж у кого на что фантазии хватит. Один из способов отрендерить тени - под каждый объект создать "объёмную тень" - вертикальную призму, которая повторяет контуры спрайта, закрытую сверху "крышкой". В фрагмент-шейдере нужно только выставить цветом пикселя текущую цветовую маску соответствующего источника света. В вертекс-шейдере нужно сдвинуть координаты вертексов в противоположную сторону от источника света в зависимости от того, под каким углом на этот вертекс падает "свет" от этого источника - если угол таков, что тень сразу же упрётся в землю, то сдвиг не нужен, но чем меньше угол, тем дальше простирается тень и тем сильнее нужно смещение.

/// glsl_shadows_mask
attribute vec3 in_Position;
attribute vec4 in_Colour;

varying vec4 v_vColour;

uniform vec3 u_Light1;
uniform vec3 u_Light2;
uniform vec3 u_Light3;
uniform vec3 u_Light4;

void main()
{
    vec4 lpos =  vec4 ( u_Light1 * in_Colour.r + 
                        u_Light2 * in_Colour.g + 
                        u_Light3 * in_Colour.b +
                        u_Light4 * in_Colour.a, 1.0 );
                   
    vec4 vpos = gm_Matrices[ MATRIX_WORLD ] * vec4 ( in_Position, 1.0 );
    
    float q = 1.0 / max ( ( lpos.z - vpos.z ) / lpos.z, 0.001 ) - 1.0;
    vpos.xy += ( vpos.xy - lpos.xy ) * q;

    vpos = gm_Matrices[ MATRIX_VIEW ] * vpos;
    gl_Position = gm_Matrices[ MATRIX_PROJECTION ] * vpos;
    
    v_vColour = in_Colour;
}
/// glsl_shadows_mask
varying vec4 v_vColour;

void main()
{
    gl_FragColor = v_vColour;
}

Фрагмент-шейдер по-брутальному прост - не производится вообще никаких вычислений, и цвет полигона напрямую скармливается в пиксели. В вертекс-шейдере объявлены четыре трёхмерных юниформа под координаты источников света. Источник света, от которого нужно отбросить тень, выбирается из четырёх указанных по цвету маски: для первого источника - красный, для второго - синий, для третьего - зелёный, для четвёртого - альфа. Полигон, отрисовываемый с соответствующим цветом, будет отбрасывать тень от соответствующего источника света. Поскольку координаты вертексов указаны в локальном пространстве, а координаты источников света - в пространстве "мира", то перед искривлением полигона относительно координат света нужно преобразовать их координаты к общей трансформации. Для этого можно преобразовать вертекс полигона к координатам мира через MATRIX_WORLD и затем искривлённый полигон далее преобразовать в экранные координаты последовательно умножая на MATRIX_VIEW и MATRIX_PROJECTION. После умножения на MATRIX_WORLD, все трансформации d3d_transform* принимают силу и вертексы будут реально находиться там, куда эти трансформации перемещали исходный объект. Далее по функции вычисляется степень смещения вертексов в зависимости от высоты вертекса и источника света. Чем выше источник света, тем короче его тени у всех объектов, и чем выше полигон, тем длиннее его личная тень. Чтобы величина смещения не обращалась в бесконечность при делении на ноль и в минус при полигоне, который выше источника света, установлено ограничение 0.001, то есть тысячекратное увеличение длины тени, что должно быть предостаточно. Затем координаты вертекса смещаются в направлении (и магнитуде) от источника света с вычисленным коэффицентом. Теперь вертекс должен быть преобразован в координаты экранного пространства. Для этого нужно его умножить далее на матрицу вида и матрицу проекции. Можно один раз умножить вектор на заранее перемноженные матрицы, но поскольку матрицы MATRIX_VIEW_PROJECTION не предусмотрено, а перемножение матриц это очень ресурсоёмкая операция по сравнению с умножением матрицы на вектор, то лучше последовательно два раза умножить вертекс на матрицы.

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

/// create
surf_shadowmask = surface_create ( 800, 600 );
surf_lightmap = surface_create ( 800, 600 );
application_surface_draw_enable ( false );
/// post_draw
var ulightmapsampler = shader_get_sampler_index ( glsl_shadows_render, "u_LightMapSampler" );
texture_set_stage ( ulightmapsampler, surface_get_texture ( surf_lightmap ) );
shader_set ( glsl_shadows_render );
draw_surface ( application_surface, 0, 0 );
shader_reset ( );
/// draw_end
surface_set_target ( surf_lightmap );
draw_clear_alpha ( make_color_rgb ( 5, 10, 15 ), 1 ); // solid ambient light
surface_reset_target ( );

var k = 0, s = ds_list_size ( lights );
while ( true )
{
    var a = -1, b = -1, c = -1, d = -1;
    
                 a = ds_list_find_value ( lights, k ); k++;
    if ( k < s ) b = ds_list_find_value ( lights, k ); k++;
    if ( k < s ) c = ds_list_find_value ( lights, k ); k++;
    if ( k < s ) d = ds_list_find_value ( lights, k ); k++;
    
    gml_render_shadows ( a, b, c, d );
    
    if ( k >= s )
        break;
}
/// gml_render_shadows
surface_set_target ( surf_shadowvolume );
draw_clear_alpha ( c_white, 1 );
shader_set ( glsl_shadow_volume );
draw_set_blend_mode_ext ( bm_zero, bm_inv_src_color ); // subtractive
d3d_set_projection_ortho ( view_xview[ 0 ], view_yview[ 0 ], 800, 600, 0 );
if ( argument0 != -1 ) shader_set_uniform_f ( light1, argument0.x, argument0.y, argument0.z );
if ( argument1 != -1 ) shader_set_uniform_f ( light2, argument1.x, argument1.y, argument1.z );
if ( argument2 != -1 ) shader_set_uniform_f ( light3, argument2.x, argument2.y, argument2.z );
if ( argument3 != -1 ) shader_set_uniform_f ( light4, argument3.x, argument3.y, argument3.z );

with ( obj_shadowcaster )
    event_perform ( ev_other, ev_user0 );
d3d_transform_set_identity ( );
draw_set_color ( c_white ); 
draw_set_alpha ( 1 );

shader_reset ( );
draw_set_blend_mode_ext ( bm_one, bm_one ); // additive

with ( obj_shadowcaster )
    event_perform ( ev_other, ev_user1 );
surface_reset_target ( );

surface_set_target ( surf_lightmap ); 
shader_set ( glsl_shadow_lightmap );
texture_set_stage ( shadowsampler, surface_get_texture ( surf_shadowvolume ) );
d3d_set_projection_ortho ( view_xview[ 0 ], view_yview[ 0 ], 800, 600, 0 );

var i = 0; repeat ( 4 )
{
    if ( argument[ i ] == -1 ) continue;
    shader_set_uniform_f ( obj_test.lightmask, ( i == 0 ), ( i == 1 ), ( i == 2 ), ( i == 3 ) );
    with ( argument[ i ] ) event_perform ( ev_other, ev_user0 );
    i++;
}

surface_reset_target ( );
shader_reset ( );
draw_set_blend_mode ( bm_normal );
/// event_user_0
d3d_transform_set_rotation_z ( image_angle );
d3d_transform_add_translation ( x, y, 0 );
vertex_submit (global.boxshadow_vertexbuffer, pr_trianglestrip, -1 );
/// event_user_1
draw_sprite_ext ( spr_rect, 0, x, y, 1, 1, image_angle, c_white, 1 );
/// event_user_0
draw_sprite_ext ( sprite_index, image_index, x, y, 2, 2, image_angle, color, 1 );
/// create
vertex_format_begin ( );
vertex_format_add_position_3d ( );
vertex_format_add_colour ( );
global.shadowvertexformat = vertex_format_end ( );

global.boxshadow_vertexbuffer = vertex_create_buffer ( );
vertex_begin ( global.boxshadow_vertexbuffer, global.shadowvertexformat );

vertex_position_3d ( global.boxshadow_vertexbuffer, -16, 16, 1 );  
vertex_colour ( global.boxshadow_vertexbuffer, $0000FF, 0 );  
vertex_position_3d ( global.boxshadow_vertexbuffer, 16, 16, 1 ); 
vertex_colour ( global.boxshadow_vertexbuffer, $0000FF, 0 );  
vertex_position_3d ( global.boxshadow_vertexbuffer, -16, -16, 1 ); 
vertex_colour ( global.boxshadow_vertexbuffer, $0000FF, 0 );  

/* * * * * * * * * * */

vertex_end ( global.boxshadow_vertexbuffer );
vertex_freeze ( global.boxshadow_vertexbuffer );

Как видно, код достаточно сложный. В Create эвенте объекта-контроллера теней создаются сурфейсы и отключается автоматическое реднерение главного сурфейса. В Post Draw эвенте рендерится главный сурфейс с шейдером, анаогично предыдущему эффекту. А вот в Draw End эвенте рендерятся маска теней и лайтмап. Первым делом, очищается сурфейс лайтмапа до цвета эмбиент-освещения, то есть минимальный уровень освещения в любой точке. Тут тоже можно рисовать произвольные изображения и они прямо так и появятся в итоговом лайтмапе - например, сюда нужно рендерить маски объектов, которые не должны подвергаться освещению. Чтобы обрабатывать большое количество света на маске теней с ограничением в 4 источника света за раз (по одной маске на цветовой канал), рендерение вынесено в отдельную фукнцию gml_render_shadows, которой передаются по четыре штуки источники света из списка. Каким образом формировать список - каждый решает сам, но один из простых способов это в Create эвенте источника света добавить его id в этот список.

Наиболее вычислительно-сложная часть всего эффекта это функция рендерения лайтмапа. Здесь сначала рендерится маска теней, а потом через эту маску рендерятся источники света. В начале кода устанавливается сурфейс маски теней и очищается до белого цвета - цвет маски полигонов будет вычитаться из него, таким образом образвывая "дыры" в цветовых каналах. При рендерении источников света, спрайт света будет рендериться только там, где нет соответствующих "дыр", таким образом образуя тени - участки, куда свет не попал. Для этого устанавливается соответствующий режим смешивания (бленд-мод). Поскольку при установке сурфейса, проекция автоматически "сбрасывается", её нужно вручную установить обратно в нужную точку. Далее устанавливаются координаты источников света. После этого, вызывается пользовательский эвент (который тут используется для отрисовки теней) для каждого объекта, который может отбрасывать тень. Как видно в коде этого эвента, там просто отрисовывается вертексбуффер со специально подготовленной четырёхцветной (RGBA) моделькой тени, которая генерируется в Create эвенте - таким образом, эта моделька может за один проход отрисовки отрисовать сразу четыре маски (а неиспользуемые маски попросту не будут видны на итоговой картинке). Шейдер искажения геометрии отменяется и устанавливается "прибавляющий" режим смешивания для рендерения лайтмапа. Чтобы из теней "вырезать" собственно очертания спрайта, чтобы он не затенял сам себя, вызывается другой эвент, в котором в эту же маску белым цветом отрисовывается фигура по форме спрайта, таким образом "стирая" все тени в этой точке. Это устраняет артефакты, когда объекты затеняют сами себя, но при этом объекты не смогут затенять друг друга. Можно пойти другим путём и объявить модель тени "вывернутой" наизнанку (при этом крышка не понадбится) и выключить отрисовку бекфейсов через d3d_set_culling, с тем, чтобы рисовались "нижние" стороны тени, а "верхние" не рисовались, что позволяет затенять другие объекты и при этом у выпуклых моделек не возникает проблема с необходимостью вырезания из них тени по контуру спрайта, но зато на вогнутых модельках могут возникать уродливые артефакты самозатенения - или не уродливые, а наоборот, какие надо. Каждый оптимизирует под себя.

Далее, с полученной маской рендерится лайтмап. Устанавливается сурфейс и шейдер лайтмапа, устанавливается проекция. Далее устанавливается сэмплер - текстура на сэмплере поменяется именно в этот момент, поэтому если переключить слишком поздно или слишком рано, лайтмап будет "запаздывать". В шейдер лайтмапа передаётся цветовая маска и вызывается эвент отрисовки источника света. Здесь эвент представляет собой просто отрисовку спрайта света, но можно много чего интересного делать. Так же необязательно именно вызывать эвент - можно выполнять отрисовку в этом же скрипте. После этого этапа, лайтмап считается законченным и можно приступать к рендерению следующей пачки теней и источников света.

В коде выше существует огромный простор для оптимизации - моожно увеличить количество рендеримых масок за раз, если использовать более совершенную формулу выбора цветовой маски. Еще можно не рендерить источники света, спрайты которых целиком лежат за пределами вида, таким образом свет от них не может дойти до экрана, точно так же можно не рендерить тени, которые целиком окажутся за экраном. Модели теней от статичных объектов можно заталкивать в огромные вертекс-буфферы и рендерить сразу все за один проход. Ну и конечно можно попросту использовать более эффективный цикл отрисовки - тот, что указан здесь, это просто для примера. И хотя этот код и так имеет хорошую производительность, это, конечно, не значит, что оптимизировать не надо - наоборот, надо, ещё как! Ведь чем лучше оптимизирован код, тем больше источников света и теней он сможет сожрать без просадки производительности. Однако нужно всегда измерять производительность - вполне возможно, что от добавления очередной ступени оптимизации, производительность не выросла, а наоборот упала. Такое часто бывает, когда код оптимизации становится слишком жирным по сравнению с шейдерами отрисовки. А учитывая их простоту и прямолинейность, оптимизировать лучше "аккуратно", стараться использовать объекты "большими кусками" и так далее - лучше отрисовать кучу лишнего хлама, который никогда не попадёт на экран, чем тратить слишком много вычислительного времени на аккуратное отсеивание этого хлама из алгоритма отрисовки.

[править] Мультитекстуры

Одна важная особенность это возможность использовать сразу несколько текстур для рендерения - мультитекстурирование. Вообще, почти все предыдущие полноэкранные эффекты и так включают в себя мультитекстурирование - на этапе рендерения главного сурфейса через шейдер, например. Но тут речь пойдёт о мультитекстурировании отдельных спрайтов. Мультитекстуры наиболее полезны в 3д, так как с ними можно делать очень много всего интересного. Но и в 2д мультитекстурам есть применение, причём зачастую аналогичное.

Мультитекстурирование на террейне.
Альфамап низкого разрешения управляет текстурированием.

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

/// glsl_multitexture
attribute in_Position;

varying vec2 v_vMaskcoord;
varying vec2 v_vTexcoord;

void main()
{
    vec4 pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * pos;
    
    v_vMaskcoord = in_Position.xy;
    v_vTexcoord = in_Position.xy * 3.0;
}
/// glsl_multitexture
varying vec2 v_vTexcoord;
varying vec2 v_vMaskcoord;

uniform sampler2D s_Texture1;
uniform sampler2D s_Texture2;
uniform sampler2D s_Texture3;
uniform sampler2D s_Texture4;

void main()
{
    vec4 mask = texture2D ( gm_BaseTexture, v_vMaskcoord );
    gl_FragColor = texture2D ( s_Texture1, v_vTexcoord ) * mask.r +
                   texture2D ( s_Texture2, v_vTexcoord ) * mask.g +
                   texture2D ( s_Texture3, v_vTexcoord ) * mask.b +
                   texture2D ( s_Texture4, v_vTexcoord ) * mask.a ;
}

Тут используется 5 мультитекстур - одна "главная" текстура gm_BaseTexture и четыре дополнительных. В юниформ-сэмплеры шейдера передаются четыре текстуры для отрисовки, а четырёхцветная (красный-зеленый-синий-альфа) маска главной текстуры отрисовки опреедляет, какую именно текстуру отобразить на пикселе. Для отображения террейнов в фрагмент-шейдер из вертекс-шейдера передаются две текстурных координаты - одна для маски, а вторая для текстур, которая домножается в вертекс-шейдере на 5-10-20, в зависимости от масштаба земли. Таким образом маск-мап низкого разрешения управляет текстурированием огромного террейна с (повторяющимися) текстурами высокого разрешения. В шейдере остался незадействованный альфа-канал маски - можно и его применить и тогда станет доступно сразу четыре текстуры, но тогда создавать маску в графическом редакторе будет очень проблематично, нужно будет разработать инструмент "окрашивания земли текстурами", который заполнит этот маск-мап программно.

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

/// glsl_multitexture_recolor
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D s_Texture1;
uniform vec3 u_Color1;
uniform vec3 u_Color2;
uniform vec3 u_Color3;

void main ( )
{
    vec3 colormask = texture2d ( s_Texture1, v_vTexcoord ).rgb;
    gl_FragColor = texture2D ( gm_BaseTexture, v_vTexcoord );
    gl_FragColor.rgb += u_Color1 * colormask.r + 
                        u_Color2 * colormask.g + 
                        u_Color3 * colormask.b;
    gl_FragColor *= v_vColour;
}

Таким образом получается спрайт, лишённый недостатков предыдущих шейдеров перекрашивания - невозможности использовать не-указанные цвета и необходимости использования костылей даже на такие просте вещи, как серый цвет. Спрайт должен быть специально подготовлен - на цветном спрайте все зоны перекрашивания должны быть чёрные, а на спрайте перекраски - наоборот, все "цветные" зоны должны быть чёрные, чтобы при наложении не было засветки. Также, если очень постараться, то можно использовать все четыре канала спрайта перекраски для собственно перекраски.

При отрисовке спрайтов таким способом нужно учитывать то, что гамак обычно заталкивает их все в один общий текстурный атлас, так что если спрайты находятся на одном и том же атласе то использовать дополнительные сэмплеры, в общем, не нужно - достаточно сэмплить одну и ту же текстуру с разных точек. Но тогда нужно будет передать в вертекс-шейдер текстурные координаты второго спрайта (которые можно получить через sprite_get_uvs), который эти текстуры вторым набором передаст в фрагмент-шейдер. Можно использовать одни и те же текстурные координаты на разных сэмплерах, если сделать так, чтобы основной спрайт оказался на одном атласе, а вторичный - на втором, причём их координаты точно совпадали, что может быть проблематично. Или можно пометить спрайты как Used for 3D, в результате чего GameMaker выделит под каждый спрайт по отдельной текстуре и не будет их автоматически обрезать до минимума.

Вообще, применения мультитекстурам очень много и готовые рецепты есть разве что под самые распространённые методы, так что тут - кто во что горазд, что называется, и аналогичными способами можно делать самые разные эффекты, если проявить изобретательность. Стандарт GL ES 2.0 предписиывает устройствм иметь как минимум 8 текстурных юнитов (то есть gm_BaseTexture и 7 сэмплеров-юниформов), но на реальном устройстве может быть и больше. Устройства с GL ES версий ниже 2.0 могут иметь гораздо меньше мультитекстур - вплоть до двух (это нижний минимум). В GameMaker не предусмотрено специальной функции, которая вернула бы это значнеие, но оно доступно внутри шейдера в виде int gl_MaxTextureImageUnits - можно отрисовать пиксель с этой величиной и считать её в скрипте. Сэмплить текстуры, строго говоря, можно и в вертекс-шейдере, но GL ES 2.0 не предусматривает таких юнитов ( int gl_MaxVertexTextureImageUnits = 0 ), и если на реальном устройстве это соответствует, то сэмплиться ничего не будет.

Здесь не были упомянуты многие из стандартных функций (хотя они все есть в спецификациях), gl_FragData так как гамак не поддерживает multiple render targets, textureCube и samplerCube так как гамак не поддерживает cubemap-текстуры, и gl_FragCoord который возвращает координаты фрагмента на экране - по какой-то причине, в гамаке он не работает корректно, даже как кейворд не подсвечивается (хотя и компилируется как надо).

Перед использованием шейдеров в игре, нужно оценить их производительность: сделать бенчмарк и запустить шейдер на выполнение сотни и тысячи раз, и замерять время выполнения. Кривым шейдером можно очень легко сожрать все ресурсы видеокарты, поэтому оптимизация шейдеров очень важна. В статье от Apple описаны основные принципы оптимизации шейдеров: http://gmakers.ru/index.php?topic=6803.0

На этом мегатуториал по шейдерам закончен! Развивайте свои навыки, изобретайте алгоритмы для красивых эффектов, и пользуйтесь доступным в наш век функционалом графических чипов!

[править] Дополнительно

[править] Аппендикс 1: специальные переменные шейдеров.

Доступны только во фрагмент-шейдере:

  • vec4 gl_Position - позиция вертекса
  • float gl_PointSize - размер точки (при отрисовке точек)

Доступны только в вертекс-шейдере:

  • vec4 gl_FragColor - цвет пикселя
  • vec4 gl_FragData[] - данные дополнительных буферов (не поддерживается)
  • vec4 gl_FragCoord - координаты фрагмента на экране
  • vec2 gl_PointCoord - координаты фрагмента на точке (при отрисовке точек)
  • bool gl_FrontFacing - повёрнут ли полигон лицевой стороной к камере или нет (true или false)

[править] Аппендикс 2: стандартные типы данных.

  • void - для функций которые ничего не возвращают либо имеют пустой список параметров
  • bool - булевый тип для логических операций, принимает значения true или false
  • int - целочисленный тип со знаком (плюс-минус)
  • float - число с плавающей запятой, скаляр
  • vec2 - двухмерный вектор
  • vec3 - трёхмерный вектор
  • vec4 - четырёхмерный вектор
  • bvec2, bvec3, bvec4 - двух/трёх/четырёхмерный булевый вектор
  • ivec2, ivec3, ivec4 - двух/трёх/четырёхмерный целочисленный вектор
  • mat2 - матрица чисел размерностью 2×2
  • mat3 - матрица чисел размерностью 3×3
  • mat4 - матрица чисел размерностью 4×4
  • sampler2D - идентификатор-указатель на двухмерную текстуру
  • samplerCube - идентификатор-указатель на куб-мапную текстуру (не поддерживается в гамаке)

[править] Аппендикс 3: стандартные функции.

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

Тригонометрические функции:

  • genType radians (genType degrees) - Преобразует градусы в радианы.
  • genType degrees (genType radians) - Преобразует радианы в градусы.
  • genType sin (genType angle) - Тригонометрический синус угла.
  • genType cos (genType angle) - Тригонометрический косинус угла.
  • genType tan (genType angle) - Тригонометрический тангенс угла.
  • genType asin (genType x) - Арксинус. Возвращает угол, синус которого равен X.
  • genType acos (genType x) - Арккосинус. Возвращает угол, косинус которого равен Х.
  • genType atan (genType y_over_x) - Арктангенс. Возвращает угол, тангенс которого будет y_over_x (y делённое на x).
  • genType atan (genType y, genType x) - Арктангенс. Возвращает угол, образуемый вектором (Y,X). Корректно вычисляет квадрант угла на основании знаков аргументов. Аналогично функции point_direciton.

Экспоненциальные функции:

  • genType pow (genType x, genType y) - Возводит X в степень Y.
  • genType exp (genType x) - Возвращает натуральную экспоненту, то есть Xе.
  • genType log (genType x) - Натуральный логарифм. Возвращает число, в степень которого нужно взвести e, чтобы получить X.
  • genType log2 (genType x) - Логарифм по основанию 2. Возвращает число, в степень которого нужно возвести двойку, чтобы получить X.
  • genType sqrt (genType x) - Корень квадратный.
  • genType inversesqrt (genType x) - Обратный квадратный корень. Возвращает 1.0 / sqrt ( X ).

Функции общего назначения:

  • genType abs (genType x) - Возвращает модуль числа Х, то есть его положительную величину.
  • genType sign (genType x) - Возвращает знак числа Х. Возвращает 0.0 если Х равно 0.0.
  • genType floor (genType x) - Возвращает Х с округлением вниз.
  • genType ceil (genType x) - Возвращает Х с округлением вверх.
  • genType fract (genType x) - Возвращает дробную часть числа Х.
  • genType mod (genType x, float y) - Остаток от деления.
genType mod (genType x, genType y)
  • genType min (genType x, genType y) - Возвращает меньшее из двух чисел.
genType min (genType x, float y)
  • genType max (genType x, genType y) - Возвращает большее из двух чисел.
genType max (genType x, float y)
  • genType clamp (genType x, genType minVal, genType maxVal) - Возвращает Х, ограниченное minVal снизу и maxVal сверху. Аналогично комбинации min и max.
genType clamp (genType x, float minVal, float maxVal)
  • genType mix (genType x, genType y, genType a) - Возвращает среднее между X и Y, со смещением указанным в A.
genType mix (genType x, genType y, float a)
  • genType step (genType a, genType b) - Возвращает 0.0 если B меньше A, в противном случае возвращает 1.0.
genType step (float a, genType b)
  • genType smoothstep (genType a, enType b, genType x) - Возвращает 0.0 если Х меньше A и 1.0 если X больше B, и выполняет плавную интерполяцию между 0 и 1.
genType smoothstep (float a, float b, genType x)

Геометрические функции:

  • float length (genType x) - Возвращает длину вектора Х.
  • float distance (genType p0, genType p1) - Возвращает дистанцию между точками p0 и p1.
  • float dot (genType x, genType y) - Возвращает результат скалярного умножения векторов X и Y. Результат показывает косинус угла между этими векторами, умноженный на длины этих векторов.
  • vec3 cross (vec3 x, vec3 y) - Возаращает результат векторного умножения X и Y. Результат показывает вектор, перпендикулярный указанным двум векторам. Математически, векторное умножение определено только в 3-мерном и 7-мерном пространстве.
  • genType normalize (genType x) - Возвращает вектор, совпадающий по направлению с X но имеющий длину 1.
  • genType faceforward (genType N, genType I, genType Nref) - Возвращает N если вектор Nref указывает в ту же сторону, что и вектор I (в пределах 90 градусов), в противном случае возвращает -N.
  • genType reflect (genType I, genType N) - Отражает вектор I от поверхности N. Нормаль N должна иметь единичную длину (быть нормализованной).
  • genType refract (genType I, genType N, float eta) - Преломляет вектор I через поверхность N в зависимости от коэффицента преломления eta. Векторы N и I должны быть нормализованы.
Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
Инструменты