Шум Перлина
Perlin noise (Шум Перлина, также иногда Классический шум Перлина) — математический алгоритм по генерированию процедурной текстуры псевдо-случайным методом. Используется в компьютерной графике для увеличения реализма или графической сложности поверхности геометрических объектов.
Шум Перлина — это градиентный шум, состоящий из набора псевдослучайных единичных векторов (направлений градиента), расположенных в определенных точках пространства и интерполированных функцией сглаживания между этими точками. Для генерации шума Перлина в одномерном пространстве необходимо для каждой точки этого пространства вычислить значение шумовой функции, используя направление градиента (или наклон) в указанной точке.
Шум Перлина широко используется в двухмерной и трёхмерной компьютерной графике для создания таких визуальных эффектов, как дым, облака, туман, огонь и т.д. Он также очень часто используется как простая текстура, покрывающая геометрическую модель. В отличие от растровых текстур, шум Перлина является процедурной текстурой, и поэтому он не занимает память, но вместе с тем исполнение алгоритма требует неких вычислительных ресурсов. Ссылка на источник
Данная статья приводит реализацию шума Перлина на языке шейдеров GLSL с использованием WebGL.
Для работы с шейдерами в HTML/js нам необходимо создать файл index.html.
Пишем стандартную структуру и добавляем элемент canvas id="canvas":
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> Шумы </title> </head> <body> <main> <canvas id="canvas" width="500" height="500"></canvas> </main>
Теперь создаем два скрипта типа shader. Первый - вершинный, id="Vshader". Второй - фрагментный, id="Fshader".
<!-- VERTEX SHADER --> <script type="shader" id="Vshader"> </script> <!-- FRAGMENT SHADER --> <script type="shader" id="Fshader"> </script>
JS-скрипт:
<script> </script> </body> </html>
Со структурными моментами разобрались, возвращаемся в к скрипту js. Создадим внутри него функцию которая будет создавать все необходимое для работы с шейдерами:
function startWebGL() {
получаем исходники шейдеров и webgl контекст:
//получаем исходники шейдеров var vertexShaderText = document.getElementById('Vshader').text; var fragmentShaderText = document.getElementById('Fshader').text; //получаем webgl context var canvas = document.getElementById('canvas'); var gl = canvas.getContext('webgl');
Теперь создадим наши шейдеры в контексте, передадим ему исходный код шейдеров, скомпилируем их, создадим программу и подключим к ней шейдеры:
var vertexShader = gl.createShader(gl.VERTEX_SHADER); //создаем шейдеры var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(vertexShader, vertexShaderText); //передаем исходники gl.shaderSource(fragmentShader, fragmentShaderText); //компилируем. В случае неудачной компиляции выводим ошибку gl.compileShader(vertexShader); if(!gl.getShaderParameter(vertexShader,gl.COMPILE_STATUS)){ alert('ERROR COMPILING SHADER'); console.error('SHADER ERR: ',gl.getShaderInfoLog(vertexShader)); } gl.compileShader(fragmentShader); if(!gl.getShaderParameter(fragmentShader,gl.COMPILE_STATUS)){ alert('ERROR COMPILING SHADER'); console.error('SHADER ERR: ',gl.getShaderInfoLog(fragmentShader)); } var program = gl.createProgram(); //создаем программу gl.attachShader(program, vertexShader); //подключаем скомпилированные шейдеры gl.attachShader(program, fragmentShader); gl.linkProgram(program); //связываем
Немного о работе вершинных шейдеров. Дело в том, что внутри самой шейдерной программы создать массив данных, исходя из которых он будет строиться, мы не можем. Эта задача передается, в нашем случае, js. Для того, чтобы наладить связь между js и шейдерами, существуют atribut-, uniform- и varing-переменные. Сейчас мы создадим буффер вершин и передадим данные в атрибут-переменную:
var vertexBuffer = gl.createBuffer(); //создаем буфер вершин gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); //связываем var vertexArray = [ //массив вершин двух треугольников(из которых получена наша область работы) 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0 ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexArray), gl.STATIC_DRAW);//передаем данные нашего массива вершин в буфер вершин webgl //создаем атрибут, с помощью которого шейдер будет получать данные из буфера var positionAttribLocation = gl.getAttribLocation(program, 'vertexPosition'); gl.vertexAttribPointer( positionAttribLocation, //индекс для атрибута 2, //количество считываемых на одну вершину элементов gl.FLOAT, //тип данных gl.FALSE, //не нормализуем вершины 0, //нет шага 0 // нет смещения ); gl.enableVertexAttribArray(positionAttribLocation);//включаем наш атрибут
Теперь создадим uniform-переменную времени:
var timeLocation = gl.getUniformLocation(program, "u_time");//создаем униформ переменную времени function renderLoop(timeStamp) {//рекурсивная функция отрисовки gl.uniform1f(timeLocation, timeStamp/1000.0); //передаем время в униформ переменную gl.drawArrays(gl.TRIANGLES, 0, 6); //отрисовываем два треугольника, дающих наш квадрат window.requestAnimationFrame(renderLoop); //вызываем функцию для зацикливания } gl.useProgram(program); //выбираем используемую программу(в нашем случае вариантов не много) window.requestAnimationFrame(renderLoop); // запускаем рекурсивную функцию, которая создаст нам анимацию и uniform переменную. }
С js почти все, осталось только запустить функцию:
startWebGL();
Теперь переходим к скриптам с шейдерами.
С вершинным все просто:
attribute vec2 vertexPosition; //атрибут вершин void main(){ gl_Position = vec4(vertexPosition, 0.0, 1.0);//строим вершинный шейдер }
Фрагментный шейдер:
precision mediump float; //устанавливаем точность для чисел с плавающей точкой uniform float u_time; //униформ переменная времени
Итак, переходим к генерации шума. Первое, что нам здесь нужно - функция имитирующая случайность. Вообще, основой для данной функции служит y=fract(sin(x)*a). При больших значениях а получается некое подобие беспорядка, псевдо-случайность (можно посмотреть здесь www.iquilezles.org/apps/graphtoy/?f1(x)=fract(sin(x)*10000000) ). В нашем случае нужно было перенести этот эффект на плоскость, поэтому мы использовали скалярное произведение. Функция от скалярного произведения возвращает единственное число в диапазоне от 0.0 до 1.0 в зависимости от взаимного расположения векторов:
float random (in vec2 st) { //функция генерации случайных чисел //dot - скалярное произведение return fract(sin(dot(st.xy,vec2(12.9898,78.233)))* 43758.5453123); }
переходим непосредственно к шуму:
//функция шума float noise (in vec2 st) { //in - квалификатор(переданная переменная предназначена //только для чтения)
разделим наш входной вектор на целую и дробную части:
vec2 i = floor(st); //целая часть st vec2 f = fract(st); //дробная часть st
теперь возьмем случайные значения для 4 углов нашей области, относительно которых будет проводится дальнейшая работа:
float a = random(i + vec2(0.0, 0.0)); //нижний левый угол float b = random(i + vec2(1.0, 0.0)); //нижний правый угол float c = random(i + vec2(0.0, 1.0)); //верхний левый угол float d = random(i + vec2(1.0, 1.0)); //верхний правый угол
создадим вектор, который будет сглаживать наш шум:
//кубическая кривая vec2 u=smoothstep(0.0,1.0, f);
И наконец, интерполируем:
return mix(a, b, u.x) + //интерполируем между четырмя случайными значениями (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y; }
в основной функции мы нормализуем область отрисовки, масштабируем и создаем переменную, содержащую шум:
void main(){ vec2 st = gl_FragCoord.xy/vec2(500, 500);//нормализуем область, разделяя их на разрешение изображения vec2 pos = vec2(st*4.0); //масштабируем float n = noise(pos); //накладываем шум //капельки n+=smoothstep(.15,.2,noise(st*20.));
осталось только передать наше шумовое значение переменной цвета.Чтобы значения цвета варьировались, создавая градиенты, я умножил цвет на uniform-переменную u_time, разделил ее на пять (чтобы замедлить анимацию) и все это взял в функцию fract(), чтобы значения не выходили за пределы [0.0; 1.0]. Я выбрал красный цвет, но можно присвоить и другим цветам, более того, можно создать несколько переменных и взять их в какие-нибудь функции:
gl_FragColor = vec4(vec3(fract(n*u_time/5.),0.0,0.0), 1.0); //передаем цвет }
На этом все. Благодарю за внимание.