Динамичное 2D освещение в GameMaker Studio 2 - Часть 2

Динамичное 2D освещение в GameMaker Studio 2 - Часть 2


Lighting_blog2

В предыдущей статье я показал вам, как создать основы 2D системы освещения, поэтому в этой я хочу продолжить текущий пример и сделать его более похожим на фактический точечный свет. Мы также познакомися с использованием шейдеров, чтобы облегчить наш труд (да, шейдеры могут быть простыми). А затем сделаем свет цветным. Давайте вспомним, на чем мы остановились в прошлый раз.

wall projection

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

В событии создания источника света добавьте эту переменную:

surf = -1;

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

if( !surface_exists(surf) ){
    surf = surface_create(room_width,room_height);
}

Теперь перед tile циклом и перед vertex_begin () мы установим эту поверхность как текущую, чтобы рендеринг теневого объема переместился на нее вместо основного экрана. Мы изменим наш цикл, чтобы он выглядел примерно так 

surface_set_target(surf);
draw_clear_alpha(0,0);

vertex_begin(VBuffer, VertexFormat);
for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
            // создание теневого объема. 
    }
}
vertex_end(VBuffer);    
vertex_submit(VBuffer,pr_trianglelist,-1);
surface_reset_target();

draw_surface(surf,0,0);

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

Теперь мы создадим наш шейдер и попробуем обработать эту новую поверхность. Щелкните правой кнопкой мыши на элемент Shader и выберите команду create. Это должно добавить шейдер и открыть его для редактирования.

wall projection

Это даст нам шейдер по умолчанию, и все, что нам нужно сделать, это установить его перед рисованием поверхности, а затем переопределить после. Это происходит в вызова draw_surface () (и предполагает, что вы только что назвали новый шейдер shader0).

shader_set(shader0);
draw_surface(surf,0,0);
shader_reset();

Опять же, все будет выглядеть как и раньше, только теперь мы можем добавить к нему частицы! Сначала нам нужно передать определенное пользователем значение, которое мы можем передать из нашего события рисования источника света. Внутри кода Fragment shader добавьте эту строку чуть выше void main()...

uniform vec4 u_fLightPositionRadius;        // x=lightx, y=lighty, z=light radius, w=unused

Затем нам нужно передать координату комнаты в Fragment shader из Vertex шейдера. Это довольно легко, так как координата, которую мы генерируем для теневых объемов, фактически переведена в координаты комнаты, поэтому нам нужно просто передать ее значение. Для этого мы добавляем еще переменные в Vertex shader:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;

Затем в нижней части скрипта Vertex шейдера, перед символом } мы скопируем местоположение.

v_vScreenPos =  vec2( in_Position.x, in_Position.y );

Вам также нужно вставить строчку «varying vec2 v_vScreenPos;» в Fragment шейдер. Теперь, когда в Fragment шейдере есть все, что ему нужно, мы можем передать положение источника света внутрь его события Draw. Для этого нам нужно получить дескриптор переменной u_fLightPositionRadius внутри события создания объекта light, что не очень быстро, потому мы сделаем это с самого начала. В нижней части события создания света добавьте сей строку:

LightPosRadius = shader_get_uniform(shader0,"u_fLightPositionRadius");


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

shader_set(shader0);
shader_set_uniform_f( LightPosRadius, lx,ly,rad,0.0 );
draw_surface(surf,0,0);
shader_reset();

Если мы запустим игру сейчас, то она просто должна работать. Визуально ничего не изменится.

Однако теперь мы, наконец, можем использовать эту дополнительную информацию в Fragment шейдере. Мы находим позицию относительно комнаты (v_vScreenPos), рассчитываем расстояние до источника света (u_fLightPositionRadius.xy), а затем проверяем, оно больше или меньше радиуса. И если это не в диапазоне света, то мы выводим черный пиксель. Вы также должны увеличить радиус света (переменная в событии рисования) примерно до 256. Давайте посмотрим, как мы используем это в Fragment шейдере. Его код:

varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vScreenPos;


uniform vec4 u_fLightPositionRadius;        // x=lightx, y=lighty, z=light radius, w=unused

void main()
{
    // рссчет вектора от положения комнаты до света
    vec2 vect = vec2( v_vScreenPos.x-u_fLightPositionRadius.x, v_vScreenPos.y-u_fLightPositionRadius.y );

    // рассчет длинны этого вектора
    float dist = sqrt(vect.x*vect.x + vect.y*vect.y);

    // если в диапазоне, то используем текстуру тени, а если нет, то вставляем черный.
    if( dist< u_fLightPositionRadius.z ){
        gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    }else{
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}

Как вы можете видеть, это довольно простой расчет, но это даст нам довольно крутой эффект точечного света, ведь теперь все вокруг него черное !

wall projection

Теперь у нас есть прохладные точечные тени. Мы можем улучшить этот простой эффект, немного уменьшив полученное значение ALPHA. Перед } в Fragment шейдере добавьте эту строку :

gl_FragColor.a *= 0.5;

Суть в том, чтобы взять любую окончательную альфа (которая в нашем случае равна 1.0 для того, чтобы быть в тени, или 0.0 для не в тени), и масштабировать ее, делая более прозрачной. Это приводит к изображению ниже ...

wall projection

Позвольте мне сделать небольшое отступление. Вы уже заметили, что при работе с переменной vect, я кастую vec2 (x-x, y-y). Я делаю это здесь, чтобы все выглядело чуть более ясно. Шейдеры работают в векторах, и это означает, что мы можем объединить несколько операция в одну. Эта строка должна выглядеть так :

vec2 vect = v_vScreenPos.xy-u_fLightPositionRadius.xy;

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

Следующее, что я хотел бы добавить, это немного цвета, но прежде чем сделать это, я хочу исправить рассеивание света. Если вы не очень близки к свету, то никогда не получите такой жесткий край, который у нас есть. Свет будет постепенно исчезать, поэтому было бы неплохо реализовать это и у нас. Мы сделаем это линейным спадом, но вы можете использовать кривую или что-то еще, если почувствуете себя умным. Расчет рассеивания достаточно простой, ведь у нас уже есть необходимые нам значения. Расстояние от света и его радиус - все, что нам нужно. Так как у нас уже есть код, который работает только тогда, когда находится от радиуса света, до его центра, то мы можем легко рассчитать масштабирование от 0.0 до 1.0. А затем произвести линейную интерполяцию теневого объема. Вот обновленный код Fragment шейдера.

// если в диапазоне, то используем текстуру тени, а если нет, то вставляем черный.

if( dist< u_fLightPositionRadius.z ){
    // рассчитываем значение 0.0 - 1.0 от центра до края радиуса
    float falloff = dist/u_fLightPositionRadius.z;          
    // получаем текстуру тени
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );        
    // теперь применяем линейную интерполяцию от теневого объема до всей тени
    gl_FragColor = mix( gl_FragColor, vec4(0.0,0.0,0.0,0.7), falloff);          
}else{
    // вне радиуса - полностью в тени
    gl_FragColor = vec4(0.0, 0.0, 0.0, 0.7);
}

Теперь нам стоит убрать «gl_FragColor.a *= 0.5;» из конца скрипта Fragment шейдера.

shadow falloff

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

shadow falloff

После того, как вы это сделали, измените строку в событии рисования света, где мы отрисовываем поверхность, так чтобы мы могли передать этот цвет, например:

draw_surface_ext(surf,0,0,1,1,0,image_blend,0.5);

Это проставит зеленый цвет (и 0,5 альфа) в шейдере, так что мы можем использовать этот прием, чтобы установить цвет света. Затем мы снова изменим внутреннее ядро Frgment шейдера, чтобы справиться с цветом. Итак, где у нас была строка «gl_FragColor = v_vColour * texture2D ()» , мы теперь заменим на это:

    vec4 col =  texture2D( gm_BaseTexture, v_vTexcoord );
    if( col.a<=0.01 ){
        gl_FragColor = v_vColour;           
    }else{      
        gl_FragColor = col;
    }

Мы проверяем значение alpha (col.a), поскольку оно равно 1.0 для полной тени и 0,0 для света. Но вы не можете быть уверены в том, какое значение оно будет иметь, поэтому всегда должны иметь запас для ошибки. Теперь, если мы запустим это, вы должны получить прекрасный зеленый цвет!

shadow falloff

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

Итак ... вот и все, теперь вы на пути к созданию собственного движка освещения! Ниже вы можете увидеть мои собственные усилия, когда я вышел за рамки одного источника света в несколько. Щелчок по изображению ниже приведет вас к набору видеороликов, показывающим мой прогресс, используя точно такую же систему, и хотя некоторые из моих решений более продвинутые (например, наложение нескольких теней на одну поверхность), вам, безусловно, не нужно попробовать их, чтобы получить свою рабочую систему освещения. Она будет работать отлично, и без каких-либо причудливых дополнений.
Ссылка из картинки.

Lighting demo


Переведено отсюда


Report Page