Salve a tutti,
con questo articolo mostreremo un metodo piuttosto pratico per importare i propri modelli 3D dentro la nostra applicazione webGL , utilizzando l’ultima versione delle librerie e l’inseparabile Blender ( versione 2.60).

Inutile affermare l’importanza di tale argomento:i giochi odierni puntano sull’introduzione di oggetti sempre più realistici e complessi nella nostra scena; nell’ultima lezione avevamo mostrato due classi javascript create per l’occasione ( ele_sta() e ele_din() ) ma avevo già anticipato che tale soluzione risultava lenta e pesante per il nostro pc.

Per tale motivo ho sviluppato uno script semplice e due nuove classi per alleggerire tutta l’operazione dandoci risultati più che gradevoli.


Molti sono i formati con cui possiamo definire un modelli 3D : 3d studio(.3ds), ogre(-xml),Collada(.dae) … per il nostro esempio utilizzeremo modelli definiti secondo lo standard Wavefront ( .obj, .mtl).
Tale standard è molto semplice, praticamente si basa su due tipi di file:
-nomefile.obj -> contiene informazioni sulle coordinate del vertice, sulle coordiante texture, sulle normali
-nomefile.mtl -> contiene informazioni sui tipi di materiali usati dal modello ( colore, nome texture,illuminazione).

E’ un formato completo ma molto semplice , perfetto per creare un parsing non particolarmente complicato ;).

Abbiamo perciò sviluppato una nuova classe javascript, che dovrebbe sostituire al momento l’ele_sta cioè solo gli elementi statici ( nel nostro esempio noteremo che la classe obsoleta ele_sta è rimasta, ma se ne può fare tranquillamente a meno e toglierla dallo script ).
La nuova classe ELE() risulta più semplice della precedente e ci permette di definire i suoi attributi, cioè la posizione dei vertici, delle texture e delle normali, chiamando delle semplici funzioni:

function ELE()
{
this.Draw = DrawElement2; //Ele.Draw() -> funzione per disegnare
this.SetColor = ELE_Color; //Ele.SetColor(…) -> setta il colore dell’oggetto
this.SetTexture = ELE_Texture; // Ele.SetTexture(…)-> setta coordinate e immagine texture
this.SetVertex = ELE_Vertex; // Ele.SetVertex(…) -> setta i vertici
this.SetNormals = ELE_Normal; //Ele.SetNormals(…) -> setta le normali dei vertici

}

Il nostro parser non farà altro che leggere i file, istanziare un array di classi ELE() e settare tutti i loro attributi fondamentali. Il nostro solo compito sarà quello di passare l’url al nostro parser e di invocare il Render del modello chiamando La Draw() nella funzione DrawScene().

Notere subito dall’esempio che tale approccio è nettamente più veloce del precedente: infatti per il javascript è molto più complesso gestire degli array giganteschi inchiodati direttamente nel codice che invece crearne durante la fase di esecuzione.
Il rovescio della medaglia sarà un aumento della complessità degli shader e l’introduzione di qualche variabile in più nella InitShader.Spiegare il codice dei metodi della classe ELE() non è molto rilevante, praticamente tutte sono riassumibili col seguente schema:

function NOME(array)
{
Buffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, Buffer);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(array), gl.STATIC_DRAW);
Buffer.itemSize = numITemSize;
Buffer.numItems = array.length/numITemSize;
this.array=Buffer;
}

Si crea il Buffer e lo si associa alla classe grazie alla chiamata this.nomevariabile, in tale modo l’array sarà poi disponibile per la fase di Disegno. La funzione leggermente più complessa risulta essere quella dei colori:
in tale caso infatti bisogna creare un array dei colori con le stesse dimensioni di quello dei vertici; inoltre è stata data la possibilità di inserire direttamente un array dei colori ( nel caso ad esempio volessi creare un cubo multicolore ), ma questo ovviamente presuppone che l’utente accetti l’interpolazione lineare del colore.

Una delle novità introdotte in tale lezione è la lettura di file tramite javascript:

function LoadObjF(url)
{
var request = new XMLHttpRequest();
request.open(“GET”, url);
request.onreadystatechange = function () {
if (request.readyState == 4) {
ParseFile(request.responseText);
}
}

request.send();

}

Questa semplice funzione javascript usa l’Ajax per inviare una richiesta asincrona; tale richiesta produrrà uno stream di dati che sarà poi inviata alla funzione ParseFile(…) se tutto va correttamente. Il nostro Parser avrà il compito di analizzare questi dati ricevuti in lettura. Ecco uno schema molto semplificato del parser.

function handleObjFile(data)
{
//inizializzare variabili
var lines = data.split(“\n”); //divide in righe
for (var j in lines) {
var vals = lines[j].replace(/^\s+/, “”).split(/\s+/);
//divide ogni riga in colonne
if(vals[0] == “mtllib”)
{
//mi salvo nuovo materiale
}

if(vals[0] == “usemtl”)
{
mi salvo quale materiale devo usare per i successivi vertici
}
f (vals[0] == “vn”)
{
//è una normale, lo salvo
}

if (vals[0] == “v” )
{
// è un vertice, lo salvo
}
if (vals[0] == “vt”)
{
//texture, lo salvo
}
if (vals[0] == “f” )
{
// creo array dei vertici, delle texture e delle normali
}

}

A causa di alcune problematiche legate ai tempi di elaborazione del javascript per aprire i file e alla asincronicità delle chiamate ajax,non basta una sola passata del parser per elaborare il tutto. Toccherà infatti analizzare il file obj una prima volta per estrapolare i nomi dei file .mtl che contengono i materiali, parsare suddetti file per crearsi un array delle definizioni dei materiali ed infine rileggere una seconda volta il file obj per creare i buffer necessari al disegno ( sarebbe anche possibile tramite chiamate sincrone e ritardi voluti fare tutto con una sola passata ma ho preferito questo altro approccio).

Ora Sorge la domanda: come posso inserire un nuovo modello nella mia applicazione?

I passi da fare sono piuttosto semplici:
1- creare una variabile globale javascript ( in modo tale che sia visibile in ogni punto del progetto. In soldoni, basta andare all’inizio del file o in qualunque punto fuori da una funzione e scrivere var nomeoggetto ; ).
2- andare nella Funzione InitBuffer() ed inserire il codice:nomeoggetto= new LoadObjF(url_file_obj);
tutto questo innescherà il parsing e il savaltaggio dei dati.
3-nella funzione DrawScene() invochiamo il metodo Draw() (nomeoggetto.Draw() ). Ovviamente se il modello non è nella posizione corretta, consiglio di usare le solita funzioni di scalamento, rotazione e traslazione come nell’esempio seguente:


mvPushMatrix();
Scale([0.05, 0.05, 0.05]);
mvTranslate([0, 1.5,0]);
prova.Draw();
mvPopMatrix();

Per aumentare il realismo, abbiamo inserito in InitBuffer() anche una sorgente luminosa copiandola da una nostra precedente lezione; in questo modo modelli che non usano texture mostreranno maggior profondità ma inevitabilmente costringiamo l’utente a darci informazioni anche sulle normali.

Ultima cosa: come formattiamo correttamente i file obj da importare? Per aumentare al massimo la compatibilità ed evitare brutte sorprese, consiglio di appoggiarsi a Blender (versione 2.60), ecco riportati i semplici passi da seguire:

-Creare un nuovo progetto blender.
-Importare un modello 3D usando qualunque tipo di formato (.3ds,.dae, un altro.obj,…)
-Ad operazione completata, esportare il file stavolta usando lo standar Wavefront (.obj); prima di far partire l’operazione però, consigliamo di settare le opzioni come sotto indicato, includendo perciò la definizione delle Normali, dei materiale e che le facce siano triangolari:

-Diamo l’ok e salviamo i file .obj e .mtl nella medesima cartella della nostra applicazione , preoccupandoci inoltre di copiare anche le eventuali texture legate al modello. Prima di far partire l’applicazione, apriamo il/i file .mtl e controlliamo che il nome riportiamo a fianco dei campi map_ka o map_kd corrispondano al nome delle immagini e che l’indirizzo non punti a qualche strano path. Mostriamo un esempio:

nomi delle texture: body1.png,body2.png

File .mtl
newmtl RefRep.22_BODY1.PNG
Ns 96.078431
Ka 0.000000 0.000000 0.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ni 1.000000
d 1.000000
illum 2
map_Kd Body1.png -> da cambiare, il nome della texture è in realtà body1.png

newmtl RefRep.24_BODY2.PNG
Ns 96.078431
Ka 0.000000 0.000000 0.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ni 1.000000
d 1.000000
illum 2
map_Kd C:\Users\Ciao\Documents\Blender\cbody2.png -> queto path punta ad un indirizzo del mio pc ma la mia app è caricata sul server ed inoltre il modello è nella stessa cartella dell’applicazione, perciò gliere questo path e lasciare direttamente cbody2.png

Per potere vedere l’esempio appena mostrato ( lo trovate come lesson11 e potete scaricare il pacchetto completto lesson11.zip) e scaricare il codice che è stato presentato nell’articolo, collegarsi al solito repository Lab .

Grazie per l’attenzione,

Andrea