netlib.narod.ru | < Назад | Оглавление | Далее > |
Джон Декстер (John Dexter)
В этой статье я познакомлю вас с картами высот, а затем покажу основные способы раскрашивания и текстурирования созданных на основе карт высот ландшафтов. Я попытаюсь показать слабые стороны этих методов, прежде чем перейти к рассмотрению системы, позволяющей использовать реалистичные текстуры с высокой степенью детализации для множества различных типов поверхности.
Ландшафт на основе карты высот (heightmapped terrain) обычно представляется в виде двухмерной сетки точек у каждой из которых есть своя высота. Рисуется такой ландшафт путем визуализации трехмерных треугольников, использующих эти вершины:
В данном примере у нас есть сетка 4 × 4 — в ней 16 точек и 9 блоков (tile). Обратите внимание, что в сетке N × M будет (N–1) × (M–1) блоков. Хотя представленная на рисунке сетка выглядит как двухмерная мозаичная поверхность, у каждой вершины есть высота, и для каждого блока мы рисуем два треугольника, на что указывают диагональные линии. Я помещаю начало координат (точку с координатами 0, 0) в нижнем левом углу сетки, при этом координата X увеличивается вправо, а координата Y — вверх. Я всегда разделяю квадраты как показано на верхнем рисунке, но вы можете использовать другой способ разделения:
Такое представление ландшафта делает его визуализацию очень простой — вы просто рисуете каждый квадрат, или пару треугольников, с соответствующей высотой. Это также упрощает создание и редактирование карты высот. В качестве источника данных о высоте вы можете использовать изображение и, следовательно, создавать ваш ландшафт в Microsoft Paint! Также несложно создать редактор для установки данных точек. Визуализация такой карты высот приведет к следующему результату:
Затем, скорее всего, вы захотите сделать ландшафт более реалистичным, добавив к нему цвета и текстуры. Проще всего применить цвета. Вы можете назначить цвет каждой вершине, или объединить их в текстуру, которая будет растягиваться для покрытия всей карты. Эти варианты требуют минимальных знаний DirectX/OpenGL и хорошо подходят для начальной стадии проекта, но никогда не позволят создать действительно интересную графику.
Хотя на дальних расстояниях ландшафт выглядит хорошо проработанным (на рисунке карта раскрашена с разрешением 1 × 1 м), недостаток мелких деталей делает передний план унылым, и вам сложно понять, что именно на нем изображено. Есть еще один небольшой шаг, который вы можете сделать, чтобы ваш ландшафт выглядел более интересно — добавление детализированных текстур...
Давайте предположим, что у вас есть визуализатор, который рисует полигоны для карты высот и также накладывает текстуру на всю карту целиком. Соглашения о размерах означают, что текстура может хранить только базовую информацию о цвете — если у вас есть карта высот размером 1000 × 1000, представляющая область площадью 1 кв. км, то текстура размером 4096 × 4096 дает разрешение 0.25 метра на пиксель текстуры, что явно недостаточно для отображения мелких деталей. Так что мы будем использовать эту текстуру только для задания общего цвета — зеленого для растительности, коричневого для земли и т.д. Теперь, чтобы получить детали, мы накладываем на карту другую текстуру. Но мы не будем растягивать ее на всю карту — мы повторим ее много раз. Эта текстура может выглядеть как шум на экране телевизора — черно-белое изображение из точек со случайно выбранной яркостью. Если просто покрыть ландшафт такими блоками, мы получим весьма детализированную картину ближайшей местности, но картина в целом будет выглядеть очень однообразно. Однако, если мы применим ее вместе с текстурой, задающей цвета, то получим ландшафт с крупномасштабными деталями, представляющими грязь, траву, снег и т.д, и в то же время обладающую детализацией близких объектов, так что глазу будет за что зацепиться. Эта техника часто встречается в примерах визуализации ландшафта, поскольку она достаточно проста, и может служить хорошей вехой в разработке — первое по-настоящему интересное представление вашего ландшафта, особенно при наличии соответствующего освещения и теней. Но если вы сделали визуализатор ландшафта по этим рекомендациям, то вскоре заметите, что он все еще не слишком впечатляющ, что показывает следующая иллюстрация:
Несколько дополнительных объектов, таких как машины, дома и т.д, могут скрыть это от зрителя, но тот факт, что все ваши типы поверхности имеют одно и то же представление, не способствует реалистичности. И пусть старые игры делали вещи, подобные этой, окрашивая отдельные биты карт в коричневый или зеленый цвет, сейчас это никого не одурачит! Нам необходимо изменение способа визуализации близко расположенных типов поверхностей (травы, песка...), а не просто смена базового цвета.
Вообще-то довольно очевидно, в чем мы нуждаемся. Каждый тип поверхности (ground type) — такой как трава номер 4, грязь номер 23, шоссе и т.д. — нуждается в собственной детализированной текстуре, похожей на моделируемый материал. Такая текстура может быть черно-белой и смешиваться с общей большой текстурой, задающей цвет отдельных фрагментов, или можно не заморачиваться этим и просто создать несколько высокодетализированных цветных текстур для различных типов поверхности, которые вы хотите иметь на вашей карте. Вопрос в том, как представлять какому фрагменту карты какой тип поверхности соответствует, а очевидный ответ заключается в том, что эта дополнительная информация должна храниться в данных вершины.
Как и в первых попытках, кажется логичным продолжать подход с массивом двухмерных блоков и установить для каждой вершины тип поверхности.
На данной иллюстрации при текстурировании каждого блока используется тот тип поверхности, который указан для вершины, являющейся его левым нижним углом: блоки (0, 0), (2, 0) и (1, 2) используют зеленую поверхность, блоки (1, 0), (0, 2) и (2, 2) используют синюю поверхность, а блоки (0, 1), (1, 1) и (2, 1) используют желтую поверхность.
Такая система лишь слегка сложнее для реализации, чем рассмотренное ранее текстурирование деталей, хотя при определении того, в каком порядке рисовать объекты, возникают проблемы с производительностью. К сожалению, результат выглядит, возможно, даже хуже чем вариант с одной текстурой детализации. Ниже приведен снимок экрана, иллюстрирующий данный метод:
Хотя мы и получили выгоду от более реалистично выглядящих текстур поверхности (несмотря на низкое качество иллюстрации, вы можете явно отличить серую гальку от других текстур), недостатком являются отчетливо видимые переходы между различными типами поверхностей. Прежде чем мы посмотрим, что я сделал для того, чтобы обойти эту проблему, взгляните на снимки экрана для той же сцены в текущей версии моего редактора:
Ясно, что границы между различными типами поверхностей выделяются ужасающей пикселизацией. Если текстуры детализации черно-белые, то цветовые данные могут слегка улучшить ситуацию, обеспечив гладкое смешивание — вы получите только резкую смену шаблона, а не и цвета тоже, — но, поверьте мне, это также неприемлемо. Нам требуется способ получения плавного перехода между различными шаблонами и цветами.
После некоторых исследований в Сети я нашел только одно реальное решение. Вместо того, чтобы рисовать блок с одной текстурой поверхности, каждая вершина должна «знать» какое количество каждого типа поверхности в ней используется. Например, 30% травы, 10% песка и 60% грязи. В базовой интерпретации все полигоны поверхности на экране рисуются по одному разу для каждого типа поверхности, и альфа-значение вершины устанавливается в соответствии с тем, сколько процентов рисуемого типа поверхности задано для вершины. Возможен альтернативный подход, когда для каждого типа поверхности существует одна альфа-текстура — эта альфа-текстура растягивается на всю карту и определяет сколько от данного типа поверхности должно быть в различных местах. Поискав вы обнаружите снимки экранов, иллюстрирующие данную технику, и по общему признанию они замечательно выглядят. Но мне эта система не слишком нравится...
Как я уже говорил, есть два способа определять сколько текстуры поверхности рисовать: хранение общей растягиваемой на всю карту альфа-текстуры для каждого типа поверхности и запись альфа-значений для каждого типа поверхности в данные вершины. Альфа-текстуры хороши тем, что вы просто устанавливаете детализированную текстуру и соответствующую ей альфа-карту и рисуете все треугольники, повторяя данный процесс для всех типов поверхности. Очень легко для кодирования. Однако, вы визуализируете каждый треугольник по одному разу для каждого типа поверхности, используемого на карте. Обучающие и демонстрационные программы, которые я встречал, обычно используют для карты только четыре текстуры (обычно трава, песок, снег и камни), что не слишком ужасно, но даже в этом случае примите во внимание, какая часть площади вашей карты требует, чтобы на ней одновременно рисовалось более одного типа поверхности. Кроме того, я не могу представить хороший уровень игры, в котором используются только четыре типа поверхности. Замечательно выглядящую демонстрацию визуализации ландшафта — да, а настоящую игру — нет. В моем проекте (гонки) мне нужны по крайней мере следующие типы:
Пыль
Грязь
Трава
Гравий/галька
Песок
Снег
Асфальт
Бетон
Вполне вероятно, что для своего проекта вы измените некоторые пункты списка, но даже в очень консервативном наборе для ландшафта число текстур будет где-то 16. Визуализировать каждый полигон вашего ландшафта 16 раз просто глупо. Конечно, вы можете разделить вашу карту на фрагменты и указывать, какие типы поверхностей в каком фрагменте используются. Тогда вы сможете видеть, какие типы поверхностей действительно присутствуют в сцене, которую вы собираетесь визуализировать. Но все равно, вам придется визуализировать каждый треугольник 3–5 раз, хотя большинство из них используют только одну текстуру. Также учтите большой расход памяти в этом методе. Предположим, у вас есть карта размером 2 × 2 километра, и вы хотите моделировать ее с разрешением 1 метр. Вам понадобится альфа-текстура размером 2048 × 2048 для каждого типа поверхности (2048 потому что это 211, а всегда лучше использовать текстуры, размеры которых являются степенями двойки), что дает 4 Мб на текстуру. Для нашего набора из 16 типов поверхности это 64 Мб альфа-текстур. В наши дни вы можете использовать сжатые текстуры, но на картах нижнего ценового диапазона все равно будет расходоваться большая часть их ресурсов, ведь текстуры детализации и модели тоже нуждаются в месте для хранения. И, как я сказал, 16 текстур и карта размером 2000 × 2000 это весьма консервативное решение — я бы хотел иметь 50 типов поверхностей и карты размером до 4000 × 4000, что дает 800 Мб несжатых данных текстур!
Вместо этого вы можете хранить альфа-значение для каждого типа поверхности в данных каждой вершины. Чтобы сэкономить память и сократить количество визуализируемых полигонов каждая вершина может говорить, сколько типов поверхностей она использует и каков вклад каждого типа в общий результат. Вы можете также задать максимальное количество типов поверхностей, которое может использовать одна вершина. Однако в этом случае вам придется выполнить несколько зачастую бесполезных проверок для каждого типа поверхности и каждой вершины. Тем не менее именно этот подход показался мне наилучшим для адаптации и упрощения в моих целях...
Я решил создать собственный метод визуализации ландшафта, свободный от проблем с которыми обычно сталкивались известные мне методы. Вот исходные требования для него:
Простота создания и редактирования уровней
Возможность поддержки многих типов поверхностей (до 100 и более)
Небольшой расход памяти
Мы начали с реализации идеи о нескольких типах поверхностей путем указания типа поверхности для каждой вершины карты высот, и я вернулся, чтобы взглянуть на нее еще раз.
Мы уже видели эту иллюстрацию назначения типа поверхности для каждой вершины. Хотя типы поверхностей и рисуются как хотелось бы, границы между различными типами пикселизированы и прерывисты. Однако, каждый квадрат рисуется с использованием только одного типа поверхности — смешивания нет вообще. Вместо этого мы при рисовании каждого треугольника можем просмотреть типы поверхности всех трех, образующих его вершин. В теории это очень просто — если мы работаем с типом поверхности X, то для каждого полигона ландшафта мы устанавливаем альфа-значение равным 1.0 (или 0xFF или 255, в зависимости от вашего представления цветов) для вершин, использующих тип поверхности X, или равным 0 для вершин, не использующих данный тип. Конечно, мы не хотим рисовать те треугольники, которые не используют тип поверхности X ни для одной из своих вершин, и процесс решения, какие полигоны рисовать, требует размышлений и работы, чтобы получить быструю визуализацию. Фактически, это сокращенная версия хранения данных о том, сколько каждого типа поверхности использует вершина, ограничивающая каждую вершину только одним типом поверхности, но эти упрощения значительно сокращают объем работы центрального процессора, который душит видеопроцессор. Также возможно выполнить предварительную генерацию данных при запуске, что еще больше сократит объем работы за счет возросшего расхода памяти. Я не буду углубляться дальше, поскольку это приведет к некоторым зависящим от аппаратуры вопросам, не являющимся целью обсуждения. Абсолютно неоптимизированная, но простая реализация этой идеи может выглядеть так:
for(i=0 to numGroundTypes) { SetGroundTypeTexture(i); for(x=0 to mapWidth) for(y=0 to mapHeight) { GT_BL=GetGroundType(x,y); Alpha_BL=(GT_BL==i) ? 1 : 0 GT_BR=GetGroundType(x+1,y); Alpha_BR=(GT_BR==i) ? 1 : 0 GT_TL=GetGroundType(x,y+1); Alpha_TL=(GT_TL==i) ? 1 : 0 GT_TR=GetGroundType(x+1,y+1); Alpha_TR=(GT_TR==i) ? 1 : 0 if(Alpha_BL || Alpha_TL || Alpha_TR) DrawTriangle(x,y,Alpha_BL, x,y+1,Alpha_TL, x+1,y+1,Alpha_TR); if(Alpha_BL || Alpha_BR || Alpha_TR) DrawTriangle(x,y,Alpha_BL, x+1,y+1,Alpha_TR, x+1,y,Alpha_BR); } }
Следующая проблема, перед которой я оказался — получить одну и ту же интенсивность цвета для каждой части визуализируемого треугольника. Представим треугольник, чьи вершины используют типы поверхности A, B и C — проблема возникает из-за способа, которым альфа-значения интерполируются внутри треугольника.
Отметим на треугольнике середину каждой стороны и геометрический центр треугольника. Треугольник рисуется три раза и цвета пикселей суммируются для получения конечного результата. В каждом проходе для обной вершины альфа-значение равно 1, а для двух других — 0. Альфа-значения интерполируются, чтобы решить сколько от каждого типа поверхности будет в действительности добавлено к пикселю. Если пиксель получает тип поверхности A, визуализируемый с альфа-значением 0.7, B с 0.1 и C с 0.2, то сумма альфа-значений равна 1 и пиксель будет правильно окрашен. С другой стороны, если пиксель получает типы поверхностей A, B и C, визуализируемые с альфа-значением 0.2 каждый, итоговый пиксель будет очень темным. Из-за способа визуализации треугольников в их центре будет возникать данный эффект. В результате вы получите ландшафт с повторяющимися черными пятнами, что выглядит очень уродливо.
Мое решение заключается в первоначальной визуализации ландшафта вообще без смешивания, используя одну вершину каждого треугольника в качестве источника информации о типе поверхности. Затем поверх мы в случае необходимости рисуем прозрачные треугольники, но они объединяются с первым слоем путем использования альфа-смешивания, а не простым сложением. Если треугольник использует один и тот же тип поверхности для всех вершин, ничего не произойдет. Если одна из вершин использует другой тип поверхности, то мы, используя этот тип поверхности, рисуем другой треугольник, у которого соответствующая вершина будет непрозрачной, а две другие — прозрачные. Небольшой фрагмент псевдокода прояснит для вас процесс:
for(i=0 to numGroundTypes) { SetGroundTypeTexture(i); RenderOpaqueTriangles(i,VertexBuffer); } for(i=0 to numGroundTypes) { SetGroundTypeTexture(i); RenderTransparentTriangles(i,VertexBuffer); }
Вот как выглядит пример такого рода смешивания:
На изображении показан фрагмент ландшафта, для одной вершины которого в качестве типа поверхности указана трава — обратите внимание на плавные переходы.
Причина, по которой мне потребовались два прохода, в том, что простая установка альфа-значений вершины не обеспечивает правильного распределения интенсивности цвета по поверхности треугольника. Нет никаких причин, по которым альфа-значения должны просто интерполироваться из вершин таким грубым способом. Вместо этого разумно предположить, что мы могли бы создать альфа-текстуру, гарантирующую, что каждый пиксел треугольника получит точно необходимое количество каждого типа поверхности. Для этого может потребоваться несколько текстур, и обычно использование другого блока текстуры может вызвать проблемы. Однако, даже если это не даст видимого улучшения, метод означает, что нам не придется дважды в цикле перебирать все типы поверхностей, что сокращает количество смен текстуры — значительное улучшение производительности. Однако, это только теория, и я не делал никаких попыток реализовать ее, — если кто-то желает попробовать, или имеет другие предложения, мне было бы очень интересно услышать об этом!
Вот и все, о чем я хотел рассказать здесь. Я намеренно не стал углубляться в специфические детали и не приводил код, поскольку хотел поговорить об алгоритме, а не о реализации. Не стесняйтесь задавать любые вопросы относящиеся к изложенному здесь материалу по адресу webmaster@johndexter.co.uk.
netlib.narod.ru | < Назад | Оглавление | Далее > |