Podstawy WebGL

Gregg Tavares
Gregg Tavares

Podstawy WebGL

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 API. Dlaczego tak się dzieje?

W WebGL istotne są tylko 2 rzeczy. Współrzędne miejsca Clipspace w 2D i kolorach. Zadaniem programisty korzystającym z WebGL jest zapewnienie technologii WebGL o tych 2 elementach. Aby to zrobić, dostarczasz 2 „shadery”. cieniowanie Vertex, który udostępnia współrzędne miejsca klipu i panel do cieniowania fragmentów, który zapewnia kolor. Współrzędne Clipspace zawsze prowadzą od -1 do +1, niezależnie od wielkości 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 shaki

<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 mają wartość od -1 do +1 niezależnie od rozmiaru obszaru roboczego. W powyższym przykładzie widać, że nie robimy nic poza bezpośrednim przekazywaniem danych o pozycji. Ponieważ dane o pozycji są już w clipspace, nie trzeba nic robić. Jeśli chcesz zobaczyć 3D, to Ty musisz dostarczyć cieniowanie, które konwertuje z 3D na 2D, ponieważ WebGL IS A 2D API. W przypadku materiałów 2D prawdopodobnie lepiej byłoby pracować w pikselach niż w klimacie przycinania, więc zmieńmy cieniowanie, abyśmy mogli dostarczać prostokąty w pikselach i przekształcać je w Clipspace. 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>

Teraz możemy zmienić zakres danych z klipów 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);

Prostokąt znajdziesz u dołu tego obszaru. WebGL przyjmuje, że lewy dolny róg ma wartość 0,0. Aby był to bardziej tradycyjny lewy górny róg używany w interfejsach API grafiki 2D, po prostu odwracamy współrzędną Y.

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

Utwórzmy kod definiujący prostokąt w funkcję, abyśmy mogli wywoływać ją w przypadku prostokątów o różnych rozmiarach. Przy okazji wybierzemy kolory. Najpierw ustawiamy jednolity kolor danych wejściowych w cieniowaniu fragmentów.

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

uniform vec4 u_color;

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

Oto nowy kod, który służy do rysowania 50 prostokątów w losowych miejscach w 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 przekonasz się, że WebGL to całkiem prosty interfejs API. Przetwarzanie 3D może być bardziej skomplikowane, ale widżet jest dodawany przez Ciebie (programistę) w postaci bardziej złożonych systemów do cieniowania. Sam interfejs API WebGL jest dwuwymiarowy i dość prosty.

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

Tagi <script> domyślnie zawierają JavaScript. Nie możesz wpisać żadnego typu albo możesz umieścić type="javascript" lub type="text/javascript", a przeglądarka zinterpretuje zawartość jako JavaScript. Jeśli dodasz coś innego, przeglądarka zignoruje zawartość tagu skryptu.

Możemy używać tej funkcji do przechowywania cieniowania w tagach skryptu. Co więcej, możemy wymyślić własny typ i w języku JavaScript szukać tego, aby zdecydować, czy należy skompilować program do cieniowania jako cieniowanie wierzchołków, czy jako fragment do cieniowania.

W tym przypadku funkcja createShaderFromScriptElement szuka skryptu z określonym atrybutem id, a następnie sprawdza type, aby zdecydować, jaki typ programu do cieniowania należy utworzyć.

Przetwarzanie obrazu w WebGL

Przetwarzanie obrazu w WebGL jest proste. Czy to łatwe? Przeczytaj poniżej.

Do rysowania obrazów w WebGL musimy używać tekstur. WebGL oczekuje współrzędnych tekstury podczas odczytu tekstury, a nie pikseli, przez co WebGL oczekuje ich współrzędnych. Współrzędne tekstury mają wartość od 0,0 do 1,0 niezależnie od wymiarów tekstury. Ponieważ rysujemy tylko jeden prostokąt (cóż, 2 trójkąty), musimy określić WebGL, które miejsca w teksturze odpowiadają poszczególnym punktom. Informacje te przekażemy z cieniowania wierzchołków do cieniowania fragmentów, używając specjalnej zmiennej o nazwie „zmienna”. Nazywa się odmiennym, ponieważ jest różny. WebGL będzie interpolować wartości podane w cieniowaniu wierzchołków, gdy rysuje każdy piksel za pomocą cieniowania fragmentów. Używając cieniowania wierzchołków z końca poprzedniej sekcji, należy dodać atrybut, który przekazuje współrzędne tekstury i przekazuje go 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 narzędzie do cieniowania fragmentów, które sprawdza kolory 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 trzeba załadować obraz, utworzyć teksturę i skopiować go do tekstury. Obrazy w przeglądarce są wczytywane asynchronicznie, więc musimy jeszcze trochę zmienić układ kodu, 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 dostosujmy obraz. Może zamiast czerwonego z niebieskim wystarczy zamienić czerwony na niebieski?

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

A jeśli chcesz skorzystać z funkcji przetwarzania obrazu, która analizuje inne piksele? Jako że WebGL odwołuje się do tekstur o współrzędnych teksturach z zakresu od 0,0 do 1,0, możemy obliczyć, ile przemieścić się przez 1 piksel, wykonując proste obliczenia matematyczne onePixel = 1.0 / textureSize. Oto narzędzie do cieniowania 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);
...

Teraz gdy wiemy już, jak odwoływać się do innych pikseli, użyjmy jądra konwolucyjnego, by przeprowadzić typowe przetwarzanie 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 w tablicy reprezentuje, ile 8 pikseli wokół renderowanego piksela należy pomnożyć. Następnie dzielimy wynik przez wagę jądra (sumę wszystkich wartości w jądro) lub 1,0, co zawsze jest większe. Oto dobry artykuł na ten temat. Oto kolejny artykuł z rzeczywistym kodem, na którym można pisać odręcznie w języku C++. W naszym przypadku zrobimy to w shingerze, więc teraz przedstawimy nowy cieniowanie 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 JavaScripcie musimy dostarczyć 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 Ci się przekonać Cię, że przetwarzanie obrazu w WebGL jest całkiem proste. Teraz omówię, jak zastosować do obrazu więcej niż jeden efekt.

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

To tylko konwencja nazewnictwa. a_ dla atrybutów, które są danymi przekazywanymi przez bufory. u_ w przypadku jednolitych danych wejściowych do cieniowania, v_ dla zmiennych, które są wartościami przekazywanymi z cienia wierzchołków do cieniowania fragmentów i interpolowane (lub zmieniane) między wierzchołkami dla każdego narysowanego piksela.

Stosowanie wielu efektów

Następne najbardziej oczywiste pytanie dotyczące przetwarzania obrazu dotyczy stosowania wielu efektów.

Można spróbować wygenerować cieniowanie na bieżąco. Udostępnij interfejs, który pozwala użytkownikowi wybrać efekty, których chce użyć, a następnie wygeneruj program do cieniowania, który wykonuje wszystkie efekty. Nie zawsze jest to możliwe, ale ta technika jest często stosowana do tworzenia efektów do grafiki w czasie rzeczywistym. Bardziej elastyczniejszy sposób to użycie 2 dodatkowych tekstur i renderowanie po kolei na każdą teksturę, pingowanie w obie strony i za każdym razem nakładanie kolejnego efektu.

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

W tym celu musimy utworzyć bufory ramek. W WebGL i OpenGL nazwa Framebuffer jest w rzeczywistości słaba. Komponent WebGL/OpenGL Framebuffer to w rzeczywistości zbiór stanów, a w rzeczywistości żaden bufor. Dołączając teksturę do bufora ramki, możemy w niej renderować. Najpierw zamieńmy stary kod 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żyjmy tej funkcji, aby utworzyć jeszcze 2 tekstury i połączyć je w 2 buforach klatek.

// 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 i listę ich 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 użyjemy każdego z nich i wskażmy teksturę, którą również renderujemy, używając pingów.

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

Kwestie, które muszę poprawić.

Wywołanie gl.bindFramebuffer z null informuje WebGL, że chcesz renderować go w obszarze roboczym, a nie w jednym z buforów ramek. Interfejs WebGL musi zmienić format obrazu z klipu z powrotem na piksele. Jest to określone na podstawie ustawień usługi gl.viewport. Gdy zainicjujemy WebGL, ustawienia gl.viewport domyślnie ustawiają rozmiar obszaru roboczego. Ponieważ bufory ramek, w których renderujemy dane, mają inny rozmiar, więc musimy odpowiednio ustawić widoczny obszar. Na koniec w przykładach dotyczących podstawowych funkcji WebGL odwrócoliśmy współrzędne Y podczas renderowania, ponieważ WebGL wyświetla obszar roboczy z 0,0 w lewym dolnym rogu, a nie w bardziej tradycyjnym układzie 2D w lewym górnym rogu. Nie jest ona potrzebna w przypadku renderowania do bufora ramek. Nigdy nie jest on wyświetlany, więc to, która część jest u góry, a jaka na dole, nie ma znaczenia. Ważne jest tylko to, że piksel 0,0 w kolumnie framebuffer odpowiada w obliczeniach wynikowi 0,0. Aby temu zaradzić, udało mi się określić, czy ma zostać odwrócona, czy nie, dodając jeszcze 1 źródło wejściowy do cieniowania.

<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ć go przy renderowaniu za pomocą

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

W tym przykładzie wykorzystam pojedynczy program GLSL, który może przynieść wiele efektów. Jeśli chcesz w pełni skorzystać z przetwarzania obrazu, prawdopodobnie będziesz potrzebować wielu programów GLSL. Program dostosowania barw, nasycenia i luminancji. Druga do jasności i kontrastu. Jedno do odwracania, inne do regulacji poziomów itp. Konieczna będzie zmiana kodu, aby przełączyć programy GLSL i zaktualizować parametry dla tego konkretnego programu. Zastanawiam się nad napisaniem tego przykładu, ale jego treść powinna przekazać czytelnikowi, ponieważ wiele programów GLSL, z których każdy ma własne potrzeby dotyczące parametrów, prawdopodobnie wymaga refaktoryzacji, która pozwoli uniknąć bałaganu spaghetti. Mam nadzieję, że to i poprzednie przykłady sprawiły, że WebGL będzie teraz łatwiejszy do zrozumienia. Mam nadzieję, że rozpoczęcie od 2D ułatwi zrozumienie technologii WebGL. Jeśli znajdę czas, postaram się napisać kilka artykułów o trójwymiarowych eksploracjach oraz bardziej szczegółowe informacje o tym, co naprawdę robi WebGL pod maskowaniem.

WebGL i Alpha

Zauważyłem, że niektórzy programiści OpenGL mają problemy z tym, jak WebGL traktuje wersję alfa w buforze wstecznym (tj. na obszarze roboczym), dlatego warto omówić różnice między WebGL a Open alfa.

Największą różnicą między OpenGL i WebGL jest to, że renderuje się ono na buforze wstecznym, który nie jest z niczym komponowany lub nie jest komponowany z żadnym elementem przez menedżera okien systemu operacyjnego, więc nie ma znaczenia, jaka jest wersja alfa. WebGL jest komponowany przez przeglądarkę z stroną internetową, a domyślnie jest używany wstępnie zmnożony alfa, taki sam jak tagi .png <img> z przezroczystością i tagami kanwy 2D. W WebGL dostępne jest kilka sposobów, aby bardziej przypominać OpenGL.

#1) Powiedz WebGL, że chcesz je skomponować z niewstępnie mnożoną wartością alfa

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

Wartość domyślna to true (prawda). Oczywiście wynik będzie nadal skomponowany na stronie, niezależnie od tego, jaki kolor tła znajdzie się pod obszarem roboczym (kolor tła obszaru roboczego, kolor tła kontenera obszaru roboczego, kolor tła strony, elementy za obszarem roboczym > 0 itd.), Inaczej mówiąc, kod CSS koloru jest zdefiniowany dla tego obszaru strony internetowej. Naprawdę mogę sprawdzić, czy masz jakieś problemy z wersją alfa, czyli ustawić tło płótna na jasne, takie jak czerwony. Od razu zobaczysz, co się dzieje.

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

Możesz też ustawić kolor czarny, by ukryć wszystkie problemy z wersją alfa.

#2) Powiedz WebGL, że nie chcesz korzystać z wersji alfa w buforze wstecznym

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

Dzięki temu będzie działać podobnie jak OpenGL, ponieważ w obrębie wstecznego bufora będzie tylko podświetlenie RGB. To prawdopodobnie najlepsza opcja, bo przy dobrej przeglądarce nie ma wersji alfa, a przede wszystkim optymalizuje sposób łączenia WebGL. Oczywiście oznacza to również, że nie będzie ona mieć wersji alfa w pamięci podręcznej, więc jeśli używasz w tym celu kodu alfa w jakimś celu, które może nie działać w Twoim przypadku. Niewiele aplikacji, o których wiem, że używało wersji alfa w buforze wstecznym. Prawdopodobnie powinno to być ustawienie domyślne.

3) Usuń wersję 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 przebiega zwykle bardzo szybko, ponieważ w przypadku większości urządzeń jest to szczególny przypadek. Tak było w większości prezentacji. Gdybym był mądry, wybrałbym metodę nr 2 powyżej. Może zrobię to zaraz po tym, jak to opublikuję. Wygląda na to, że większość bibliotek WebGL powinna używać tej metody domyślnie. Ci niewielu programistów, którzy tak naprawdę używają wersji alfa do efektów komponowania, może o to poprosić. Pozostali użytkownicy uzyskują najlepsze wyniki przy jak najmniej niespodziankach.

4. Jednokrotne usunięcie wersji alfa i zaprzestanie jej renderowania

// 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 elementy we własnych buforach ramek, być może trzeba będzie ponownie włączyć renderowanie do wersji alfa, a potem wyłączyć je po przejściu do renderowania w obszarze roboczym.

5. Obsługa obrazów

Jeśli pliki PNG z wersją alfa są wczytywane do tekstur, domyślnie jest wstępnie mnożona ich wartość alfa, co zwykle NIE jest wykonywane w większości gier. Aby temu zapobiec, musisz skonfigurować WebGL za pomocą

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6. Wykorzystanie równania mieszania działającego ze wstępnie mnożonym alfa

Prawie wszystkie aplikacje OpenGL, które piszę lub w których używam

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Działa to w przypadku tekstur alfa, które nie zostały wstępnie zmnożone. Jeśli chcesz pracować ze wstępnie zwielodzonymi teksturami alfa, lepiej

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

To są metody, które znam. Jeśli znasz więcej informacji, opublikuj je poniżej.