Реализация процедурных текстур методом Шума Перлина.
Автор: Волошин Дмитрий aka WDW
Часть 1
Часть 2
Полезные линки
Часть 1
При написании данной статьи я не делал своей целью перевод англоязычных статей на эту тему, которых я перечитал довольно много в интернете или создание супер-мега-классного плагина для 3DSM с классной фотореалистичной графикой облаков, которая тем не менее рендерится минут 5, я хотел сделать динамичные красивые облака (возможно даже фейковые), которые можно использовать в играх , не сильно влияя на ФПС.
Простейшим методом реализации разнообразных «случайных» текстур есть метод Кена Перлина (http://mrl.nyu.edu/perlin/) и названный в его честь. В этом методе генерируется несколько текстур «октав» и слагая их вместе мы получаем довольно интересную текстуру, вид которой зависят от нескольких параметров.
В этой статье я расскажу о 2-мерном случае, но это можно легко расширить до n-мерного случая. Шум Перлина использует псевдо-рандомный генератор случайных чисел, который возвращает число от –1 до 1 по заданных параметрах
Причем это число постоянное для конкретных параметров.
_inline float CMyNoise::Noise2D(int x, int y) { int n = x + y * 57; n = ( n<<13) ^ n; return ( 1.0f - ( ( n * ( n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0f); }
Обратите внимание на количество параметров, их 2 поскольку шум двухмерный, но их может быть и 1, и 3 и больше. Постарайтесь это понять, так что функция возвращает для 2-х параметров число в плоскости, для трех в пространстве и. т.д., может это и неверно с математической точки зрения, но именно это помогло мне понять “How does it work”.
Следующей важнейшей вещью есть формула интерполяции, которая используется для сглаживания значений шума. Чаще всего эта функция принимает 3 параметра – a и b – два параметра, между которыми мы производим интерполяцию и х – число от 0 до 1, от которого и зависит результат. Если х=0, то возвращается a, если х=1, возвращается b, в ином случае возвращается некоторое значение между а и b.
Есть несколько формул, которые мы можем использовать, но те которые дают хорошие результаты используют больше системных ресурсов. Наиболее популярной формулой есть
result= a*(1-x) + b*x
//алгоритм «плазма» в ввиду своей простоты очень подходит для рилтайма ////////////////////////////***************************///////////////////// float ft = x * 3.1415927; float f = (1 - cosf( ft)) * 0.5; result= a*( 1-f) + b*f; //косинусная интерполяция, возвращает более «сглаженные значения» // но все знают быстроту функции cosf(); ////////////////////////////////*****************************/////////////// float fac1 = 3*powf( 1-a, 2) - 2*powf( 1-a,3); float fac2 = 3*powf( a, 2) - 2*powf( a, 3); result=x*fac1 + y*fac2;// James Long- использовал этот метод в своей статье ////////////////////********************************///////////////////
(Статья James Long: http://www.animeimaging.com/asp/PerlinNoise.aspx)
Есть еще формула кубической интерполяции, но ввиду того, что облака генерируется в рилтайме, а ее результаты ввиду своей ресурсоемкости не сильно отличаются от косинусной интерполяции, я ее здесь не навожу.
И также функция сглаживания, которая возвращает цвет пикселя в зависимости от исходного цвета и цвета соседних пикселей :
//////////////////////////////////////////////////////////////////////////// _inline float CMyNoise::SmoothedNoise2D(float x, float y) { float corners = ( Noise2D( x-1, y-1)+Noise2D( x+1, y-1)+ Noise2D( x-1, y+1)+Noise2D( x+1, y+1) ) / 16; float sides = ( Noise2D( x-1, y) +Noise2D( x+1, y) + Noise2D( x, y-1) +Noise2D( x, y+1) ) / 8; float center = Noise2D( x, y) / 4; return corners + sides + center; } //////////////////////////////////////////////////////////////////////////// //Функция, которая возвращает значение сглаженного и //интерполированного шума float CMyNoise::CompileNoise( float x, float y) { float int_X = int( x);//целая часть х float fractional_X = x - int_X;//дробь от х //аналогично у float int_Y = int( y); float fractional_Y = y - int_Y; //получаем 4 сглаженных значения float v1 = SmoothedNoise2D( int_X, int_Y); float v2 = SmoothedNoise2D( int_X + 1, int_Y); float v3 = SmoothedNoise2D( int_X, int_Y + 1); float v4 = SmoothedNoise2D( int_X + 1, int_Y + 1); //интерполируем значения 1 и 2 пары и производим интерполяцию между ними float i1 = Cosine_Interpolate( v1 , v2 , fractional_X); float i2 = Cosine_Interpolate( v3 , v4 , fractional_X); //я использовал косинусною интерполяцию ИМХО лучше //по параметрам быстрота-//качество return Cosine_Interpolate( i1 , i2 , fractional_Y); }
В следующей функции, основной с точки зрения определения параметров текстуры, мы познакомимся с несколькими новыми понятиями. Октава – каждая выполненная функция шума. Дело в том что каждая последующая функция шума принимает удвоенное значение частоты, вспомните уроки пения :) ). Persistence (настойчивость, инерционность), от этого и зависит как бы насыщенность облаков и значение амплитуды.
int CMyNoise::PerlinNoise_2D(float x,float y,float factor) { float total = 0; // это число может иметь и другие значения хоть cosf(sqrtf(2))*3.14f // главное чтобы было красиво и результат вас устраивал float persistence=0.5f; // экспериментируйте с этими значениями, попробуйте ставить // например sqrtf(3.14f)*0.25f или что-то потяжелее для понимания J) float frequency = 0.25f; float amplitude=1;//амплитуда, в прямой зависимости от значения настойчивости // вводим фактор случайности, чтобы облака не были всегда одинаковыми // (Мы ведь помним что ф-ция шума когерентна?) x+= ( factor); y+= ( factor); // NUM_OCTAVES - переменная, которая обозначает число октав, // чем больше октав, тем лучше получается шум for( int i=0;i<NUM_OCTAVES;i++) { total += CompileNoise( x*frequency, y*frequency) * amplitude; amplitude *= persistence; frequency*=2; } //здесь можно перевести значения цвета по какой-то формуле //например: //total=sqrt(total); // total=total*total; // total=sqrt(1.0f/float(total)); //total=255-total;-и.т.д все зависит от желаемого результата total=fabsf( total); int res=int( total*255.0f);//приводим цвет к значению 0-255… return res; }
Пример интересных текстур:
Следующая функция принимает массив, который она заполняет шумом исходя из размера(псевдокод)
void CMyNoise::GenNoise(unsigned char pNoise[],int size) { srand( GetTickCount( )); // случайное число, которое призвано внести // случайность в нашу текстуру float fac =Random( PI*2*10,PI*3*10); for( int i=0 ;i<size;i++) { for( int j=0 ;j<size;j++) { //проходим по всем элементам массива и заполняем их значениями pNoise[i*size+j]=PerlinNoise_2D( float( i),float( j),fac); } } }
18 января 2004 (Обновление: 12 июня 2009)