[JavaScript, WebGL] Знакомство фронтендера с WebGL: первые наброски (часть 2)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
11-Июл-2021 16:32

Это история в несколько частей:
Вспоминаем ради чего мы начали изучать WebGLПосле недельного чтения ресурса и экспериментов у меня появился кустарный REPL, в котором я мог быстро набрасывать шейдеры и другой код, чтоб поэкспериментировать и найти решение.
Я пишу статью, спустя 2 месяца после того как закончил работу над задачей и осталась только такая песочница ->
Извините, данный ресурс не поддреживается. :( Вооружись знаниями и реплом, я пошел искать то, что сможет распарсить мне .obj файлик на вершины.Единственное в интернете которое более/менее правильно распарсило мне файлик это был npm пакет webgl-obj-loader.Первые наброскиС помощью библиотеки, я сразу смог добиться какого-то результата в своей песочнице.
// 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, // отключаем нормализацию (это чтоб не пыталось конвертировать числа больше 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();
На выход я получил такую штуку:
Мне кажется это маленький шаг для 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 точек (на самом деле мне это объяснили в чате).Я сразу побежал писать жалобу на проблему, а потом нашел pull request который правил эту багу и прикрепил его к своему issue.Отказ от webgl-obj-loaderПосмотрев на результат мучительной работы, я понял, что это не то, чего я хотел. Линии были толстыми, плюс чем сильней было скругление, тем плотней становилась область.
Еще я понимал, что есть какое-то другое решение, потому что когда я открывал модельку в просмотрщиках моделей, они правильно рисовали результат и красиво прорисовали линии.
Видя это, я понимал, что все можно рассчитать программно, но не знал как...И в это время появился рыцарь в сияющих доспехах и спас меня из логова бессилия. Он был тем кто предложил:
Распарси .obj сам и получили нужные значения.
На тот момент я не понимал, что вообще это значит и как мне это поможет. И человек накидал в песочнице пример на three.js.Извините, данный ресурс не поддреживается. :( Этот пример был светом. Я сразу же понял, что можно выкидывать webgl-obj-loader и зажить как человек. Выбросил его без каких либо сожалений.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_webgl, #_java, #_webgl, #_novichok (новичок), #_frontend, #_vanilla (ванилла), #_javascript, #_webgl
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 13:50
Часовой пояс: UTC + 5