Představujeme Frontender do WebGL:First Sketches

Pamatujte si, proč jsme se začali učit WebGL

Po týdnu čtení zdroje a experimentování jsem měl provizorní REPL, ve kterém jsem mohl rychle hodit shadery a další kód, abych experimentoval a našel řešení.

Vyzbrojen znalostmi a zástupcem jsem šel hledat něco, co dokáže analyzovat můj soubor .obj až nahoru.
Jediný na internetu, který mi soubor více/méně správně analyzoval, byl balíček npm.
webgl-obj-loader. I když to mělo nepříjemný bug, který mi zkazil hodně krve.

Síť:Oprava chyby s triangulací #66

qtip zveřejněno

Triangulační funkce rozdělí ngon na jednotlivé trojúhelníky. Thiscommit opravuje chybu, kdy kód pro analýzu sítě ignoroval emitované trojúhelníky a místo toho používal původní indexy ngon.

Zobrazit na GitHubu

První koncepty

S pomocí knihovny jsem byl okamžitě schopen dosáhnout nějakého výsledku ve svém pískovišti.

Vrchol:

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.); // заливаем красным
}

Samotný kód

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();

V důsledku toho jsem dostal něco takového:

Myslím, že je to malý krok pro webgl, ale velký skok pro frontend .

Co je index ?

Při práci s oběma jsem se dozvěděl o nové věci:indexy.
Ve skutečnosti jsou v souboru .obj kromě vrcholů souřadnice textury, normály, plochy (plochy).
O co jde?

  • Souřadnice textury jsou pole čísel, které se předávají do shaderu fragmentů a umožňují shaderu pochopit, kde se aktuálně v modelu nachází, aby překryl pixel v závislosti na obecné poloze. Pokud žádné nejsou, ukáže se, že shader je obecně izolovaný a může malovat pouze pixely, aniž by věděl, kde přesně v tuto chvíli maluje. Souřadnice textury jsou předány jako atributy.
  • Normály jsou také souřadnice, ale lze je použít v shaderu fragmentů, abyste pochopili, jak nakreslit stín z objektu (modelu) v závislosti na tom, jak má na objekt dopadat světlo.
  • Povrch je pole indexů, které ukazují na index v poli vrcholů, textur a normál. Plochy jsou servisní data pro editor modelů (ala cinema4d a další), které umožňují spojovat polygony do čtverců a dalších složitějších tvarů. To je nutné zejména pro přesné vykreslení modelu, takže indexy jsou plochy. Řekněme, že jsme předali data z vrcholů a souřadnic textur do 2 atributů. A webgl se podívá na aktuální index a podle parametrů atributu (nezapomeňte, že jsme zadali velikost, kolik čísel vzít, abychom získali vrchol), vezme požadovanou sadu čísel z každého atributu a předá je shaderům.

Dále jsem zkusil změnit gl.TRIANGLES dne gl.LINES . A dostal následující výsledek:


No, vůbec ne to, co jsem očekával. Kde jsou moje krásné linie jako designér a co jsou trojúhelníky. Tehdy jsem si poprvé uvědomil jednoduchou pravdu, že všechno je palačinka na trojúhelnících. V této situaci jsem běžel na chat a pak vytvořil místní meme.

Jen jsem nevěděla co dál a požádala o radu. Mezi nimi bylo několik:

- Použijte ve fragmentu uv shader k nakreslení čar sami.

- Analyzujte samotný .obj a získejte požadované hodnoty.

- Vytvořte uv vidličku a roztáhněte texturu obrázku.

Z 1 odpovědi jsem nepochopil co je to uv, z nějakého důvodu mi pak nikdo nevysvětlil, že se jedná o souřadnice textury. A kde vzít tyto UV také nebylo jasné.

Z druhé odpovědi jsem také nepochopil, co dělat a jaké hodnoty použít.

A třetí odpověď se ukázala být, sice také záhadná, ale vysvětlili mi, co to znamená. Přes editor modelů bylo nutné vytvořit souřadnice textury a pod ně nakreslit texturu.

Na internetu jsem našel návody, jak udělat uv markup v kině 4d a na stejném místě jsem našel, jak nakreslit texturu. V editoru bylo možné vytvořit obrázek a vyplnit požadovanou barvu podél okrajů ploch (tváří). Myslel jsem, že to můj problém hned vyřeší. Vyplivnutím texture.png a nového obj s uv (tak se nazývají souřadnice textury).

Chyba, která mi zničila nervy

Běžel jsem si přečíst článek na webgl fondu jak natáhnout texturu. Bylo tam více kódu, ale neviděl jsem žádné potíže. Udělal jsem to jako v průvodci a myslel jsem si, že teď bude všechno v pořádku!

Vrchol

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);
    // ...
  }

A po tuně kódu dostaneme toto monstrum :

Co to??

A tady jsem začal epos za celý pracovní den, abych problém vyřešil. Moc jsem tomu nerozuměl a myslel jsem si, že se tu flákám já, a ne ten, který používám. Zpočátku jsem skutečně vrátil kód textury a jen jsem se pokusil překreslit a znovu získal neuvěřitelné výsledky.

Co to sakra?

Pak jsem se rozhodl, že problém je v exportu a obecně v tom, co jsem dělal s UV mapováním. Poté, co jsem si pár hodin hrál s exportem, rozhodl jsem se zkusit export v mixéru a ejhle, model byl opraven!

Poté, co jsem strávil spoustu hodin snahou zjistit, co je špatně. Všiml jsem si, že blender ve výchozím nastavení převádí 4 bodové povrchy na 3 bodové. A když jsem tuto funkci vypnul, modely se zase rozbily. A pak jsem si uvědomil, že problém byl celou tu dobu v knihovně webgl-obj-loader. Zlomila se, pokud dostala plochy ze 4 bodů (ve skutečnosti mi to bylo vysvětleno v chatu).

Okamžitě jsem běžel napsat stížnost na problém a pak jsem našel žádost o stažení, která tuto chybu opravila a připojila ji k mému problému.

Odmítání webgl-obj-loader

Při pohledu na výsledek bolestivé práce jsem si uvědomil, že to není to, co jsem chtěl. Čáry byly tlusté a navíc čím silnější byl filet, tím hustší byla oblast.
Také jsem pochopil, že existuje nějaké jiné řešení, protože když jsem model otevřel v prohlížečích modelů, správně nakreslily výsledek a krásně nakreslily čáry.

Když jsem to viděl, pochopil jsem, že vše lze vypočítat programově, ale nevěděl jsem, jak ...

A v té době se objevil rytíř v lesklé zbroji a vysvobodil mě z doupěte impotence. Byl to on, kdo navrhl:

Tehdy jsem vůbec nechápal, co to znamená a jak mi to pomůže. A ten člověk hodil příklad na three.js v sandboxu.

Tento příklad byl lehký. Okamžitě jsem si uvědomil, že můžete vyhodit webgl-obj-loader a žít jako člověk. Bez výčitek jsem to zahodil.

Je tu pokračování.