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

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


Lighting_blog

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

Результат

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

Итак, давайте создадим простой tileset и tilemap для тестирования. (сначала tileset)

tileset

Более темные плитки находятся на одном слое (называемом «ground»), а более светлые - на другом (называемом «walls»).

tilemap

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

light on tilemap

Чтобы это обработать, нам нужно перебрать эти тайлы и выяснить, есть там стена или нет.

var lx = mouse_x;       // положение света, базируется наположении мыши location
var ly = mouse_y;
var rad = 96            // радиус света
var tile_size = 32;     // размер тайла
var tilemap = layer_tilemap_get_id("walls");


var startx = floor((lx-rad)/tile_size);
var endx = floor((lx+rad)/tile_size);
var starty = floor((ly-rad)/tile_size);
var endy = floor((ly+rad)/tile_size);

draw_set_color(c_yellow);
draw_rectangle(startx*tile_size,starty*tile_size, endx*tile_size,endy*tile_size,true);  


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

for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
        var tile = tilemap_get(tilemap,xx,yy);
        if( tile!=0 ){

        }
    }
}


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

/// @description init shadow casting
vertex_format_begin();
vertex_format_add_position();
vertex_format_add_color();
VertexFormat = vertex_format_end();

VBuffer = vertex_create_buffer();


С этим закончили, теперь мы готовы построить теневые объемы (здесь - геометрическая фигура, из которой состоит тень), но прежде чем мы это сделаем, мы должны ответить на вопрос, как мы все таки отбрасываем тень? Вернемся к нашему изображению радиуса света, но на этот раз соединим его центр с каждой точкой одного тайла. Лучи, которые выступают из дальней части блока, представляют собой теневой объем, которым мы пользуемся. Теперь, самые внимательные среди вас заметят, что передние 2 ребра проецируют ту же форму, что и задние. Просто они начинаются ближе к свету. Это удобно, так как нам нужно проецировать только обращенные к свету края.

Light projection

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

Light edge projection

Пока что мы просто проецируем все 4 стороны и посмотрим, что произойдет. Итак, давайте изменим наш цикл обработки, чтобы фактически создать наши буферы.

vertex_begin(VBuffer, VertexFormat);
for(var yy=starty;yy<=endy;yy++)
{
    for(var xx=startx;xx<=endx;xx++)
    {
        var tile = tilemap_get(tilemap,xx,yy);
        if( tile!=0 )
        {
            // get corners of the 
            var px1 = xx*tile_size;     // top left
            var py1 = yy*tile_size;
            var px2 = px1+tile_size;    // bottom right
            var py2 = py1+tile_size;


            ProjectShadow(VBuffer,  px1,py1, px2,py1, lx,ly );
            ProjectShadow(VBuffer,  px2,py1, px2,py2, lx,ly );
            ProjectShadow(VBuffer,  px2,py2, px1,py2, lx,ly );
            ProjectShadow(VBuffer,  px1,py2, px1,py1, lx,ly );                      
        }
    }
}
vertex_end(VBuffer);    
vertex_submit(VBuffer,pr_trianglelist,-1);


Фокус в ProjectShadow заключается в том, чтобы получить вектор от света к каждой точке нужной линии. Мы делаем это, используя Point1X-LightX и Point1Y-LightY, а также Point2X-LightX и Point2Y-LightY. Это дает необходимые нам векторы. Затем мы хотим сделать «единичные» векторы, то есть те, что длиной 1.0. Это удобно, так как мы можем масштабировать этот единичный вектор на точную величину, иначе, если вы очень близко к блоку, одна из сторон может быть вплотную к вам, а другая - где-то в дали. Это позволяет им иметь одинаковый размер и проекцию. Вот как вы разрабатываете единичный вектор.

Adx = PointX-LightX;        
Ady = PointY-LightY;        
len = sqrt( (Adx*Adx)+(Ady*Ady) );
Adx = Adx / len;    
Ady = Ady / len;    


Adx и Ady теперь содержат вектор длиной 1, то есть sqrt ((Adx * Adx) + (Ady * Ady)) == 1.0. Единичные векторы используются повсюду при вычислении, от нормалей 3D-моделей для освещения, до векторов направленности движения. Например, если вы хотите двигаться с постоянной скоростью, даже если вы идете по диагонали (поскольку x ++; y ++; заставит вас двигаться быстрее по диагонали, чем по прямой линии),но вместо этого вы можете использовать единичный вектор и просто умножать его на скорость, с которой вы хотите двигаться. Теперь, когда у нас есть единичные векторы, мы можем масштабировать их на большое значение и ставить на позиции. Это даст нам отдаленные точки, чтобы составить наш пятиугольник. Вот скрипт ProjectShadow:

/// @description cast a shadow of this line segment from the point light
/// @param VB Vertex buffer
/// @param Ax  x1
/// @param Ay  y1
/// @param Bx  x2
/// @param By  y2
/// @param Lx  Light x
/// @param Ly  Light Y

var _vb = argument0;
var _Ax = argument1;
var _Ay = argument2;
var _Bx = argument3;
var _By = argument4;
var _Lx = argument5;
var _Ly = argument6;

// тени почти бесконечны, этого достаточно, чтобы выйти за экран
var SHADOW_LENGTH = 20000;

var Adx,Ady,Bdx,Bdy,len

// get unit length to point 1
Adx = _Ax-_Lx;      
Ady = _Ay-_Ly;      
len = (1.0*SHADOW_LENGTH)/sqrt( (Adx*Adx)+(Ady*Ady) );
Adx = _Ax + Adx * len;
Ady = _Ay + Ady * len;

// get unit length to point 2
Bdx = _Bx-_Lx;      
Bdy = _By-_Ly;      
len = (1.0*SHADOW_LENGTH) / sqrt( (Bdx*Bdx)+(Bdy*Bdy) );
Bdx = _Bx + Bdx * len;
Bdy = _By + Bdy * len;


// теперь составляем пятиугольник
vertex_position(_vb, _Ax,_Ay);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, _Bx,_By);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, Adx,Ady);
vertex_argb(_vb, $ff000000);

vertex_position(_vb, _Bx,_By);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, Adx,Ady);
vertex_argb(_vb, $ff000000);
vertex_position(_vb, Bdx,Bdy);
vertex_argb(_vb, $ff000000);


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

demo projection

Наконец, давайте исправим необходимость проецирования всех 4 ребер. Как уже упоминалось выше, мы можем получить тот же результат,используя только 2 ребра, которые обращены к источнику света. Мы делаем это, проверяя, с какой стороны тайла находится свет.

    if( !SignTest( px1,py1, px2,py1, lx,ly) ){
        ProjectShadow(VBuffer,  px1,py1, px2,py1, lx,ly );
    }
    if( !SignTest( px2,py1, px2,py2, lx,ly) ){
        ProjectShadow(VBuffer,  px2,py1, px2,py2, lx,ly );
    }
    if( !SignTest( px2,py2, px1,py2, lx,ly) ){
        ProjectShadow(VBuffer,  px2,py2, px1,py2, lx,ly );
    }
    if( !SignTest( px1,py2, px1,py1, lx,ly) ){
        ProjectShadow(VBuffer,  px1,py2, px1,py1, lx,ly );                      
    }


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

/// @description which side of a line is the point on.
/// @param Ax 
/// @param Ay 
/// @param Bx
/// @param By
/// @param Lx
/// @param Ly

var _Ax = argument0;
var _Ay = argument1;
var _Bx = argument2;
var _By = argument3;
var _Lx = argument4;
var _Ly = argument5;

return ((_Bx - _Ax) * (_Ly - _Ay) - (_By - _Ay) * (_Lx - _Ax));


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

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

wall projection

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

Report Page