Доброго времени, Хабр-сообщество.
Много лет назад, натолкнулся на пост (1). Тогда меня озадачила возможность создать интересные элементы для геймплея в roguelike (2). Допустим противник может находиться за стеной, мы его не видим, пока мы не столкнёмся с ним в зоне прямой видимости. Но более мне по душе ситуация, когда мы, путешествуя по коридорам подземелья, раскрываем особенности расположения объектов постепенно на основе области видимости.
Позже в постах: (3), (4) и (5) рассматривались вопросы наложения теней в 2D играх. Как отмечалось как самими авторами, так и в комментариях, что расчёт теней достаточно объёмная и не простая задача, как для вычислителя, так и для дизайна.
Как-то появилось у меня несколько свободных дней, и я решил вернуться к вопросу более перспективных теней. Понятное дело, что видеокарта справляется с тенями успешно и быстро, но в данном случае, хотелось обрабатывать тени для 2D игры, и переводить вычисления на видеокарту мне показалось лишним. Да и процессорная мощность за последние годы в целом подросла, собственно пост о том, что в итоге получилось.
Программа писалась на Pascal, просто потому что я его неплохо знаю, а Lazarus открытая IDE с широким набором компонентов.
Первоначальная идея заключалась в том, чтобы провести прямые, от наблюдателя через каждый из углов тайла, а затем затемнить получившуюся фигуру.
Однако такая тень выглядит несколько неестественно, когда меняется угол зрения. Тени становятся то шире, то уже.
Тень от круглого объекта выглядит гораздо лучше. Для того чтобы построить такую тень, нужно провести две касательные от точки наблюдения к окружности, и до границ экрана. Диаметр окружности будет соответствовать размеру тайла.
В своей программе я использовал следующую функцию:
//Нахождение координат точек пересечения прямой с окружностью
function tangent_to_circle(x1,y1,x2,y2,r:Single; var x3,y3,x4,y4:Single):Boolean;
var l,dx,dy,i,ii,ij:Single;
begin
dx := x1 - x2;
dy := y1 - y2;
l := Sqrt(dx*dx + dy*dy);
if l<=r then
begin
tangent_to_circle:=false;
exit;
end;
i := r/l;
ii := i*i;
ij := i * Sqrt(1 - ii);
x3 := x2 + dx*ii - dy*ij;
y3 := y2 + dx*ij + dy*ii;
x4 := x2 + dx*ii + dy*ij;
y4 := y2 - dx*ij + dy*ii;
tangent_to_circle:=true;
end;
Где (x1,y1) – точка наблюдения, (x2,y2)- центр окружности, ®- её радиус, а (x3,y3) и (x4,y4) точки пересечения прямых и окружности. Функция возвращает истинность только когда наблюдатель находиться вне окружности.
Поскольку процессор не очень дружит с тригонометрией, постарался по минимуму её использовать. Собственно опирался на простое правило (грубая модель), специалисты подскажут почему.
(Плохо)SIN,COS..>‘/’,SQRT>‘DIV’,’MOD’>‘SHR’,’SHL’>‘*’>‘:=’,’+’,’-’,’AND’,’XOR’..(Хорошо)
Реализовывать графическую часть примитивов по канве, то ещё удовольствие, существует множество библиотек и движков, которые облегчают работу. При разработке на Delphi приходилось использовать библиотеку Agg2D, на Lazarus существует её порт (6), на нём и решил воплотить задумку. Собственно, выигрыш от библиотеки в том, что к RGB цветам добавляется альфаканал, и примитивы получаются сглаженными, а также за счёт прямого доступа к пикселям и различных ухищрений обработка значительно быстрее канвы.
При рисовании тени тайла, изначально собирался заливать сектор тенью, но тогда изображение внутри тайла было плохо различимо (рассматриваемый сектор на рис 3. залит зелёным). Поэкспериментировав с различными вариантами, остановился на выделении сектора из области тени.
Для рисования сектора нам нужен угол в радианах, без тригонометрии всё же не обошлось. (arctan2 – библиотечная функция модуля math)
// Получаем угол в радианах
function alpha_angle(x1,y1,x2,y2:Single):Single;
begin
alpha_angle := arctan2(y1 - y2, x1 - x2);
end;
Собственно, всё готово для сборки изображения. Берём карту тайлов и на отдельном слое последовательно наносим тени, одну за одной. Для деревьев тени потемнее, для иных объектов тени более прозрачные.
Готовое изображение наноситься поверх основного слоя тайлов. Немного оформления заднего фона и подобрать тайлсеты поколоритнее. Собственно, на поиск подходящих тайлсетов у меня ушло два дня, те что в открытом доступе или очень низкого качества или стоят денег. В итоге деревья рисовал сам, а иные элементы позаимствовал у пользователя Joe Williamson (7) (отличный стиль).
На этом бы всё и завершилось, но остался некоторый осадок по поводу производительности. При количестве объектов около полутысячи начинается просадка. Рассматривались разные способы оптимизации, и разбиение по ядрам и ограничение области прорисовки некоторым радиусом, изменение формы тени (на менее затратную, чем дуги), даже думал перенести расчёт на видео.
В итоге пришёл к выводу что лучшим вариантом будет уменьшение дискретизации изображения служащего теневой маской. Так как значительно уменьшается количество вычислений, а также появляется неожиданный эффект пикселизации контуров тени, что придаёт определённый олдскульный шарм.
Эффект так понравился мне, что пришлось сделать масштабирование динамическим процессом, через заданный параметр кратности.
Оставалось только создать непрозрачные стены и представить результат сообществу.
Жду новых игр, использующих данный эффект или его развитие.
Спасибо!
Демоверсия где можно пощупать ручками (exe для виндовс).
2) ru.wikipedia.org/wiki/Roguelike
3) habr.com/post/204782/
4) habr.com/post/305252/
5) habr.com/post/319530/
6) wiki.freepascal.org/BGRABitmap
7) twitter.com/joecreates
Автор: rebuilder