Программист, геймдевелопер
0,3
рейтинг
20 января 2015 в 23:28

Разработка → Объемные планеты в 2D через шейдер

А помните, как вы просили меня про шейдеры написать? Помните? Нет? А вот я помню и даже написал. Милости просим, поговорим о прекрасном.

Сегодня я поведу речь о том, как я делал объемные вращающиеся планеты для нашей игры blast-off. Тоесть они, конечно, совершенно плоские, всего пара треугольников, но выглядят как объемные.



Заинтересовало? Прошу под кат. Картинок прилично.

Вводный текст


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

К чему это я? Ах да, мои эффекты тоже не претендуют на звание самых честных и правдоподобных. Строго говоря, мне совершенно плевать как будет правильно, лишь бы выглядело круто! При разработке того или иного эффекта я больше руководствуюсь соотношением его качества (нравится он мне или нет), а также скоростью его работы и сложностью исполнения. Это я говорю не в последнюю очередь для того, чтобы избежать споров. Быть может, если бы я не давал пояснений и исходного кода, вы врядли бы заметили мой обман «на глаз».

Вообще многие задаются вопросом — «а чего бы вам не нанять художника?» или «а почему не 3Д?». Ну серьезно, ну зачем? Мы с вами взрослые люди и знаем, что инди разработчики делают игры для удовольствия и делают их кропотливо. Холят и лилеят. И конечно же, очень любят велосипеды. Я не исключение, я их тоже люблю и переодически делаю. Тут должна была бы быть тирада о «псевдо-инди» разработчиках, маскирующихся под независимых и пытающихся срубить куш, но её я опущу. Итак, ты создаешь мир, ты творишь то, что хочешь и как хочешь. Ты отвественнен за каждый пиксель на финальной картинке и только ты в силах сделать его таким, каким он должен быть!

2D в полный рост


Сегодня я поведу речь о том, как я делал объемные вращающиеся планеты для нашей игры blast-off. Тоесть они, конечно, совершенно плоские, всего пара треугольников, но выглядят как объемные. Игра у нас полностью двумерная, так что вариантов реализации такого эффекта не сказать чтобы много. Вариант пререндерить много кадров для каждой планеты, а потом менять кадр за кадром, как это делали в старых играх, можно отсечь. Он громоздкий и не позволяет сделать, например, динамические облака, грозы на поверхности планеты или менять направление освещения. Тоесть, конечно, даёт, но для этого надо для каждого вида планеты делать просто кучу спрайтов. Разумеется, это не приемлемо.

Решение было очевидно. Нужно сделать все на лету, шейдером. У многих тут же возникнет мысль «ну нифига себе очевидно! Очевидно — это делать 3D для 3D планеты, разве нет?». Нет. Потому что игра у нас исключительно двумерная. Остаётся только вопрос как. Изменять геометрию текстуры достоверно — довольно громоздкий код, только если не использовать карту смещений. С картой смещений код становится проще и, как результат, намного быстрее.

К слову о карте смещений, так как она содержит в себе вектора, а каждая составляющая цвета всего лишь 8 бит, то несложно посчитать, что мы можем закодировать смещения не более чем на 128 пикселей. Конечно мы можем взять за расчет, что каждая градация это 2 пикселя, и тогда мы сможем кодировать смещения до 256 пикселей в каждую сторону. Однако в нашем случае нам хватало размера обычной карты смещений. Увеличение размера я компенсировал линейной фильтрацией в шейдере.
Тут надо оговориться, что на текущий момент мой движок quad-engine поддерживает только текстуры формата A8R8G8B8.

Конвеер


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

Дальнейшими шагами будет нанести атмосферу и затенение планеты. Это можно сделать уже без шейдера, однако и тут может применить шейдер, усложнив его, введя параметр точки освещённости. Я же обошёлся более простым решением с заранее нарисованными текстурами. Разумеется планета может быть, скажем, без атмосферы и без облаков. Планета может раскалываться на части и испускать свет по всей своей поверхности, иметь гейзеры и вулканы, быть покрыта городами или технологичными сооружениями. Вариантов для выбора масса.

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

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


Инструментарий


  • Delphi XE5 Starter — для написания кода
  • Quad-engine — для вывода графики
  • fxc.exe — для компиляции шейдеров в бинарный код
  • QuadShade — для редактирования кода шейдеров
  • FilterForge — для процедурной генерации текстур


Шейдер
float2 Velocity : register(c0);
sampler2D DiffuseMap  : register(s0);   
sampler2D DuDvMap     : register(s1);
 
float4 std_PS(vertexOutput Input) : COLOR {  
                                 
    float2 source = Input.TexCoord;      

    source.y -= 2.0 / 512.0;
    source.y = max(source.y, 0.0);

    source.x += 2.0 / 512.0;
    float4 right = (tex2D(DuDvMap, source) * 2.0) - 1.0;
                                
    source.x -= 4.0 / 512.0;                                 
    source.y = max(source.x, 0.0);
    float4 left = (tex2D(DuDvMap, source) * 2.0) - 1.0;
    
    
    source.y += 4.0 / 512.0;
    
    float4 left2 = (tex2D(DuDvMap, source) * 2.0) - 1.0;
    
    source.x += 4.0 / 512.0;
    float4 right2 = (tex2D(DuDvMap, source) * 2.0) - 1.0;
    
    
    float4 middle = right / 4.0 + left / 4.0 + right2 / 4.0 + left2 / 4.0;
    
    float2 coord = middle.rg;
    coord.x = coord.x / 4.0;
    coord.y = coord.y / 2.0;
    
    coord += Velocity;
                                                         
    float4 result = tex2D(DiffuseMap, coord);
    return float4(result.rgb, middle.a * result.a) * Input.Color;
}



От теории к практике


Начнем мы работать с сеткой. Сетка позволит нам понять насколько близко мы подошли к результату, который нас устраивает. Учтем, что длина текстуры в ширину должна быть вдвое больше, чем в высоту, так как у планеты обе стороны. Для стандарта берем текстуру разрешением 1024х512 точек.



Второй нужной текстурой станет та самая dUdV карта с закодированными векторами. Она у нас равна 512х512 точек. Тут размеры меньше. Почему? Потому что видим мы только квадратный кусочек сетки, искаженный шейдером. А значит нам нужно сказать шейдеру какой именно квадратный кусок сетки мы отдаем. Постепенно меняя квадрат со сдвигом, мы получим эффект вращения.



Накладываем шейдер и искажаем исходную текстуру сетки с помощью второй текстуры.



На скриншоте довольно четко видно зашумлённость черных линий. Они будто бы покрылись пиксельными волнами. Зрение вас не обманывает и проблема в ограничениях dUdV текстуры. Карта смещений имеет всего 256 градаций (как я писал выше), тоесть по 128 в плюс и в минус. Это ограничение текстур в формате A8R8G8B8. А размер текстуры 512 пикселей. Из за этого градиент получается ступенчатый, местами с повторяющимися пикселями. Как итог — картинка искажается в целом верно, но на пиксельном уровне содержит артефакты. Конечно, размер планеты и её текстура позволяют пренебречь этим визуальным артефактом, но обидно.

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

В dUdV карте у нас не 2, а 3 канала. RGB. Альфа гововорит шейдеру о форме планеты, сглаживании по краям и поэтому не рассматривается. R и G используются для кодирования смещения. Остаётся еще 1 байт пустой. Его можно было бы использовать, чтобы закодировать по дополнительных 4 бита на каждый цвет (8+4). Это позволило бы очень существенно увеличить точность и избежать любых проблем с мерцанием и искажениями. Очевидно, это следующий шаг на пути развития эффекта.

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



Меняем текстурку на поверхность планетны и начинаем играться уже с ней:



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



Скомбинируем облака с уже имеющейся планетой. Сделаем облака на 1.5% шире, чем планету, они же не очень низкие. И в итоге получим вот такое вот непотребство:



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



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



Совсем другое дело.

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



Вот. Почти красиво, но есть одно НО — на темной стороне планеты ничего не светится. Берем в руки карту свечения (также сгенерированную вместе с текстурой поверхности) и добавляем еще и её. Логично будет добавить свечение уже поверх всего, чтобы никакие тени не влияли на свет, верно?



Нет, не верно! Я бы сказал что это красиво и то, что надо, но облака (дым) не перекрывают свечение, значит последовательность слоёв неправильная.

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

Возможно это Марс? Ну или что-то подобное, но с атмосферой и огромнейшей взлетной полосой посередине? Нет, это не баг, как может показаться сначала, это такая текстура.



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

Ну и заодно надо бы баг убрать, который вылезает сверху и снизу от планеты. Давайте исправим, красивее будет.



Не планетами едиными


Создадим этим же методом шкалу для заряда электропушки или маны. Это уже как кому больше нравится. Для наглядности отрежем кусочек сверху, мы немного электричества уже потратили. Обязательно посмотрите видео, так как эта штука крутится и выглядит несколько иначе, нежели в статике.

Diablo3:



Наша реализация:



Выводы


Да чего уж тут. Шейдеры это круто. Вот и все выводы! Серьезно, без шейдеров сделать очень красивую и динамичную игру будет подобно каторге. Почти вся графика (кроме поверхности Земли) генерированная и вмешательства художника не требует. Это позволяет делать игру с минимальным привлечением людей извне.

Напоследок даю видео:

Илья Проходцев @Darthman
карма
61,2
рейтинг 0,3
Программист, геймдевелопер
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +2
    Просто и со вкусом, напомнило псевдо-3д планеты из Космических Рейнджеров. Укажите пожалуйста источник музыки из ролика и шрифт!
    • +7
      Шрифт моего авторства Quad regular. Музыка — один из треков к нашей игре blastoff авторства Егора Федотова www.egorfedotov.ru
  • +2
    У многих тут же возникнет мысль «ну нифига себе очевидно! Очевидно — это делать 3D для 3D планеты, разве нет?». Нет. Потому что игра у нас исключительно двумерная.

    Что значит «исключительно двумерная»? Вы не используете ни OpenGL, ни Direct3D для рисования?
    • 0
      Значит что всё, что у нас рисуется использует только 2д вывод графики. Direct3D, но в случае с этой планетой можете считать что это всего лишь 2 треугольника.
      У нас нет 3D моделей. Нет и не будет.
      • 0
        А почему не взять тогда что-нибудь вроде SDL?
        • 0
          Опс, с SDL отбой, это, оказывается, надстройка над OpenGL/Direct3D.

          Но вопрос остаётся, почему не взять библиотеку для двухмерной графики?
          • +2
            Сейчас все библиотеки для двухмерной графики — это надстройка над OpenGL и/или Direct3D. Потому что это основные API, с которыми работает железо.
          • 0
            Давайте уточним. Что есть библиотека для двумерной графики? OpenGL это библиотка для двумерной графики?

            В моем понимании я использую свою библиотеку для двумерной графики. Надстройку на Direct3D 9c quad-engine.com/
        • +2
          Минимально потому что его под Delphi нет и потому что это ничего абсолютно не даст. Какая разница чья надстройка над графическим API?
      • –1
        От того, что вы свои треугольники не хотите называть 3D-моделями, они ими быть не перестают.
        В любом случае, как мне кажется, можно обойтись без карты смещения и считать координаты налету, заодно и точность повысится. Карту-то вы поди не руками рисовали?
        • 0
          Для сферы и правда вынести аналитически dU, dV можно, но если захочется рисовать нечто сложнее, чем сферу, с каким-нибудь искажением, то аналитический вывод уже малоприменим.
        • 0
          Карта универсальное решение, которое позволит с формой или искажениями играться, к тому же шустро работает, как я и сказал в статье. Карта рисуется в граф редакторе легко и быстро свой градиентов и сферизацией результата.
  • +3
    Хе-хе, знакомые беседы. Я уже публиковал реализацию аналогичного спецэффекта, правда там я таки решил проблему с недостатком градаций, используя 16-битные координаты. ;)
    • –3
      Да, ваша статья мне тогда очень понравилась. Все по порядку и кодом не пожадничали, в отличие от автора данной статьи.
      • 0
        Напишите какого кода Вам не хватает, исправим.
        • 0
          Да код есть, не сразу увидела, прошу прощения!
          • 0
            Если нужен код самого бинарника, то я могу выложить. Проблема в том, что хабрасторейдж не дает класть архивы, а файлообменник привлекать не хочется. Так что если нужно, пришлите в личку почту, я скину исходники проекта.
            • +1
              Спасибо код уже у меня есть, правда только для IOS. Сделан на основе 2-х треугольников по аналогией с примером выше. В любом случае, интересно кто, что и как делает.
  • 0
    А насколько накладно рассчитывать карту смещения на-лету, в шейдере, чтобы избавится от ошибки округления?

    PS: забавлялся подобным эффектом на первых курсах института, используя MMX команды процессора.
    • +2
      А смысл её делать на лету? Если Вы знаете как надо исказить всё, то и искажать тогда уж сразу проще, зачем тогда в принципе карта. Но тут дело в другом. А если на поверхности планеты появится кратер? Или это будет звезда смерти? :) Искажения как считать будете? С картами всё можно адаптировать, с формулами уже все становится невероятно сложно.

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

      В моем случае затраты по времени очень хорошо сопоставимы с результатом. Да есть ошибки округления, но когда на экране крутится что-то движащееся и не во весь экран, Вы их практически не видите, а значит ими можно пренебречь.
      • 0
        А если на поверхности планеты появится кратер? Или это будет звезда смерти?

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

        И вообще, я просто поинтересовался, какая разница в производительности при расчёте сферической карты на лету и загрузке из текстуры (если вы их сравнивали, конечно). Например, для мобильных приложений каждый килобайт видеопамяти может быть на счету, тогда может быть на-лету считать будет выгоднее.
        • 0
          Промахнулся с ответом. Ответ см. ниже.
  • 0
    Согласен, тогда надо будет делать карту смещений так как сейчас делается сама планета.

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

    А сколько видеопамяти на современных мобильных устройствах?
  • +1
    Спасибо за статью!

    Подскажите, а насколько подход 1-2-несколько шейдеров для ОДНОГО ТИПА объектов оправдан, по вашему мнению?

    Да это дает нам возможность делать крутые визуальные штуки только за счет написания кода, без художников (ура!). Но ведь в дополнение мы получаем необходимость поддерживать\оптимизировать прорву шейдерного кода, который с одной стороны индивидуален для каждого типа объектов (для планет — один набор шейдеров, для кораблей другой, для космических мух — третий), с другой стороны многие куски кода будут повторяться (к прим. расчет освещенности от звезд). Также, переключение между шейдерами при отрисовке не очень дешевая операция, поэтому есть риски и по производительности.
    • 0
      Всегда пожалуйста! *улыбнулся*

      Не совсем понял вопроса, но попробую ответить. Надеюсь верно интерпретировал.

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

      Если речь идет о разных эффектах, то да, там без разных шейдеров не обойтись.
      Тонн кода нет, есть десяток-два шейдеров, этого достаточно чтобы покрыть почти всё, что нужно в 2д игре, как показывает практика. Программного кода гораздо больше, чем шейдерного.
      • 0
        да, вы правильно поняли. Проблема в том что мы либо пишем один универсальный «шейдер для всего», но тогда в нашем случае текстура dUdV не будет делать ничего в 90% случаев (для объектов отличных от планет), либо пишем отдельный «шейдер для планет», который рисуем только планеты
        • 0
          Как раз обратная ситуация. dUdV шейдер мы можем использовать еще много где. Например горячий воздух, линзы, и кто знает что еще. А вот если мы закодируем трансформацию в сферу внутри шейдера, то его мы только для создания шариков и будем использовать. dUdV является тут компромиссом. И достаточно универсально и достаточно качественно для данной задачи. В нашем случае планеты врядли будут по 512 пикселей размером вертеться в кадре.
  • +1
    Для тех кто любит планеты и пиксельные шейдеры пара маленьких и простеньких примеров (вдруг пригодится):
    пример1

    самый простой текстурный пиксельный рейдер
    пример2
  • 0
    Шейдер это хорошо, но технически это довольно просто повторить и просто 2D графикой, как и было в КР.
    habrahabr.ru/post/180353/#comment_6261603
    Конечно, годы идут, руки у людей все прямее и мой старый пример уже не смотрится без облаков, свечения и пр., но все же задумайтесь, вы пишете микропрограмму для 3D ускорителя, чтобы в 2D имитировать 3D!
    • +1
      А в чем проблема, если эта микропрограмма выполняется в GPU быстрее чем в CPU? Разгрузили CPU, получили больше возможностей для AI и прочих полезностей.
      • 0
        Именно проблем в нестандартных подходах я не вижу вообще, да и сам этим грешу. Но в этом конкретном случае подозреваю, что «чесное» 3D было для планет как минимум быстрее и читабельнее.
        • 0
          Ни разу :)
          Для честного 3D надо будет доделывать движок, чтобы он поддерживал это самое честное 3д. Затенение по фонгу делать, и кучу всего прочего. А для 2д и шейдеров уже все есть. Так что все просто и ооочень читаемо в коде.
          • 0
            Раз движок не поддерживает 3D, то это явно оправдано. Но если бы поддерживал, то интересно было бы сравнить скорость. Без «честного» света, конечно же, он тут не нужен — хватило бы простого шейдера с маской.
            • 0
              Не поддерживает, авторитетно как его автор заявляю. Целенаправленно не делалось 3д, чтобы не распыляться и не делать тонны ненужной работы.
  • 0
    А что с игрой? Она в процессе или уже опубликована?
    • 0
      Она в процессе разработки и сбора голосов на гринлайте.
  • 0
    Зацепило. Пойду Космических Рейнджеров скачаю. Nostalgie…
  • 0
    Выглядит классно. Тоже напомнило Космических Рейнджеров.
    Очень хочу научится работать с шейдерами, но к сожалению имею опыт только с#/java и код из статьи мне не совсем понятен, может кто подскажет уроки по данной теме?
  • 0
    Всех кого зацепило и кто не знает о ресурсе shadertoy.com — советую заценить, там целые raytracer'ы, pathtracer'ы и ray-marcher'ы пишут в фрагментном (пиксельном) шейдере и выставляют на всеобщий показ.
    P.S. Осторожно, нужен включенный WebGL, может тормозить.

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