В этом уроке мы разберём процесс создания направленного освещения с одним источником света и рисования падающих теней. Хочу отметить, что урок не рассчитан на новичков, у вас должно быть хотя бы поверхностное представление о том, как в GMS 2 работает 3D графика, шейдеры и сюрфейсы.
ПодготовкаДля начала прочитайте спойлер, если вы не знаете, что такое:
Если вы когда либо пользовались переменными x и y, то вы пользовались вектором [x,y]. Говоря по простому, вектор — это математический объект обладающий направлением и величиной (длинной). Вектора можно использовать, например, чтобы обозначить координаты точки в пространстве или разницу (смещение) между двумя точками.
Нормализованный вектор (он же единичный вектор) — это вектор, величина которого равна 1.
Нормализовать вектор
a в 3D пространстве можно так:
mag = sqrt(a[X]*a[X] + a[Y]*a[Y] + a[Z]*a[Z]);
a[X] /= mag;
a[Y] /= mag;
a[Z] /= mag;
С помощью скалярного произведения очень удобно находить проекции векторов. Результатом скалярного произведения векторов a и b будет число, равное длине проекции a на b, умноженной на длину b. Результат будет больше 0, если вектора "смотрят" в одну сторону, равен 0, вектора перпендикулярны и меньше 0, если вектора "смотрят" в разные стороны.
Найти скалярное произведение векторов
a и
b в 3D пространстве можно так:
dot = a[x] * b[x] + a[y] * b[y] + a[z] * b[z];
Нормаль поверхности — это нормализованный вектор, перпендикулярный к поверхности.
Прежде чем рисовать тени, нам понадобится 3d-модель, которая будет эти самые тени отбрасывать и камера. Они уже готовы в стартовом проекте, и поскольку они не имеют прямого отношения к теме урока, я не буду на этом останавливаться.
Скачав
СТАРТОВЫЙ ПРОЕКТ и запустив игру, вы увидите что-то вроде этого:
Управление: Мышь — поворачивает камеру по орбите;
WASD — поворачивает модель.
Сейчас рисование выполняется через стандартный шейдер, и как видите, он вообще не заморачивается с освещением. Придётся написать парочку своих!
Можно выделить два вида теней — собственные и падающие:
Собственные тениЭтот вид теней образуется на поверхностях повёрнутых от света.
Чтобы определить насколько затенена та или иная поверхность, нам нужно проецировать её нормаль на вектор, указывающий направление лучей света. Если проекция направлена в сторону противоположную этому вектору, то поверхность освещена:
Нормали поверхностей уже записаны в
ob_Mesh, а вот про свет мы ничего не знаем. Исправим это!
Вектор, указывающий направление света мы будем хранить в виде массива
lightForward, состоящего из трёх элементов [X, Y, Z]. Присвоим ему начальное значение и нормализуем.
#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 пикселей то начала координат в направлении луча света.
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 для направления света.
//
// 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 и передаём в него информацию, о направлении света.
... ... ...
normalize(lightForward);
dirl_LightForward = shader_get_uniform(
sh_directional_lighting, "u_LightForward");
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, чтобы исправить это, просто подставим знак минус. Далее передаём получившееся значение освещённости во фрагмент-шейдер.
... ... ...
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;
}
Во фрагмент-шейдере умножаем цвет целевого фрагмента на его освещённость.
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; во-вторых получившийся результат необходимо нормализовать.
... ... ...
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);
... ... ...
Выше был описан упрощённый способ трансформации нормалей, но работает корректно только при равномерном (одинаковом по всем осям) масштабировании объекта. Если вам необходимо рассчитать нормали при неравномерном масштабировании, то матрицу 3x3 придётся предварительно инвертировать и транспонировать.
Нормали исправлены! Однако есть ещё одна проблема: картинка в целом стала темнее. Произошло это из-за того, что мы умножаем цвет фрагмента на его освещённость, а освещённость практически везде будет меньше чем 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 — соответствующие им выходные значения.
... ... ...
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.
#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, то уже догадались, что они делают.
/// @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 );
}
Добавим возможность поворачивать свет стрелками на клавиатуре.
... ... ...
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;
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, раз уж теперь мы знаем позицию источника света.
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]);
... ... ...
Заодно нарисуем вектора вверх и вправо, чтобы убедиться, что они взаимно перпендикулярны и направлены в нужную сторону.
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 для направления, позиции источника света и длинны лучей соответственно.
//
// 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;
}
Во фрагмент-шедере тоже уберём всё лишнее.
//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main()
{
gl_FragColor = vec4(1.0);
}
Прежде чем продолжать работу над шейдэром, неплохо было бы видеть, что он делает. Давайте вернёмся в
ob_Draw3D и переключимся на него.
... ... ...
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");
... ... ...
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) и передать его во фрагмент-шейдер.
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;
}
Во фрагмент-шейдере давайте пока просто запишем дистанцию до фрагмента, как оттенок серого.
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 и возвращает соответствующий ему цвет.
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 переменные для матриц и сюрфейса.
... ... ...
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, сразу, как только изменилось направление света.
... ... ...
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);
... ... ...
Подменяем матрицы на время рисования карты:
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);
Наконец, нужно поменять цель рисования. Прежде чем рисовать что-либо на сюрфейс не забудьте убедиться, что он существует, и очистите его.
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 слое, чтобы видеть, что на там происходит.
... ... ...
draw_surface_ext(surfShadowMap, 0, 0, 0.25, 0.25, 0, c_white, 1);
Запустите игру.
ob_Mesh ожидаемо пропала из основной области и появилась на сюрфейсе. Поверните источник света — изображение на сюрфейсе изменится. Мы уже очень близки к финалу.
Повторно нарисуем
ob_Mesh с
sh_directional_lighting шейдером.
... ... ...
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. Добавим в его вертекс-шейдер новые униформы для направлений вверх и вправо, позиции источника света, его размера и длинны лучей; во фрагмент-шейдер — униформу-семплер для карты теней и передадим в эти униформы соответствующие данные.
... ... ...
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]
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying float v_vIllumination;
uniform sampler2D u_LightMap;
void main()
... ... ...
... ... ...
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");
... ... ...
... ... ...
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 и сравнить получившееся значение с тем, что было записано на карту. Сначала просто вычислим расстояние:
... ... ...
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;
}
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.
... ... ...
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);
}
Во фрагмент-шейдере пока что просто покажем проекцию карты.
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-координаты нужно отзеркалить как минимум по вертикали.
... ... ...
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.
... ... ...
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);
}
}
Тень появилась, но всё покрылось странными узорами. Это произошло из-за того, что разрешение карты теней ограниченно, и когда свет находится под углом к поверхности, каждый пиксель карты проецируется на несколько пикселей поверхности.
Простым выходом из этой ситуации станет смещение полученного из карты значения чуть-чуть вперёд — это позволит избавиться от большинства нежелательных теней. А оставшиеся артефакты на почти-параллельных свету поверхностях можно спрятать, приподняв нижний порог собственных теней.
... ... ...
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, или стать основой для более сложных эффектов. Производительность так же может быть улучшена, при написании этого урока я стремился к простоте и доступности, а не к оптимизации. Хотя даже в своём нынешнем состоянии производительность приемлема, и код может быть использован "как есть".
Вы можете использовать/модифицировать код из этого урока, как вам заблагорассудится, и если у вас есть вопросы, замечания или предложения — не стесняйтесь писать их в комментариях.
Успехов!