Game Maker - создание игр | HellRoom Games
Январь 16, 2025, 09:34:05 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.

Войти
Новости:
 
   Начало   Game Maker Помощь Правила форума Поиск Календарь Войти Регистрация  
Страниц: [1]   Вниз
  Печать  
Автор Тема: Урок: Направленное освещение и падающие тени в 3D  (Прочитано 23348 раз)
0 Пользователей и 1 Гость смотрят эту тему.
ГоК
Активный участник
*****

Репутация: 70
Offline Offline

Пол: Мужской
API: GameMaker Studio 2
Сообщений: 278


Аррр


« : Март 18, 2020, 01:02:28 »



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



Подготовка

Для начала прочитайте спойлер, если вы не знаете, что такое:

Прежде чем рисовать тени, нам понадобится 3d-модель, которая будет эти самые тени отбрасывать и камера. Они уже готовы в стартовом проекте, и поскольку они не имеют прямого отношения к теме урока, я не буду на этом останавливаться.
Скачав СТАРТОВЫЙ ПРОЕКТ и запустив игру, вы увидите что-то вроде этого:



Управление:
 Мышь — поворачивает камеру по орбите;
 WASD — поворачивает модель.

Сейчас рисование выполняется через стандартный шейдер, и как видите, он вообще не заморачивается с освещением. Придётся написать парочку своих!
Можно выделить два вида теней — собственные и падающие:





Собственные тени

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



Нормали поверхностей уже записаны в ob_Mesh, а вот про свет мы ничего не знаем. Исправим это!
Вектор, указывающий направление света мы будем хранить в виде массива lightForward, состоящего из трёх элементов [X, Y, Z]. Присвоим ему начальное значение и нормализуем.

Цитата: ob_Draw3D - Create

#macro X 0
#macro Y 1
#macro Z 2
lightForward = array_create(3);
lightForward[X] = -0.50;
lightForward[Y] =  1.00;
lightForward[Z] =  0.75;
normalize(lightForward);



Так-же давайте нарисуем этот вектор на GUI слое в виде синей стрелки, чтобы мы могли видеть в какую сторону светит свет. position — точка на экране, соответствующая началу координат в мире игры; forward — соответствует точке смещённой на 100 пикселей то начала координат в направлении луча света.

Цитата: ob_Draw3D - Draw GUI

var length   = 100,
    position = world_to_GUI([0, 0, 0]),
    forward  = world_to_GUI([
        lightForward[X] * length,
        lightForward[Y] * length,
        lightForward[Z] * length]);

draw_set_color(c_blue);
draw_arrow(
    position[X], position[Y],
    forward[X],  forward[Y], 10);





Добавляем новый шейдер, называем его sh_directional_lighting. В вёрекс-шейдэре снимаем комментарий с атрибута in_Normal и добавляем новую униформу u_LightForward для направления света.

Цитата: sh_directional_lighting.vsh

//
// Simple passthrough vertex shader
//

attribute vec3 in_Position;       // (x,y,z)
//attribute vec3 in_Normal;       // (x,y,z)   unused in this shader.
attribute vec4 in_Colour;         // (r,g,b,a)
attribute vec2 in_TextureCoord;   // (u,v)

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3 u_LightForward;

void main()
... ... ...


Возвращаемся к ob_Draw3D. Переключаемся на шейдер sh_directional_lighting и передаём в него информацию, о направлении света.

Цитата: ob_Draw3D - Create

... ... ...
normalize(lightForward);

dirl_LightForward  = shader_get_uniform(
    sh_directional_lighting, "u_LightForward");



Цитата: ob_Draw3D - Draw

gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);

shader_set(sh_directional_lighting);
    shader_set_uniform_f_array(dirl_LightForward,  lightForward);


    with (ob_Mesh) event_user(0);

shader_reset();

gpu_set_zwriteenable(false);
gpu_set_ztestenable(false);


Теперь ob_Mesh будет рисоваться через sh_directional_lighting. Однако, если вы запустите игру, то заметите, что ничего не изменилось. Дело в том, что хоть шейдер и получает информацию о направлении света, он ничего с ней не делает. Исправим это!
Возьмём нормаль точки из атрибута in_Normal, наш вектор u_LightForward и найдём их скалярное произведение (dot product). В результате этого действия, т.к. оба вектора нормализованны, мы получим число от -1 до 1, соответствующее длине проекции нормали на луч света. Для поверхностей направленных к свету значение получится меньше 0, а для направленных от света больше 0, чтобы исправить это, просто подставим знак минус. Далее передаём получившееся значение освещённости во фрагмент-шейдер.

Цитата: sh_directional_lighting.vsh

... ... ...

varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;

uniform vec3  u_LightForward;

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;

    float illumination = -dot(in_Normal, u_LightForward);

    v_vColour       = in_Colour;
    v_vTexcoord     = in_TextureCoord;
    v_vIllumination = illumination;
}


Во фрагмент-шейдере умножаем цвет целевого фрагмента на его освещённость.

Цитата: sh_directional_lighting.fsh

varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;

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




Получилось! На кубике появились освещённые и затенённые поверхности! Но есть проблемы...
Покрутив кубик кнопками WASD, вы заметите, что тени как будто "прилипли" к своим граням. Так получается из-за того, что нормали in_Normal записаны в пространстве объекта и, при его трансформации в пространстве мира, никак не меняются:



Трансформировать нормали необходимо вручную, чтобы сделать это, достаточно умножить на них матрицу мира (gm_Matrices[MATRIX_WORLD]), но и здесь есть свои тонкости: во-первых, из матрицы необходимо исключить информацию о перемещении в пространстве (т.к. нормаль — это направление, а не точка), для этого размерность матрицы нужно уменьшить до 3x3; во-вторых получившийся результат необходимо нормализовать.

Цитата: sh_directional_lighting.vsh

... ... ...
    vec4 object_space_pos = vec4(in_Position.x, in_Position.y, in_Position.z, 1.0);
    vec3 world_space_norm = normalize(mat3(gm_Matrices[MATRIX_WORLD]) * in_Normal);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;

    float illumination = -dot(world_space_norm, u_LightForward);    
... ... ...





Нормали исправлены! Однако есть ещё одна проблема: картинка в целом стала темнее. Произошло это из-за того, что мы умножаем цвет фрагмента на его освещённость, а освещённость практически везде будет меньше чем 1. Поправить ситуацию нам помогут функции smoothstep и mix.

smoothstep(A, B, x) — возвращает 0.0 если x меньше A, 1.0 если x больше B, а остальных случаях выполняет плавную интерполяцию между 0 и 1.

mix (x, y, A) — Интерполирует число между x и y, со смещением A.
С помощью smoothstep мы сможем указать минимальное и максимальное входные значения, а с помощью mix — соответствующие им выходные значения.

Цитата: sh_directional_lighting.fsh

... ... ...
    gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);

    float smooth_illumination = smoothstep(0.0, 0.8, v_vIllumination);
    gl_FragColor.rgb *= mix(0.3, 1.0, smooth_illumination);
... ... ...






Падающие тени

Этот вид теней образуется на поверхностях, заслонённых от света другим объектом.
Одним из самых распространённых способов построения таких теней является "Shadow Mapping". Суть этого способа заключается в следующем:

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



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



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



Чтобы рисовать и использовать карту одного только направления света (lightForward) недостаточно, нам так же нужно знать позицию источника света в мире, где у него верх и право, размеры карты и насколько далеко распространяются его лучи.
Добавим соответствующие вектора и константы в ob_Draw3D.

Цитата: ob_Draw3D - Create

#macro LIGHT_SIZE   1024
#macro LIGHT_LENGHT 512

#macro X 0
#macro Y 1
#macro Z 2
lightForward  = array_create(3);
lightRight    = array_create(3);
lightUp       = array_create(3);
lightPosition = array_create(3);

... ... ...


До сих пор направление света было жестко указанно в коде (x = -0.50, y = 1.00, z = 0.75). Давайте вынесем настройку позиции и направления света в отдельный скрипт. В качестве аргументов но будет принимать две точки и направление: from, to и up — если вы раньше работали с 3D в GameMaker, то уже догадались, что они делают.

Цитата: Script - set_light

/// @arg from
/// @arg to
/// @arg up
var from = argument0,
    to   = argument1,
    up   = argument2;

with (ob_Draw3D)
{
    lightPosition[X] = from[X];
    lightPosition[Y] = from[Y];
    lightPosition[Z] = from[Z];
    lightForward[X]  = to[X] - from[X];
    lightForward[Y]  = to[Y] - from[Y];
    lightForward[Z]  = to[Z] - from[Z];
    lightUp[X]       = up[X];
    lightUp[Y]       = up[Y];
    lightUp[Z]       = up[Z];

    normalize(lightForward);
    cross_product(lightForward, lightUp,      lightRight);

    normalize(lightRight);
    cross_product(lightRight,   lightForward, lightUp   );
}



Добавим возможность поворачивать свет стрелками на клавиатуре.

Цитата: ob_Draw3D - Create

... ... ...
lightPosition = array_create(3);
lightForward[@X] = -0.50;
lightForward[@Y] =  1.00;
lightForward[@Z] =  0.75;
normalize(lightForward);


dirl_LightForward  = shader_get_uniform(
 sh_directional_lighting, "u_LightForward");

lightHor  =  30;
lightVer  = -60;
lightDist = LIGHT_LENGHT * 0.5;



Цитата: ob_Draw3D - Step

lightHor += 2 * (
    keyboard_check(vk_right) -
    keyboard_check(vk_left));
lightVer -= 2 * (
    keyboard_check(vk_up) -
    keyboard_check(vk_down));
lightVer = clamp(lightVer, -80, 80);
var dis = lengthdir_x(lightDist, lightVer),
    lx  = lengthdir_y(dis,       lightHor),
    ly  = lengthdir_y(lightDist, lightVer),
    lz  = lengthdir_x(dis,       lightHor);
set_light(
    [-lx, -ly, -lz],
    [ 0,   0,   0 ],
    [ 0,  -1,   0 ]);



Наконец, давайте поправим слой GUI, раз уж теперь мы знаем позицию источника света.

Цитата: ob_Draw3D - Draw GUI

var length   = 100,
 position = world_to_GUI(lightPosition),
 forward  = world_to_GUI([
  lightPosition[X] + lightForward[X] * length,
  lightPosition[Y] + lightForward[Y] * length,
  lightPosition[Z] + lightForward[Z] * length]);
... ... ...


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

Цитата: ob_Draw3D - Draw GUI

var length   = 50,
    position = world_to_GUI(lightPosition),
   right    = world_to_GUI([
        lightPosition[X] + lightRight[X] * length,
        lightPosition[Y] + lightRight[Y] * length,
        lightPosition[Z] + lightRight[Z] * length]),
    up       = world_to_GUI([
        lightPosition[X] + lightUp[X] * length,
        lightPosition[Y] + lightUp[Y] * length,
        lightPosition[Z] + lightUp[Z] * length]),

    forward  = world_to_GUI([
        lightPosition[X] + lightForward[X] * length,
        lightPosition[Y] + lightForward[Y] * length,
        lightPosition[Z] + lightForward[Z] * length]);

draw_set_color(c_red);
draw_arrow(
    position[X], position[Y],
    right[X],    right[Y],   10);

draw_set_color(c_lime);
draw_arrow(
    position[X], position[Y],
    up[X],       up[Y],      10);


draw_set_color(c_blue);
draw_arrow(
    position[X], position[Y],
    forward[X],  forward[Y], 10);


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



Идём дальше. Создаём новый шейдер sh_shadow_mapping. С его помощью мы будем создавать карту теней. В вертекс-шейдере следует удалить все атрибуты, кроме in_Position, и добавить униформы u_LightForward, u_LightPosition и u_LightLenght для направления, позиции источника света и длинны лучей соответственно.

Цитата: sh_light_mapping.vsh

//
// Simple passthrough vertex shader
//

attribute vec3 in_Position;                  // (x,y,z)
//attribute vec3 in_Normal;                  // (x,y,z)     unused in this shader.
attribute vec4 in_Colour;                    // (r,g,b,a)
attribute vec2 in_TextureCoord;              // (u,v)

varying vec2 v_vTexcoord;
varying vec4 v_vColour;


uniform vec3 u_LightForward;
uniform vec3 u_LightPosition;
uniform float u_LightLenght;


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;

}


Во фрагмент-шедере тоже уберём всё лишнее.

Цитата: sh_light_mapping.fsh

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;


void main()
{
    gl_FragColor = vec4(1.0);
}


Прежде чем продолжать работу над шейдэром, неплохо было бы видеть, что он делает. Давайте вернёмся в ob_Draw3D и переключимся на него.

Цитата: ob_Draw3D - Create

... ... ...
lightPosition = array_create(3);

smap_LightForward  = shader_get_uniform(
    sh_shadow_mapping, "u_LightForward");
smap_LightPosition = shader_get_uniform(
    sh_shadow_mapping, "u_LightPosition");
smap_LightLenght   = shader_get_uniform(
    sh_shadow_mapping, "u_LightLenght");


dirl_LightForward  = shader_get_uniform(
    sh_directional_lighting, "u_LightForward");
... ... ...


Цитата: ob_Draw3D - Draw

gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);

shader_set(sh_shadow_mapping);
    shader_set_uniform_f_array(dirl_LightForward,  lightForward);
   shader_set_uniform_f_array(smap_LightPosition, lightPosition);
    shader_set_uniform_f(      smap_LightLenght,   LIGHT_LENGHT);


    with (ob_Mesh) event_user(0);

shader_reset();

gpu_set_zwriteenable(false);
gpu_set_ztestenable(false);


Само собой, пока что шейдер ничего не делает, но это не на долго.



Нам необходимо найти расстояния то рисуемой карты до вершин.
Не будем повторять старые ошибки: так же как и ранее с нормалями, атрибут in_Position содержит позицию вершины в пространстве объекта и должен быть трансформирован матрицей мира gm_Matrices[MATRIX_WORLD]. Далее из получившегося вектора нужно вычесть позицию источника света. Результатом этих действий станет вектор в пространстве мира от источника света до вершины (см. картинку ниже).



Наконец, как вы уже возможно догадались, нам осталось только найти скалярное произведение с вектором света. Получившееся число и будет расстоянием от источника света до вершины, которое нужно разделить на длину лучей (чтобы получить значение от 0 до 1) и передать его во фрагмент-шейдер.

Цитата: sh_light_mapping.vsh

attribute vec3 in_Position;

varying float v_vLightDistance;

uniform vec3  u_LightForward;
uniform vec3  u_LightPosition;
uniform float u_LightLenght;

void main()
{
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
    vec4 world_space_pos  = gm_Matrices[MATRIX_WORLD] * object_space_pos;
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    
   vec3  light_to_object   = vec3(world_space_pos) - u_LightPosition;
    float light_distance    = dot(light_to_object, u_LightForward);
    float light_distance_01 = clamp(light_distance / u_LightLenght, 0.0, 1.0);

    v_vLightDistance = light_distance_01;

}


Во фрагмент-шейдере давайте пока просто запишем дистанцию до фрагмента, как оттенок серого.

Цитата: sh_light_mapping.fsh

varying float v_vLightDistance;

void main()
{
    vec3 packed_distance = vec3(v_vLightDistance);
    gl_FragColor = vec4(packed_distance, 1.0);
}

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



К сожалению, в своём нынешнем состоянии шейдер не эффективен. Чтобы карта состояла из оттенков серого мы записываем во все три RGB канала одно и то же значение. Фактически, из доступных четырёх байтов на пиксель мы используем только один.
В интернете можно найти массу разных способов закодировать число с плавающей точкой в цвет и обратно. Я остановился на том, который использует только RGB и не трогает Альфа-канал, чтобы можно было разглядеть текстуру.
Функция packFloatInto8BitVec3(val01) принимает число с плавающей точкой val01 от 0 до 1 и возвращает соответствующий ему цвет.

Цитата: sh_light_mapping.fsh

varying float v_vLightDistance;

const float SCALE_FACTOR = 256.0 * 256.0 * 256.0 - 1.0;
vec3 packFloatInto8BitVec3(float val01)
{
    float zeroTo24Bit = val01 * SCALE_FACTOR;
    return floor(
        vec3(
            mod(zeroTo24Bit, 256.0),
            mod(zeroTo24Bit / 256.0, 256.0),
            zeroTo24Bit / 256.0 / 256.0
        )
    ) / 255.0;
}


void main()
{
    vec3 packed_distance = packFloatInto8BitVec3(v_vLightDistance);
    gl_FragColor = vec4(packed_distance, 1.0);
}

Результат будет довольно сумасшедшим:



На этом шейдер sh_light_mapping можно считать готовым. Однако, как вы возможно помните, выше я писал, что карта теней должна быть нарисована с позиции источника света, а не камеры. Чтобы этого добиться, нам нужно на время рисования подменить матрицы вида и проекции, а после вернуть всё как было. Кроме того, целью рисования должен быть специально отведённый для карты сюрфейс, а не окно игры. Добавим в ob_Draw3D переменные для матриц и сюрфейса.

Цитата: ob_Draw3D - Create

... ... ...
lightForward  = array_create(3);
lightRight    = array_create(3);
lightUp       = array_create(3);
lightPosition = array_create(3);
lightViewMat  = matrix_build_identity();
lightProjMat  = matrix_build_identity();

surfShadowMap = -1;

... ... ...


Присваивать значения матрицам будем в скрипте set_light, сразу, как только изменилось направление света.

Цитата: Script - set_light

... ... ...
normalize(lightForward);
cross_product(lightForward, lightUp,      lightRight);

normalize(lightRight);
cross_product(lightRight,   lightForward, lightUp   );

lightViewMat = matrix_build_lookat(
 from[X], from[Y], from[Z],
 to[X],   to[Y],   to[Z],
 up[X],   up[Y],   up[Z]);
lightProjMat = matrix_build_projection_ortho(
 -LIGHT_SIZE, LIGHT_SIZE, 0, LIGHT_LENGHT);

... ... ...


Подменяем матрицы на время рисования карты:

Цитата: ob_Draw3D - Draw

var cameraProj = matrix_get(matrix_projection),
    cameraView = matrix_get(matrix_view);


gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);

matrix_set(matrix_projection, lightProjMat);
matrix_set(matrix_view,       lightViewMat);

shader_set(sh_shadow_mapping);
 
... ... ...

shader_reset();
matrix_set(matrix_projection, cameraProj);
matrix_set(matrix_view,       cameraView);


gpu_set_zwriteenable(false);
gpu_set_ztestenable(false);

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

Цитата: ob_Draw3D - Draw

var cameraProj = matrix_get(matrix_projection),
    cameraView = matrix_get(matrix_view);

if (!surface_exists(surfShadowMap))
    surfShadowMap = surface_create(
        LIGHT_SIZE, LIGHT_SIZE);


gpu_set_zwriteenable(true);
gpu_set_ztestenable(true);

surface_set_target(surfShadowMap);

    matrix_set(matrix_projection, lightProjMat);
    matrix_set(matrix_view,       lightViewMat);
    shader_set(sh_shadow_mapping);
        shader_set_uniform_f_array(lmap_LightForward,  lightForward);
        shader_set_uniform_f_array(lmap_LightPosition, lightPosition);
        shader_set_uniform_f(      lmap_LightLenght,   LIGHT_LENGHT);
  
        draw_clear(c_white);
        with (ob_Mesh) event_user(0);

    shader_reset();

surface_reset_target();

matrix_set(matrix_projection, cameraProj);
matrix_set(matrix_view,       cameraView);

gpu_set_zwriteenable(false);
gpu_set_ztestenable(false);


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

Цитата: ob_Draw3D - Draw GUI

... ... ...
draw_surface_ext(surfShadowMap, 0, 0, 0.25, 0.25, 0, c_white, 1);


Запустите игру. ob_Mesh ожидаемо пропала из основной области и появилась на сюрфейсе. Поверните источник света — изображение на сюрфейсе изменится. Мы уже очень близки к финалу.



Повторно нарисуем ob_Mesh с sh_directional_lighting шейдером.

Цитата: ob_Draw3D - Draw

... ... ...
surface_reset_target();

matrix_set(matrix_projection, cameraProj);
matrix_set(matrix_view,       cameraView);
shader_set(sh_directional_lighting);
    shader_set_uniform_f_array(dirl_LightForward,  lightForward);

    with (ob_Mesh) event_user(0);

shader_reset();

... ... ...




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

Цитата: sh_directional_lighting.vsh

... ... ...
varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;

uniform vec3  u_LightForward;
uniform vec3  u_LightRight;
uniform vec3  u_LightUp;
uniform vec3  u_LightPosition;
uniform float u_LightSize;
uniform float u_LightLenght;


void main()
... ... ...

/size]

Цитата: sh_directional_lighting.fsh

varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;

uniform sampler2D u_LightMap;

void main()
... ... ...


Цитата: ob_Draw3D - Create

... ... ...
dirl_LightForward  = shader_get_uniform(
    sh_directional_lighting, "u_LightForward");
dirl_LightRight    = shader_get_uniform(
    sh_directional_lighting, "u_LightRight");
dirl_LightUp       = shader_get_uniform(
    sh_directional_lighting, "u_LightUp");
dirl_LightPosition = shader_get_uniform(
    sh_directional_lighting, "u_LightPosition");
dirl_LightSize     = shader_get_uniform(
    sh_directional_lighting, "u_LightSize");
dirl_LightLenght   = shader_get_uniform(
    sh_directional_lighting, "u_LightLenght");
dirl_LightMap      = shader_get_sampler_index(
    sh_directional_lighting, "u_LightMap");

... ... ...


Цитата: ob_Draw3D - Draw

... ... ...
shader_set(sh_directional_lighting);
    shader_set_uniform_f_array(dirl_LightForward,  lightForward);
   shader_set_uniform_f_array(dirl_LightRight,    lightRight);
    shader_set_uniform_f_array(dirl_LightUp,       lightUp);
    shader_set_uniform_f_array(dirl_LightPosition, lightPosition);
    shader_set_uniform_f(      dirl_LightSize,     LIGHT_SIZE);
    shader_set_uniform_f(      dirl_LightLenght,   LIGHT_LENGHT);
    var texShadowMap = surface_get_texture(surfShadowMap);
    texture_set_stage(         dirl_LightMap,      texShadowMap);


    with (ob_Mesh) event_user(0);
 
shader_reset();
... ... ...


Чтобы построить падающие тени нам нужно повторно рассчитать расстояние до карты теней, точно так же как мы делали это в sh_shadow_mapping и сравнить получившееся значение с тем, что было записано на карту. Сначала просто вычислим расстояние:

Цитата: sh_directional_lighting.vsh

... ... ...
varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;
varying float v_vLightDistance;
... ... ...
void main()
{
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
   vec4 world_space_pos  = gm_Matrices[MATRIX_WORLD] * object_space_pos;
    vec3 world_space_norm = normalize(mat3(gm_Matrices[MATRIX_WORLD]) * in_Normal);
 
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    
    float illumination      = -dot(world_space_norm, u_LightForward);
   vec3  light_to_object   = vec3(world_space_pos) - u_LightPosition;
    float light_distance    = dot(light_to_object, u_LightForward);
    float light_distance_01 = clamp(light_distance / u_LightLenght, 0.0, 1.0);


    v_vColour        = in_Colour;
    v_vTexcoord      = in_TextureCoord;
    v_vIllumination  = illumination;
   v_vLightDistance = light_distance_01;
}


Цитата: sh_directional_lighting.fsh

varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;
varying float v_vLightDistance;

uniform sampler2D u_LightMap;
... ... ...


Но как же нам получить значение из карты? Для начала нам нужно определить UV координаты соответствующие рисуемым вершинам. Как и в случае с расчётом расстояния, нужен вектор в пространстве мира от источника света до вершины. Проецировать его на вектора вправо и вверх. Получившиеся 2D-координаты необходимо разделить на размер карты и добавить 0.5. Результатом станут искомые UV координаты в промежутке от 0 до 1.



Цитата: sh_directional_lighting.vsh

... ... ...
varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;
varying float v_vLightDistance;
varying vec2  v_vLightMapPosition;
... ... ...
void main()
{
    ... ... ...
    float light_distance    = dot(light_to_object, u_LightForward);
    float light_distance_01 = clamp(light_distance / u_LightLenght, 0.0, 1.0);
   float light_map_U       = 0.5 + dot(light_to_object, u_LightRight) / u_LightSize;
    float light_map_V       = 0.5 + dot(light_to_object, u_LightUp   ) / u_LightSize;


    v_vColour           = in_Colour;
    v_vTexcoord         = in_TextureCoord;
    v_vIllumination     = illumination;
    v_vLightDistance    = light_distance_01;
   v_vLightMapPosition = vec2(light_map_U, light_map_V);
}


Во фрагмент-шейдере пока что просто покажем проекцию карты.

Цитата: sh_directional_lighting.fsh

varying vec2  v_vTexcoord;
varying vec4  v_vColour;
varying float v_vIllumination;
varying float v_vLightDistance;
varying vec2  v_vLightMapPosition;

uniform sampler2D u_LightMap;

void main()
{
    gl_FragColor = texture2D(u_LightMap, v_vLightMapPosition);
 
    //gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
    //float smooth_illumination = smoothstep(0.0, 0.8, v_vIllumination);
    //gl_FragColor.rgb *= mix(0.3, 1.0, smooth_illumination);
}




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

Цитата: sh_directional_lighting.vsh

... ... ...
 float light_map_U       = 0.5 + dot(light_to_object, u_LightRight) / u_LightSize;
 float light_map_V       = 0.5 - dot(light_to_object, u_LightUp   ) / u_LightSize;
... ... ...




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



Осталось лишь преобразовать цвет из карты обратно в расстояние и сравнить с теми, что было посчитано в v_vLightDistance. Если v_vLightDistance больше, чем расстояние из карты, то на фрагмент падает тень от другого объекта.
Функция unpack8BitVec3IntoFloat(valRGB) принимает цвет полученный из функции packFloatInto8BitVec3 и возвращает соответствующее число от 0 до 1.

Цитата: sh_directional_lighting.fsh

... ... ...
uniform sampler2D u_LightMap;

const float SCALE_FACTOR = 256.0 * 256.0 * 256.0 - 1.0;
const vec3  SCALE_VECTOR = vec3(1.0, 256.0, 256.0 * 256.0) / SCALE_FACTOR * 255.0;
float unpack8BitVec3IntoFloat(vec3 valRGB)
{
    return dot(valRGB, SCALE_VECTOR);
}


void main()
{
   vec3  packed_distance = texture2D(u_LightMap, v_vLightMapPosition).rgb;
   float distanse = unpack8BitVec3IntoFloat(packed_distance);
    
   //gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
    
   if (v_vLightDistance > distanse)
    {
        gl_FragColor.rgb *= 0.3;
    }
    else
    {

       //float smooth_illumination = smoothstep(0.0, 0.8, v_vIllumination);
       //gl_FragColor.rgb *= mix(0.3, 1.0, smooth_illumination);
   }
}




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



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

Цитата: sh_directional_lighting.fsh

... ... ...    
    gl_FragColor = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord);
    
    if (v_vLightDistance > distanse + 0.005)
    {
        gl_FragColor.rgb *= 0.3;
    }
    else
    {
        float smooth_illumination = smoothstep(0.1, 0.8, v_vIllumination);
        gl_FragColor.rgb *= mix(0.3, 1.0, smooth_illumination);
    }
... ... ...




И вот, наконец-то тени можно считать готовыми! По этой ссылке вы можете скачать ГОТОВЫЙ ПРОЕКТ.



Заключение

В этом уроке мы довольно подробно разобрали один из наиболее простых и эффективных способов создания направленного освещения одним источником света в среде Game Maker Studio 2. Весь описанный здесь код с небольшими изменениями может быть адаптирован для Game Maker Studio 1.4, или стать основой для более сложных эффектов. Производительность так же может быть улучшена, при написании этого урока я стремился к простоте и доступности, а не к оптимизации. Хотя даже в своём нынешнем состоянии производительность приемлема, и код может быть использован "как есть".
Вы можете использовать/модифицировать код из этого урока, как вам заблагорассудится, и если у вас есть вопросы, замечания или предложения — не стесняйтесь писать их в комментариях.

Успехов!

* 3D_Directional_Lighting_starter.yyz (22.75 Кб - загружено 325 раз.)
* 3D_Directional_Lighting_final.yyz (28 Кб - загружено 304 раз.)
« Последнее редактирование: Март 18, 2020, 02:15:54 от ГоК » Записан
SilentPhil
Norland
GM Pro user
*

Репутация: 479
Offline Offline

Пол: Мужской
Награды:
Первое место на HellRoom Jam #7 [Hell in Your Fridge]500 сообщений!За постоянность! [50 дней на форуме]За лояльность! [+150 репутации]Настоящий игродел!Второе место на HellRoom Jam #6 [По следам Артакса]...
API: GameMaker Studio 2
Деятельность: GML, Pixel Art
Сообщений: 1363



WWW
« Ответ #1 : Март 18, 2020, 09:00:54 »

Доступно и интересно, спасибо!
Записан

         
Да, на них можно кликать.
ГоК
Активный участник
*****

Репутация: 70
Offline Offline

Пол: Мужской
API: GameMaker Studio 2
Сообщений: 278


Аррр


« Ответ #2 : Март 18, 2020, 13:31:00 »

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

Добавлено: Сентябрь 08, 2020, 16:51:12
Реализовал, более продвинутые эффекты освещения, в частности:
  • код был переписан с использованием возможностей 2.3;
  • прозрачные текстуры (альфа = 0);
  • несколько источников света одновременно;
  • цветное освещение;
  • прожекторы

<a href="http://www.youtube.com/watch?v=Hfpdr8tLffg" target="_blank">http://www.youtube.com/watch?v=Hfpdr8tLffg</a>

Ссылка на скачивание примера: https://www.dropbox.com/s/eftjt08dqsp59v8/3D_Lighting_and_Shadows.yyz?dl=1

Добавлено: Июль 01, 2021, 07:21:07
Работа над шейдером освещения продолжается. Был взят курс на 3D-пиксельарт и вот мои результаты на данный момент:

bandicam 2021-07-01 02-21-29-142.png
Урок: Направленное освещение и падающие тени в 3D
* bandicam 2021-07-01 02-21-29-142.png (17.11 Кб, 768x576 - просмотрено 737 раз.)
« Последнее редактирование: Июль 01, 2021, 07:21:49 от ГоК » Записан
SilentPhil
Norland
GM Pro user
*

Репутация: 479
Offline Offline

Пол: Мужской
Награды:
Первое место на HellRoom Jam #7 [Hell in Your Fridge]500 сообщений!За постоянность! [50 дней на форуме]За лояльность! [+150 репутации]Настоящий игродел!Второе место на HellRoom Jam #6 [По следам Артакса]...
API: GameMaker Studio 2
Деятельность: GML, Pixel Art
Сообщений: 1363



WWW
« Ответ #3 : Июль 04, 2021, 11:50:18 »

Офигенно!
Интересно, как шейдер ведёт себя с движущимися объектами.
Записан

         
Да, на них можно кликать.
ГоК
Активный участник
*****

Репутация: 70
Offline Offline

Пол: Мужской
API: GameMaker Studio 2
Сообщений: 278


Аррр


« Ответ #4 : Июль 06, 2021, 19:57:27 »

Спасибо!
Вот видео со слегка доработанной версии:

<a href="http://www.youtube.com/watch?v=QlHkaZvayaw" target="_blank">http://www.youtube.com/watch?v=QlHkaZvayaw</a>


Цитировать
Интересно, как шейдер ведёт себя с движущимися объектами.
Тонкие элементы изображения (обводка, трава и тд) "мерцают", как сумасшедшие. Это довольно большая проблема, с которой я пытаюсь бороться. Прямо сейчас экспериментирую с антиэлисингом + последующим "восстановлением палитры". Если есть другие идеи, с радостью их испытаю. В худшем случае, если ничего не выйдет, придётся просто аккуратнее работать с камерой: использовать только ортогональную проекцию и избегать медленных плавных поворотов как камеры и источников света, так и объектов.
« Последнее редактирование: Июль 06, 2021, 22:04:36 от ГоК » Записан
2009yasha2009
GM Pro user
*

Репутация: 114
Offline Offline

Пол: Мужской
Награды:
За постоянность! [50 дней на форуме]500 сообщений!
API: GameMaker Studio 2
Сообщений: 776



« Ответ #5 : Июль 07, 2021, 13:01:33 »

Очень и очень круто и доступно! Большое спасибо за труды! И особенно за то, что поделились с другими:)
Записан
ГоК
Активный участник
*****

Репутация: 70
Offline Offline

Пол: Мужской
API: GameMaker Studio 2
Сообщений: 278


Аррр


« Ответ #6 : Июль 17, 2021, 01:07:35 »

Ребята, хочу услышать ваше мнение! Первая или вторая?




bandicam 2021-07-17 02-56-58-021.png
Урок: Направленное освещение и падающие тени в 3D
* bandicam 2021-07-17 02-56-58-021.png (25.59 Кб, 1024x768 - просмотрено 1279 раз.)
bandicam 2021-07-17 02-57-26-541.png
Урок: Направленное освещение и падающие тени в 3D
* bandicam 2021-07-17 02-57-26-541.png (30.21 Кб, 1024x768 - просмотрено 918 раз.)
Записан
Dmi7ry
Гл. Администратор
*

Репутация: 1379
Offline Offline

Пол: Мужской
Награды:
5000 сообщений!За постоянность! [200 дней на форуме]За лояльность! [+1000 репутации]За помощь в развитии форума!Знаток Game Maker!За помощь новичкам!
API: GameMaker Studio Master
Деятельность: Code, design
Сообщений: 6626



WWW
« Ответ #7 : Июль 17, 2021, 07:51:28 »

Если закос под пиксельарт, то первый вариант
Записан

- А какой, собственно, командой процессора колобок ест черта?
- Командой EAT...
Справка и FAQ в правом верхнем углу...
Страниц: [1]   Вверх
  Печать  
 
Перейти в:  

HellRoom Games © 2006-2012 All Rights Reserved
Powered by SMF 1.1.21 | SMF © 2013, Simple Machines
Страница сгенерирована за 0.265 секунд. Запросов: 30.