Podstawy WebGL

Gregg Tavares
Gregg Tavares

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

WebGL dba o 2 rzeczy. współrzędne przestrzeni rzutowania w 2D i kolory; Jako programista przy użyciu WebGL musisz udostępnić te 2 elementy. W tym celu musisz przesłać 2 „shadery”. Moduł do cieniowania Vertex udostępniający współrzędne obszaru klipu oraz program do cieniowania fragmentów, który określa kolor. współrzędne przestrzeni ujętej w ramki zawsze mieszczą się w zakresie od -1 do +1, niezależnie od rozmiaru Twojego płótna. Oto prosty przykład WebGL, który pokazuje tę bibliotekę w jej 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>

Współrzędne przestrzeni klipu zawsze mieszczą się w zakresie od –1 do +1 niezależnie od rozmiaru płótna. Jak widać w powyższym przykładzie, nie robimy nic innego, tylko przekazujemy bezpośrednio dane o pozycji. Ponieważ dane o pozycji są już w przestrzeni klipu, nie trzeba niczego robić. Jeśli chcesz uzyskać obraz 3D, musisz dostarczyć shadery, które przekształcą obraz 3D w 2D, ponieważ WebGL to interfejs API 2D. W przypadku elementów 2D prawdopodobnie wolisz pracować w pikselach, a nie w przestrzeni clip. Zmieńmy więc shader, abyśmy mogli dostarczać prostokąty w pikselach i aby on sam przekształcał je w przestrzeń clip. Oto nowy shader wierzchołkowy

<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>

Teraz możemy zmienić dane z clipspace na piksele

// 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. W WebGL lewy dolny narożnik ma współrzędne 0,0. Aby uzyskać bardziej tradycyjny róg górny lewy używany w interfejsach API grafiki 2D, wystarczy odwrócić współrzędną y.

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

Zmieńmy kod definiujący prostokąt na funkcję, aby można było go wywoływać w przypadku prostokątów o różnych rozmiarach. Przy okazji umożliwimy też zmianę koloru. Najpierw spowodujemy, aby fragment shadera przyjmował dane wejściowe w postaci jednolitej barwy.

<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 tutaj jest 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 jest w istocie dość prostym interfejsem API. Choć praca w 3D może być bardziej skomplikowana, programista dodaje widżety w postaci bardziej złożonych cieniowania. Interfejs WebGL API jest dwuwymiarowy i całkiem prosty.

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

Tagi <script> domyślnie zawierają kod JavaScript. Możesz użyć typu „none” (brak typu) lub „type="javascript"” (lub „type="text/javascript"”), a przeglądarka zinterpretuje zawartość jako JavaScript. Jeśli wpiszesz coś innego, przeglądarka zignoruje zawartość tagu skryptu.

Możemy używać tej funkcji do przechowywania shaderów w tagach skryptu. Co więcej, możemy utworzyć własny typ i w naszym kodzie JavaScript sprawdzić, czy skompilować shader jako shader wierzchołkowy czy fragmentowy.

W tym przypadku funkcja createShaderFromScriptElement wyszukuje skrypt o określonym id, a potem sprawdza type, aby zdecydować, jaki typ shadera utworzyć.

Przetwarzanie obrazu w WebGL

Przetwarzanie obrazu jest łatwe w WebGL. Jak łatwo? Więcej informacji znajdziesz poniżej.

Do rysowania obrazów w WebGL musimy użyć tekstur. Podobnie jak w przypadku renderowania, gdy zamiast pikseli WebGL oczekuje współrzędnych przestrzeni ujętej w ramki, tak samo podczas odczytu tekstury oczekuje współrzędnych tekstury. Współrzędne tekstury mają wartości od 0,0 do 1,0 niezależnie od wymiarów tekstury. Rysujemy tylko jeden prostokąt (czyli 2 trójkąty), więc musimy podać WebGL, któremu miejscu w teksturze odpowiada każdy punkt prostokąta. Przekażemy te informacje z shadera wierzchołkowego do shadera fragmentowego, używając specjalnego typu zmiennej o nazwie „zmienna”. Nazywa się to różnicą, ponieważ jest różna. WebGL będzie interpolować wartości podane w cieniowaniu wierzchołków podczas rysowania każdego piksela za pomocą cieniowania fragmentów. Korzystając z modułu cieniowania wierzchołków z końca poprzedniej sekcji, musimy dodać atrybut przekazujący współrzędne tekstury, a następnie przekazać go 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 shadera, aby wyszukiwać 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 załadować obraz, utworzyć teksturę i skopiować obraz do tekstury. Obrazy wczytują się asynchronicznie, więc musimy trochę zmienić kod, aby poczekać na załadowanie tekstury. Narysujemy go, gdy się załaduje.

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

Niezbyt ekscytujące, więc trzeba manipulować obrazem. Może warto zamienić czerwony i niebieski?

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

Co zrobić, jeśli chcemy przetworzyć obraz, który uwzględnia inne piksele? Ponieważ WebGL odwołuje się do tekstur we współrzędnych tekstur w skali od 0,0 do 1,0, możemy obliczyć ruch o 1 piksel, korzystając z funkcji onePixel = 1.0 / textureSize. Oto cieniowanie fragmentów, które 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);
...

Wiemy już, jak odwoływać się do innych pikseli. Użyjmy teraz jądra konwolucji do wykonania kilku typowych operacji przetwarzania obrazu. W tym przypadku użyjemy jądra 3 x 3. Kernel konwolucji to po prostu macierz 3 x 3, w której każdy element reprezentuje mnożnik dla 8 pikseli wokół piksela, który renderujemy. Następnie dzielimy wynik przez wagę jądra (sumę wszystkich wartości w jądrze) lub 1,0 – zależnie od tego, która wartość jest większa. Oto całkiem dobry artykuł na ten temat Tutaj znajdziesz kolejny artykuł, w którym znajdziesz kod, który możesz napisać ręcznie w C++. W naszym przypadku wykonamy to w shaderze, więc oto nowy shader 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 JavaScript musimy podać jądro konwolucji.

...
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 to przekonało Was do przetwarzania obrazów w WebGL. W następnym filmie pokażę, jak zastosować do obrazu więcej niż 1 efekt.

Co oznaczają prefiksy a, u i v_ w przypadku zmiennych w GLSL?

To tylko konwencja nazewnictwa. a_ w przypadku atrybutów, które są danymi dostarczanymi przez bufory. u_ dla uniformów, które są wejściami dla shaderów, v_ dla zmiennych, które są wartościami przekazywanymi z shadera wierzchołkowego do shadera fragmentowego i interpolowanych (lub zróżnicowanych) między wierzchołkami dla każdego rysowanego piksela.

Stosowanie wielu efektów

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

Możesz spróbować wygenerować shadery w locie. udostępnić interfejs, w którym użytkownik może wybrać efekty, których chce użyć, a potem wygenerować program do cieniowania, który obsługuje wszystkie te efekty; Nie zawsze jest to możliwe, ale ta technika jest często używana do tworzenia efektów dla grafiki w czasie rzeczywistym. Bardziej elastycznym sposobem jest użycie 2 dodatkowych tekstur i renderowanie każdej z nich z kolei, przechodząc od jednej do drugiej 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ć framebuffery. W WebGL i OpenGL nazwa Framebuffer jest niezbyt trafna. WebGL/OpenGL Framebuffer to tak naprawdę kolekcja stanów, a nie żaden bufor. Jednak dołączając teksturę do framebuffera, możemy ją renderować. Najpierw przekształcimy stary kod do tworzenia tekstur 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żyjemy tej funkcji, aby utworzyć 2 tekstury i dołączyć je do 2 ramek.

// 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ąder, a potem listę, na której je zastosujemy.

// 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"
];

I na koniec zastosujmy każdą 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);
}

Mam kilka spraw do omówienia.

Wywołanie funkcji gl.bindFramebuffer z argumentem null informuje WebGL, że chcesz renderować na płótnie zamiast na jednym z ramek. WebGL musi przekształcić przestrzeń ujętą w ramki z powrotem w piksele. Robi to na podstawie ustawień gl.viewport. Ustawienia gl.viewport domyślnie przyjmują rozmiar rysunku podczas inicjowania WebGL. Ponieważ framebuffery, które renderujemy, mają inny rozmiar niż płótno, musimy odpowiednio ustawić widoczny obszar. Na koniec w przykładach z podstaw WebGL odwrócono współrzędną Y podczas renderowania, ponieważ WebGL wyświetla obszar roboczy, gdzie lewy dolny róg to 0,0, a nie tradycyjny lewy górny 2D. Nie jest to konieczne podczas renderowania do framebuffera. Ponieważ framebuffer nigdy nie jest wyświetlany, nie ma znaczenia, która część jest górą, a która dołem. Ważne jest tylko to, aby piksel 0,0 w ramce framebuffera odpowiadał 0,0 w naszych obliczeniach. Aby rozwiązać ten problem, dodałem do shadera jeszcze jedno wejście, dzięki czemu można ustawić, czy ma być odbicie lustrzane, czy nie.

<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>

a potem ustawić go podczas renderowania

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

W tym przykładzie zachowałem prostotę, używając pojedynczego programu GLSL, który może uzyskać wiele efektów. Jeśli chcesz przeprowadzić pełne przetwarzanie obrazu, prawdopodobnie będziesz potrzebować wielu programów GLSL. Program do regulacji barwy, nasycenia i luminancji. Kolejna opcja dotyczy jasności i kontrastu. Jeden do odwracania, drugi do dostosowywania poziomów itp. Musisz zmienić kod, aby przełączyć programy GLSL i zaktualizować parametry danego programu. Rozważałem napisanie tego przykładu, ale pozostawiam to ćwiczenie czytelnikowi, ponieważ wiele programów GLSL z własnymi parametrami wymaga prawdopodobnie gruntownej refaktoryzacji, aby nie stało się to wielką plątaniną. Mam nadzieję, że to i poprzednie przykłady sprawiły, że interfejs WebGL stał się bardziej przystępny, i mam nadzieję, że rozpoczęcie od 2D pomoże nieco lepiej zrozumieć WebGL. Jeśli znajdę czas, napiszę jeszcze kilka artykułów o tym, jak tworzyć grafikę 3D, oraz o tym, co tak naprawdę dzieje się w WebGL.

WebGL i Alfa

Zauważyłem, że niektórzy deweloperzy OpenGL mają problemy z tym, jak WebGL traktuje kanał alfa w backbufferze (czyli na płótnie), więc pomyślałem, że warto omówić niektóre różnice między WebGL a OpenGL dotyczące kanału alfa.

Największa różnica między OpenGL a WebGL polega na tym, że OpenGL renderuje do bufora podrzędnego, który nie jest łączony z niczym przez menedżera okien systemu operacyjnego, więc nie ma znaczenia, jaka jest wartość alfa. WebGL jest łączony przez przeglądarkę z stroną internetową, a domyślnie używa premultiplikowanej wartości alfa, tak samo jak tagi <img> .png z przezroczystością i tagi 2D canvas. Interfejs WebGL ma kilka sposobów, aby przypominał on tryb OpenGL.

#1) Powiedz WebGL, że chcesz go złożyć z niespremultiplikowaną przezroczystością.

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

Wartość domyślna to prawda (true). Oczywiście wynik nadal będzie złożony na stronie z dowolnym kolorem tła, który znajduje się pod kanwą (kolor tła kanwy, kolor tła kontenera kanwy, kolor tła strony, elementy za kanwą, jeśli kanwa ma kolejność nakładania elementów > 0 itd.). Innymi słowy, kolor CSS jest definiowany dla danego obszaru strony internetowej. Dobrym sposobem na sprawdzenie, czy masz problemy z przeźroczystością, jest ustawienie tła obrazu na jasny kolor, np. czerwony. Natychmiast zobaczysz, co się dzieje.

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

Możesz też ustawić czarny kolor, który ukryje wszystkie problemy z alfa.

#2) Poinformuj WebGL, że nie chcesz alfa w buforze pomocniczym

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

Dzięki temu będzie on działać bardziej jak OpenGL, ponieważ backbuffer będzie miał tylko RGB. To prawdopodobnie najlepsza opcja, bo dobra przeglądarka może wykryć brak wersji alfa i zoptymalizować sposób łączenia WebGL. Oczywiście oznacza to też, że w backbufferze nie będzie alfa kanału, więc jeśli używasz go w jakimś celu, może to nie działać. Z tego, co wiem, niewiele aplikacji używa przezroczystości w backbufferze. Myślę, że 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 danych jest zwykle bardzo szybkie, ponieważ w większości urządzeń jest to szczególne. Wykorzystałam to w większości moich prezentacji. Jeśli byłbym mądry, przełączyłbym się na metodę 2. Może zrobię to zaraz po wysłaniu tego posta. Wygląda na to, że większość bibliotek WebGL powinna domyślnie używać tej metody. Możesz poprosić o to tych nielicznych deweloperów, którzy faktycznie używają przezroczystości do tworzenia efektów. Pozostali będą liczyć na najlepsze wyniki i najmniej niespodzianek.

#4) Wyczyść alfa raz, a potem nie renderuj już 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 framebufferów, może być konieczne ponowne włączenie renderowania do alfa i jeszcze raz wyłączenie go, gdy przełączysz się na renderowanie do kanwy.

#5) Obsługa obrazów

Poza tym przy ładowaniu do tekstur plików PNG z kanałem alfa domyślnie ich wartość alfa jest wstępnie mnożona, co w większości gier NIE działa. Jeśli chcesz temu zapobiec, musisz użyć WebGL z pomocą

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Używanie równania mieszania, które działa z uprzednio pomnożoną wartością alfa

Prawie wszystkie aplikacje OpenGL, które napisałem lub nad którymi pracowałem, używają

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Działa to w przypadku tekstur z alfa-kanałem, które nie zostały zwielokrotnione. Jeśli chcesz pracować z wstępnie pomnożoną teksturą alfa, prawdopodobnie

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

To są metody, o których wiem. Jeśli wiesz więcej na ten temat, zamieść je poniżej.