Concetti fondamentali di WebGL

Gregg Tavares
Gregg Tavares

Concetti fondamentali di WebGL

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

WebGL si occupa di 2 cose. Coordinate di Clipspace in 2D e colori. Il tuo compito in qualità di programmatore che utilizza WebGL è fornire queste due funzionalità. Per farlo, devi fornire 2 "shader". Uno Shader Vertex che fornisce le coordinate dello spazio per il clip e uno strumento di tonalità dei frammenti che fornisce il colore. Le coordinate di Clipspace vanno sempre da -1 a +1, a prescindere 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 2 Shaker

<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 clip vanno sempre da -1 a +1, indipendentemente dalle dimensioni del canvas. Nel caso sopra riportato, puoi vedere che non stiamo facendo nulla, ma stiamo trasferendo direttamente i dati sulla posizione. Dal momento che i dati di posizione sono già nello spazio clip, non c'è da fare. Se vuoi usare il 3D, spetta a te fornire Shader in grado di eseguire la conversione dal 3D al 2D perché WebGL è un'API 2D. Per i contenuti 2D probabilmente preferiresti lavorare in pixel anziché in clipspace, quindi cambiamo ilshar in modo da poter fornire i rettangoli in pixel e trasformarli in clipspace. Ecco il nuovo Vertex Shader

<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 nostri 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 vicino alla parte inferiore dell'area. WebGL considera l'angolo in basso a sinistra pari a 0,0. Per renderlo l'angolo in alto a sinistra più tradizionale utilizzato per le API grafiche 2D, è sufficiente invertire la coordinata y.

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

Creiamo il codice che definisce un rettangolo in una funzione in modo da chiamarlo per rettangoli di dimensioni diverse. Mentre ci siamo, renderemo il colore impostabile. Per prima cosa, facciamo in modo che lo strumento di identificazione dei frammenti esegua 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 luoghi casuali e con 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 veda che WebGL è in realtà un'API piuttosto semplice. Anche se il 3D può diventare più complicato, le complicazioni vengono aggiunte da te, i programmatori, sotto forma di mesh più complessi. L'API WebGL è di per sé in 2D ed è abbastanza semplice.

Cosa significano type="x-shader/x-vertex" e type="x-shader/x-fragment"?

I tag <script> contengono JavaScript per impostazione predefinita. Non puoi inserire nessun 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 usare questa funzione per memorizzare i cursori nei tag di script. Meglio ancora, possiamo creare il nostro tipo e cercarlo nel codice JavaScript per decidere se compilare lo strumento di ShaD come Vertex o Shadr dei Frammenti.

In questo caso, la funzione createShaderFromScriptElement cerca uno script con il valore id specificato, quindi guarda in type per decidere il tipo di smoothr da creare.

Elaborazione immagini WebGL

Con WebGL, l'elaborazione delle immagini è semplice. Quanto è facile? Leggi quanto riportato di seguito.

Per disegnare immagini in WebGL, occorre utilizzare le texture. In modo simile al modo in cui WebGL prevede le coordinate dello spazio clip durante il rendering anziché i pixel, WebGL prevede le coordinate della texture durante la lettura di una texture. Le coordinate della texture vanno da 0,0 a 1,0, indipendentemente dalle dimensioni della texture. Dato che stiamo disegnando un solo rettangolo (ovvero due triangoli), dobbiamo indicare a WebGL a quale posizione della texture corrisponde ciascun punto del rettangolo. Passeremo queste informazioni da Vertex Shader a quest'ultimo usando un tipo speciale di variabile chiamata "varia". È chiamato "variazione" perché varia. WebGL interpola i valori forniti nello strumento di shadowr dei vertici mentre disegna ogni pixel utilizzando lo strumento di shadowing dei frammenti. Usando lo Shadr Vertex alla fine della sezione precedente, dobbiamo aggiungere un attributo per passare le coordinate della texture e poi passarle allo Shadr frammento.

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

Forniamo quindi uno strumento di shadowing dei frammenti per cercare i colori della 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. Dal momento che le immagini del browser vengono caricate in modo asincrono, dobbiamo riorganizzare il nostro codice un po' prima di attendere il caricamento della texture. 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 troppo eccitante, quindi manipoliamo l'immagine. Che ne dici di scambiare solo i colori rosso e blu?

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

E se volessimo effettuare un'elaborazione di immagini che esamini altri pixel? Poiché WebGL fa riferimento a texture nelle coordinate delle texture che vanno da 0,0 a 1,0, possiamo calcolare di quanto muoversi per 1 pixel con la semplice formula matematica onePixel = 1.0 / textureSize. Di seguito è riportato uno strumento di tonalità dei frammenti che calcola la media dei pixel di sinistra e di destra 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>

Dobbiamo quindi trasferire la dimensione della texture 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, usiamo un kernel di convoluzione per effettuare alcune delle comuni elaborazioni di immagini. In questo caso utilizzeremo un kernel 3x3. Un kernel di convoluzione è solo una matrice 3x3 in cui ogni voce nella matrice rappresenta quanto moltiplicare gli 8 pixel intorno al pixel che stiamo eseguendo. Quindi dividiamo il risultato per il peso del kernel (la somma di tutti i valori nel kernel) o per 1,0, a seconda del valore maggiore. Ecco un bel articolo a riguardo. Ecco un altro articolo che mostra un po' di codice effettivo, se si scrive a mano in C++. Nel nostro caso lo faremo nello strumento, quindi ecco il nuovo strumento di esplorazione dei frammenti.

<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 vi abbia convinto che l'elaborazione delle immagini in WebGL sia piuttosto semplice. Ora vedremo come applicare più di un effetto all'immagine.

Come vengono utilizzati i prefissi a, u e v_ delle variabili in GLSL?

Questa è solo una convenzione di denominazione. a_ per gli attributi ovvero i dati forniti dai buffer. u_ per le uniformi che sono un input per gli Shampor, v_ per le variazioni che sono valori trasmessi da uno Shadr vertice a uno strumento di shadowing dei frammenti e interpolati (o variabili) tra i vertici per ogni pixel disegnato.

Applicazione di più effetti

L'altra domanda più ovvia per l'elaborazione delle immagini è come si applicano più effetti?

Beh, potresti provare a generare ombreggiatori al volo. Fornire un'interfaccia utente che consenta all'utente di selezionare gli effetti che vuole usare, quindi generare uno Shader che li esegue. Questo non è sempre possibile, anche se questa tecnica viene spesso utilizzata per creare effetti per la grafica in tempo reale. Un modo più flessibile consiste nell'utilizzare altre 2 texture ed eseguire il rendering di ciascuna texture a turno, ping pong avanti e indietro 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 dei framebuffer. In WebGL e OpenGL, Framebuffer è in realtà un nome scadente. Un WebGL/OpenGL Framebuffer è in realtà solo un insieme di stati e non un buffer di alcun tipo. Tuttavia, attaccando una texture a un framebuffer possiamo eseguire il rendering in quella texture. Innanzitutto trasformiamo il vecchio codice di creazione della texture 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 usiamo questa funzione per creare altre 2 texture e collegarle a 2 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 insieme di kernel e un elenco dei 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, applichiamoli una per una, ping pong e a quale texture stiamo eseguendo il rendering.

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

Se chiami gl.bindFramebuffer con null, indichi a WebGL che vuoi eseguire il rendering sulla tela anziché su uno dei buffer del frame. WebGL deve convertire lo spazio clip in pixel. Lo fa in base alle impostazioni di gl.viewport. Le impostazioni di gl.viewport vengono impostate automaticamente sulle dimensioni della tela al momento dell'inizializzazione di WebGL. Poiché i framebuffer in cui stiamo eseguendo il rendering hanno una dimensione diversa rispetto al canvas, dobbiamo impostare correttamente l'area visibile. Infine, negli esempi fondamentali di WebGL, abbiamo invertito la coordinata Y durante il rendering, poiché WebGL visualizza la tela con 0,0 nell'angolo in basso a sinistra, anziché il più tradizionale per il 2D in alto a sinistra. Ciò non è necessario per il rendering in un framebuffer. Poiché il framebuffer non viene mai visualizzato, quale parte superiore e inferiore non è pertinente. Ciò che conta è che nei nostri calcoli il pixel 0,0 nel framebuffer corrisponde a 0,0. Per risolvere il problema, ho reso possibile decidere se girare o meno aggiungendo un altro input nello strumento di orientamento.

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

Poi possiamo impostarlo quando eseguiremo il rendering

...
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 in grado di ottenere più effetti. Se volessi fare tutto l'elaborazione delle immagini, probabilmente avrai bisogno di molti programmi GLSL. Un programma per la regolazione di tonalità, saturazione e luminanza. Un'altra per luminosità e contrasto. Uno per invertire, un altro per regolare i livelli, ecc. Bisogna cambiare il codice per cambiare programmi GLSL e aggiornare i parametri per quel particolare programma. Avrei pensato di scrivere quell'esempio, ma è un esercizio che è meglio lasciare al lettore perché più programmi GLSL, ciascuno con le proprie esigenze di parametri, probabilmente significano un refactoring importante per evitare che tutto diventi un grande pasticcio. Spero che questo e gli esempi precedenti abbiano reso WebGL un po' più accessibile e spero che iniziare con il 2D contribuisca a rendere WebGL un po' più facile da capire. Se trovo un po' di tempo, provo a scrivere qualche altro articolo su come realizzare il 3D e ulteriori dettagli su come WebGL sta realmente facendo.

WebGL e Alpha

Ho notato che alcuni sviluppatori di OpenGL hanno problemi nel modo in cui WebGL tratta alpha nel backbuffer (ovvero la tela), quindi ho pensato che fosse utile esaminare alcune delle differenze tra WebGL e OpenGL in relazione alla versione alpha.

La principale differenza tra OpenGL e WebGL è che OpenGL esegue il rendering su un backbuffer che non è composto da nessun elemento o non è stato composito con nulla dal gestore di finestre del sistema operativo, quindi non importa quale sia la versione alpha. WebGL viene composto dal browser con la pagina web e l'impostazione predefinita prevede l'utilizzo della versione alpha premoltiplicata dei tag <img> .png con trasparenza e tag canvas 2D. WebGL ha diversi modi per rendere questa modalità più simile a OpenGL.

1) Comunicare a WebGL che deve essere composita con alpha non premoltiplicata

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

Il valore predefinito è true. Ovviamente il risultato sarà comunque composto sulla pagina con qualsiasi colore di sfondo che finisca 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 z-index > 0 e così via), in altre parole, il colore CSS definisce per quell'area della pagina web. Un ottimo modo per capire se hai problemi con le versioni alpha è impostare lo sfondo della tela su un colore luminoso come il rosso. Vedrai immediatamente che cosa sta succedendo.

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

Puoi anche impostarlo sul nero per nascondere eventuali problemi della versione alpha.

2) Comunica a WebGL che non vuoi che la versione alpha sia nel backbuffer

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

In questo modo funzionerà più come OpenGL, poiché il backbuffer avrà solo RGB. Questa è probabilmente l'opzione migliore perché un buon browser potrebbe riscontrare che non è disponibile la versione alpha e ottimizzare la modalità di composizione di WebGL. Naturalmente questo significa anche che in realtà non avrà alpha nel backbuffer, quindi se utilizzi alpha nel backbuffer per qualche scopo che potrebbe non funzionare per te. Poche app che conosco utilizzano la versione alpha nel backbuffer. Probabilmente questa avrebbe dovuto essere l'impostazione predefinita.

3) Cancella i caratteri alfa al termine 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);

La cancellazione è in genere molto rapida perché esiste un caso speciale per questo processo nella maggior parte dei componenti hardware. L'ho fatto nella maggior parte delle demo. Se fossi intelligente passerei al metodo n. 2. Forse lo farò subito dopo la pubblicazione. Sembra che la maggior parte delle librerie WebGL dovrebbe utilizzare questo metodo per impostazione predefinita. I pochi sviluppatori che usano l'alpha per la composizione degli effetti possono richiederlo. Gli altri otterranno il miglior rendimento possibile e le 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 sui tuoi framebuffer, potresti dover riattivare il rendering ad alpha e disattivarlo di nuovo quando passi al rendering sulla tela.

5) Gestione delle immagini

Inoltre, se carichi file PNG con alfa nelle texture, l'impostazione predefinita prevede che la versione alfa sia premoltiplicata, il che di solito NON è il modo in cui la maggior parte dei giochi esegue le operazioni. Se vuoi evitare questo comportamento, devi comunicare a WebGL con

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) Utilizzo di un'equazione di fusione che funziona con gli alfa premoltiplicati

Quasi tutte le app OpenGL che ho scritto o che ho utilizzato

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Funziona per le texture alfa non premoltiplicate. Se vuoi effettivamente lavorare con texture alfa premoltiplicate, allora probabilmente

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Questi sono i metodi che conosco. Se ne sai di più, pubblicale qui sotto.