Podstawy WebGL

Gregg Tavares
Gregg Tavares

WebGL umożliwia wyświetlanie w przeglądarce niesamowitej grafiki 3D w czasie rzeczywistym, ale wiele osób nie wie, że WebGL to w rzeczywistości interfejs API 2D, a nie 3D. Dlaczego tak się dzieje?

WebGL interesują tylko 2 rzeczy: Współrzędne Clipspace w 2D i kolory. Twoim zadaniem jako programisty korzystającego z WebGL jest dostarczenie tych 2 elementów. Aby to zrobić, musisz podać 2 „shadery”. Vertex shader, który dostarcza współrzędne clipspace, i fragment shader, który dostarcza kolor. Współrzędne przestrzeni przycinania zawsze mieszczą się w zakresie od -1 do +1, niezależnie od rozmiaru obszaru roboczego. Oto prosty przykład WebGL, który pokazuje WebGL w najprostszej formie.

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        -1.0, -1.0,
         1.0, -1.0,
        -1.0,  1.0,
        -1.0,  1.0,
         1.0, -1.0,
         1.0,  1.0]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);

Oto 2 shadery.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

Przypominam, że współrzędne clipspace zawsze mieszczą się w zakresie od –1 do +1 niezależnie od rozmiaru obszaru roboczego. W powyższym przypadku nie robimy nic poza bezpośrednim przekazywaniem danych o pozycji. Ponieważ dane o położeniu są już w przestrzeni wycinka, nie trzeba nic robić. Jeśli chcesz uzyskać efekt 3D, musisz dostarczyć shadery, które przekonwertują obraz z 3D na 2D, ponieważ WebGL TO INTERFEJS API 2D. W przypadku obiektów 2D prawdopodobnie wolisz pracować w pikselach niż w przestrzeni wycinka, więc zmieńmy cieniowanie, abyśmy mogli podawać prostokąty w pikselach i konwertować je na przestrzeń wycinka. Oto nowy program do cieniowania wierzchołków

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

Now we can change our data from clipspace to pixels

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

Możesz zauważyć, że prostokąt znajduje się u dołu tego obszaru. WebGL uważa lewy dolny róg za 0,0. Aby uzyskać bardziej tradycyjny lewy górny róg używany w interfejsach API grafiki 2D, wystarczy odwrócić współrzędną Y.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Przekształćmy kod definiujący prostokąt w funkcję, aby można było go wywoływać w przypadku prostokątów o różnych rozmiarach. Przy okazji umożliwimy ustawianie koloru. Najpierw sprawimy, że fragment shader będzie przyjmować dane wejściowe w postaci koloru.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

A oto nowy kod, który rysuje 50 prostokątów w losowych miejscach i losowych kolorach.

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

Mam nadzieję, że widzisz, że WebGL to właściwie dość prosty interfejs API. Chociaż tworzenie grafiki 3D może być bardziej skomplikowane, to Ty, jako programista, dodajesz te komplikacje w postaci bardziej złożonych programów do cieniowania. Sam interfejs WebGL API jest 2D i jest dość prosty.

Co oznaczają type="x-shader/x-vertex" i type="x-shader/x-fragment"?

Tagi <script> domyślnie zawierają JavaScript. Możesz nie podawać typu lub podać type="javascript" albo type="text/javascript", a przeglądarka zinterpretuje zawartość jako JavaScript. Jeśli wpiszesz coś innego, przeglądarka zignoruje zawartość tagu skryptu.

Możemy użyć tej funkcji do przechowywania obiektów cieniujących w tagach skryptów. Co więcej, możemy utworzyć własny typ i w kodzie JavaScript wyszukać go, aby zdecydować, czy skompilować program do cieniowania jako program do cieniowania wierzchołków czy fragmentów.

W tym przypadku funkcja createShaderFromScriptElement szuka skryptu z określonym id, a następnie sprawdza type, aby zdecydować, jaki typ shadera utworzyć.

Przetwarzanie obrazu WebGL

Przetwarzanie obrazów jest łatwe w WebGL. Jak łatwo? Przeczytaj poniższe informacje.

Aby rysować obrazy w WebGL, musimy używać tekstur. Podobnie jak WebGL oczekuje współrzędnych przestrzeni przycinania podczas renderowania zamiast pikseli, WebGL oczekuje współrzędnych tekstury podczas odczytywania tekstury. Współrzędne tekstury mieszczą się w zakresie od 0,0 do 1,0 niezależnie od jej wymiarów. Ponieważ rysujemy tylko 1 prostokąt (czyli 2 trójkąty), musimy poinformować WebGL, któremu miejscu w teksturze odpowiada każdy punkt prostokąta. Przekażemy te informacje z programu do cieniowania wierzchołków do programu do cieniowania fragmentów, używając specjalnego rodzaju zmiennej o nazwie „varying”. Nazywa się zmienną, ponieważ się zmienia. WebGL interpoluje wartości podane w programie do cieniowania wierzchołków, rysując każdy piksel za pomocą programu do cieniowania fragmentów. Korzystając z programu do cieniowania wierzchołków z końca poprzedniej sekcji, musimy dodać atrybut, aby przekazać współrzędne tekstury, a następnie przekazać je do programu do cieniowania fragmentów.

attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
   ...
   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points
   v_texCoord = a_texCoord;
}

Następnie dostarczamy fragment shader, który wyszukuje kolory z tekstury.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // Look up a color from the texture.
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

Na koniec musimy wczytać obraz, utworzyć teksturę i skopiować obraz do tekstury. Ponieważ jesteśmy w przeglądarce, obrazy są wczytywane asynchronicznie, więc musimy nieco zmienić kod, aby poczekać na wczytanie tekstury. Gdy się załaduje, narysujemy go.

function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // MUST BE SAME DOMAIN!!!
  image.onload = function() {
    render(image);
  }
}

function render(image) {
  ...
  // all the code we had before.
  ...
  // look up where the texture coordinates need to go.
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

  // provide texture coordinates for the rectangle.
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0,  0.0,
      1.0,  0.0,
      0.0,  1.0,
      0.0,  1.0,
      1.0,  0.0,
      1.0,  1.0]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set the parameters so we can render any size image.
  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.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  // Upload the image into the texture.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}

Nie jest to zbyt ekscytujące, więc zmodyfikujmy ten obraz. A może po prostu zamienić czerwony i niebieski?

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

A co, jeśli chcemy przetwarzać obraz, biorąc pod uwagę inne piksele? WebGL odwołuje się do tekstur we współrzędnych tekstury, które mieszczą się w zakresie od 0,0 do 1,0, więc możemy obliczyć, o ile przesunąć się o 1 piksel, za pomocą prostego działania matematycznego: onePixel = 1.0 / textureSize. Oto fragment kodu cieniowania, który uśrednia lewy i prawy piksel każdego piksela w teksturze.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // compute 1 pixel in texture coordinates.
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

   // average the left, middle, and right pixels.
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>

Następnie musimy przekazać rozmiar tekstury z JavaScriptu.

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Skoro już wiemy, jak odwoływać się do innych pikseli, użyjmy jądra splotu, aby wykonać kilka typowych operacji przetwarzania obrazu. W tym przypadku użyjemy jądra 3 x 3. Jądro splotu to po prostu macierz 3 x 3, w której każdy wpis reprezentuje, przez ile należy pomnożyć 8 pikseli wokół renderowanego piksela. Następnie dzielimy wynik przez wagę jądra (sumę wszystkich wartości w jądrze) lub przez 1,0, w zależności od tego, która wartość jest większa. Oto całkiem dobry artykuł na ten temat. Tutaj znajdziesz kolejny artykuł z kodem, który możesz napisać ręcznie w C++. W naszym przypadku wykonamy tę pracę w programie do cieniowania, więc oto nowy program do cieniowania fragmentów.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   float kernelWeight =
     u_kernel[0] +
     u_kernel[1] +
     u_kernel[2] +
     u_kernel[3] +
     u_kernel[4] +
     u_kernel[5] +
     u_kernel[6] +
     u_kernel[7] +
     u_kernel[8] ;

   if (kernelWeight <= 0.0) {
     kernelWeight = 1.0;
   }

   // Divide the sum by the weight but just use rgb
   // we'll set alpha to 1.0
   gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>

W języku JavaScript musimy podać jądro splotu.

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

Mam nadzieję, że udało mi się przekonać Cię, że przetwarzanie obrazów w WebGL jest dość proste. Następnie omówię, jak zastosować do obrazu więcej niż 1 efekt.

Co oznaczają prefiksy a, u i v_ przed zmiennymi w GLSL?

To tylko konwencja nazewnictwa. a_ dla atrybutów, czyli danych dostarczanych przez bufor. u_ dla zmiennych uniform, które są danymi wejściowymi dla programów do cieniowania, v_ dla zmiennych varying, które są wartościami przekazywanymi z programu do cieniowania wierzchołków do programu do cieniowania fragmentów i interpolowanymi (lub zmienianymi) między wierzchołkami dla każdego narysowanego piksela.

Stosowanie wielu efektów

Kolejne oczywiste pytanie dotyczące przetwarzania obrazów brzmi: jak zastosować wiele efektów?

Możesz spróbować generować shadery na bieżąco. Udostępnij interfejs, który pozwala użytkownikowi wybrać efekty, których chce użyć, a następnie wygenerować cieniowanie, które wykonuje wszystkie efekty. Nie zawsze jest to możliwe, ale ta technika jest często używana do tworzenia efektów w grafice w czasie rzeczywistym. Bardziej elastycznym sposobem jest użycie 2 dodatkowych tekstur i renderowanie ich na zmianę, przechodząc tam i z powrotem i za każdym razem stosując kolejny efekt.

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

Aby to zrobić, musimy utworzyć bufor ramki. W WebGL i OpenGL bufor ramki to niezbyt trafna nazwa. Bufor ramki WebGL/OpenGL to tak naprawdę tylko zbiór stanów, a nie bufor. Jeśli jednak dołączymy teksturę do bufora ramki, możemy renderować do tej tekstury. Najpierw przekształćmy stary kod tworzenia tekstury w funkcję.

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  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.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

Teraz użyjmy tej funkcji, aby utworzyć jeszcze 2 tekstury i dołączyć je do 2 buforów ramki.

// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
  var texture = createAndSetupTexture(gl);
  textures.push(texture);

  // make the texture the same size as the image
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);

  // Create a framebuffer
  var fbo = gl.createFramebuffer();
  framebuffers.push(fbo);
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}

Teraz utwórzmy zestaw jądra, a następnie listę jądra do zastosowania.

// Define several convolution kernels
var kernels = {
  normal: [
    0, 0, 0,
    0, 1, 0,
    0, 0, 0
  ],
  gaussianBlur: [
    0.045, 0.122, 0.045,
    0.122, 0.332, 0.122,
    0.045, 0.122, 0.045
  ],
  unsharpen: [
    -1, -1, -1,
    -1,  9, -1,
    -1, -1, -1
  ],
  emboss: [
     -2, -1,  0,
     -1,  1,  1,
      0,  1,  2
  ]
};

// List of effects to apply.
var effectsToApply = [
  "gaussianBlur",
  "emboss",
  "gaussianBlur",
  "unsharpen"
];

Na koniec zastosujmy każdy z nich, przełączając się między teksturami, które renderujemy.

// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);

// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
  // Setup to draw into one of the framebuffers.
  setFramebuffer(framebuffers[ii % 2], image.width, image.height);

  drawWithKernel(effectsToApply[ii]);

  // for the next draw, use the texture we just rendered to.
  gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}

// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer(fbo, width, height) {
  // make this the framebuffer we are rendering to.
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Tell the shader the resolution of the framebuffer.
  gl.uniform2f(resolutionLocation, width, height);

  // Tell webgl the viewport setting needed for framebuffer.
  gl.viewport(0, 0, width, height);
}

function drawWithKernel(name) {
  // set the kernel
  gl.uniform1fv(kernelLocation, kernels[name]);

  // Draw the rectangle.
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

Muszę omówić kilka kwestii.

gl.bindFramebuffer z parametrem null informuje WebGL, że chcesz renderować na elemencie canvas, a nie w jednym z buforów ramki. WebGL musi przekonwertować przestrzeń wycinka z powrotem na piksele. Robi to na podstawie ustawień gl.viewport. Ustawienia gl.viewport są domyślnie ustawione na rozmiar obszaru roboczego podczas inicjowania WebGL. Ponieważ bufory klatek, które renderujemy, mają inny rozmiar niż obszar roboczy, musimy odpowiednio ustawić widoczny obszar. W przykładach podstaw WebGL odwróciliśmy współrzędną Y podczas renderowania, ponieważ WebGL wyświetla obszar roboczy z punktem 0,0 w lewym dolnym rogu, a nie w lewym górnym, jak to jest zwykle w przypadku grafiki 2D. Nie jest to konieczne podczas renderowania do bufora ramki. Ponieważ bufor ramki nigdy nie jest wyświetlany, nie ma znaczenia, która część jest górna, a która dolna. Istotne jest tylko to, że piksel 0,0 w buforze ramki odpowiada wartości 0,0 w naszych obliczeniach. Aby rozwiązać ten problem, dodałem do cieniowania jeszcze jedno wejście, które pozwala określić, czy obraz ma być odwrócony.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...

void main() {
   ...
   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
   ...
}
</script>

Następnie możemy ustawić tę wartość podczas renderowania za pomocą

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

W tym prostym przykładzie użyłem jednego programu GLSL, który może osiągnąć wiele efektów. Jeśli chcesz przeprowadzić pełne przetwarzanie obrazu, prawdopodobnie potrzebujesz wielu programów GLSL. Program do regulacji odcienia, nasycenia i luminancji. Kolejny do jasności i kontrastu. Jeden do odwracania, inny do dostosowywania poziomów itd. Aby przełączać programy GLSL i aktualizować parametry danego programu, musisz zmienić kod. Rozważałem napisanie tego przykładu, ale jest to ćwiczenie, które najlepiej pozostawić czytelnikowi, ponieważ wiele programów GLSL, z których każdy ma własne potrzeby dotyczące parametrów, prawdopodobnie wymaga poważnej refaktoryzacji, aby nie stało się to wielkim bałaganem. Mam nadzieję, że ten i poprzednie przykłady sprawiły, że WebGL wydaje się nieco bardziej przystępny, a rozpoczęcie od 2D ułatwi zrozumienie WebGL. Jeśli znajdę czas, postaram się napisać jeszcze kilka artykułów o tym, jak tworzyć grafikę 3D, a także o tym, jak WebGL działa „pod maską”.

WebGL i alfa

Zauważyłem, że niektórzy deweloperzy OpenGL mają problemy z tym, jak WebGL traktuje kanał alfa w buforze tylnym (czyli na elemencie canvas), więc pomyślałem, że warto omówić niektóre różnice między WebGL a OpenGL związane z kanałem alfa.

Największa różnica między OpenGL a WebGL polega na tym, że OpenGL renderuje do bufora tylnego, który nie jest z niczym kompozytowany, a przynajmniej nie jest kompozytowany przez menedżera okien systemu operacyjnego, więc nie ma znaczenia, jaka jest wartość alfa. WebGL jest kompozytowany przez przeglądarkę ze stroną internetową, a domyślnie używa wstępnie pomnożonej wartości alfa, tak samo jak tagi .png <img> z przezroczystością i tagi canvas 2D. WebGL ma kilka sposobów na to, aby bardziej przypominać OpenGL.

1. Poinformuj WebGL, że chcesz, aby kompozycja była tworzona z kanałem alfa bez wstępnego mnożenia.

gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});

Wartość domyślna to true (prawda). Oczywiście wynik będzie nadal nakładany na stronę z kolorem tła, który ostatecznie znajdzie się pod obszarem roboczym (kolor tła obszaru roboczego, kolor tła kontenera obszaru roboczego, kolor tła strony, elementy za obszarem roboczym, jeśli obszar roboczy ma indeks Z > 0 itd.). Innymi słowy, będzie to kolor zdefiniowany w CSS dla tego obszaru strony internetowej. Dobrym sposobem na sprawdzenie, czy występują problemy z kanałem alfa, jest ustawienie tła obszaru roboczego na jasny kolor, np. czerwony. Od razu zobaczysz, co się dzieje.

<canvas style="background: red;"></canvas>

Możesz też ustawić kolor czarny, który ukryje wszelkie problemy z kanałem alfa.

2. Poinformuj WebGL, że nie chcesz kanału alfa w buforze tylnym

gl = canvas.getContext("experimental-webgl", {alpha: false});

Dzięki temu będzie działać bardziej jak OpenGL, ponieważ bufor tylny będzie zawierać tylko RGB. To prawdopodobnie najlepsza opcja, ponieważ dobra przeglądarka może zauważyć, że nie masz kanału alfa, i zoptymalizować sposób kompozycji WebGL. Oczywiście oznacza to również, że w buforze tylnym nie będzie kanału alfa, więc jeśli używasz kanału alfa w buforze tylnym do jakiegoś celu, może to nie zadziałać. Znam niewiele aplikacji, które używają kanału alfa w buforze tylnym. Moim zdaniem to powinno być ustawienie domyślne.

3. Wyczyść kanał alfa na końcu renderowania

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

Czyszczenie jest zwykle bardzo szybkie, ponieważ w większości urządzeń jest to specjalny przypadek. Robiłem to w większości moich wersji demonstracyjnych. Jeśli chcesz postąpić mądrze, przejdź do metody 2 powyżej. Może zrobię to zaraz po opublikowaniu tego posta. Wygląda na to, że większość bibliotek WebGL powinna domyślnie używać tej metody. Nieliczni deweloperzy, którzy faktycznie używają kanału alfa do efektów kompozytowych, mogą o niego poprosić. Pozostałe będą po prostu zapewniać najlepszą wydajność i najmniej niespodzianek.

4. Wyczyść kanał alfa raz, a potem już nie renderuj do niego

// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);

// Turn off rendering to alpha
gl.colorMask(true, true, true, false);

Oczywiście, jeśli renderujesz do własnych buforów ramki, może być konieczne ponowne włączenie renderowania do kanału alfa, a następnie wyłączenie go po przełączeniu się na renderowanie do elementu canvas.

5. Obsługa obrazów

Jeśli wczytujesz pliki PNG z kanałem alfa do tekstur, domyślnie kanał alfa jest wstępnie mnożony, co zwykle NIE jest stosowane w większości gier. Jeśli chcesz temu zapobiec, musisz poinformować WebGL za pomocą

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6. Używanie równania mieszania, które działa z wstępnie pomnożoną wartością alfa

Almost all OpenGL apps I've writing or worked on use

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Działa to w przypadku tekstur alfa, które nie są wstępnie mnożone. Jeśli chcesz pracować z teksturami z wstępnie przemnożoną przezroczystością, prawdopodobnie

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

To są metody, które znam. Jeśli znasz inne, opisz je poniżej.