Concetti fondamentali di WebGL

Gregg Tavares
Gregg Tavares

Concetti fondamentali di WebGL

WebGL consente di visualizzare straordinarie immagini 3D in tempo reale nel browser, ma molti non sanno che in realtà è un'API 2D, non 3D. Mi spiego meglio.

WebGL si occupa solo di due cose. Coordinate dello spazio di clip in 2D e colori. Il tuo compito come programmatore che utilizza WebGL è fornire a WebGL queste due cose. A tal fine, fornisci due "shader". Uno snapshot Vertex che fornisce le coordinate del clipspace e uno shaker per frammenti che fornisce il colore. Le coordinate dello spazio di clip vanno sempre da -1 a +1, indipendentemente dalle dimensioni della tela. Ecco un semplice esempio di WebGL che mostra WebGL nella sua forma più semplice.

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

Ecco i due shader

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

Anche in questo caso, le coordinate dello spazio di clip vanno sempre da -1 a +1, indipendentemente dalle dimensioni della tela. Nel caso precedente, noterai che stiamo solo trasmettendo i dati sulla posizione. Poiché i dati di posizione si trovano già all'interno di clipspace, non è necessario svolgere alcuna operazione. Se vuoi il 3D, spetta a te fornire strumenti per la conversione dal 3D al 2D perché WebGL È un'API 2D. Per gli elementi 2D, probabilmente è preferibile lavorare in pixel anziché clipspace, quindi cambiamo lo shaker in modo da poter fornire dei rettangoli in pixel e farli convertire in clipspace. Ecco il nuovo Vertex Shar

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

Ora possiamo cambiare i dati da clipspace a pixel

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

Potresti notare che il rettangolo si trova nella parte inferiore dell'area. WebGL considera l'angolo in basso a sinistra pari a 0,0. Per ottenere l'angolo in alto a sinistra più tradizionale utilizzato per le API di grafica 2D, basta capovolgere la coordinata y.

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

Trasformiamo il codice che definisce un rettangolo in una funzione in modo da poterlo chiamare per rettangoli di dimensioni diverse. A proposito, renderemo il colore impostabile. Innanzitutto, facciamo in modo che lo shader frammento riceva un input uniforme di colore.

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

uniform vec4 u_color;

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

Ed ecco il nuovo codice che disegna 50 rettangoli in posizioni e colori casuali.

...

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

Spero che tu possa vedere che WebGL è in realtà un'API piuttosto semplice. Anche se può essere più complicato realizzare il 3D, questa complicazione viene aggiunta da te, il programmatore, sotto forma di shader più complessi. L'API WebGL stessa è 2D e abbastanza semplice.

Che cosa significano type=&quot;x-shader/x-vertex&quot; e type=&quot;x-shader/x-fragment&quot;?

Per impostazione predefinita, i tag <script> contengono JavaScript. Non puoi inserire alcun tipo oppure puoi inserire type="javascript" o type="text/javascript" e il browser interpreterà i contenuti come JavaScript. Se inserisci qualcos'altro, il browser ignora i contenuti del tag script.

Possiamo utilizzare questa funzionalità per memorizzare gli shader nei tag script. Ancora meglio, possiamo creare il nostro tipo e cercarlo nel codice JavaScript per decidere se compilare lo shader come shader per vertici o shader per frammenti.

In questo caso, la funzione createShaderFromScriptElement cerca uno script con id specificato e poi esamina type per decidere quale tipo di shader creare.

Elaborazione di immagini WebGL

L'elaborazione delle immagini è facile in WebGL. Quanto è facile? Leggi di seguito.

Per disegnare immagini in WebGL, dobbiamo utilizzare le texture. Analogamente al modo in cui WebGL prevede le coordinate dello spazio dei clip durante il rendering al posto dei pixel, WebGL prevede le coordinate della texture durante la lettura di una texture. Le coordinate texture vanno da 0,0 a 1,0 indipendentemente dalle dimensioni della texture. Poiché stiamo disegnando un solo rettangolo (o meglio, 2 triangoli), dobbiamo dire a WebGL a quale posizione della texture corrisponde ogni punto del rettangolo. Trasferiremo queste informazioni dallo shader vertex allo shader fragment utilizzando un tipo speciale di variabile chiamato "varying". Viene definita "variazione" perché varia. WebGL interpolerà i valori forniti nello shader vertex mentre disegna ogni pixel utilizzando lo shader fragment. Utilizzando lo shader vertex della fine della sezione precedente, dobbiamo aggiungere un attributo per passare le coordinate texture e poi trasmetterle allo shader fragment.

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

Poi forniamo un frammento shader per cercare i colori dalla texture.

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

Infine, dobbiamo caricare un'immagine, creare una texture e copiare l'immagine nella texture. Poiché siamo in un browser, le immagini vengono caricate in modo asincrono, quindi dobbiamo riorganizzare un po' il codice per attendere il caricamento della trama. Una volta caricato, lo disegneremo.

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

Non è molto interessante, quindi manipoliamo l'immagine. Che ne dici di scambiare il rosso e il blu?

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

E se volessimo eseguire un'elaborazione delle immagini che esamini altri pixel? Poiché WebGL fa riferimento alle texture nelle coordinate texture che vanno da 0,0 a 1,0, possiamo calcolare quanto spostarci per 1 pixel con la semplice matematica onePixel = 1.0 / textureSize. Ecco uno shaker per frammenti che calcola la media dei pixel sinistro e destro di ogni pixel nella texture.

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

Poi dobbiamo passare le dimensioni della trama da JavaScript.

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

Ora che sappiamo come fare riferimento ad altri pixel, utilizziamo un kernel di convoluzione per eseguire alcune comuni elaborazioni delle immagini. In questo caso utilizzeremo un kernel 3x3. Un kernel di convoluzione è solo una matrice 3x3 in cui ogni voce della matrice rappresenta il fattore di moltiplicazione degli 8 pixel intorno al pixel di cui stiamo eseguendo il rendering. Quindi dividiamo il risultato per il peso del kernel (la somma di tutti i valori nel kernel) o per 1,0, o meno. Ecco un ottimo articolo in merito. Ecco un altro articolo che mostra del codice effettivo se dovessi scriverlo a mano in C++. Nel nostro caso, eseguiremo questo lavoro nello shader, quindi ecco il nuovo shader di frammento.

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

In JavaScript dobbiamo fornire un kernel di convoluzione.

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

Spero che questo ti abbia convinto che l'elaborazione delle immagini in WebGL è piuttosto semplice. Adesso ti mostrerò come applicare più di un effetto all'immagine.

Che cosa significano i prefissi a, u e v_ davanti alle variabili in GLSL?

Si tratta solo di una convenzione di denominazione. a_ per gli attributi, ovvero i dati forniti dai buffer. u_ per le uniformi, ovvero gli input degli shader, v_ per le varianti, ovvero i valori passati da un vertex shader a un fragment shader e interpolati (o modificati) tra i vertici per ogni pixel disegnato.

Applicazione di più effetti

La domanda più ovvia successiva per l'elaborazione delle immagini è come applicare più effetti.

Potresti provare a generare shader dinamicamente. Fornire una UI che consenta all'utente di selezionare gli effetti da utilizzare e poi di generare uno shaker che esegua tutti gli effetti. Sebbene questa tecnica venga spesso utilizzata per creare effetti per le grafica in tempo reale, potrebbe non essere sempre possibile. Un modo più flessibile è utilizzare altre due texture e eseguire il rendering in ciascuna texture a turno, passando da una all'altra e applicando ogni volta l'effetto successivo.

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

Per farlo, dobbiamo creare framebuffer. In WebGL e OpenGL, Framebuffer è in realtà un nome improprio. Un Framebuffer WebGL/OpenGL è in realtà solo un insieme di stati e non un buffer di qualsiasi tipo. Tuttavia, l'aggiunta di una texture a un framebuffer consente il rendering della texture. Per prima cosa, trasformiamo il vecchio codice di creazione della trama in una funzione

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

Ora utilizziamo questa funzione per creare altre due texture e associarle a due framebuffer.

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

Ora creiamo un set di kernel e poi un elenco di tutti i kernel da applicare.

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

Infine, applichiamo ogni texture, passando da una all'altra.

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

Devo esaminare alcune cose.

La chiamata a gl.bindFramebuffer con null indica a WebGL che vuoi eseguire il rendering nella tela anziché in uno dei tuoi frame buffer. WebGL deve riconvertire dallo spazio dei clip in pixel. Lo fa in base alle impostazioni di gl.viewport. Le impostazioni di gl.viewport corrispondono in modo predefinito alle dimensioni del canvas durante l'inizializzazione di WebGL. Poiché i framebuffer in cui eseguiamo il rendering hanno dimensioni diverse rispetto alla tela, dobbiamo impostare l'area visibile in modo appropriato. Infine, negli esempi relativi ai concetti fondamentali di WebGL, abbiamo capovolto la coordinata Y durante il rendering, perché WebGL visualizza la tela con 0,0 nell'angolo in basso a sinistra anziché il più tradizionale per il rendering 2D in alto a sinistra. Non è necessario quando si esegue il rendering in un framebuffer. Poiché il framebuffer non viene mai visualizzato, le parti superiori e inferiori non sono pertinenti. Ciò che conta è che il pixel 0,0 nel framebuffer corrisponda a 0,0 nei nostri calcoli. Per risolvere il problema, ho reso possibile l'impostazione del cambio o meno aggiungendo un altro input nello shaker.

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

e poi possiamo impostarlo durante il rendering con

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

Ho mantenuto questo esempio semplice utilizzando un singolo programma GLSL che può ottenere più effetti. Se volevi sfruttare appieno l'elaborazione delle immagini, probabilmente avresti bisogno di molti programmi GLSL. Un programma per la regolazione di tonalità, saturazione e luminanza. Un altro per luminosità e contrasto. Uno per l'inversione, un altro per la regolazione dei livelli e così via. Dovrai modificare il codice per cambiare i programmi GLSL e aggiornare i parametri per quel determinato programma. Avevo considerato di scrivere questo esempio, ma è un esercizio meglio lasciato al lettore perché più programmi GLSL, ciascuno con i propri requisiti di parametro, probabilmente richiedono un refactoring importante per evitare che tutto diventi un grande pasticcio di spaghetti. Spero che questo e gli esempi precedenti abbiano reso WebGL un po' più accessibile e spero che iniziare con la 2D renda WebGL un po' più facile da capire. Se trovo il tempo, cercherò di scrivere altri articoli su come creare contenuti 3D e su ulteriori dettagli su cosa fa realmente WebGL sotto il cofano.

WebGL e alpha

Ho notato che alcuni sviluppatori OpenGL hanno problemi con il modo in cui WebGL tratta l'alpha nel backbuffer (ovvero la tela), quindi ho pensato che sarebbe stato utile esaminare alcune delle differenze tra WebGL e OpenGL relative all'alpha.

La principale differenza tra OpenGL e WebGL è che OpenGL esegue il rendering su un backbuffer che non è composto da alcun elemento o che non è composto da elementi dal gestore delle finestre del sistema operativo, quindi non importa quale sia la versione alpha. WebGL viene composito dal browser con la pagina web e l'impostazione predefinita prevede l'utilizzo di un alfa premoltiplicato allo stesso modo dei tag .png <img> con trasparenza e tag canvas 2D. WebGL offre diversi modi per rendere l'esperienza più simile a OpenGL.

1) Indica a WebGL che vuoi che il compositing venga eseguito con un'alpha non premoltiplicata

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

Il valore predefinito è true. Ovviamente, il risultato verrà comunque composto sulla pagina con il colore di sfondo che si trova sotto la tela (il colore di sfondo della tela, il colore di sfondo del contenitore della tela, il colore di sfondo della pagina, gli elementi dietro la tela se la tela ha un indice z > 0 e così via), in altre parole, il colore definito dal CSS per quell'area della pagina web. Un ottimo modo per scoprire se hai problemi con l'alpha è impostare lo sfondo della tela su un colore brillante come il rosso. Vedrai immediatamente cosa sta succedendo.

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

Puoi anche impostarlo su nero per nascondere eventuali problemi di trasparenza.

2) Indica a WebGL che non vuoi l'alpha nel backbuffer

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

In questo modo, il backbuffer avrà solo RGB e il comportamento sarà più simile a quello di OpenGL. Probabilmente è l'opzione migliore perché un buon browser potrebbe rilevare che non hai alpha e ottimizzare il modo in cui viene composto WebGL. Ciò significa anche che non avrà l'alpha nel backbuffer, quindi se lo utilizzi per qualche scopo, questa opzione potrebbe non essere adatta a te. Poche app che conosco utilizzano l'alpha nel backbuffer. Penso che avrebbe dovuto essere l'impostazione predefinita.

3) Cancella alpha alla fine del rendering

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

Lo svuotamento è generalmente molto veloce perché esiste un caso speciale per la maggior parte dell'hardware. L'ho fatto nella maggior parte delle mie demo. Se fossi furbo, passerei al metodo 2 sopra indicato. Forse lo farò subito dopo aver pubblicato questo messaggio. Sembra che la maggior parte delle librerie WebGL utilizzi questo metodo per impostazione predefinita. Solo i pochi sviluppatori che usano alpha per gli effetti di composizione possono richiederla. Il resto avrà le migliori prestazioni e meno sorprese.

4) Cancella una volta la versione alpha, quindi non esegui più il rendering

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

Ovviamente, se esegui il rendering nei tuoi framebuffer, potrebbe essere necessario riattivare il rendering in alpha e disattivarlo di nuovo quando passi al rendering nella tela.

5) Gestione delle immagini

Inoltre, se stai caricando file PNG con alpha nelle texture, l'impostazione predefinita è che la loro alfa è premoltiplicata, il che in genere NON è il modo in cui la maggior parte dei giochi fa le cose. Se vuoi impedire questo comportamento, devi indicare a WebGL di

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) Utilizzo di un'equazione di miscelazione che funzioni con l'alpha premoltiplicato

Quasi tutte le app OpenGL che ho scritto o su cui ho lavorato utilizzano

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Questo funziona per le texture alpha non premoltiplicate. Se vuoi davvero lavorare con texture alfa premoltiplicate, probabilmente

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Questi sono i metodi di cui sono a conoscenza. Se ne conosci altri, pubblicali di seguito.