Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Использование шумовой функции Перлина 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 и т.д. были периодическими.
Периодичность для них означает, что значения на левом крае должны совпадать с соответствующими значениями на правом крае, а значения на верхним крае - совпадать со значениями на нижнем крае.
Этого легко добиться путем незначительного изменения функции, вычисляющей расстояние между двумя точками - в случае когда расстояние (по х или у) превышает половину соответствующего размера текстуры, то производится коррекция расстояния:
def distance ( p1, p2 ): # L2 distance function dx = abs ( p1 [0] - p2 [0] ) dy = abs ( p1 [1] - p2 [1] ) if dx > 0.5: dx = 1 - dx if dy > 0.5: dy = 1 - dy d = math.sqrt ( dx*dx + dy*dy ) / maxDist if d > 1: d = 1 return d
Обычно бывает удобным ограничить диапазон изменения расстояния некоторым отрезком [0,maxDist].
Клетки, для которых строились случайные точки, можно также использовать и для оптимизации вычисления функций F1, F2, F3 и др.
Для этого обратим внимание, что в большинстве случаев (кроме случая очень малой плотности точек), точки, на которых реализуются значения функций F1, F2 и F3, лежат либо в той же самой клетке, где и точка (x,y), или в одной из соседних с ней клеток.
Это обстоятельство позволяет значительно сократить перебор точек для вычисления функций F1, F2 и F3.
Именно этот подход и реализован в приводимой ниже программе на языке Python, вычисляющей значения функций F1, F2 и F3 и записывающих их в RGB-каналы текстуры.
import math, random, PIL, Image width = 256 # image size height = width cellSize = 8 # 8 cells per width(height) density = 4.0 # point density maxDist = 0.11 k1 = 1.0 / float ( cellSize ) k2 = 1.0 / float ( cellSize ) # compute probabilities for verious # numbers of points pointProbs = [] mFact = 1.0 # m! lPower = 1.0 # pow ( density, m ) s = 0.0 for m in range ( 10 ): p = lPower * math.exp ( -density ) / mFact s = s + p mFact = mFact * (m + 1) lPower = lPower * density pointProbs.append ( s ) def numPointsForCell (): r = random.random (); for i in range ( 10 ): if r < pointProbs [i]: return i return i def distance ( p1, p2 ): # L2 distance function dx = abs ( p1 [0] - p2 [0] ) dy = abs ( p1 [1] - p2 [1] ) if dx > 0.5: dx = 1 - dx if dy > 0.5: dy = 1 - dy d = math.sqrt ( dx*dx + dy*dy ) / maxDist if d > 1: d = 1 return d im = Image.new ( "RGB", (width, height) ) points = [] # generate random points cells = [] for i1 in range ( cellSize ): cells.append ( [] ) for i2 in range ( cellSize ): cells [i1].append ( [] ) x1 = i1 * k1 y1 = i2 * k2 numPoints = numPointsForCell () for i3 in range ( numPoints ): x = x1 + k1 * random.random () y = y1 + k2 * random.random () points.append ( ( x, y ) ) cells [i1][i2].append ( ( x, y ) ) # create textures F1, F2, F3, F4 for x in range ( width ): for y in range ( height ): d = [] pt = ( float ( x ) / float ( width ), float ( y ) / float ( height ) ) # cell of this point i1 = (x * cellSize) / width j1 = (y * cellSize) / height cellList = [(i1, j1)] # create candidate list of cells for i in range ( -1, 2 ): # add every neighbouring cell for j in range ( -1, 2 ): i2 = i1 + i j2 = j1 + j if i2 < 0: i2 = cellSize + i2 if i2 >= cellSize: i2 = i2 - cellSize if j2 < 0: j2 = cellSize + j2 if j2 >= cellSize: j2 = j2 - cellSize cellList.append ( ( i2, j2 ) ) for (i,j) in cellList: for p in cells [i][j]: d.append ( distance ( p, pt ) ) d.sort () # now take four closest distances and map to colors # excluding duplicates fs = [] j = 0 for i in range ( 4 ): if j >= len ( d ): j = len ( d ) - 1 v = d [j] j = j + 1 fs.append ( v ) while j < len ( d ) and d [j] == v: j = j + 1 red = int ( 255 * fs [0] + 0.5 ) green = int ( 255 * fs [1] + 0.5 ) blue = int ( 255 * fs [2] + 0.5 ) im.putpixel ( (x, y), (red, green, blue) ) im.save ( "celltex.png", "png" )
Обратите внимание, что данный код использует библиотеку PIL для работы с изображениями.
В шейдерах можно использовать как сами базисные функции F1, F2, F3, так и различные их комбинации.
На следующих рисунках приводятся различные варианты процедурных текстур, получаемых при помощи базисных функций F1, F2 и F3.
Рис 4. Варианты комбинирования базисных функций.
Одним из простейших применений функции F1 может быть создание карты нормалей (bump map), соответствующей данной клеточной структуре. При этом сам расчет вектора нормали можно производить непосредственно в фрагментном шейдере.
На следующем рисунке функция pow(1-F1,2) используется как для определения цвета, так и для отклонения карты нормалей.
Рис 5. Пример использования клеточной текстуры.
Использованный при этом фрагментный шейдер на GLSL приводится на следующем листинге.
varying vec3 lt; varying vec3 ht; uniform sampler2D cellMap; const vec3 one = vec3 ( 1.0 ); vec2 dcell ( in vec2 tex ) { float vx1 = texture2D ( cellMap, tex + vec2 ( 0.01, 0.0 ) ).x; float vx2 = texture2D ( cellMap, tex - vec2 ( 0.01, 0.0 ) ).x; float vy1 = texture2D ( cellMap, tex + vec2 ( 0.0, 0.01 ) ).x; float vy2 = texture2D ( cellMap, tex - vec2 ( 0.0, 0.01 ) ).x; vec2 res = 4.0*vec2 ( vx1*vx1 - vx2*vx2, vy1*vy1 - vy2*vy2 ); return res; } void main(void) { vec2 tex = vec2 ( 1.0, 2.0 )*gl_TexCoord [0].xy; vec3 cel = texture2D ( cellMap, tex ).xyz; vec3 cel2 = 0.5*texture2D ( cellMap, 2.0*tex ).xyz; cel += cel2; vec2 dn = dcell ( tex ); vec3 n = vec3 ( 0.0, 0.0, 1.0 ); vec3 t = vec3 ( 1.0, 0.0, 0.0 ); vec3 b = vec3 ( 0.0, 1.0, 0.0 ); vec3 nn = normalize ( n + dn.x * t + dn.y * b ); vec3 l2 = normalize ( lt ); vec3 h2 = normalize ( ht ); float diffuse = 0.4 + max ( 0.0, dot ( nn, l2 ) ); float spec = pow ( max ( 0.0, dot ( nn, h2 ) ), 30.0 ); gl_FragColor = pow ( 1.0-cel.x, 2.0 )*diffuse * vec4 ( 1.0, 0.0, 0.0, 1.0 ) + vec4 ( 1.0, 1.0, 1.0, 1.0 ) * spec; }
За счет комбинирования шумовой функции и базисных функций F1, F2 и F3 можно создавать анимированные, как бы "дышащие" текстуры (см. рис 6.).
Рис 6. Пример анимированной текстуры, использующей как базисные функции F1, F2 и F3, так и шумовую функцию noise.
Для получения изображения с рисунка 6 использовался следующий фрагментный шейдер.
varying vec3 lt; varying vec3 ht; varying vec3 p; uniform sampler3D noiseMap; uniform sampler2D cellMap; uniform float time; const float freq = 2.071935; const float freq2 = freq * freq; const vec3 one = vec3 ( 1.0 ); float f ( const in vec2 tex ) { float t = texture2D ( cellMap, tex ).x; return t*t; } vec3 df ( const in vec2 p ) { const float dx = 0.01; float fx = f ( p + vec2 ( dx, 0.0 ) ) - f ( p - vec2 ( dx, 0.0 ) ) / (2.0*dx); float fy = f ( p + vec2 ( 0.0, dx ) ) - f ( p - vec2 ( 0.0, dx ) ) / (2.0*dx); return vec3 ( fx, fy, 0.0 ); } void main(void) { vec3 ns1 = texture3D ( noiseMap, p + 4.0*time * vec3 ( 0.1, 0.0831, 0.119 ) ).xyz; vec2 tex = vec2 ( 1.0, 2.0 )*gl_TexCoord [0].xy + 0.02*ns1.xy; float d1 = abs ( f ( tex + vec2 ( 0.01, 0.0 ) ) - f ( tex ) ); float d2 = abs ( f ( tex + vec2 ( 0.0, 0.01 ) ) - f ( tex ) ); vec3 n = vec3 ( 0.0, 0.0, 1.0 ); vec3 nn = normalize ( n - 7.0*vec3 ( d1, d2, 0.0 ) ); vec3 ln = normalize ( lt ); vec3 hn = normalize ( ht ); float diffuse = 0.4 + max ( 0.0, dot ( nn, ln ) ); float spec = 0.5*pow ( max ( 0.0, dot ( nn, hn ) ), 30.0 ); gl_FragColor = vec4 ( f(tex)+f(2.0*tex), 0.0, 0.0, 1.0 )*diffuse + spec*vec4 ( 1.0, 1.0, 0.0, 1.0 ); }
Очень хороший результат дает применение следующей функции:
При достаточно большом значении параметра scale получается просто разбиение на плитки, что хорошо видно на следующем рисунке.
Рис 7. "Плитки".
Ниже приводится соответствующий фрагментный шейдер
varying vec3 lt; varying vec3 ht; varying vec3 p; uniform sampler2D cellMap; void main(void) { vec2 tex = 0.5*vec2 ( 1.0, 2.0 ) * gl_TexCoord [0].xy; vec4 f = texture2D ( cellMap, tex ); float scale = 30.0; float bias = 0.04; float v = scale*(f.y - f.x - bias); vec3 n = vec3 ( 0.0, 0.0, 1.0 ); vec3 nn = n; vec3 ln = normalize ( lt ); vec3 hn = normalize ( ht ); float diffuse = 0.4 + max ( 0.0, dot ( nn, ln ) ); float spec = 0.5*pow ( max ( 0.0, dot ( nn, hn ) ), 30.0 ); gl_FragColor = vec4 ( vec3 ( clamp ( v, 0.0, 1.0 ) ) * diffuse, 1.0 ); }
При этом параметр bias позволяет управлять размером зазора между плитками - увеличение этого параметра приводит к увеличению зазора.
На основе этого шейдера легко создать эффект разбитых плиток из какого-то материала.
Рис 8. Разбитые плитки.
Соответствующий фрагментный шейдер приводится ниже.
varying vec3 lt; varying vec3 ht; varying vec3 p; uniform sampler2D cellMap; uniform sampler2D decalMap; void main(void) { vec2 tex = 0.5*vec2 ( 1.0, 2.0 ) * gl_TexCoord [0].xy; vec4 f = texture2D ( cellMap, tex ); vec3 ct = texture2D ( decalMap, tex ).xyz; float scale = 30.0; float bias = 0.04; float v = scale*(f.y - f.x - bias); vec3 n = vec3 ( 0.0, 0.0, 1.0 ); vec3 nn = n; vec3 ln = normalize ( lt ); vec3 hn = normalize ( ht ); float diffuse = 0.4 + max ( 0.0, dot ( nn, ln ) ); float spec = 0.5*pow ( max ( 0.0, dot ( nn, hn ) ), 30.0 ); vec3 clr = vec3 ( clamp ( v, 0.0, 1.0 ) ); gl_FragColor = vec4 ( ct*clr*diffuse, 1.0 ); }
По аналогии с функциями 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.