Знакомство фронтендера с WegGL (часть 2)

Вспоминаем ради чего мы начали изучать WebGL

После недельного чтения ресурса и экспериментов у меня появился кустарный REPL, в котором я мог быстро набрасывать шейдеры и другой код, чтоб поэкспериментировать и найти решение.

Я пишу стать…


This content originally appeared on DEV Community and was authored by Nikita Nafranets

Вспоминаем ради чего мы начали изучать WebGL

После недельного чтения ресурса и экспериментов у меня появился кустарный REPL, в котором я мог быстро набрасывать шейдеры и другой код, чтоб поэкспериментировать и найти решение.

Я пишу статью, спустя 2 месяца после того как закончил работу над задачей и осталась ссылка только на такую песочницу.

Вооружись знаниями и реплом, я пошел искать то, что сможет распарсить мне .obj файлик на вершины.
Единственное в интернете которое более/менее правильно распарсило мне файлик это был npm пакет
webgl-obj-loader. Хотя и в нем был досадный баг который попортил мне немало крови.

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

После webgl fund у меня появилась песочница в которой я мог упражняться в рендере. Так что большую часть времени я использовал ее как шпаргалку.

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

Vertex:

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

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

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

Fragment:

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

void main() {
  gl_FragColor = vec4(1., 0., 0., 1.); // заливаем красным
}

Сам код

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, // отключаем нормализацию
    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();

На выход я получил такую штуку:
почти результат

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

Что такое index?

При работе с либой узнал про новую вещь: индексы.
На самом деле в .obj файле кроме вершин есть текстурные координаты и нормали и поверхности (faces).
Что это вообще такое?

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

Дальше я попробовал изменить gl.TRIANGLES на gl.LINES. И получил следующий результат:
еще чуть-чуть

Мда, совершенно не то, что я ожидал. Где мои красивые линии как у дизайнера и че за треугольники. Я тогда впервые осознал простую истину, что все блин на треугольниках. В данной ситуации, я побежал в чат и тогда породил локальный мем.

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

Я просто не знал что делать дальше и спрашивал советов. Среди них было несколько:

- Используй в фрагментом шейдере uv, чтоб рисовать линии сам.

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

- Сделай uv разветку и натяну текстуру картинку.

Я не понял из 1 ответа, что такое uv, почему-то тогда мне никто не объяснил, что это и есть текстурные координаты. Да и где эти uv брать тоже было не понятно.

Из второе ответа, я тоже не понял что мне делать и какие значения использовать.

А третий ответ оказался хоть тоже загадочным, но мне объяснили что это значит. Нужно было через редактор моделей создать текстурные координаты и нарисовать под них текстуру.

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

Баг который попортил нервы

Я побежал читать статью на webgl fund как натянут текстуру. Кода стало больше, но не увидел сложностей. Сделал как в гайде и думал, щас будет все отлично!

Vertex

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

precision mediump float;

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

void main(){
  gl_FragColor = texture2D(u_texture, v_texture_coords);
}
  //...
  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);
    // ...
  }

И после тонны кода получаем вот это чудовище:

шаг назад

Что за??

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

боже спасите меня

Что за фигня?

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

я не знаю что я делаю

Потратив еще кучу часов в попытке разобраться, в чем же дело. Я заметил, что blender по умолчанию преобразовывал поверхности из 4 точек в поверхности из 3 точек. И когда я отключал данную функцию, то модельки опять ломались. И тогда, я понял, что проблема все это время была в библиотеке webgl-obj-loader. Она ломалась если ей подавали поверхности из 4 точек (на самом деле мне это объяснили в чате).

Я сразу побежал писать ишью с проблемой, а потом нашел pr который правил эту багу и прикрепил его к своему ишью.

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

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

сранная либа

Видя это, я понимал, что все можно рассчитать программно, но не знал как...

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

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

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

спаситель есть

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

Продолжение есть.


This content originally appeared on DEV Community and was authored by Nikita Nafranets


Print Share Comment Cite Upload Translate Updates
APA

Nikita Nafranets | Sciencx (2021-07-06T22:31:31+00:00) Знакомство фронтендера с WegGL (часть 2). Retrieved from https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/

MLA
" » Знакомство фронтендера с WegGL (часть 2)." Nikita Nafranets | Sciencx - Tuesday July 6, 2021, https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/
HARVARD
Nikita Nafranets | Sciencx Tuesday July 6, 2021 » Знакомство фронтендера с WegGL (часть 2)., viewed ,<https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/>
VANCOUVER
Nikita Nafranets | Sciencx - » Знакомство фронтендера с WegGL (часть 2). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/
CHICAGO
" » Знакомство фронтендера с WegGL (часть 2)." Nikita Nafranets | Sciencx - Accessed . https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/
IEEE
" » Знакомство фронтендера с WegGL (часть 2)." Nikita Nafranets | Sciencx [Online]. Available: https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/. [Accessed: ]
rf:citation
» Знакомство фронтендера с WegGL (часть 2) | Nikita Nafranets | Sciencx | https://www.scien.cx/2021/07/06/%d0%b7%d0%bd%d0%b0%d0%ba%d0%be%d0%bc%d1%81%d1%82%d0%b2%d0%be-%d1%84%d1%80%d0%be%d0%bd%d1%82%d0%b5%d0%bd%d0%b4%d0%b5%d1%80%d0%b0-%d1%81-weggl-%d1%87%d0%b0%d1%81%d1%82%d1%8c-2/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.