Flying memes

QBox: uno slideshow in WebGL

Di ritorno da un bellissimo capodanno a Nantes ho deciso di spendere ancora qualche ora sul nuovo binding tra Javascript e OpenGL ES 2.0, formalmente noto col nome di WebGL; il risultato ha preso il nome di QBox, uno slideshow che recupera un certo numero di immagini mappandole su di un cubo rotante (le immagini possono essere anche più di quattro, c’è un meccanismo di sostituzione automatico).

Non mi dilungo oltre ne nella descrizione ne nelle procedure di installazione in quanto è tutto specificato sulla homepage del progetto (c’è anche un video e una demo live se il vostro browser supporta WebGL, testato con Webkit); vorrei invece spendere questo post nello spiegare un paio di funzioni che ho scritto e che potrebbero IMHO rivelarsi utili a qualche lettore, ecco la prima:

function createTextureFromCanvas(ctx, canvas_imagedata) {

    var pixels = new WebGLUnsignedByteArray(canvas_imagedata.data);
    var texture = ctx.createTexture();
    ctx.bindTexture(ctx.TEXTURE_2D, texture);
    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST);
    ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.NEAREST);
    ctx.pixelStorei(ctx.UNPACK_ALIGNMENT, 1);
    ctx.texImage2D(ctx.TEXTURE_2D, 0, ctx.RGBA, canvas_imagedata.width, canvas_imagedata.height, 0, ctx.RGBA, ctx.UNSIGNED_BYTE, pixels);
    ctx.generateMipmap(ctx.TEXTURE_2D)
    ctx.bindTexture(ctx.TEXTURE_2D, null);

    return texture;
}

In questa funzione si concentra il cuore dello slideshow, non tanto dal punto di vista operativo, quanto di mio personale sforzo concettuale; il principale problema che ho incontrato nello sviluppo di questo script infatti riguardava la necessità di mappare immagini di dimensioni potenzialmente diverse su di un cubo (le cui facce devono essere tutte uguali). Ecco quindi il passaggio forzato attraverso dei canvas bidimensionali di appoggio, sui quali effettuare le operazioni di resize delle immagini prima di passarle come textures sul cubo. Questi canvas sono anche serviti per gestire i margini neri che venivano a crearsi naturalmente in quanto la proporzione tra le dimensioni delle fotografie non è sempre 1:1.

Ma una volta ottenuto questi canvas contenenti le immagini normalizzate come procedere per metterle su di una texture ? Dopo aver seguito senza successo un gist sul tema mi sono imbattuto nel salvifico esempio ‘Texture test‘ che contiene una funzione che disegna 4 pixel su di una texture:


function createCheckerboardTexture() {
    var pixels = new WebGLUnsignedByteArray([255, 255, 255,
                                             0,   0,   0,
                                             0,   0,   0,
                                             255, 255, 255]);
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    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);
    //  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    //  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 2, 2, 0, gl.RGB, gl.UNSIGNED_BYTE, pixels);
    return texture;
}

Da qui è stato facile; al posto che specificare i singoli bit dell’immagine ho passato nella variabile pixels il contenuto dei miei canvas di appoggio e, con un paio di altre modifiche sono riuscito ad ottenere l’effetto desiderato.

La seconda funzione di cui vorrei far menzione è invece quella che recupera un immagine e la mappa, centrata, su di un canvas 2d, per scrivere queste righe ho utilizzato il metodo drawImage del context 2D in un modo che non conoscevo (suggeritomi da qui), cioè con 5 parametri:

  • Canvas di destinazione;
  • Posizione x e y da cui disgnare l’immagine sul canvas di destinazione;
  • Dimensioni dell’immagine sul canvas di destinazione (width e height).

Ecco la funzione, in ingresso riceve l'<img> da adattare al canvas:

function initTexture(e)
{
  var textureWidth  = 500;

  if($(e).width() > $(e).height()){
     var magnitude = textureWidth/$(e).width();
  } else {
     var magnitude = textureWidth/$(e).height();
  }

  var new_width  = Math.floor($(e).width()*magnitude);
  var new_height = Math.floor($(e).height()*magnitude);
  var textureCanvas     = document.createElement("canvas");
  textureCanvas.width   = textureCanvas.height = textureWidth;
  var textureContext    = textureCanvas.getContext("2d");
  textureContext.drawImage(e,Math.floor((textureWidth-new_width)/2.0),Math.floor((textureWidth-new_height)/2.0),new_width,new_height); 

  return textureCanvas;
}

Se la cosa vi interessa sappiate che tutto il progetto è, come al solito, disponibile sul mio account di github.

Tags: , ,