Процедурное построение клеточных текстур
Использование шумовой функции Перлина noise (и полученных с ее помощью других функций) позволило построить широкий класс процедурных текстур (таких как, облака, мрамор, лава, огонь и т.п.).
Однако довольно часто встречается потребность в текстурах, которые должны иметь ярко-выраженный клеточный (cellular) характер. К числу таких текстур относится большое количество так называемых органических текстур, т.е. текстур, используемых для моделирования поверхностей живых существ.
Очень простой подход к созданию таких текстур был предложен Стивеном Уорли (Steven Worley) - одним из авторов книги "Texturing & Modelling. A procedural approach".
Ниже мы рассмотрим только двухмерную версию его подхода, хотя он легко переносится на случай произвольного числа измерений.
Разобьем прямоугольную текстуру (размером width на height текселов) на n1*n2 одинаковых ячейки. В каждой из них выберем некоторое количество случайных точек, равномерно распределенных в пределах соответствующей ячейки.
Рис 1. Распределение точек по ячейкам.
Удобно использовать не одно и то же постоянное количество точек на ячейку, а определять это число случайным образом исходя из желаемой средней плотности точек.
Проще всего для этого использовать распределение Пуассона - если через lambda обозначить среднюю плотность точек в ячейке, то вероятность того, что в ячейке окажется ровно m точек задается следующей формулой:
Поэтому можно перед генерацией точек по заданной средней плотности точек рассчитать вероятности того, что в ячейку попадет m точек для всех m=0,1,2. .
Для этого достаточно построить массив Sn сумм вероятностей для m=1. n. Фактически Sn является вероятностью того, что в ячейку попадет не менее Sn точек.
Тогда для каждой ячейки для получения числа точек m достаточно взять случайное число r, равномерно распределенное в отрезке [0,1] и найти первый такой номер m, что Sm >= r. Этот номер и будет числом точек в данной ячейке.
Построив таким образом массив случайных точек на в прямоугольнике [0,width-1]*[0,height-1], на нем можно определить функцию F1(x,y), как ближайшее расстояние от точки (x,y) до массива случайных точек.
Обычно на этом прямоугольнике вводят нормированные координаты, т.е. считают его совпадающим с единичным квадратом.
Рис 2. График функции F1(x,y).
Аналогичным образом можно определить функцию F2(x,y) как второе ближайшее расстояние от (x,y) до массива случайных точек, функцию F3(x,y) как третье ближайшее расстояние и т.д.
Рис 3. Графики функции F2(x,y) и F3(x,y).
Ясно, что всегда выполняется следующее неравенство
В большинстве случаев желательно, чтобы построенные таким образом функции (текстуры) F1, F2, F3 и т.д. были периодическими.
Периодичность для них означает, что значения на левом крае должны совпадать с соответствующими значениями на правом крае, а значения на верхним крае - совпадать со значениями на нижнем крае.
Этого легко добиться путем незначительного изменения функции, вычисляющей расстояние между двумя точками - в случае когда расстояние (по х или у) превышает половину соответствующего размера текстуры, то производится коррекция расстояния:
Обычно бывает удобным ограничить диапазон изменения расстояния некоторым отрезком [0,maxDist].
Клетки, для которых строились случайные точки, можно также использовать и для оптимизации вычисления функций F1, F2, F3 и др.
Для этого обратим внимание, что в большинстве случаев (кроме случая очень малой плотности точек), точки, на которых реализуются значения функций F1, F2 и F3, лежат либо в той же самой клетке, где и точка (x,y), или в одной из соседних с ней клеток.
Это обстоятельство позволяет значительно сократить перебор точек для вычисления функций F1, F2 и F3.
Именно этот подход и реализован в приводимой ниже программе на языке Python, вычисляющей значения функций F1, F2 и F3 и записывающих их в RGB-каналы текстуры.
Обратите внимание, что данный код использует библиотеку PIL для работы с изображениями.
В шейдерах можно использовать как сами базисные функции F1, F2, F3, так и различные их комбинации.
На следующих рисунках приводятся различные варианты процедурных текстур, получаемых при помощи базисных функций F1, F2 и F3.
Рис 4. Варианты комбинирования базисных функций.
Одним из простейших применений функции F1 может быть создание карты нормалей (bump map), соответствующей данной клеточной структуре. При этом сам расчет вектора нормали можно производить непосредственно в фрагментном шейдере.
На следующем рисунке функция pow(1-F1,2) используется как для определения цвета, так и для отклонения карты нормалей.
Рис 5. Пример использования клеточной текстуры.
Использованный при этом фрагментный шейдер на GLSL приводится на следующем листинге.
За счет комбинирования шумовой функции и базисных функций F1, F2 и F3 можно создавать анимированные, как бы "дышащие" текстуры (см. рис 6.).
Рис 6. Пример анимированной текстуры, использующей как базисные функции F1, F2 и F3, так и шумовую функцию noise.
Для получения изображения с рисунка 6 использовался следующий фрагментный шейдер.
Очень хороший результат дает применение следующей функции:
При достаточно большом значении параметра scale получается просто разбиение на плитки, что хорошо видно на следующем рисунке.
Ниже приводится соответствующий фрагментный шейдер
При этом параметр bias позволяет управлять размером зазора между плитками - увеличение этого параметра приводит к увеличению зазора.
На основе этого шейдера легко создать эффект разбитых плиток из какого-то материала.
Рис 8. Разбитые плитки.
Соответствующий фрагментный шейдер приводится ниже.
По аналогии с функциями turbulence и fBm можно ввести фрактальные версии базисных функций. Ниже приводится простейший вариант подобной функции:
Следующее изображение получено именно таким путем из функции F1.
Рис 8. Фрактальная версия функции F1.
Можно изменить характер получаемых текстур за счет изменения метрики, используемой для вычисления расстояния между точками.
Ранее использовалась следующая метрика:
Ниже приводятся еще два варианта вычисления расстояния между точками.
Ниже приводятся графики функции F1 для этих двух метрик.
Рис 9. Графики F1 для новых метрик.
Существует простой и красивый метод, позволяющий вычислять значение функции F1 прямо на GPU используя для этого рендеринг в текстуру.
Пусть у нас есть набор точек на плоскости P1,P2. Pn. Тогда для каждой такой точки Pi нарисуем квадрат с центром в этой точке. Размер квадрата выбирается таким образом, чтобы все точки, для которых Pi будет ближайшей, гарантированного накрывались этим квадратом.
При выводе такого квадрата будем использовать специальный фрагментный шейдер, который для каждого выводимого фрагмента вычисляет его расстояние до центра квадрата (точки Pi) и выдает это расстояние как глубину данного фрагмента.
В результате этого простое применение буфера глубины приведет к тому, что в этом буфере у нас окажется искомая функция F1.
Используя подход, описанный в статье про depth peeling можно аналогичным образом получить и остальные базисные функции - получение каждой следующей базисной функции требует одного прохода.
Таким образом всего за три прохода можно получить текстуру, содержащую первые три базисные функции, по заданному набору точек. Подобная схема в деталях описана в книге ShaderX3, в прилагаемом к статье исходном коде содержится вариант реализации подобного алгоритма.
По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows, Linux и Mac OS X.