Давайте напишем ... MMO! Часть 4: Прозрачность

Опубликовано NowhereMan - пн, 05/11/2020 - 14:13

15 декабря 2010

Текстуры, которые мы используем, задаются в красном, зеленом и синем каналах. Дисплей также поддерживает четвертый "альфа" канал, который может быть использован для реализации прозрачности. Библиотеки DirectX (и OpenGL) на самом деле поддерживают всевозможные правила для комбинирования новой текстуры с тем, что уже есть на экране, но, наиболее распространенным является линейное комбинирование умножением на альфа-значение.

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

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

Мы также используем прозрачность для рисования неба. В файле docs/textures/starpatch.jpg есть текстура (Рисунок 1), которую я вырезал из одного из изображений NASA Astronomy Picture of the Day.

Рис. 1: Кусочек звезд
Рис. 2: Узор брызг
Рис. 3: Очень звездное небо

Мы могли бы просто повторить это изображение по всему небу, но вы легко увидите повторы (глаз очень хорошо умеет улавливать такие вещи!) Поэтому вместо этого я разбрызгиваю его по всему небу наугад несколько сотен раз. Вращение тоже случайное, и еще я произвольно переворачиваю текстуру по горизонтали и вертикали (Рисунок 2). Это разбивает любую закономерность и создает очень звездное небо (Рис. 3).

На самом деле я использую это изображение в качестве альфа канала, а не RGB-каналов. В моем первом демо я использовал его для обоих. Я подумал, что было бы неудобно создать маску вокруг каждой звезды, чтобы правильное значение RGB было видно насквозь. Тогда я понял, что использование одного и того же изображения для RGB и альфа все равно неправильно. Если у тебя 50% серого цвета для бледной звезды, то используй 50% и для альфа, на самом деле ты получишь 25% серого на экране.

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

Изображения солнца и луны - это также текстуры с альфа-каналом. Для этих текстур я создал маску (см. Рис. 4), хотя, поскольку это монохромные изображения, я мог бы использовать тот же трюк, что и со звездами.

Рис. 4: Солнце и его альфа-канал

Z Буферизация

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

Рис. 5: Z-буфер

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

На рисунке 5 справа - изображение, которое мы рисуем. Слева - линия сканирования, которую мы видим сверху. Желтым цветом показаны значения, которые были записаны в Z-буфер при рисовании куба. Теперь, когда окружность (кусочек круглой головы аватара) нарисована, каждая точка проверяется на предмет того, что уже находится в Z буфере. Точки, находящиеся за тем, что уже записано в Z-буфер, пропускаются.

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

Мой демо-код этого не делает. Я регенерирую список треугольников (граней кубов, с двумя треугольниками на грань) в каждом цикле отображения. Я даже устраняю треугольники, обращенные назад, которые не будут нарисованы. Так мы делали это до того, как появились специальные графические процессоры. Как отметил комментатор Florian Bösch, было бы быстрее оставить треугольники, обращенные назад, и пусть об этом позаботится графический процессор. Имея полный список треугольников уже в видеокарте, мне вообще не пришлось бы их регенерировать, что сэкономило бы время.

Прозрачность и Z-буфер

Рис. 6: Прозрачное должно рисоваться от дальнего к ближнему

К сожалению, когда дело доходит до прозрачности, Z-буфер нам не поможет, потому что видеокарта не реализует "прозрачного" изображения. Вместо этого у нее есть правила для сложения нового треугольника с изображением на экране. Используя альфа-канал для их смешивания мы получаем видимость прозрачности. Но мы не можем рисовать треугольники в любом порядке.

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

К сожалению, это не всегда так просто, как кажется. На рисунке 7 мы имеем патологический случай. Это выглядит странно в двух измерениях, но представьте себе треугольники в трех измерениях, все наклоненные к вам, но перекрывающиеся с вашей точки зрения. Нет способа отсортировать этот набор треугольников. Любой выбранный вами порядок приведет к тому, что они будут нарисованы неправильно, так как красный находится поверх синего, который поверх зеленого, а зеленый - поверх красного...

Чтобы нарисовать все это правильно, нам надо разбить их на фрагменты по пересечениям и затем отсортировать и нарисовать эти фрагменты в правильном порядке. См. Рис. 8. Это нужно делать при каждом обновлении сцены, что медленно и сложно.

Рис. 7: Несортируемые треугольники
Рис. 8: Разбитые на фрагменты

Альфа-тестирование

Есть еще одна техника, которую мы можем использовать для прозрачных текстур, которая все еще использует Z-буфер и позволяет избежать сортировки. Она называется "Альфа-тестирование". При включенном тесте на дисплее будут отрисовываться только те пиксели, которые пройдут тест. Если мы установим в тесте значение "альфа больше нуля", то прозрачные пиксели не будут отрисовываться. Это означает, что в Z-буфер не будут записываться никакие точки, а также не будут блокироваться более поздние пиксели. Как будто мы нарисовали много маленьких непрозрачных фигур, а не одну большую прозрачную текстуру.

Рис. 9: Текст, нарисованный с помощью альфа-тестирования
Рис. 10: Сглаживание приводит к ошибкам

На рисунке 9 видно, что все правильно. Тэги с именами либо непрозрачны, либо прозрачны. Прозрачные пиксели не отрисовываются, поэтому можно рисовать в любом порядке. Z-буфер позволит позже нарисовать под тэгами другую графику (например, аватар).

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

Посмотрите вдоль края "О", где она пересекается с лицом аватара. Лестница пикселей, проходящая через глаз, является частью фонового рельефа, который был на экране, когда было нарисован тег с именем. Они были смешаны с пикселами тега, и все получили Z-буферное значение плоскости, содержащей тег. Когда позже был нарисован аватар, его пиксели считались "за" пикселами имени и не были нарисованы.

Рисование Прозрачных кубов

Рис. 11: Сортировка Octree

Как все это относится к демо?

У нас есть преимущество в связи с природой мира, который мы рисуем. Кубы мира не могут пересекаться, поэтому мы можем рисовать местность, не беспокоясь об этом. Сначала мы рисуем все непрозрачные кубы. Их можно хранить в памяти, чтобы ускорить процесс. Затем мы сортируем прозрачные кубы от дальних к ближним и рисуем их.

Кстати, так как наш мир - это все кубы, которые хранятся в Octree, нам не нужно их сортировать. У каждого узла Octree восемь потомков. В зависимости от того, где относительно центра находится глаз, существует порядок сортировки (см. рис. 11). Например, куб 4, включая всех его детей, находится позади куба 8 и его детей.

Существует только восемь возможных случаев (глаз может быть в одном из восьми секторов), и мы можем легко сделать выбор. Применяя этот тест к каждому узлу дерева при его пересечении, мы можем перебрать все кубы от дальних к ближним.

Обратите внимание, что это работает только потому, что мы храним кубы в дереве. Если у листьев дерева будут объекты, выходящие за пределы ячейки листа, то эта сортировка не сработает.

Так что я отсортировал свои прозрачные кубы, нарисовал их после непрозрачных кубов, и получил изображение на Рисунке 12.. Упс... Если вы помните из первой части, то каждая граница куба отмечена флагом, указывающим, видна ли она. Сторона, которая касается другого куба, не видна и не нарисована. Это означает, что все кубы под водой были выключены... и поэтому, когда вода становится прозрачной, под ней ничего нет.

Рис. 12: Боже мой, он полон звезд!

Мы можем исправить это, переопределив правило видимости. Теперь лицо куба видно, если оно соприкасается с воздухом, или прозрачным кубом. Это правило дает нам Рисунок 13. Ой, опять! Так как кубы воды касаются других прозрачных кубов (больше воды), их стороны видны.

Рис. 13: Недостаточно прозрачный...

Поэтому мы изменяем правило, чтобы сделать сторону видимой, если она касается прозрачного куба другого типа (или воздуха). Это дает нам изображение на Рисунке 14, которое мы и хотим.

Рис. 14: Успех!

Чтобы проверить, что все нарисовано в правильном порядке, я построил несколько полупрозрачных кубиков и сложил их друг перед другом. (см. Рис. 15) Сначала я подумал, что они выглядят правильно, но потом понял, что в кубах нет ни задних, ни верхних сторон. Это потому, что я до сих пор устраняю поверхности кубиков, которые направлены в сторону от глаз. Это оптимизация при рисовании непрозрачных кубов, но это меняет ситуацию при рисовании прозрачных кубов (см. рисунок 16).

Рис. 15: Односторонние поверхности кубов
Рис. 16: Двусторонние поверхности кубов

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

Минекрафт не рисует воду или стекло с обратными сторонами (рис. 17.) Когда он рисует блоки листьев, чтобы составить дерево, выглядит так, как будто он также пропускает обратные стороны. Тем не менее, я думаю, что он включает внутренние стороны, когда два блока листьев соприкасаются, в отличие от воды или стекла. На Рис. 18, блок в центре явно не имеет ни верхней, ни дальней стороны, но имеет другую сторону, нарисованную позади него, от следующего блока. Предположительно, он делает это, чтобы листьев казалось больше. Если бы не были включены внутренние поверхности (как в случае со стеклом или водой), деревья выглядели бы как большие шары с нарисованными на них листьями.

Рис. 17: Стекло одностороннее
Рис. 18: Листья странные

Это все на сегодня. В следующей части нам потребуется дополнительная инфраструктура. Нам нужен графический интерфейс.

Демо

Новое демо The Part 4 Demo. Оно было протестировано на Windows 7, 32-bit и 64-bit, и Windows XP, 32-bit.

Так как у нас теперь есть экран помощи, просто нажмите F1 за помощью. Нажмите ESC, чтобы выйти из демо.

Мы не делаем ничего амбициозного с графикой, так что если вы можете запустить любую 3D игру, вы должны быть в состоянии запустить демо. Если вы получаете сообщение об ошибке об отсутствии "d3dx9_42.dll", вам нужно обновить версию DirectX.

Исходники

Качайте архив The Part 4 Source с C++ кодом, дорожную карту к исходникам и каталог сборки. Сюда входит исполняемая демо-версия и необходимые файлы.