Объемные планеты в 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:
Наша реализация:
Выводы Да чего уж тут. Шейдеры это круто. Вот и все выводы! Серьезно, без шейдеров сделать очень красивую и динамичную игру будет подобно каторге. Почти вся графика (кроме поверхности Земли) генерированная и вмешательства художника не требует. Это позволяет делать игру с минимальным привлечением людей извне.Напоследок даю видео:[embedded content]