VBO multipli con OpenGL ES 2.0

Emscripten e WebAssembly

Di recente ho iniziato un nuovo progettino, per il quale sto usando SDL e OpenGL ES 2.0.

Durante il lockdown, avevo imparato a fare qualcosa di base con OpenGL 3.3, e avevo usato sempre SDL e un po’ Bullet, ma poi avevo lasciato stare il tutto. Adesso ho ricominciato, ma con OpenGL ES 2.0 perché offre una possibilità molto interessante: con Emscripten, si può compilare il progetto per WebAssembly, e GLES 2.0 viene trasformato direttamente in WebGL.

Pur non essendo più molto appassionato di sviluppo web, questa cosa mi attrae particolarmente. Innanzitutto, pubblicando sul web si possono raggiungere più persone, anche per il solo fatto che aspettare qualche secondo che si carichi una pagina è molto meno impegnativo che far scaricare uno zip, decompattarlo, e far avviare il programma. E non parliamo nemmeno degli installanti…

Il codice può essere sviluppato praticamente in qualsiasi linguaggio (C, C++, o qualcosa che abbia un interprete/una VM scritta in questi linguaggi), ma poi si può interfacciare al JavaScript che gira nel browser. Questo vuol dire che potrei fare le parti di rendering 3D in C++, ma le parti di UI in HTML, CSS e JavaScript. Le ultime volte che ho provato a fare qualcosa in JS mi sono stufato subito, però, per fare UI, lo stack web ha pochi eguali.

Nulla vieta di non sfruttare questa funzionalità e fare comunque tutto come applicazione desktop, in maniera portatile, e poi trattare WebAssembly come uno dei target del progetto stesso, quindi per esempio avere una versione binaria per Linux, una per Windows e una per WebAssembly.

OpenGL ES e WebGL

Come ho detto in passato, secondo me gli standard dovrebbero essere sviluppati molto lentamente e con la giusta parsimonia, anziché rilasciare molto spesso tante nuove funzionalità, che poi non vengono neppure implementate, o solo parzialmente/scorrettamente. Per OpenGL ES e il corrispettivo WebGL per browser le cose sono andate abbastanza piano.

La creazione di WebGL è iniziata nel 2009 ed è stato rilasciato nel 2011. È basato su OpenGL ES 2.0, rilasciato nel Marzo 2007, cioè un anno prima di OpenGL 3.0, in cui sono stati fatti moltissimi cambiamenti sulla gestione degli oggetti.

Diversi di questi cambiamenti sono stati implementati in OpenGL ES 3.0, rilasciato nel 2012, e usato come base WebGL 2.0, iniziato nel 2013 e rilasciato come stabile nel 2017.

Secondo la documentazione di Emscripten, per avere migliori prestazioni è meglio usare WebGL 2.0. Da quanto ho capito, quindi, l’unico motivo per scegliere OpenGL ES 2.0 è la compatibilità con i client. E se il target è solo il desktop, be’, persino l’ultima versione di Firefox per Windows XP supporta WebGL 2.0.

Per quanto riguarda la compatibilità OpenGL-WebGL, né la 2.0, né la 3.0 sono completamente compatibili con WebGL, dunque per entrambe serve fare un po’ di attenzione a cosa si può fare e cosa non si può fare.

VAO multipli

Il mio primo errore, nell’affacciarmi a questo mondo, è stato di comprensione: stando a della documentazione vecchia, issue su GitHub, supporto di altri progetti basati su Emscripten, avevo capito che usare OpenGL ES 2.0 fosse sempre preferibile alla 3.0, anche come supporto di Emscripten stesso; quindi avevo optato per OpenGL ES 2.0, per il mio nuovo progetto.

In questo modo è emerso il mio secondo errore, che in realtà mi stavo portando dietro da mesi. Su OpenGL 3.3, è necessario usare i VAO, e purtroppo ho frainteso/imparato scorrettamente che modo il per gestire più oggetti fosse di creare un VAO per oggetto.

Avendo sempre solo cominciato diversi progetti, ma non avendoli mai sviluppati più di tanto, e non avendo una grande vena artistica, non ho mai creato scene così tanto complesse. Quindi, per i miei test semplici, non mi ero mai imbattuto in problemi.

VAO multipli si possono sì creare, ma se la struttura dei dati è sempre la stessa, è meglio usare un unico VAO, perché il cambiamento di VAO è un cambiamento di stato ed è un’operazione costosa.

In OpenGL ES 2.0 questo problema non si pone, perché i VAO non esistono. Per WebGL esiste l’estensione OES_vertex_array_object per introdurli, che però non può essere usata direttamente con OpenGL ES 2.0.

VBO multipli

Mentre usare più VAO è semplice, basta chiamare glBindVertexArray, per un VBO non è altrettanto semplice, e chiamando glBindBuffer veniva disegnato solamente l’ultimo oggetto.

Non sapevo nemmeno come descrivere il problema e trovavo pochi risultati nel web, nessuno che risolveva il mio problema. Anche i tutorial, spesso insegnano a disegnare un unico oggetto.

Il motivo è che usare un unico VBO ha prestazioni migliori, sia perché non si fa il cambio di stato, sia perché sono possibili anche altre ottimizzazioni, come il batching. Di fatto, sia glDrawArrays che glDrawElements includono tra i vari parametri offset e contatore, che permettono di disegnare usando solo una parte di dati, quindi più oggetti, o parti dello stesso oggetto che usano shader diversi.

A questo punto, però, volevo capire lo stesso come fare a usare più VBO.

Quello che mi mancava era l’informazione su come è strutturato un VBO, cioè le varie chiamate a glVertexAttribPointer, che vanno ripetute ogni volta che si riattiva il VBO, non solo quando si caricano i dati la prima volta. Quindi, nella funzione di disegno, per usare più VBO, il codice deve assomigliare al seguente:

glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
// Etc...
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
// Etc...
glDrawArrays(GL_TRIANGLES, 0, numVertices1);

glBindBuffer(GL_ARRAY_BUFFER, vbo2);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// Etc...
glEnableVertexAttribArray(0);
// Etc...
glDrawArrays(GL_TRIANGLES, 0, numVertices2);

È lungo, richiede diverse chiamate, non è ottimizzato perché c’è un cambio di contesto, tuttavia ci sono dei motivi per farlo.

Il primo è lo stesso che porterebbe a usare più VAO: differenti layout dei buffer, o buffer con informazioni diverse (e.g. uno non ha le normali, oppure uno ha le coordinate delle texture ma non i colori e viceversa).

Un altro motivo che mi viene in mente è per buffer con utilizzi diversi, per esempio una griglia disegnata con GL_STATIC_DRAW, oggetti che vengono modificati spesso e sono un in un buffer GL_DYNAMIC_DRAW, o un buffer GL_STREAM_DRAW per linee di debug, etc.

Un’altra ragione, è per andare più veloci a implementare il codice, almeno in una fase di sviluppo iniziale/di prototipo. Una volta noto il trucco, è semplice anche fare qualche helper per non dover ripetere tutto quel codice.

Conclusioni

Quando una cosa con una API così diffusa come OpenGL non è immediata da fare e c’è poco materiale al riguardo, be’, deve suonare un campanello d’allarme, e probabilmente c’è qualche altro errore in mezzo.

Tuttavia, alla fine ho trovato il modo per fare quello che volevo, quindi so come farlo, ma anche quando e perché va bene farlo, o non farlo.

E soprattutto, per queste coincidenze ho scoperto anche un errore che altrimenti non sarebbe stato facile trovare.

Riferimenti