image

Доброго времени, Хабр-сообщество.

Много лет назад, натолкнулся на пост (1). Тогда меня озадачила возможность создать интересные элементы для геймплея в roguelike (2). Допустим противник может находиться за стеной, мы его не видим, пока мы не столкнёмся с ним в зоне прямой видимости. Но более мне по душе ситуация, когда мы, путешествуя по коридорам подземелья, раскрываем особенности расположения объектов постепенно на основе области видимости.

Позже в постах: (3), (4) и (5) рассматривались вопросы наложения теней в 2D играх. Как отмечалось как самими авторами, так и в комментариях, что расчёт теней достаточно объёмная и не простая задача, как для вычислителя, так и для дизайна.

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

Программа писалась на Pascal, просто потому что я его неплохо знаю, а Lazarus открытая IDE с широким набором компонентов.

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

image

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

image

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

В своей программе я использовал следующую функцию:

//Нахождение координат точек пересечения прямой с окружностью
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;

Собственно, всё готово для сборки изображения. Берём карту тайлов и на отдельном слое последовательно наносим тени, одну за одной. Для деревьев тени потемнее, для иных объектов тени более прозрачные.

image

Готовое изображение наноситься поверх основного слоя тайлов. Немного оформления заднего фона и подобрать тайлсеты поколоритнее. Собственно, на поиск подходящих тайлсетов у меня ушло два дня, те что в открытом доступе или очень низкого качества или стоят денег. В итоге деревья рисовал сам, а иные элементы позаимствовал у пользователя Joe Williamson (7) (отличный стиль).

image

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

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

image

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

image

Оставалось только создать непрозрачные стены и представить результат сообществу.

image

Жду новых игр, использующих данный эффект или его развитие.
Спасибо!

Демоверсия где можно пощупать ручками (exe для виндовс).

Ссылки:
1) habr.com/post/16927/
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

Комментарии (24)


  1. 1dNDN
    19.11.2018 21:38

    Механика интересная, но стоило бы границы теней делать более размытыми. Так же что-то мне подсказывает, что при быстром передвижении все на экране будет мелькать. Я бы сделал задержку перед убиранием тени. image Так же такие случаи неплохо было бы тоже обрабатывать. Красным — курсор


    1. rebuilder Автор
      19.11.2018 22:07

      Ситуация как Вы обозначили решается элементарно, уменьшением радиуса тени на 0.5px, для деревьев это имеет смысл. Для непрозрачных стен, наоборот, радиус слегка увеличен, что бы не было просветов между блоками, исправим.
      По поводу размытости теней: пробовал два варианта:
      1) Размытие теневого слоя
      2) Разделение тени на две, где вторая чуть меньшего радиуса
      Оба эффекта не зацепили, а падение производительности было внушительным.


      1. 1dNDN
        19.11.2018 22:30

        Может стоит все-таки использовать видеокарту? Накладывать на края градиент альфа канала непроизводительно?


        1. rebuilder Автор
          19.11.2018 22:41

          Думал о порте на видеокарту. Тогда, думается, проще заменить тайлы 3D объектами и сделать тень на плоскость, земли. Хотя эффект уже будет совершенно другим.


      1. GCU
        20.11.2018 16:01

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


  1. Sirion
    19.11.2018 23:40

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


  1. tas
    20.11.2018 12:50

    По поводу производительности:

    Можно попробовать считать «затененность» для объектов от 1 до, например, 5. Где 1 — легкое затенение, а 5 — полное и не накладывать тени для объектов, у которых «затененность» больше или равна 5.


    1. rebuilder Автор
      20.11.2018 13:06

      Если я правильно понял, вы предлагаете предварительно рассчитать освещённость каждого объекта, а затем прорисовывать тень только для тех тайлов, что не затенены? Расчётов будет больше, но за счёт уменьшения прорисовки при большом количестве объектов может быть выигрыш. Как вариант можно попробовать.

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


      1. AngReload
        20.11.2018 17:38

        Есть такая штука — 1D shadow mapping, не знаю будет ли быстрой будучи реализованной на процессоре.


  1. GCU
    20.11.2018 12:52

    А какая функция используется для наложения теней одна на другую — просто альфа-смешивание с чёрным?


    1. rebuilder Автор
      20.11.2018 12:56

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


      1. GCU
        20.11.2018 14:55

        Это не очень «реалистично» :).
        Обычно в основе реалистичности лежит какая-то модель, приближённая к реальности, ну например:
        — деревья перекрывают (k) половину освещённости
        — соответственно два дерева перекроют k*k, три k*k*k и т.д. (получается не альфа смешивание, а просто умножение на коэффициент «тени»)
        — рассчитанный коэффициент пересчитывается для гамма-кодированных значений цвета и уже по нему делается альфа-смешивание.
        Ну какой-то такой «реализм» :)


  1. Foror
    20.11.2018 13:42

    Пикселизированная равнина выглядит интересно, а вот тени в подземелье очень резкие. Выглядит не очень.


  1. dev96
    20.11.2018 14:08

    Если будет интересно, то вот:
    image
    image

    Чистый рэйкастинг. С большим количеством геометрии(> 2000) работает отлично


    1. GCU
      20.11.2018 15:06

      Можно не останавливать луч при контакте с поверхностью, а лишь «ослаблять» его ярокость в зависимости от пройденного «внутри» поверхности пути (как в тумане, луч вошёл в тучку :) )
      Тогда тени будут мягче. Картинка темновата, вне контакта с препятствиями яркость можно не менять. Думаю что для плотного леса должно хорошо смотреться, т.к. будет видно что все деревья как-то участвуют в построении тени, а не только первый ряд, который всё перекрыл


      1. dev96
        20.11.2018 15:09

        Увы, у меня не было задачи корректно это рендерить.
        Я год как занимаюсь физ. движком.
        По производительности он уже шустрее Box2D.

        P.S. не сразу понял, что вы веткой промахнулись…


      1. rebuilder Автор
        20.11.2018 15:17

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


  1. Aquahawk
    20.11.2018 17:35
    +1

    На тему механики. Посмотрите игру NOX. Все придумано до нас.


    1. rebuilder Автор
      20.11.2018 17:47

      В ноксе прикольные были тени. А про то, что придумано до нас, не мешаешь ещё раз пройти путь предшественников.


  1. nightrain912
    20.11.2018 18:05

    Ух ты, на мою статью ссылаются! Спасибо!
    А вообще есть пара мыслей.


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


    Мне кажется, что можно не рисовать рисовать дугу в начале тени — так станет проще и не будет видно резких углов, как на последнем скрине. Эта дуга сейчас необходима, иначе тень будет закрывать сам объект частично. Если не менять подход к отрисовке, но хочется убрать эти кружки, возникнет проблема.
    Придется отрисовывать объект их тень от самых дальних к игроку до самых ближних. При этом тени объектов будут рисоваться в таком порядке: объект1, тень1, объект2, тень2 и т.д. И тогда нельзя будет поправить наложение теней, про которое я говорил выше. Собственно, стандартная пробема с алгоритмом художника.


    Мой совет — попробовать рисовать все на видеокарте. Не знаю, насколько сложно это делать на современном Delphi, возможно есть нужные библиотеки.
    Если проект делается ради фана и опыта, можно попробовать переделать его на движок, более удобный для работы с 2d/3d, да тот же Unity3d мой любимый.


    Просто все эти вкусные вещи с тенями, освещением и т.д. делаются в разы проще, если можно оперировать ими как 3д/2д объектами на видеокарте, с буфферами, шейдерами, стенсилом и т.д.


    1. rebuilder Автор
      20.11.2018 19:15
      +1

      По-моему, если два объекта с некоторой прозрачностью перекрывают область видимости, то тени и должны становиться темнее. Собственно, это не совсем честная тень, а скорее способность персонажа замечать объект. Изначально план был в том, что при определённом уровне затенённости просто прятать врага или предмет от взора. А готовые тени сразу использовать в игровой механике, что куда сложнее сделать если рендерить на GPU.

      Про подход к прорисовке дуги, тут вы абсолютно правы, тоже приходил к такому выводу. В Дельфях/ Лазаре конечно же есть возможность использовать видеокарту. Приходилось ваять на OpenGL, но тёплый ламповый пиксель на CPU мне куда милее клонированных GPU’шных конвееров.


      1. nightrain912
        20.11.2018 20:48

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


        Знаете, на gpu можно делать очень ламповый пиксель :)


        1. rebuilder Автор
          20.11.2018 21:12

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


  1. 1dNDN
    21.11.2018 18:41

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