100 jazyků Speedrun:Episode 41:WebGL Shader Language

WebGL umožňuje webům používat GPU. Abychom to hodně zjednodušili, GPU fungují takto:

  • pošlete jim nějaký popis scény, většinou hodně trojúhelníků a související data
  • GPU spustí "vertex shader" pro každý roh trojúhelníku, aby určil, kam má být vykreslen
  • pro každý trojúhelník GPU zjistí, které pixely pokrývá a který trojúhelník je v každém bodě nejblíže k fotoaparátu
  • pak GPU spustí "fragment shader" (také známý jako "pixel shader") pro každý pixel každého trojúhelníku, který se nakreslí - tento program určí, jakou barvu vykreslí pixel, a poradí si s texturami, blesky a tak dále

Proč GPU

Důvodem, proč jsou GPU tak hloupě rychlé v tom, co dělají, je to, že spouštějí stejný program tisíckrát nebo milionkrát. Dokážete si tedy představit, že GPU obsahuje stovky nebo tisíce mini-CPU, z nichž každý je docela slabý a na všech může vždy běžet pouze stejný program, ale je jich hodně.

U běžných programů by GPU byly příliš pomalé na to, aby něco dělaly, ale kromě grafiky existuje několik dalších aplikací, kde musíte dělat to samé milionkrát, a GPU jsou perfektním řešením. Nejzřetelnější jsou těžba kryptoměn a neuronové sítě.

Jazyk WebGL Shader

Co uděláme, je řešení čistého shader jazyka. Nebude zde žádná skutečná geometrie a žádný skutečný vertex shader – pouze jeden velký čtverec pokrývající celé plátno. Nebo přesněji dva trojúhelníky, protože GPU nemají rády žádné tvary, které nejsou trojúhelníky. Vše bude provedeno ve fragment shaderu.

WebGL je velmi obtížný a normálně byste ho používali s nějakým rámcem, který se zabývá všemi těmi nesmysly nízké úrovně. Ukážu popis jen jednou a bez velkého vysvětlování.

Deska kotle

Jediná věc, kterou se budeme zabývat, je fragmentShaderSource . Zbytek zatím považujte za irelevantní:

<style>
  body {
    margin: 0;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
  }
</style>

<canvas height="800" width="800"></canvas>

<script>
  let canvas = document.querySelector("canvas")
  let gl = canvas.getContext("webgl")
  let vertexShaderSource = `
  attribute vec2 points;
  void main() {
    gl_Position = vec4(points, 0.0, 1.0);
  }`

  let fragmentShaderSource = `
  void main() {
    mediump vec2 pos = gl_FragCoord.xy / vec2(800, 800);
    gl_FragColor = vec4(0, pos.x, pos.y, 1.0);
  }`

  let program = gl.createProgram()

  // create a new vertex shader and a fragment shader
  let vertexShader = gl.createShader(gl.VERTEX_SHADER)
  let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)

  // specify the source code for the shaders using those strings
  gl.shaderSource(vertexShader, vertexShaderSource)
  gl.shaderSource(fragmentShader, fragmentShaderSource)

  // compile the shaders
  gl.compileShader(vertexShader)
  gl.compileShader(fragmentShader)

  // attach the two shaders to the program
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)
  gl.linkProgram(program)
  gl.useProgram(program)
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program))
  }

  let points = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1, 1, -1, -1, 1])
  let buffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
  let pointsLocation = gl.getAttribLocation(program, "points")
  gl.vertexAttribPointer(pointsLocation, 2, gl.FLOAT, false, 0, 0)
  gl.enableVertexAttribArray(pointsLocation)
  gl.drawArrays(gl.TRIANGLES, 0, 6)
</script>

Ahoj, světe!

Pojďme si projít zdroj fragment shaderu:

  void main() {
    mediump vec2 pos = gl_FragCoord.xy / vec2(800, 800);
    gl_FragColor = vec4(0, pos.x, pos.y, 1.0);
  }

gl_FragCoord je vstup - jeho pozice na obrazovce. Je to divné, když nastavíme canvas velikost s <canvas height="800" width="800"></canvas> , pak to funguje, ale pokud nastavíme canvas velikost s CSS, WebGL bude považovat plátno za 300x150.

gl_FragCoord má 4 souřadnice:x , y zobrazující pozici na plátně (nepříjemně vlevo dole jako 0, 0 místo vlevo nahoře), z je to, jak hluboký je fragment – ​​což nevadí, protože nemáme žádné překrývající se trojúhelníky a w není pro nás skutečně relevantní.

gl_FragColor je barva, také 4 vektor - se třemi složkami jsou RGB a poslední je neprůhlednost. Jsou na stupnici od 0 do 1, na rozdíl od CSS 0 až 255.

mediump vec2 pos deklaruje lokální proměnnou - dvouprvkový vektor se střední přesností. Ve WebGL musíte dát všemu přesnost, to neplatí ani v tradičním OpenGL.

gl_FragCoord.xy / vec2(800, 800) - trvá to xy část gl_FragCoord vektor a vydělí je 800. Je to stejné jako vec2(gl_FragCoord.x / 800, gl_FragCoord.y / 800) . WebGL používá mnoho takových vektorových operací, takže je lepší si na ně zvyknout.

Tím se vygeneruje následující obrázek:

Jak vidíte, vpravo je zelenější a nahoře modřejší. Červená je nula, neprůhlednost je max.

Šachovnice

Tato šachovnice není moc hezká, ale cílem je ukázat, že máme číslo buňky v cell a umístění v buňce s t .

  void main() {
    mediump vec2 pos = gl_FragCoord.xy / vec2(80, 80);
    mediump vec2 cell = floor(pos);
    mediump vec2 t = fract(pos);
    mediump float u = fract((cell.x + cell.y) / 2.0);
    if (u == 0.0) {
      gl_FragColor = vec4(t.y, 0, t.x, 1.0);
    } else {
      gl_FragColor = vec4(0, t.x, t.y, 1.0);
    }
  }

Tím se vygeneruje následující obrázek:

Nástěnka FizzBuzz

Dalším krokem k provedení funkčního FizzBuzzu je zacházet s těmito buňkami jako s čísly 1 až 100 (vlevo nahoře je 1, pak v přirozeném pořadí psaní).

  • Fizz je červený
  • Hlášky jsou zelené
  • FizzBuzz je modrý
  • Čísla jsou odstíny šedé, proporcionální od 1 do 100
  // a % b returns "integer modulus operator supported in GLSL ES 3.00 and above only"
  // so we do it old school
  bool divisible(int a, int b) {
    return a - (a / b) * b == 0;
  }

  void main() {
    mediump vec2 pos = gl_FragCoord.xy / vec2(80, 80);
    mediump vec2 cell = floor(pos);
    int n = int(cell.x) + (9 - int(cell.y)) * 10 + 1;
    mediump float nf = float(n);

    if (divisible(n, 15)) {
      gl_FragColor = vec4(0.5, 0.5, 1.0, 1.0);
    } else if (divisible(n, 5)) {
      gl_FragColor = vec4(0.5, 1.0, 0.5, 1.0);
    } else if (divisible(n, 3)) {
      gl_FragColor = vec4(1.0, 0.5, 0.5, 1.0);
    } else {
      gl_FragColor = vec4(nf/100.0, nf/100.0, nf/100.0, 1.0);
    }
  }

Můžeme také přepnout skript na požadovanou verzi spuštěním s #version 300 es , ale to by vyžadovalo další změny, takže pokračujme v tom, co jsme začali.

Na normálním CPU bychom nemuseli přepínat na celá čísla, protože plovoucí dělení je přesné, pokud je to vůbec možné. 45.0 / 15.0 je přesně 3.0 , ne když ne, ale o tom. Na GPU (alespoň s mediump ), ne tak moc. Dostali bychom něco blízko 3.0, ale to by celý algoritmus dost znervózňovalo. To je další způsob, jak GPU vyhrávají závod – pro kreslení pixelů tuto plnou přesnost nepotřebujete.

FizzBuzz Digits

Určitě se tam dostáváme, dalším krokem by bylo zobrazení každé číslice zvlášť. Jakékoli číselné pole by se tedy rozdělilo na dvě - levé by byla první číslice, pravé by byla druhá číslice. Děláme 1-100, ale 100 je Buzz, takže nikdy nepotřebujeme tři číslice. Měli bychom také přeskočit úvodní číslici, pokud je to nula, ale máme jen tolik barev.

  bool divisible(int a, int b) {
    return a - (a / b) * b == 0;
  }

  void main() {
    mediump vec2 pos = gl_FragCoord.xy / vec2(80, 80);
    mediump vec2 cell = floor(pos);
    int n = int(cell.x) + (9 - int(cell.y)) * 10 + 1;
    bool right_half = fract(pos.x) > 0.5;
    int tens = n / 10;
    int ones = n - tens * 10;

    if (divisible(n, 15)) {
      gl_FragColor = vec4(0.5, 0.5, 1.0, 1.0);
    } else if (divisible(n, 5)) {
      gl_FragColor = vec4(0.5, 1.0, 0.5, 1.0);
    } else if (divisible(n, 3)) {
      gl_FragColor = vec4(1.0, 0.5, 0.5, 1.0);
    } else if (right_half) {
      gl_FragColor = vec4(float(ones)/10.0, float(ones)/10.0, float(ones)/10.0, 1.0);
    } else {
      gl_FragColor = vec4(float(tens)/10.0, float(tens)/10.0, float(tens)/10.0, 1.0);
    }
  }

FizzBuzz

V tuto chvíli to můžeme vzít dvěma způsoby – buď mít veškerý složitý kód k vykreslení každého znaku a číslice jako u epizody Logo. Nebo použijte texturu. Myslím, že řešení textury by bylo více v souladu s tím, o čem je WebGL, i když to znamená více standardní.

Nejprve je zde textura:

A zde je celý program s aktualizovaným vzorem:

<style>
  body {
    margin: 0;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
  }
</style>

<canvas height="800" width="800"></canvas>

<script>
let img = new Image()
img.crossOrigin = ""
img.src = `./texture.png`
img.onload = () => {
  startWebGL()
}

let startWebGL = () => {
  let canvas = document.querySelector("canvas")
  let gl = canvas.getContext("webgl")
  let vertexShaderSource = `
  attribute vec2 points;
  void main() {
    gl_Position = vec4(points, 0.0, 1.0);
  }`

  let fragmentShaderSource = `
  uniform sampler2D sampler;

  bool divisible(int a, int b) {
    return a - (a / b) * b == 0;
  }

  void main() {
    mediump vec2 pos = gl_FragCoord.xy / vec2(80, 80);
    mediump vec2 cell = floor(pos);
    mediump float px = fract(pos.x);
    mediump float py = fract(pos.y);
    int n = int(cell.x) + (9 - int(cell.y)) * 10 + 1;
    bool right_half = px > 0.5;
    int tens = n / 10;
    int ones = n - tens * 10;
    mediump float cx, cy;

    cx = gl_FragCoord.x / 800.0;

    if (divisible(n, 15)) {
      cx = 15.0;
    } else if (divisible(n, 5)) {
      cx = 13.0;
    } else if (divisible(n, 3)) {
      cx = 11.0;
    } else if (right_half) {
      cx = float(ones);
    } else if (tens == 0) {
      cx = float(tens);
    } else {
      cx = float(tens) + 1.0;
    }

    cy = 1.0-fract(pos.y);

    gl_FragColor = texture2D(sampler, vec2((cx + px*2.0)/17.0, cy));
  }`

  let program = gl.createProgram()

  // create a new vertex shader and a fragment shader
  let vertexShader = gl.createShader(gl.VERTEX_SHADER)
  let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)

  // specify the source code for the shaders using those strings
  gl.shaderSource(vertexShader, vertexShaderSource)
  gl.shaderSource(fragmentShader, fragmentShaderSource)

  // compile the shaders
  gl.compileShader(vertexShader)
  gl.compileShader(fragmentShader)
  console.error(gl.getShaderInfoLog(fragmentShader))

  // attach the two shaders to the program
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)
  gl.linkProgram(program)
  gl.useProgram(program)
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program))
  }

  let points = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1, 1, -1, -1, 1])
  let buffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
  let pointsLocation = gl.getAttribLocation(program, "points")
  gl.vertexAttribPointer(pointsLocation, 2, gl.FLOAT, false, 0, 0)
  gl.enableVertexAttribArray(pointsLocation)

  let texture = gl.createTexture()
  gl.bindTexture(gl.TEXTURE_2D, texture)
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
  gl.activeTexture(gl.TEXTURE0)
  gl.bindTexture(gl.TEXTURE_2D, texture)
  gl.uniform1i(gl.getUniformLocation(program, "sampler"), 0)

  gl.drawArrays(gl.TRIANGLES, 0, 6)
}
</script>

Doporučuji ignorovat všechny věci související s načítáním obrázku do textury a soustředit se pouze na fragmentShaderSource což je docela pěkné. Obrázek bez ohledu na jeho velikost je považován za 0,0 až 1,0 čtverec. Náš shader tedy potřebuje vypočítat, jak každý pixel odpovídá nějakému bodu na obrázku.

Měli byste používat WebGL?

WebGL poskytuje funkce, které nejsou ve skutečnosti dosažitelné žádným jiným způsobem, jako je například vysoce výkonná grafika na telefonech, ale je extrémně nízká a je bolestné přímo psát, takže to nedoporučuji.

Naštěstí je nad WebGL postaveno mnoho frameworků, od klasického three.js přes Unity až po nový žhavý Svelte Cubed.

Rozhodně doporučuji místo toho vybrat jeden z těchto frameworků. A ve skutečnosti je snazší s nimi psát shadery WebGL Shader Language než s obyčejným WebGL, protože se za vás vypořádají se spoustou standardů kolem shaderů.

Kód

Všechny příklady kódu pro sérii budou v tomto úložišti.

Kód pro epizodu WebGL Shader Language je k dispozici zde.