Frontender představuje WebGL:Clear Lines

Vlastní analyzátor .obj, vlastní webgl

První věc, kterou jsem udělal, bylo upravit kód z karantény a použít gl.LINES.

Po předvedení návrháře jsem čekal, že uslyším, že je vše perfektní/dokonalé, odvedli jste skvělou práci! Ale slyšel jsem:

A pak jsem si uvědomil, že gl.LINES nijak mi nepomohou vyřešit problém, jen jsem šel špatnou cestou. Z nějakého důvodu se mi zdálo, že nejdůležitější jsou linie, ale pak jsem si uvědomil, že musím model vyplnit barvou a zvýraznit okraje ploch na něm jinou barvou.

Uvědomil jsem si, že stále potřebuji uvs (souřadnice textury), protože bez nich nelze postavu správně namalovat, ale ty uv, které editor modelu vygeneroval, nebyly vhodné pro malování. Generování souřadnic mělo nějakou logiku.

Upozorněte na tento problém s osobou, která analýzu ukázala. Dal mi nový sandbox, ve kterém mi ukázal, jak generovat souřadnice textury, což mi dalo novou naději. Načrtl také jednoduchý shader, který kreslí čáry. Vzal jsem jeho řešení, aktualizoval jsem svůj sandbox a aktualizoval analyzátor.
Poprvé ukážu kód parseru v článku.

const uv4 = [[0, 0], [1, 0], [1, 1], [0, 1]]; // захаркоженные координаты текстур

// функция которая парсит .obj и выплевывает вершины с текстурными координатами.
export function getVBForVSTFromObj(obj) {
  const preLines = obj.split(/[\r\n]/).filter(s => s.length);

  // функция которая отдавала все строки по первому вхождению
  const exNs = (a, fchar) =>
    a
      .filter(s => s[0] === fchar)
      .map(s =>
        s
          .split(" ")
          .filter(s => s.length)
          .slice(1)
          .map(Number)
      );

  // та же функция что выше, только для поверхностей (faces) и дополнительно парсила сами поверхности
  const exFs = s =>
    s
      .filter(s => s[0] === "f")
      .map(s =>
        s
          .split(/\s+/)
          .filter(s => s.length)
          .slice(1)
          .map(s => s.split("/").map(Number))
      );

  const vertexList = exNs(preLines, "v"); // получаем все вершины
  const faceList = exFs(preLines); // все поверхности

  const filteredFaceList = faceList.filter(is => is.length === 4); // собираем поверхности только с 4 точками, т.е. квады
  const vertexes = filteredFaceList
    .map(is => {
      const [v0, v1, v2, v3] = is.map(i => vertexList[i[0] - 1]);
      return [[v0, v1, v2], [v0, v2, v3]];
    }) // склеиваем треугольники 
    .flat(4);


  const uvs = Array.from({ length: filteredFaceList.length }, () => [
    [uv4[0], uv4[1], uv4[2]],
    [uv4[0], uv4[2], uv4[3]]
  ]).flat(4); // собираем текстурные координаты под каждую поверхность

  return [vertexes, uvs];
}

Dále jsem aktualizoval svůj fragment shader:

precision mediump float;

varying vec2 v_texture_coords; // текстурные координаты из вершинного шейдера
// define позволяет определять константы
#define FN (0.07) // толщина линии, просто какой-то размер, подбирался на глаз
#define LINE_COLOR vec4(1,0,0,1) // цвет линии. красный.
#define BACKGROUND_COLOR vec4(1,1,1,1) // остальной цвет. белый.

void main() {
  if ( 
    v_texture_coords.x < FN || v_texture_coords.x > 1.0-FN ||
    v_texture_coords.y < FN || v_texture_coords.y > 1.0-FN 
  )
    // если мы находимся на самом краю поверхности, то рисуем выставляем цвет линии
    gl_FragColor = LINE_COLOR;
  else 
    gl_FragColor = BACKGROUND_COLOR;
}

(pískoviště)



A ó můj! Zde je výsledek, který jsem tak moc chtěl. Ano, je to drsné, čáry jsou tvrdé, ale je to krok vpřed. Pak jsem přepsal kód shaderu na smoothstep (speciální funkce, která umožňuje provádět lineární interpolaci) a také změnil styl pojmenování proměnných.

precision mediump float;
uniform vec3 uLineColor; // теперь цвета и прочее передаю js, а не выставляю константы
uniform vec3 uBgColor;
uniform float uLineWidth;

varying vec2 vTextureCoords;

// функция которая высчитала на основе uv и "порога" и сколько должна идти плавность
// то есть через threshold я говорил где должен быть один цвет, а потом начинается другой, а с помощью gap определял долго должен идти линейный переход. Чем выше gap, тем сильнее размытость.
// и которая позволяет не выходить за пределы от 0 до 1
float calcFactor(vec2 uv, float threshold, float gap) {
  return clamp(
    smoothstep(threshold - gap, threshold + gap, uv.x) + smoothstep(threshold - gap, threshold + gap, uv.y), 0., 
    1.
  );
}

void main() {
  float threshold = 1. - uLineWidth;
  float gap = uLineWidth + .05;
  float factor = calcFactor(vTextureCoords, threshold, gap);
  // функция mix на основе 3 аргумента выплевывает 1 аргумент или 2, линейно интерпретируя.
  gl_FragColor = mix(vec4(uLineColor, 1.), vec4(uBgColor, 1.), 1. - factor);
}



A hle, krása! Návrhář je spokojený a já také. Ano, jsou tam nějaké maličkosti, ale tohle je to nejlepší, co jsem pak mohla rodit.

Ačkoli ti, kteří jsou obzvláště pozorní, okamžitě zaznamenají, že velikosti čtverců se zvětšily než v předchozí "hrubé" verzi.
A nebyl jsem nijak zvlášť pozorný, takže jsem si toho všiml až po 2 týdnech. Možná mi přešla euforie z úspěchu do hlavy...

Dokončení shaderu

Když jsem dokončil první implementaci renderu, šel jsem dělat další úkoly na projektu. Ale během 2 týdnů jsem si uvědomil, že jsem nespokojený s tím, jak model vypadal, rozhodně nevypadaly jako na renderu designéra a také jsem se obával, že tloušťka čar není jaksi stejná.

Nebylo mi jasné, proč mám na jablku tak velkou mřížku, ačkoliv v cinema4d a blenderu je docela malá.
Navíc jsem se rozhodl podělit se o své zážitky s kolegou v práci, a když jsem mu začal vysvětlovat, jak můj shader funguje, uvědomil jsem si, že si ani nepamatuji, jak jsem se k němu vůbec dostal a kdy jsem se mu snažil vysvětlit , začal jsem experimentovat se shaderem.

Pro začátek jsem si vzpomněl na trik z tutoriálů pro shadery a jen jsem odléval barvy na základě x souřadnic a získal jsem pro sebe zajímavý výsledek.

Uvědomil jsem si, že celou tu dobu jsem měl tak jemné pletivo, ale z nějakého důvodu jsem to ignoroval. Po nějakém dalším hraní jsem si konečně uvědomil, že jsem na každý povrch namaloval pouze 2 ze 4 obličejů, což vedlo k tomu, že moje síť byla tak hrubá.

Nemohl jsem použít kroky a podobně k implementaci mřížky, kterou jsem potřeboval, dostal jsem nějaký nesmysl.

Pak jsem se rozhodl, že nejprve budu psát neobratně a zplodil jsem takového šejdíře.

if (vTextureCoords.x > uLineWidth && vTextureCoords.x < 1.0 - uLineWidth && vTextureCoords.y > uLineWidth && vTextureCoords.y < 1.0 - uLineWidth) {
    gl_FragColor = vec4(uBgColor, 1.);
  } else {
    gl_FragColor = vec4(uLineColor, 1.);
  }

Konečně jsem dosáhl požadovaného výsledku.

Dále za hodinu spolu s dokem na funkce z webgl. Podařilo se mi přepsat kód tak, aby byl blíže k webgl.

float border(vec2 uv, float uLineWidth, vec2 gap) {
  vec2 xy0 = smoothstep(vec2(uLineWidth) - gap, vec2(uLineWidth) + gap, uv);
  vec2 xy1 = smoothstep(vec2(1. - uLineWidth) - gap, vec2(1. - uLineWidth) + gap, uv);
  vec2 xy = xy0 - xy1;
  return clamp(xy.x * xy.y, 0., 1.);
}

void main() {
  vec2 uv = vTextureCoords;
  vec2 fw = vec2(uLineWidth + 0.05);

  float br = border(vTextureCoords, uLineWidth, fw);
  gl_FragColor = vec4(mix(uLineColor, uBgColor, br), 1.);
}

Dostal jsem jemnou síťku. Hurá!

Stále jsem ale měl problém, že čím blíže k okraji, tím hůře se linky liší.
Ohledně této otázky jsem požádal o pomoc v chatu a řekli mi o OES_standard_derivatives rozšíření pro webgl. Je to něco jako pluginy, které přidávaly nové funkce do glsl nebo zahrnovaly některé funkce do vykreslování. Přidáním fwidth do kódu shaderu (nezapomeňte zahrnout rozšíření před sestavením programu, jinak budou problémy), funkce, která se objevila po připojení rozšíření. Dostal jsem, co jsem chtěl.

  #ifdef GL_OES_standard_derivatives
    fw = fwidth(uv);
  #endif

Bože, jaká krása!

Zbývá jen napsat, jak jsem udělal animaci!