Знакомство фронтендера с WebGL: первые наброски
Tags: webgl, javascript
Published: 2021-07-06 22:31:31

## Вспоминаем ради чего мы начали изучать WebGL
После недельного чтения ресурса и экспериментов у меня появился кустарный REPL, в котором я мог быстро набрасывать шейдеры и другой код, чтоб поэкспериментировать и найти решение.  
> Я пишу статью, спустя 2 месяца после того как закончил работу над задачей и осталась только такая песочница ->

{% codesandbox apple-webgl-u4pg7 %}

Вооружись знаниями и реплом, я пошел искать то, что сможет распарсить мне .obj файлик на вершины.
Единственное в интернете которое более/менее правильно распарсило мне файлик это был npm пакет
[webgl-obj-loader](https://www.npmjs.com/package/webgl-obj-loader). Хотя в нем был досадный баг который попортил мне немало крови.
{% github https://github.com/frenchtoast747/webgl-obj-loader/pull/66 %} 

### Первые наброски

С помощью библиотеки, я сразу смог добиться какого-то результата в своей песочнице.

Vertex:
```glsl
attribute vec4 a_position; // объявляем переменную в которую будем прокидывать вершины яблока.

uniform mat4 u_matrix; // матрица которая будет нам помогать трансформировать модель

void main(){
    gl_Position = u_matrix * a_position; // у glsl есть встроенные возможности по работе с матрицами. Тут он сам за нас перемножает вершины на матрицы и тем самым смещает их куда надо.
}
```

Fragment:
```glsl
precision mediump float; // точность для округления. 

void main() {
  gl_FragColor = vec4(1., 0., 0., 1.); // заливаем красным
}
```
Сам код
```js
import { vertex, fragment } from './shaders'; // через parcel импортирует тексты
import { createCanvas, createProgramFromTexts } from "./helpers"; 
import { m4 } from "./matrix3d"; // после изучение webgl на webgl fund, мне в наследство досталась библиотека которая умеет работает с 3д матрицами.
import appleObj from "./apple.obj"; // моделька яблока
import * as OBJ from "webgl-obj-loader"; // наша либа которая распарсит obj


function main() {
  const apple = new OBJ.Mesh(appleObj); // загружаем модель
  const canvas = createCanvas(); // создаю canvas и вставляю в body
  const gl = canvas.getContext("webgl"); // получаю контекст
  const program = createProgramFromTexts(gl, vertex, fragment); // создаю программу из шейдеров
  gl.useProgram(program); // линкую программу к контексту

  // получаю ссылку на атрибут
  const positionLocation = gl.getAttribLocation(program, "a_position");

  // у либы была готовая функция, которая за меня создавала буфер и прокидывала распарсенные данные в буферы. Из .obj можно было достать не только вершины, но и другие координаты которые могут быть полезны.
  OBJ.initMeshBuffers(gl, apple);

  gl.enableVertexAttribArray(positionLocation); // активирую атрибут, зачем это делать не знаю, но не сделаешь, ничего не заработает.
  gl.vertexAttribPointer(
    positionLocation,
    apple.vertexBuffer.itemSize, // либа сама определяла сколько нужно атрибуту брать чисел, чтоб получить вершину
    gl.FLOAT,
    false, // отключаем нормализацию (это чтоб не пыталось конвертировать числа больше 1 в 1. Аля 255 -> 0.255.
    0,
    0
  ); // объясняю как атрибуту парсить данные

  // получаем ссылку на глобальную переменную которая будет доступна внутри шейдеров. В нее же мы будем прокидывать матрицы
  const matrixLocation = gl.getUniformLocation(program, "u_matrix");

  let translation = [canvas.width / 2, 400, 0]; // смещаю на центр экрана по вертикали и 400 px вниз
  let rotation = [degToRad(180), degToRad(0), degToRad(0)]; // вращение по нулям
  let scale = [5, 5, 5]; // увеличиваю модельку в 5 раз. scaleX, scaleY, scaleZ

  // выставляю вью порт
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.enable(gl.DEPTH_TEST); // включаем специальный флаг, который заставляет проверять видеокарту уровень вложенности и если какой-то треугольник перекрывает другой, то другой не будет рисоваться, потому, что он не виден.

  function drawScene() {
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // очищаем канвас на каждый рендер

    const matrix = m4.multiply(
      m4.identity(), // создаем единичную матрицу. Матрицу у которой все значения по умолчанию.
      m4.orthographic(
        0,
        gl.canvas.width,
        gl.canvas.height,
        0,
        400,
        -400
      ), // Создаем матрицу которая конвертирует неудобные размеры модельки яблока в координатное пространство -1 до 1.
      m4.translation(...translation), // перемещаем модельку 
      m4.xRotation(rotation[0]), // крутим по X
      m4.yRotation(rotation[1]), // крутим по Y
      m4.zRotation(rotation[2]), // крутим по Z
      m4.scaling(...scale) // увеличиваем модельку
    ); // перемножаем матрицы друг на друга, чтоб в конце получить 1 матрицу которую и прокинем в шейдер
    gl.uniformMatrix4fv(matrixLocation, false, matrix); // прокидываем матрицу
    // подключаю буфер с индексами
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, apple.indexBuffer);

    // рисуем яблоко треугольниками с помощью индексов
    gl.drawElements(
      gl.TRIANGLES,
      apple.indexBuffer.numItems,
      gl.UNSIGNED_SHORT,
      0
    );
  }

  drawScene();

  // Тут код который настраивает всякие слайдеры, чтоб изменять матрицы.
  // ...
  //
}

main();
```

На выход я получил такую штуку:
![почти результат](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7a2q3ql47vc94bj5ihoy.png)

Мне кажется это маленький шаг для webgl, но огромный скачок для **фронтендера**.  

### Что такое **index**?
При работе с либой узнал про новую вещь: индексы.
На самом деле в .obj файле кроме вершин есть текстурные координаты, нормали, поверхности (faces).
Что это вообще такое?
- Текстурные координаты это массив цифр который прокидывается в фрагментный шейдер и позволяет шейдеру понять где он вообще сейчас находится в модельке, чтоб наложить пиксель в зависимости от общего положения. Если их нет, то шейдер получается вообще изолированным и может только красить пиксели не зная где именно сейчас он закрашивает.  
Текстурные координаты прокидываются как атрибуты.
- Нормали это тоже координаты, но их можно использовать в фрагментом шейдере, чтоб понять как рисовать тень от объекта(модель) в зависимости от того как должен падать свет на объект.
- Поверхность - это массив индексов которые указывают на индекс в массиве вершин, текстур и нормалей. Поверхности это служебные данные для редактора моделей (аля cinema4d и других), которые позволяют объединять полигоны в квадраты и другие более сложные фигуры. В частности это нужно для того как рендерить именно модельку.  
Так вот индексы это и есть поверхности. Допустим мы прокинули в 2 атрибута данные от вершин и текстурных координат. И webgl смотрит на текущий индекс и по параметрам атрибутов (помните мы указывали size, сколько нужно брать чисел, чтоб получить вершину) берет из каждого атрибута нужный набор чисел и прокидывает их в шейдеры.


Дальше я попробовал изменить `gl.TRIANGLES` на `gl.LINES`. И получил следующий результат:
![еще чуть-чуть](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jx0yc3n5yjbcx2gq36xp.png)    
Мда, совершенно не то, что я ожидал. Где мои красивые линии как у дизайнера и че за треугольники. Я тогда впервые осознал простую истину, что все блин на треугольниках. В данной ситуации, я побежал в чат и тогда породил локальный мем.

> у меня начали появляться подозрения, что рисовать фигуры можно только через треугольники.

Я просто не знал что делать дальше и спрашивал советов. Среди них было несколько:  
\- Используй в фрагментом шейдере uv, чтоб рисовать линии сам.  
\- Распарси .obj сам и получили нужные значения.  
\- Сделай uv разветку и натяну текстуру картинку.

Я не понял из 1 ответа, что такое uv, почему-то тогда мне никто не объяснил, что это и есть текстурные координаты. Да и где эти uv брать тоже было не понятно.  
Из второе ответа, я тоже не понял что мне делать и какие значения использовать.  
А третий ответ оказался хоть тоже загадочным, но мне объяснили что это значит. Нужно было через редактор моделей создать текстурные координаты и нарисовать под них текстуру.  
В интернете я нашел гайды о том как сделать в cinema 4d uv разметку и там же нашел как нарисовать текстуру. В редакторе была возможность создать картинку и залить по граням поверхностей(faces) нужный цвет. Я считал, что это сразу решает мою проблему. Выплюнув texture.png и новый obj с uv (то есть так называются текстурные координаты).  

## Баг который попортил нервы
Я побежал читать статью на [webgl fund как натянут текстуру](https://webglfundamentals.org/webgl/lessons/ru/webgl-3d-textures.html). Кода стало больше, но не увидел сложностей. Сделал как в гайде и думал, щас будет все отлично!  

Vertex
```glsl
precision mediump float;

attribute vec4 a_position;
attribute vec2 a_texture_coords; // текстурные координаты из модели

uniform mat4 u_matrix;

varying vec2 v_texture_coords;

void main(){
    gl_Position = u_matrix * a_position;

    v_texture_coords = a_texture_coords; // прокидываем во фрагментный шейдер 
}
```
Fragment
```glsl
precision mediump float;

varying vec2 v_texture_coords; // координаты из вершины
uniform sampler2D u_texture; // текстура

void main(){
  gl_FragColor = texture2D(u_texture, v_texture_coords);
}
```
```js
  //...
  const textureCoordsLocation = gl.getAttribLocation(
    program,
    "a_texture_coords"
  ); // получили ссылку на новый атрибут
  // ...
  gl.enableVertexAttribArray(textureCoordsLocation);
  gl.bindBuffer(gl.ARRAY_BUFFER, apple.textureBuffer); // забиндили буфер которая выдала либа из модели
  gl.vertexAttribPointer(
    textureCoordsLocation,
    apple.textureBuffer.itemSize,
    gl.FLOAT,
    false,
    0,
    0
  );

  const texture = gl.createTexture(); // запрашиваем место для текстуры
  gl.bindTexture(gl.TEXTURE_2D, texture); // биндим

  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA,
    1,
    1,
    0,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    new Uint8Array([0, 0, 255, 255])
  ); // сначала прокидываем пустышку, пока грузится текстура

  const image = new Image();
  image.src = textureImg; // загружаем текстуру
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.generateMipmap(gl.TEXTURE_2D);
    gl.texParameteri(
      gl.TEXTURE_2D,
      gl.TEXTURE_MIN_FILTER,
      gl.LINEAR_MIPMAP_LINEAR
    );
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // какие-то неведомые настройки, чтоб все было круто
    drawScene();
  };

  // ...
  const textureLocation = gl.getUniformLocation(program, "u_texture");

  function drawScene() {
    // ...
    gl.uniform1i(textureLocation, 0);
    // ...
  }
```
И после тонны кода получаем вот это __чудовище__:  

![шаг назад](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xyxuh1qfsf7axfn43785.png)  

__Что за??__  
И вот тут у меня началась эпопея в целый рабочий день с целью решить проблему. Я мало, что понимал и считал, что это я косячу, а не либа которую использую. Сначала я на самом деле откатил код с текстурой и просто попробовал закрасить и получил опять какой-то невероятный результат.  

![боже спасите меня](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nky6y6i5be12nejas5f2.png)

__Что за фигня?__  
Я тогда решил, что проблема в экспорте и вообще в том, что я делал с uv mapping. Поиграв пару часов с экспортом, я решил попробовать в blender экспортировать и о чудо, моделька починилась! 

![я не знаю что я делаю](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/onk85zqb6dfma1msoi44.png)

Потратив еще кучу часов в попытке разобраться, в чем же дело. Я заметил, что blender по умолчанию преобразовывал поверхности из 4 точек в поверхности из 3 точек. И когда я отключал данную функцию, то модельки опять ломались. И тогда, я понял, что проблема все это время была в библиотеке webgl-obj-loader. Она ломалась если ей подавали поверхности из 4 точек (на самом деле мне это объяснили в чате).  
Я сразу побежал писать [жалобу на проблему](https://github.com/frenchtoast747/webgl-obj-loader/issues/69), а потом нашел [pull request](https://github.com/frenchtoast747/webgl-obj-loader/pull/66) который правил эту багу и прикрепил его к своему issue. 


## Отказ от webgl-obj-loader

Посмотрев на результат мучительной работы, я понял, что это не то, чего я хотел. Линии были толстыми, плюс чем сильней было скругление, тем плотней становилась область. 
Еще я понимал, что есть какое-то другое решение, потому что когда я открывал модельку в просмотрщиках моделей, они правильно рисовали результат и красиво прорисовали линии. 

![сранная либа](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o5766s5m5xbvlm3x9ogw.png)

Видя это, я понимал, что все можно рассчитать программно, но не знал как...  
И в это время появился рыцарь в сияющих доспехах и спас меня из логова бессилия. Он был тем кто предложил:

> Распарси .obj сам и получили нужные значения.  

На тот момент я не понимал, что вообще это значит и как мне это поможет. И человек накидал в песочнице пример на three.js.  

{% codesandbox festive-dust-kvumu %}

Этот пример был светом. Я сразу же понял, что можно выкидывать __webgl-obj-loader__ и зажить как человек. Выбросил его без каких либо сожалений.

Продолжение [есть](https://dev.to/dimensi/weggl-3-3k58).