Направленное освещение и затенение в 2D-пространстве


Добрый день, Хабравчане!
Хотелось бы рассказать об одном из способов отрисовки освещения и затенения в 2D-пространстве с учетом геометрии сцены. Мне очень нравится реализация освещения в Gish и Super MeatBoy, хотя в митбое его можно разглядеть только на динамичных уровнях с разрушающимися или перемещающимися платформами, а в Гише оно повсеместно. Освещение в таких играх мне кажется таким «тёплым», ламповым, что непременно хотелось нечто подобное реализовать самому. И вот, что из этого вышло.

Тезисно что есть и что требуется сделать:
  • есть некий 2D-мир, в который нужно встроить динамическое освещение+затенение; мир не обязательно тайловый, из любой геометрии;
  • источников света должно быть, в принципе, неограниченное число (ограничиваться только производительностью системы);
  • наличие большого числа источников света в одной точке, либо одного источника света с большим коэффициентом «освещения», должно не просто освещать область на 100%, а должно засветлять её;
  • рассчитываться должно всё, конечно же, в реалтайме;



Для всего этого понадобился OpenGL, GLSL, технология FrameBuffer и немного математики. Ограничился версиями OpenGL 3.3 и GLSL 3.30 т.к. видеокарта одной из моих систем по нынешним меркам весьма устарела (GeForce 310), да и для 2D этого более чем достаточно (а более ранние версии вызывают отторжение из-за несогласованности версий OpenGL и GLSL). Сам по себе алгоритм не сложный и делается в 3 этапа:
  1. Сформировать текстуру размером с область рендера черного цвета и нарисовать в ней освещённые области (так называемая карта освещения), накапливая при этом коэффициент освещённости для всех точек;
  2. Отрендерить сцену в отдельную текстуру;
  3. В контексте рендера вывести квад, полностью его покрывающий, а во фрагментном шейдере свести полученные текстуры. На данном этапе можно «поиграться» с фрагментным шейдером, добавив, например, эффекты преломления от воды/огня, линзы, цветовая коррекция на любой вкус и прочая пост-обработка.


1. Карта освещения


Использовать будем одну из самых распространённых технологий — deferred shading, портировав её в 2D (P.S. прошу прощения, не deferred shading, а shadow map, мой косяк, спасибо за поправку). Суть этого метода — отрендерить сцену перенеся камеру в позицию источника света, заполучив буфер глубины. Нехитрыми операциями с матрицами камеры и источника света в шейдере попиксельно можно узнать о затенённости, переводя координаты пикселя рендерящейся сцены в текстурные координаты буфера. В 3D используется z-buffer, здесь же я решил создать свой одномерный буфер глубины, на CPU.
Совершенно не претендую на разумность и оптимальность сего подхода, алгоритмов освещения хватает и у каждого свои плюсы с минусами. Во время обмозговывания способ казался вполне имеющим право на жизнь, и я приступил к реализации. Замечу, что во время написания статьи обнаружил вот такой способ… ну да ладно, велосипед так велосипед.

1.1 Z-буфер aka буфер глубины


Суть Z-буфера — хранение удалённости элемента сцены от камеры, что позволяет отсекать невидимые за более ближними объектами пиксели. Если в 3D сцене буфер глубины представляет собой плоскость

, то в нашем плоском мире он станет линией или одномерным массивом. Источники света — точечные, излучающий свет от центра по всем направлениям. Соответственно, индекс буфера и значение будут соответствовать полярным координатам расположения ближайшего к источнику объекта. Размер буфера я определял опытным путём, в результате чего остановился на 1024 (конечно, зависит от размера окна). Чем меньше размерность буфера, тем больше будет заметно расхождение границы объекта и освещённой области, особенно при наличии мелких объектов, а местами могут появиться совершенно неприемлемые артефакты:
Скрытый текст


Алгоритм формирования буфера:
  • заполнить значением радиуса источника света (расстояние, на котором сила освещения достигает нуля);
  • по каждому объекту, находящемуся в радиусе источника света, взять те рёбра, что повёрнуты к источнику света лицевой стороной. Если брать рёбра, повёрнутые тыльной стороной, объекты автоматически станут подсвеченными, но появится проблема с рядом стоящими:
    Скрытый текст

  • спроецировать полученный список рёбер, преобразуя их декартовы координаты в полярные источника света. Пересчитываем point(x; y) в (φ; r):
    φ = arccos( xAxis • normalize( point ) )
    где:
    • — скалярное произведение векторов;
    xAxis — единичный вектор, соответствующий оси x (1; 0), т.к. 0 градусов соответствуют правой от центра окружности точки;
    point — вектор, направленного из центра источника света в точку, принадлежащую ребру (координаты точки ребра в системе координат источника света);
    normalize — нормализация вектора;
    r = |point| — расстояние до точки;
    Проецируем две крайние точки ребра и промежуточные. Число точек, необходимых для пересчета, соответствует числу ячеек буфера, которые покрываются проекцией ребра.
    Вычисление индекса буфера, соответствующему углу φ:
    индекс = φ / ( 2*π ) * размерБуфера;
    Таким образом находим два крайних индекса буфера, соответствующих крайним точкам ребра. Для каждого промежуточного индекса, переводим в значение угла:
    φ = index * 2*π / размерБуфера
    , строим вектор отрезок из (0; 0) под этим углом длиной, равной или большей радиуса источника света:
    v = vec2( cos( φ ), sin( φ ) ) * радиус
    и находим точку пересечения полученного отрезка и ребра, например, так:
    • заданы 2 прямые с коэффициентами A1, B1, C1 и A2, B2, C2

    • воспользовавшись методом Крамера для решения этой системы уравнений получим точку пересечения:


    • если знаменатель равняется нулю (в нашем случае — если значение знаменателя по модулю меньше некоторого значения погрешности, ибо float), то решений нет — прямые либо совпадают, либо параллельны;
    • проверить на расположение полученной точки в пределах обоих отрезков.

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


1.2 Вертексный каркас


Теперь необходимо по данным в буфере глубины построить полигональную модель, покрывающую всю ту область, что освещает источник света. Для этого удобно использовать метод Triangle Fan

Полигон формируется из первой точки, предыдущей и текущей. Соответственно, первая точка — центр источника света, а координаты остальных точек:
  for( unsigned int index = 0; index < bufferSize; ++index ) {
    float alpha = float( index ) / float( bufferSize ) * Math::TWO_PI;
    float value = buffer[ index ];
    Vec2 point( Math::Cos( alpha ) * value, Math::Sin( alpha ) * value );
    Vec4 pointColor( color.R. color.G, color.B, ( 1.0f - value / range ) * color.A );
    ...
  }
  

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

1.3 Framebuffer


Достаточно одной текстуры, привязанной к фреймбуферу — формата GL_RGBA16F, такой формат позволит хранить значения за пределами [0.0; 1.0] с точностью half-precision floating-point.
Немного 'псевдокода'
    GLuint textureId;
    GLuint frameBufferObject;

    //текстура. width и height - размеры окна
    glGenTextures( 1, &textureId );
    glBindTexture( GL_TEXTURE_2D, textureId );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL );
    glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glBindTexture( GL_TEXTURE_2D, 0 );

    //фреймбуфер
    glGenFramebuffers( 1, frameBufferObject );
    glBindFramebuffer( GL_FRAMEBUFFER, frameBufferObject );

    //аттач текстуры к буферу
    glFramebufferTexture2D( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureId, 0 );

    //вернуть рендер на место
    glBindFramebuffer( GL_FRAMEBUFFER, 0 );

    //ну и на всякий случай, если что-то пошло не так...
    if( glCheckFramebufferStatus( GL_FRAMEBUFFER_EXT ) != GL_FRAMEBUFFER_COMPLETE ) {
      ...
    }
    ...
    


Биндим буфер, выставляем аддитивный бленд glBlendFunc( GL_ONE, GL_ONE ) и «рисуем» освещённые области. Таким образом альфа-канал будет накапливать степень освещённости. Так же можно добавить глобальное освещение, нарисовав квад во всё окно.

1.4 Шейдеры


Вертексные шейдеры отрисовки лучей от источников света стандартные, с учетом положения камеры, а во фрагментном шейдере накапливаем цвет с учетом яркости:
    layout(location = 0) out vec4 fragData;
    in vec4 vColor;
    ...
    void main() {
      fragData = vColor * vColor.a;
    }
  

В итоге мы должны получить нечто подобное:


2. Рендер сцены в текстуру


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


3. Совмещение карты освещения со сценой


Фрагментный шейдер должен быть приблизительно таким:
    uniform sampler2D texture0;
    uniform sampler2D texture1;
    ...
    vec4 color0 = texture( texture0, texCoords ); //чтение из текстуры отрендеренной сцены
    vec4 color1 = texture( texture1, texCoords ); //чтение из карты освещения
    fragData0 = color0 * color1;
  

Проще некуда. Здесь к цвету сцены color0 перед перемножением можно прибавить некоторый коэффициент на случай, если сеттинг игры крайне тёмный и необходимо видеть лучи света.
Скрытый текст
fragData0 = ( color0 + vec4( 0.05, 0.05, 0.05, 0.0 ) ) * color1;

И тут…

Если персонаж не описать простой геометрией, то тень от него будет весьма и весьма неправильной. Тени у нас строятся от геометрии, соответственно, тени от спрайтового персонажа получаются как от квадрата (хм, а Митбой, интересно, из каких соображений квадратный?). Значит текстуры спрайтов должны рисоваться максимально «квадратными», оставляя как можно меньше прозрачных областей по краям? Это один из вариантов. Можно геометрию персонажа описать более подробно, сгладив углы, но не описывать же геометрию для каждого кадра анимации? Допустим, сгладили углы, теперь персонаж почти эллипс. Если сцену делать полностью тёмной, то такая тень сильно бросается в глаза. Добавив сглаживание карты освещения и глобальное освещение картинка получается более приемлемая:
    vec2 offset = oneByWindowCoeff.xy * 1.5f; //степень размытости
    fragData = (
      texture( texture1, texCoords )
      + texture( texture1, vec2( texCoords.x - offset.x, texCoords.y - offset.y ) ).r
      + texture( texture1, vec2( texCoords.x, texCoords.y - offset.y ) ).r
      + texture( texture1, vec2( texCoords.x + offset.x, texCoords.y - offset.y ) ).r
      + texture( texture1, vec2( texCoords.x - offset.x, texCoords.y ) ).r
      + texture( texture1, vec2( texCoords.x + offset.x, texCoords.y ) ).r
      + texture( texture1, vec2( texCoords.x - offset.x, texCoords.y + offset.y ) ).r
      + texture( texture1, vec2( texCoords.x, texCoords.y + offset.y ) ).r
      + texture( texture1, vec2( texCoords.x + offset.x, texCoords.y + offset.y ) ).r
      ) / 9.0;
  

, где oneByWindowCoeff — коэффициент для пересчета пиксельных координат в тексельные.
При отсутствии глобального освещения, возможно, лучше отключать тени подобным «персонажам» либо их самих делать светящимися (идеальный, на мой взгляд, вариант), ну или заморочиться и описать геометрию объекта для всех анимаций.

Записал небольшую демонстрацию, что из всех этих размышлений и допиливаний вышло:


4. Оптимизация


Как говорится, «Сначала напиши, а потом уже оптимизируй». Первоначальный код был набросан быстро и грубо, поэтому мест для оптимизации хватило. Первое, что пришло в голову — избавиться от излишнего числа полигонов, отрисовывающих освещённые области. Если в радиусе источника света нет препятствий, то нет смысла рисовать 1000+ полигонов, настолько идеальная окружность нам не нужна, глаз просто не воспринимает разницы (или может это монитор у меня слишком грязный).
Например, для буфера глубины размерностью 1024 без оптимизации:
Скрытый текст

и с оптимизацией:
Скрытый текст

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

5. Заключение


Такая техника освещения не претендует на оптимальность, быстродействие и точность, целью был сам факт реализации. Есть разные методики, как, например, построение одних теней (освещение же, как понимаю, допиливается дополнительно), зато мягких, с обилием вычислений или даже такой крайне занятный, найденный уже в процессе написания статьи (в целом логика похожа на ту, что и я использовал).
В общем, что было в планах — то реализовано, объекты отбрасывают тени, необходимая гнетущая атмосфера в игре создана, и картинка стала более приятной на мой взгляд.

6. Ссылки


Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 39

    +1
    Какие тёмные получились блоки. Это так специально сцена построена? Потому как вижу, что кусты освещены нормально.
      0
      Да, те объекты, что отбрасывают тени, свет не пропускают (только грани освещаются за счёт того, что я на пару пикселей увеличивал лучи от источников света), на них действует только общее освещение. Над этим я в процессе раздумий как бы это по адекватнее сделать, не костылями. Если брать рёбра объектов, что тыльной стороной повёрнуты, эта проблема решается на ура, но появляется проблема обтрасывания теней на соседние блоки. Вобщем, надо определиться, какую из этих двух проблем решить менее ресурсоёмко и допилить.
        0
        На вскидку:
        1. Отрисовать блоки в стенсил буфер
        2. Отрисовать в карту глубины только тыльные грани со стенсилтестом (расширив эти грани на 1 пиксель относительно блока). Таким образом грани, находящиеся на соседних блоках не пройдут стенсил тест.
      +2
      Красота.
      Так и просится вместо Марио что-нибудь темное и фентезийное, в духе Baldur's Gate…
        +1
        А что это за игра на видео? (смесь марио и IWBTG?)
          0
          Это то, что сейчас я разрабатываю в свободное от работы время, в задумках нечто подобное Митбою и Гаю, с обилием смертей и нервов. Марио здесь просто ради примера, первая раскадровка анимации персонажа, что попалась в гугле, а потом уже и тестовый уровень набросал в таком же концепте. Концепта главгероя у меня пока нет, как здесь недавно отметили — быть дизайнером и писать код проще и красивее результат, чем быть программистом и рисовать, а рисую я совсем неочень =)
            0
            Используете какой-либо готовый движок и редактор или все самописное?
              0
              Всё самописное, кроме таких «низкоуровневых» библиотек, как pcre, jpeglib, lua, pnglib и zlib. Часть кода нагло позаимствовал из исходников DooM 3 — мат. библиотека, очень уж она мне понравилась, а возиться с векторами/матрицами самому уже не интересно, слишком много велосипедного кода. Думаю в будущем перевести на SDL для линукс-совместимости. В принципе, можно и самому, по сути вынести создание окна и обработку событий.
                0
                А, если не секрет, поделитесь, чем эта библиотека лучше glm? Или вы glm не рассматривали?
                  0
                  До сего момента не сталкивался с ней, надо будет посмотреть. Вообще, по большей части из-за того, что реализовано там всё максимально просто и по минимуму (для 2D мне этого с лихвой), а так же присутствуют такие штуки, как Sqrt16, Sin16, LengthFast и т.п. для более быстрых расчетов с потерей точности. Ну и сам Джон Кармак приложил к этому делу руки =)
                    0
                    А в 2D сейчас реально имеет смысл жертвовать точностью? Мне кажется, потери производительности тут основные не в арифметике с глобальными матрицами… Хотя это надо в конкретном случае рассматривать.

                    Мне glm нравится тем, что она по синтаксису максимально близка к GLSL.
              +3
              Невероятно, все один в один как у меня. Я тоже сейчас разрабатываю игру в стиле митбоя с обилием смертей и нервов. То, что у вас в профиле [О себе] один в один про меня, за исключением того, что я не веб-программист. И судя по некоторым фразам из статьи вы с геймдева, как и я
                0
                Просмотр «Indie Game: The Movie», думаю, многих инди-разработчиков вдохновил =) На геймдеве я есть, но как просто читатель, т.к. большинство вопросов решаются либо своей головой, либо чтением спецификаций, либо гуглом.
              0
              Я посмотрел видео, и вот прям захотелось поиграть в такого Марио, без обилия смертей и нервов.
              Может реализуете, если уже есть почти готовый вариант? Можно пару новых инструментов дать Марио: фонарик, осветительная ракета и т.п. Из пейзажев просится наверху ночной (с луной и звёздами — посветлее), а в подземелье и так темно, там можно какие-нибудь катокомбы нарисовать.
              +1
              А что за редактор вы в видео используете для манипуляций над объектами мира? Самописный?
                +1
                Самописный на Lua, благо в 5.2 есть неплохое подобие объектной модели (хотя, грубо говоря, это просто синтакический сахар) и довольно удобно реализуется GUI.
                +2
                Мне кажется вы вдохнули новую жизнь в Марио, а бы с удовольствием поиграл на темных сценах!
                  0
                  Лет так 10 назад, когда еще писал игрушки, подобным образом делал туман войны в простенькой стратегии.
                    0
                    Слишком четкая граница свет-тень, хотелось бы помягче.
                    Свет от Марио как-то странно смотрится, область видимости можно сделать проникающим светом с затуханием.
                    Тень от источника света слишком темная, она должна быть намного светлее.
                    В остальном круто :)
                      +9
                      Саня, не придирайся это поправить несложно, больше волнует — почему монетки не собираются!
                      0
                      Есть способы сделать тени «pixel perfect», например так github.com/mattdesl/lwjgl-basics/wiki/2D-Pixel-Perfect-Shadows
                        0
                        Как бы эта ссылка указана в статье
                        +4
                        Описанная техника все-таки называется Shadow Mapping. Тогда как упомянутый Deferred Shading (рус. отложенное освещение) — это нечто совсем иное.
                        Deferred Shading — это техника реализации освещения (shading), позволяющая отделить геометрическую сложность сцены от количества источников света. Подробности тут
                          0
                          Более того, в статье скорее смесь Shadow Mapping и Shadow Volumes (aka Stencil Shadows), поскольку в итоге явно рисуются освещенные области в виде геометрии.
                            0
                            Спасибо за поправку, что-то в терминологии я накосячил.
                          • UFO just landed and posted this here
                              0
                              Мне кажется что чем фигуры строить и рисовать по лампочке за отдельный проход, проще:
                              — отрисовать diffuse в текстуру на весь экран
                              — рисовать квад на весь экран
                              — передавать в шейдер массив источников, массив этих 2D-shadowmap-ов
                              — для каждой лампочки считать попадает ли в тень каждой лампы данный пиксель (я так понимаю для этого надо найти угол между источником, посмотреть по нему в текстуру, и сравнить с расстоянием до источника)
                              — сложить, помножить по вкусу
                              — взять diffuse и помножить тоже по вкусу

                              В общем-то так примерно 3D-игрушки и работают. Если не нужно всякие эффекты поверх картинки — можно вообще это сделать в один проход, рисуя не квад, а чисто спрайты через этот шейдер.
                                0
                                Красивая реализация.
                                У меня возникла мысль: а что если помимо обычного освещения в 2D еще и реализовать raytracing?
                                  +1
                                  От себя добавлю, что для пущего визуального эффекта «открытых пространств», стоит отрисовывать тени с учетом геометрии уровня.
                                  А то меня смущает тень от персонажа, падающая на фон (хотя я не знаю специфики, может быть там просто очень пыльно и туман?).
                                    0
                                    Думаю, я неудачно выбрал фон, смотрелось бы лучше если бы это было, например, подземелье, и свет от игрока освещал фоновые стены. А для открытых пространств фон, наверное, рисовать вообще без учета освещения и понапускать «тумана» в некоторых местах для видимости лучей, в общем, есть над чем подумать.
                                    0
                                    Для Starling (Flash, Stage3D) есть точно такая реализация: github.com/ekeeper/Starling-Extension-Lighting (может пригодится кому).
                                      0
                                      Использовать будем одну из самых распространённых технологий — deferred shading, портировав её в 2D.

                                      Суть deffered shading-а — построение geometry buffer-а, в котором каждый пиксель описывает геометрию сцены, и потом кучей источников света освещается. Так что общего с deffered shading-ом в статье ничего нет.

                                      В части 1.2 вы предлагате читать из шадоумапы на цпу? Т.е. ждать на ЦПУ когда ГПУ отрисует шадоумапу? А зачем вообще вам понадобился вертексный каркас? Почему нельзя в шейдере проверять затенен пиксель или нет, как в классической шадоумапе? Алсо если хочется волюмных теней — то быстрее будет построить волюмы без всяких рендеров прямо на цпу.
                                        0
                                        Буфер глубины заполняется на CPU, по нему строится каркас, поэтому ничего ждать не надо. Из видеопамяти читать вроде как плохой тон. Думаю, как здесь уже предложили, попробовать не строить каркас, а передавать сгенерированные буферы глубины в шейдеры и там всё рассчитывать. А с деферред-шейдингом да, накосячил, прошу прощения.
                                          0
                                          Тогда тем более не нужно никакого буфера гулбины. Реализуем любое дерево (loose quadtree/bounding volume hierarchy например). Складываем все блоки в него. Для каждого лайта выбираем блоки, до которых он может дотянуться и рейтрейсим в вершины этих блоков. Получаем набор лучей, которые сортируем по углу и собираем в тот самый trinagle fan. Это быстрее чем софтварный рендер в шадоумапу, никаких оптимизаций как вы в пункте 4 описали — не нужно, а на выходе получается даже оптимальнее, чем у вас в пункте 4.
                                            0
                                            Здесь есть одно но — у меня в планах сделать некоторые игровые элементы, зависящие от освещённости, как, например, датчики, реагирующие на свет и тому подобное, поэтому вообще всё на GPU перенести не получится.
                                              0
                                              Я выше как раз описал как софтварно делать и проще, и оптимальнее.

                                              Проверка же датчика, реагирующего на свет — должна делаться не так. Я уже подозреваю, что вы собираетесь делать проверку датчика в каждом треугольнике меша от каждого источника света. По факту вам нужно все ровно наоборот. Вам нужно от датчика пустить луч к источнику света. Если луч достигает источника — то датчик реагирует.
                                              Так что в любом случае самый оптимальный вариант — это дерево и рейкастинг без всяких там софтварных шадоумапов.
                                                0
                                                У него же кубики там. Чтобы посмотреть по каким кубикам проходит прямая, подойдет любой алгоритм рисования отрезка.

                                                Для движущихся объектов еще может быть имеет смысл строить деревья, если их много.
                                                  0
                                                  Проверить освещённость в текущей реализации не сложно просто «суммировав» нужные значения из уже сформированных буферов глубины, тем самым точно узнав коэффициент освещённости. А строить тени аналитически я не взялся, отбросил эту мысль почти сразу, весьма геморно это для динамических уровней с тысячами тайлов в пределах экрана (это в демке просто марио-стайл с парой десятков тайлов).
                                                0
                                                Полностью аналитически форму тени будет непросто построить, хотя бы из-за движущихся объектов, и изменения уровня налету (выбивание блоков а-ля марио, например).

                                                Можно попробовать заюзать stencil buffer а-ля doom 3, но что-то мне кажется что тут уже shadowmaps лучше будут.

                                          Only users with full accounts can post comments. Log in, please.