WebGL туториал 2 (Цветная плоскость в 3d)

В предыдущей части туториала я описывал простейший случай рисования в WebGL. Теперь мы немного усложним имеющийся код. Изменения коснутся шейдеров, мы научимся передавать в них произвольные данные. Наконец появится пару строчек на тему алгебры. Нам придется задействовать матричные преобразования, чтобы говорить о 3D в привычном понимании.
Вкратце, в конце туториала мы получим квадрат раскрашенный градиентным кружочком, который будет поворачиваться вслед за курсором. Полный код примера тут.

Введение

Эта часть туториала будет сложнее предыдущей. Тут мне пришлось коснуться математки, к счастью не самого сложного её раздела. Но в целом, я разобрался с чем хотел и желаю читателю того же. Я сразу покажу результат в IFrame, возможно так чтиво будет интереснее:

Шейдеры

Первым делом, оформим наши шейдеры. На данный момент они хранятся в строках. Однако, удобнее выделить для них место среди скриптов. Мы можем получить доступ к содержимому тегов script через свойство innerHTML так же легко, как и к содержимому элементов на странице. Ниже показано, как будет выглядеть код оформленных шейдеров к концу статьи. В этом коде важно только оформление, а наполнение я объясню позже.
<head> 
 
//.... 
 
<script id="shader-fs" type="x-shader/x-fragment"> 
precision mediump float; //medium float precision

varying vec2 vCoord;

void main(void) {
 gl_FragColor = vec4(1.0,1.0,1.0, 1.0);
}
</script> 
 
<script id="shader-vs" type="x-shader/x-vertex"> 
attribute vec3 aVertexPosition; //current vertex position

uniform mat4 uMVMatrix; //move matrix
uniform mat4 uPMatrix; //perspective matrix

void main(void) {
 //Calculate vertex position
 gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
} 
</script> 
 
//.... 
 
<script type="text/javascript"> 
 
//.... 
 
 //Find and compile shader 
    function getShader(id) { 
        var shaderScript = document.getElementById(id); 
        var shader; 
        if (shaderScript.type == "x-shader/x-fragment") 
            shader = gl.createShader(gl.FRAGMENT_SHADER); 
  if (shaderScript.type == "x-shader/x-vertex")  
   shader = gl.createShader(gl.VERTEX_SHADER); 
    
        gl.shaderSource(shader, shaderScript.innerHTML); 
        gl.compileShader(shader); 
 
  console.log(gl.getShaderInfoLog(shader)); 
 
        return shader; 
    } 
 
 //Link and install compiled shaders 
    function initShaders() { 
  //Create a shader program 
        var shaderProgram = gl.createProgram(); 
        //Attach compiled shaders 
        gl.attachShader(shaderProgram, getShader("shader-vs")); 
        gl.attachShader(shaderProgram, getShader("shader-fs")); 
        //Link and install program to GPU 
        gl.linkProgram(shaderProgram); 
        gl.useProgram(shaderProgram); 
   
  //Get and save variable addresses 
        shaderMemory["aVertexPosition"] = gl.getAttribLocation(shaderProgram, "aVertexPosition"); 
        gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); 
        shaderMemory["uPMatrix"] = gl.getUniformLocation(shaderProgram, "uPMatrix"); 
        shaderMemory["uMVMatrix"] = gl.getUniformLocation(shaderProgram, "uMVMatrix"); 
    } 
     
//.... 
     
</script>

В коде можно увидеть метод getShader, используемый для получения готового шейдера по id элемента script и почти не изменившуюся функцию инициализации шейдеров. К остальному вернемся после алгебры.

Матрицы

Второе изменение, как я написал выше - введение матричных преобразований. Нам потребуется определить 2 матрицы:
  1. матрицу перспективы
  2. матрицу перемещения 
Первое, что нам понадобится - это матрица перспективного преобразования, она позволит избавиться от четвертой координаты вершины и описывать вершины в привычных декартовых координатах. Я взял шаблон матрицы из этой статьи http://triplepointfive.github.io/ogltutor/tutorials/tutorial12.html. В ней же прекрасно объяснено, что и откуда берется.

Матрица перспективного преобразования строится с использованием 4х компонентов, смысл которых я постарался проиллюстрировать рисунком ниже.

Определившись со значениями всех компонентов составляем матрицу по шаблону:
  
//Perspective matrix
var ar = gl.canvas.width / gl.canvas.height;
var angle = Math.PI / 4; //45 deg
var far = 100;
var near = 1;
var pMatrixSimple = new Float32Array(
 [1 / (ar * tan(angle / 2)), 0, 0, 0,
 0,1 / tan(angle / 2),0,0,
 0,0, (- near - far) / (near - far), (2 * near * far) / (near - far),
 0,0,-1,0]);

Матрица есть, осталось только умножить координаты каждой точки на эту матрицу. Разумеется, для лучшей производительности мы будем делать это в коде шейдеров. Напомню, что теперь можно забыть про четвертую координату вершины, она всегда будет равна единице. Массив вершин теперь выглядит вот так:
var vertices = [
1.0,  1.0,  0.0,//top left
1.0, -1.0,  0.0,//bottom left
-1.0, 1.0,  0.0,//top right
-1.0, -1.0, 0.0//bottom right
];

Теперь о матрице перемещения. Мы хотим чтобы наш квадрат крутился в след за курсором мыши и для этого должны менять координаты каждой вершины. Конкретнее, мы должны вращать вершины вокруг оси координат. В википедии довольно понятно разъяснено, как добиться этого при помощи матриц перемещения тут. Посмотрев статью, я выбрал 3 матрицы, которые помогут воплотить задумку в жизнь:
Матрица смещения по x,y,z:

Матрицы поворота вокруг осей x и y:

Матрицы есть, теперь нужно их перемножить, что я и сделал, воспользовавшись статьей тут. Обратите внимание, последовательность записи элементов матрицы в коде отличается от приведенной на картинках.
//Move matrix
var x = 0;
var y = 0;
var z = -7;
var aX = a2;
var aY = a1;
var mvMatrixSimple = new Float32Array(
   [cos(aY), 0, sin(aY), 0,
   -sin(aY) * sin(aX),  cos(aX), cos(aY) * sin(aX), 0,
   -sin(aY) * cos(aX), -sin(aX), cos(aY) * cos(aX), 0,
   x, y, z, 1]);

Передача матриц в код шейдеров

На данном этапе у нас есть обе матрицы и нам остается скормить их шейдеру. Для передачи подобных данных в код шейдеров используются переменные типа uniform. Я решил, что тут уместно будет написать о типах префиксов переменных, используемых в коде шейдеров. Их всего четыре:
  1. uniform - прямая передача параметров из кода скрипта
  2. attribute - передача в шейдер одного элемента массива, соответствующего номеру обрабатываемой вершины
  3. varying - интересная штука, использующаяся для передачи данных между шейдером вершин и шейдером фрагментов. Идея в том, что значение будет передано не на прямую а после интерполяции. Ниже я поясню, как это работает на примере цветного квадрата
  4. без префикса - обычные Сишные локальные и глобальные переменные


Из схемы выше понятно, что переменная uniform доступна из кода любого шейдера, где определена. Принцип её работы предельно интуитивен, положили данные из js - забрали данные в коде шейдера. Напомню, с переменной типа attribute мы уже сталкивались в прошлой части статьи и используем её для передачи координат вершин. Ну и с типом varying мы еще встретимся, когда будем разукрашивать квадрат.

Для того, чтобы начать использовать переменную типа uniform, придется проделать следующее:
1. Определяем uniform переменные в коде шейдера.
2. Получаем ссылку на эту переменную из кода js
3. Заливаем матрицу по выясненному адресу

По порядку:
Объявляем матрицы в коде шейдера вершин:
<script id="shader-fs" type="x-shader/x-fragment">
    //....
    uniform mat4 uMVMatrix; //move matrix
    uniform mat4 uPMatrix; //perspective matrix

Получаем ссылки на эти матрицы и помещаем в список указателей:
function initShaders(){ 
//....
    shaderMemory["uPMatrix"] = gl.getUniformLocation(shaderProgram, "uPMatrix");
    shaderMemory["uMVMatrix"] = gl.getUniformLocation(shaderProgram, "uMVMatrix");
}

Заливаем матрицы по полученным адресам перед отрисовкой:
    gl.uniformMatrix4fv(shaderMemory["uPMatrix"], false, pMatrixSimple);
    gl.uniformMatrix4fv(shaderMemory["uMVMatrix"], false, mvMatrixSimple);


Теперь матрица на своем месте и видна из кода шейдера, можно смело выполнять матричные операции.
attribute vec3 aVertexPosition; //current vertex position

uniform mat4 uMVMatrix; //move matrix
uniform mat4 uPMatrix; //perspective matrix

void main(void) {
 //Calculate vertex position
 gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}

Вращение фигуры

Чтобы убедиться в том что мы сделали настоящее 3дэ, покрутим нашу плоскость, повесим обработчик на событие onmousemove. Красивее всего это можно сделать так:

function webGLStart() {
var canvas = document.getElementById("main_canvas");
//...
canvas.onmousemove = function onMouseMove(event) {
  a1 = event.clientX / 100;
  a2 = event.clientY / 100;
  drawScene();
 };
};

Красим квадрат

После внесения всех модификаций выше, квадрат из прошлой части туториала должен начать крутиться вокруг осей OX и OY. Но, в начале урока я обещал добавить ему красок. Сделаем же это. В целом, план таков: в коде фрагментного шейдера мы получаем координаты текущего фрагмента, вычисляем расстояние до центра квадрата, и если оно укладывается в заданный диапазон, устанавливаем цвет пикселя, иначе оставляем его черным. Таким образом должна получиться окружность.
Проделав все это, мы наконец разберемся с переменными типа varying и осознаем что в коде шейдеров можно написать много всего интересного.
И так, чтобы получить координаты фрагмента, введем переменную типа varying для передачи через него двух координат текущей вершины.

<script id="shader-vs" type="x-shader/x-vertex">
//....
varying vec2 vCoord;

void main(void) {
//Calculate vertex position
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vCoord = vec2((aVertexPosition.x + 1.0) / 2.0, (aVertexPosition.y + 1.0) / 2.0);
}
Для получения координат в коде фрагментного шейдера, переменная vCoord должна быть так же видна в нем:
<script id="shader-fs" type="x-shader/x-fragment">
 varying vec2 vCoord;
А теперь, внимание "интерполяция". Из кода вершинного шейдера мы передали только координаты крайних вершин, но после предварительного расчета плоскостей, фрагментный шейдер получит промежуточные значения vCoord для каждого пикселя. Я проиллюстрировал это рисунком ниже:
Такой трюк позволяет нам многое, например таким образом можно передавать UV координаты, рисовать градиенты и возможно, еще что-то, о чем я пока не знаю.
И так, в коде фрагментного шейдера мы получаем координаты текущей точки, теперь нужно проверить расстояние до нее от центра квадрата.
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float; //medium float precision
varying vec2 vCoord;

void main(void) {
   //Calculate fragment color
   float x = vCoord.x - 0.5;
   float y = vCoord.y - 0.5;
   float d = sqrt(x * x + y * y);
   if ((d > 0.4) && (d < 0.5)) 
      gl_FragColor = vec4(0.0,vCoord, 1.0);
   else
      gl_FragColor = vec4(0.0,0.0,0.0, 1.0);
//....
Теперь, наш квадрат раскрашен и мы вплотную подошли к наложению текстур. Полный код примера тут.
А вот и живой пример:

Комментарии

Популярные сообщения из этого блога

Siege Up! Editor (beta)

STM32F4 и программный выход в DFU

Git и Yandex.Disk