Уголок tool-программСтатьи

[Конкурс] Создаем генератор процедурных текстур

Автор:

Статья описывает создание генератора процедурных текстур, написана на конкурс, проводимый сообществом Tool программистов и сайтом GameDev.ru.

Многие из нас видели впечатляющие программы-демосцены, демонстрирующие разнообразную, часто фотореалистичную, графику и занимающие при этом на диске несколько килобайт. А вспомним известный 3D редактор Bryce, имея сравнительно небольшой размер, он предоставляет пользователю огромное, практически неограниченное количество разнообразных текстур. Где они хранятся? Как достигается такое невероятное сжатие? А вопрос поставлен неверно, эти текстуры нигде не хранятся, они ГЕНЕРИРУЮТСЯ. В этой статье и будет рассмотрен вопрос разработки подобных генераторов.

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

Я хочу предложить немного другой подход, он сложнее, но гораздо гибче. В первую очередь откажемся от стадий, будем работать сразу с переменным количеством изображений, накладывать их друг на друга в произвольный сочетаниях, а результат наложения засылать в произвольно выбранное изображение. Так же объединим понятия «фильтр» и «эффект наложения» в одно понятие «фильтр», в котором может участвовать переменное количество изображений, одно из которых будет приемником, а остальные источниками.

Далее мы будем работать не с одним типом изображений RGB или RGBA, как это делалось обычно, а с несколькими, причем основным типом будет обычный Float без делений на цветовые компоненты, а RGB, RGBA или GrayScale (Byte), будем применять в последнюю очередь уже перед созданием результирующего изображения.

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

Выберем ООП интерфейс и создадим пять объектов, через интерфейсы которых и будут проходить все обращения к программе, это:
«F» для работы с генераторами и фильтрами, приемниками в которых являются Float массивы.
«Gray» – приемником является монохромное изображение (массив байтов).
«RGB» – трехбайтовая структура.
«RGBA» – четырехбайтовая структура.
«TF» – от «Texture Factory», для общих команд и настроек.

В первую очередь нам понадобится процедура для начальной инициализации (описания массивов и выделения памяти под них), у нее будет такой синтаксис:

TF.Init Size, CntF, CntGray, CntRGB, CntRGBA

Здесь Size – размер, выбранный для изображений, например 256 (ограничим его степенями двойки, это сильно упростит работу). CntF, CntGray, CntRGB и CntRGBA – это количество массивов, создаваемых под данные соответствующего типа,  например:

TF.Init 512, 2, 0, 2, 0

Здесь создается два Float и два RGB массива размером 512*512.

Попробуем для начала повторить известный пример – создание облаков на основе шума Перлина. Для этого нам понадобится сам генератор шума в формате Float, процедура, переводящая величины из Float в Gray, и процедура для сохранения изображения Gray в файл. Мы могли бы обойтись без перевода Float в Gray, а сразу генерировать Gray шум, но в дальнейшем нам понадобится шум Перлина и для других целей, в том числе таких, где точности Byte недостаточно. Классический шум Перлина представляет из себя сумму гармоник, длина волны первой гармоники равна размеру изображения, следующая вдвое короче и т. д. Таким образом, в изображении 256*256 будет 8 гармоник, так как гармоника с длиной волны в один пиксель и меньше не имеет смысла (не видна). Сами гармоники формируются из случайных чисел, наиболее равномерным получается шум, если амплитуда каждой гармоники пропорциональна ее длине волны. Более подробно вопрос генерации шума Перлина представлен во многих других статьях, здесь мы не будем на этом останавливаться, просто выберем алгоритм, позволяющий регулировать уровень высоких и низких частот в спектре (гармоник) и инициализировать генератор случайных чисел:
F.GenNoise Index, LowSpectre, HighSpectre, Id

Здесь Index – номер Float массива приемника, LowSpectre – уровень низких частот, HighSpectre – уровень высоких частот, Id – идентификатор для инициализации генератора случайных чисел. Значения LowSpectre и HighSpectre от 0 до 1, 0.5 соответствует «нормальному» распределению частот.

Преобразователь Gray во Float выглядит просто:

Gray.FromF IndexDest, IndexSrc

Здесь IndexDest – номер байтового массива приемника, а IndexSrc – номер Float массива источника. Зачения Float просто умножаются на 255 с насыщением (ограничением минимальным значением 0 и максимальным 255) и округляются до ближайшего целого.

И сохранение в файл:

Gray.SaveToFile Index, FileName

Здесь Index – номер байтового массива, а FileName – имя файла. Программа TFScript работает только с TGA форматом.
Итак, наша первая программа:

TF.Init 512, 1, 1, 0, 0
F.GenNoise 0, 0.5, 0.5, Rnd
Gray.FromF 0, 0
Gray.SaveToFile 0, "test.tga"

На полученной картинке видны облака, примерно половина просто залита черным цветом. Почему? Потому, что сгенерированный шум может иметь как положительное, так и отрицательное значение, везде, где значения меньше нуля, в результате насыщения остается ноль. Чтобы картинка вся была покрыта облачностью, нужно чтобы диапазон значений шума был от 0 до 1. Для этого напишем новый фильтр – нормализация:
F.Normalize IndexDest, IndexSrc, Min, Max

Здесь, кроме индексов массивов источника и приемника, мы задаем минимальное и максимальное значения, к которым мы хотим привести значения элементов нашего массива. Математика этого фильтра проста:
1.  Сканируем массив и ищем минимальное и максимальное значения kMin и kMax.
2.  Рассчитываем два коэффициента:

kM = (Max – Min) / (kMax – kMin)
kA = Min – kMin * kM

3.  Все элементы массива умножаем на kM и добавляем к ним kA.

Теперь программа выглядит так:

TF.Init 512, 1, 1, 0, 0
F.GenNoise 0, 0.5, 0.5, Rnd
F.Normalize 0, 0, 0, 1
Gray.FromF 0, 0
Gray.SaveToFile 0, "test.tga"

Пока мы получали черно-белое изображение, но как из одноканального Float массива получить разные цвета? Один из вариантов – использование градиента. Напишем фильтр с таким синтаксисом:

RGB.GradientFromF IndexDest, IndexSrc, Col0, Col1

Как видно из названия, приемником здесь является RGB массив, источник, по-прежнему, Float, далее идут два значения цвета, Col0 попадет в приемник, когда значение в источнике равно нулю, Col1 – когда равно единице, для остальных значений источника результат будет линейно интерполироваться для диапазона от 0 до 1 и экстраполироваться вне этого диапазона. Экстраполяция, естественно, будет происходить с насыщением, как и при преобразовании в Gray.

Переписываем программу:

TF.Init 512, 1, 0, 1, 0
F.GenNoise 0, 0.5, 0.5, Rnd
F.Normalize 0, 0, 0, 1
RGB.GradientFromF 0, 0, &H7080FF, &HFFFFFF
RGB.SaveToFile 0, "test.tga"

Теперь память резервируется не под Gray, а под RGB массив, после генерирования и нормализации мы создаем градиент между голубым (&H7080FF) и белым (&HFFFFFF) цветами и сохраняем результат в файл. В программе TFScript использован VBScript, отсюда и написание шестнадцатеричных чисел.

Следующим фильтром, который мы напишем, будет дифференцирование. Не нужно пугаться страшного слова, дифференциал (производная) – это всего лишь отношение приращения функции к приращению аргумента. Аргумента у нас два – X и Y, это координаты. Чтобы продифференцировать массив (это и есть наша функция) вдоль оси X, нужно записать в каждый элемент приемника:

Dest(x, y)

величину:

(Src(x + 1, y) – Src(x – 1, y)) / 2

Если точка находится на краю массива, например X = 0, то вместо X – 1 подставляем значение с противоположной стороны массива (255 при размере массива 256*256). Точно так же можно дифференцировать вдоль оси Y, но нам нужно дифференцировать вдоль произвольного направления, поэтому введем два коэффициента – dX и dY, а в приемник будем заносить сумму дифференциалов, умноженных на соответствующий коэффициент. Синтаксис нового фильтра такой:

F.Diff IndexDest, IndexSrc, dX, dY

Преобразуем программу, используя новый фильтр:

TF.Init 512, 1, 0, 1, 0
F.GenNoise 0, 0.5, 0.5, Rnd
F.Diff 0, 0, 0.4, 0.7
F.Normalize 0, 0, 0, 1
RGB.GradientFromF 0, 0, &H0, &HFFFFFF
RGB.SaveToFile 0, "test.tga"

Изображение получило объем, теперь это уже не полупрозрачное облако, а монолит. Дифференцирование создало эффект бокового освещения, направление которого можно выбирать, меняя dX и dY.

Попробуйте увеличить диапазон при нормализации, например так:

F.Normalize 0, 0, –2, 2
Страницы: 1 2 Следующая »

14 сентября 2007 (Обновление: 11 сен 2017)

Комментарии [15]