Il Blog di Lorenzo Vainigli

Open Sailing – Un gioco di navigazione in OpenGL

4 Marzo 2020
Grafica 3D Progetti Programmazione
11 minuti    11

Recentemente, per un esame universitario, ho dovuto realizzare un piccolo videogioco con OpenGL. Trattasi di una barca controllata dall’utente, il quale deve pilotarla per raccogliere tutte le boe presenti nello spazio circostante e con l’obiettivo di concludere la partita nel minor tempo possibile.

Di seguito riporto il contenuto del file di documentazione del progetto, dove sono illustrate le caratteristiche, le opzioni, le modalità di gioco e le scelte implementative effettuate in fase di realizzazione.

[Link al progetto su Github]

Indice

  1. Introduzione
  2. Caratteristiche
    1. Telecamere
    2. Opzioni di rendering
  3. Librerie utilizzate
  4. Svolgimento del progetto
    1. Modello della barca
    2. Ambiente di gioco
    3. Gestione delle collisioni
    4. Luci
    5. Ombre e shaders
    6. Trasparenze
  5. Implementazione
  6. Testing
  7. Ciclo di vita dell’applicazione
  8. Come si gioca
  9. Problemi conosciuti

1. Introduzione

Open Sailing è un piccolo gioco realizzato con le librerie SDL2 e OpenGL. L’utente si trova al comando di un’imbarcazione nel mezzo al mare ed è circondato da numerose boe a cui sono fissate delle bandiere. Il suo obiettivo è raccogliere tutte le boe nel minor tempo possibile.

2. Caratteristiche

Di seguito sono riportate le caratteristiche principali del gioco.

2.1. Telecamere

Durante il gioco l’utente può scegliere tra le seguenti telecamere del gioco:

  • Telecamera sul retro della barca: è la telecamera predefinita e quella che offre la miglior visuale sull’area di mare che si trova davanti all’imbarcazione.
  • Telecamera a bordo della barca: è la telecamera più realistica perché è fissata sul ponte di comando; la vista riproduce ciò che vedrebbe il pilota dell’imbarcazione.
  • Telecamera libera puntata sulla barca: offre la possibilità all’utente di ruotare di 360 gradi attorno al modello 3D dell’imbarcazione (ovvero all’origine degli assi nello spazio della barca).
  • Telecamera libera puntata sul centro del mondo: come la precedente ma questa telecamera, a differenza delle altre, è sempre puntata al centro del mondo (ovvero all’origine degli assi nello spazio mondo).
2.2. Opzioni di rendering

Il gioco dispone di alcune opzioni di rendering:

  • Attiva/disattiva wireframe: permette di visualizzare i wireframe, ovvero gli spigoli che compongono i poligoni della scena.
    Dal punto di vista dell’implementazione, vengono intercambiate le funzioni glBegin(GL_TRIANGLES)/glBegin(GL_QUADS) con glBegin(GL_LINE_LOOP).
  • Attiva/disattiva ombre: permette di abilitare o disabilitare il rendering delle ombre utilizzando gli shader. Funziona solo se è attivo il rendering avanzato.
  • Attiva/disattiva nebbia/foschia: visualizza la foschia chiara in lontananza. Funziona solo se è attivo il rendering avanzato.
  • Attiva/disattiva rendering avanzato (shader): abilita/disabilità gli shader, con i quali si ha una migliore resa grafica.

3. Librerie utilizzate

Questo gioco è stato programmato in linguaggio C/C++ utilizzando SDL2, OpenGL e le librerie di supporto GLU, GLUT, GLEW e GLFW.

4. Svolgimento del progetto

Il lavoro è iniziato partendo dal codice di progettoCar3. In un primo momento è stato effettuato un refactoring del codice per renderlo object-oriented, per poi procedere all’implementazione di nuove funzioni.

4.1 Modello della barca

Il modello della barca comandata dall’utente è memorizzato in un file OBJ e viene caricato e renderizzato con le stesse modalità del modello della macchina che vi era in precedenza. Tale modello è stato scaricato da questa pagina.

Modello della barca disponibile su cadnav.com.
Texturing

Nel pacchetto del modello sono presenti le immagini da utilizzare come texture e il file OBJ contiene le coordinate per applicarle al modello. Tuttavia, ritenendo troppo oneroso implementare una funzione per il parsing delle coordinate texture presenti nel file OBJ, ho optato per una soluzione più comoda: il file textures.dat in project/assets/ship/ contiene una mappa, costruita manualmente da me, tra identificativo della faccia e numero della texture da applicare ad essa. Il calcolo delle coordinate texture è effettuato dal programma secondo la modalità OBJECT LINEAR.
Con qualche imprecisione, è stato replicato l’aspetto originale della barca.

4.2 Ambiente di gioco

La topologia dell’ambiente di gioco, ovvero il mare su cui naviga la barca e il cielo, è rimasta sostanzialmente invariata rispetto al progetto di partenza: il mare è un piano suddiviso in poligoni bidimensionali e il cielo è una sfera.

Mare

I poligoni che compongono il mare sono concettualmente divisi in due gruppi: il mare in prossimità e il mare in lontananza. Il motivo di questa suddivisione è semplice: la creazione del movimento delle onde nel mare comporta l’inserimento di più poligoni e di conseguenza il rendering diventa più pesante.
È stata valutata la performance dell’applicazione nel caso in cui tutto il mare avesse lo stesso livello di dettaglio, ma ciò causava un drastico calo degli fps. Ho deciso quindi di adottare un compromesso: lasciare la zona di mare lontana dalla posizione della barca ad un livello di dettaglio minore rispetto alla zona nei dintorni della barca, ovvero poligoni più grandi e senza movimento delle onde.
Questa scelta è anche dettata dal fatto che le zone lontane dalla barca vengono visualizzare solo in modalità “camera libera sul mondo”, ma questo avviene in casi molto rari.

Mare in prossimità

Con questo termine si intende la zona di mare che circonda il punto dove si trova la barca in un raggio specificato nel codice sorgente. Questa parte della scena di gioco è formata da quadrati, ognuno dei quali composto di due triangoli, dispositi su righe e colonne rispetto agli assi X e Z.
L’animazione dei triangoli è resa possibile da una funzione che combina il tempo di gioco (SDL_GetTicks()) e le funzioni cos(x) e sin(x) per creare un “effetto onda” sull’asse Y.
La suddivisione in triangoli ha l’unico scopo di rendere l’animazione più dettagliata.

Mare in lontananza

Questa parte, che compone le zone che vanno dagli estremi del mare di prossimità ai limiti del mondo di gioco (definito dalla sfera che crea il cielo) è composta da quadrati non animati e non suddivisi in triangoli.

4.3 Gestione delle collisioni

Poiché lo scopo del gioco è raccogliere le boe, diventa cruciale che il programma sappia identificare quando una boa deve essere marcata come “raccolta”. Sono state effettuate molte semplificazioni a questo riguardo: quando la nave tocca una boa quest’ultima scompare e viene marcata come “raccolta”. Per stabilire quando la barca e una boa vengono a contatto è stato usato il concetto di bounding box: quando il bounding box della barca interseca il bounding box di una boa, quest’ultima viene marcata come “raccolta”.

4.4 Luci

L’intera scena è illuminata da una singola luce direzionale che simula l’illuminazione dei raggi solari. I parametri della luce sono memorizzati nella classe ShadowMapper.

4.5 Ombre e shaders

La gestione delle ombre in quest’applicazione è delegata agli shader e non è banale.
In un primo momento il rendering delle ombre è stato implementato secondo l’algoritmo presente nel file glut_OpenGL_ffp/opengl_advanced/schatten.cpp; tuttavia questo algoritmo è adatto per le superfici solide su cui poggiano gli elementi la cui ombra va proiettata, ma nel caso in cui si abbiano oggetti che galleggiano sull’acqua sorgono due problemi:

  • quando si deve proiettare l’ombra di un oggetto come una barca, solo la parte che si trova fuori dall’acqua va proiettata sulla superficie, mentre la parte immersa non va presa in considerazione;
  • l’ombra viene renderizzata solo sulla superficie del mondo, in questo caso il mare, ma nella realtà anche alcune parti dell’imbarcazione dovrebbero essere in ombra.

Queste due eventualità non sono gestite dall’algoritmo di schatten.cpp. Le ombre quindi sono visualizzate solo in modalità “rendering avanzato”.

Le ombre sono state implementate con la tecnica dello Shadow Mapping utilizzando un codice disponibile su GitHub, il cui funzionamento può essere riassunto come segue.

  1. In un buffer dedicato (z-buffer) viene renderizzata la scena dal punto di vista della fonte di luce e per ogni punto nella viewport viene memorizzata la sua profondità;
  2. Viene effettuato il rendering della scena dal punto di vista della camera e ogni punto è soggetto al deph test: le coordinate del punto (x, y) vengono mappate dallo spazio-camera allo spazio-luce e confrontate con il valore di profondità salvato nello z-buffer: se il valore di profondità di (x, y) è maggiore del valore dello z-buffer, il punto è in ombra, altrimenti è illuminato.
4.6 Trasparenze

Nella scena riprodotta non ci sono elementi trasparenti.

5. Implementazione

L’applicazione è stata implementata seguendo una progettazione object-oriented. Nella cartella project/src sono presenti i seguenti file (tranne main.cpp, ogni singolo file implementa l’omonima classe).

  • main.cpp: file principale dell’applicazione, inizializza gli oggetti principali del programma e gestire il ciclo di rendering e gli input esterni.
  • BoundingBox.cpp: classe che contiene le coordinate del bounding box, ovvero del cubo che racchiude le geometrie della barca e delle boe. Serve per la rilevazione e gestione delle collisioni.
  • Buoy.cpp: gestisce le informazioni relative alle boe (es. posizione).
  • Camera.cpp: contiene le informazioni sulla telecamera della scena.
  • Controller.cpp: si occupa di parte della gestione dell’input utente (non modificata rispetto a progettoCar3).
  • Enviroment.cpp: gestisce il rendering dell’ambiente di gioco, ovvero del mare, del cielo e anche delle boe.
  • Fog.cpp: piccola classe responsabile di memorizzare le informazioni sulla nebbia.
  • Frontier.cpp: si occupa del rendering delle frontiere, cioè il cerchio di boe che delimita lo spazio di movimento della barca.
  • Game.cpp: gestisce il flusso della singola gara e ne contiene le informazioni (tempo di gioco, messa in pausa, ecc..) e contiene metodi per fermare il tempo quando tutte le boe sono state raccolte e per la rilevazione di collisioni.
  • HUD.cpp: si occupa del rendering di tutte le informazioni del head up display (mappa, cronometro, fps, conteggio boe raccolte) e della visualizzazione dei menù di gioco.
  • Leaderboard.cpp: gestisce la tabella dei punteggi migliori e si occupa di memorizzarla in un file.
  • Mesh.cpp: responsabile della lettura di un file OBJ e del rendering della sua topologia, ma non gestisce materiali e texture. Raccoglie funzioni già implementate in progettoCar3.
  • Options.cpp: gestisce le variabili booleane che identificano le opzioni di rendering attivabili e disattivabili dall’utente.
  • ShaderParams.cpp: mette a disposizione variabili e metodi per la semplificazione del passaggio dei parametri agli shader.
  • Ship.cpp: gestisce tutte le informazioni relative alla barca come posizione, orientamento, inclinazione, velocità, ecc… Non gestisce la mesh.
  • ShipMesh.cpp: classe dedicata al rendering della mesh della barca. Questa è una sottoclasse di Mesh che aggiunge la possibilità di renderizzare parti del modello che possono mutare le loro caratteristiche. In questo caso le eliche girano se la barca si muove e i timoni seguono il movimento dello scafo.
  • Texture.cpp: classe minimale che identifica una texture.
  • TextureManager.cpp: creata per avere un punto di controllo per tutte le texture.
  • Utils.cpp: funzioni di varia utilità.

6. Testing

Il test dell’applicazione è stato effettuato su ambiente GNU/Linux con le seguenti caratteristiche.
Sistema operativo: Ubuntu 18.04.4 LTS 64-bit
Processore: Intel® Core™ i7-5500U CPU @ 2.40GHz × 4
Architettura: x86_64
Scheda grafica: Intel® HD Graphics 5500 (Broadwell GT2)
Versione OpenGL: OpenGL 3.0 Mesa 19.2.8, GLSL 1.30

7. Ciclo di vita dell’applicazione

Nel diagramma che compare di seguito è schematizzato il ciclo di vita dall’applicazione: ogni lettera identifica una singola schermata (G’ e G” sono due istanze della stessa schermata). La schermata A è quella che compare all’apertura.

Schermata A – Apertura dell’applicazione.
Schermata B – Scelta del numero di boe.
Schermata C – Inizio della gara.
Schermata D – Svolgimento della gara.
Schermata E – Gioco in pausa.
Schermata F – Comandi di gioco.
Schermata G – Tempi migliori registrati.
Schermata H – Fine della gara.

8. Come si gioca

Lo scopo del gioco è raccogliere tutte le boe nel minor tempo possibile.
Appena avviata l’applicazione all’utente viene mostrata la seguente schermata dalla quale può scegliere il suo nome. Il nome verrà registrato nella leaderboard se una partità dell’utente finirà con un punteggio che rientra fra i migliori 20.

Appena premuto Invio si passa alla schermata successiva.

Nella finestra vengono mostrate le informazioni relative allo scopo del gioco, ai comandi, l’indicazione di premere B se si vuole cambiare il numero di boe (l’utente può inserire un numero arbiatrario tra 1 e 100 compresi) e l’azione da fare per iniziare il gioco e contestualmente attivare il cronometro: muovere la barca.
Sul HUD (Heads Up Display), in basso a destra, viene mostrata una mappa del mondo di gioco in cui la freccia rossa rappresenta la barca, la sua posizione e il suo orientamento. I punti verdi sono le boe che rimangono da catturare. In alto a sinistra si trovano i pulsanti da premere per i relativi comandi (pausa e lista dei comandi), mentre sempre in alto ma sulla destra sono mostrati gli FPS.
In basso a sinistra è presente il contatore delle boe catturate.
Durante il gioco la schermata appare in questo modo:

Durante il gioco il giocatore può mettere in pausa e riprendere in un istante successivo, purché non chiuda il programma. Il gioco non possiede un sistema di salvataggio e quindi quando si chiude l’applicazione tutti i salvataggi andranno persi. Una volta che l’utente avrà raccolto tutte le boe il gioco termina mostrando il tempo complessivo; da questa schermata si può premere N avviando una nuova sessione di gioco.

8. Come si gioca

Nel programma sono presenti alcuni bug che non è stato possibile risolvere.

  • In modalità rendering avanzato lo shader calcola le coordinate texture in base alla modalità SPHERE MAP, tuttavia la texture del cielo non è mappata correttamente come invece avviene con il rendering normale.
  • L’ombra creata con lo shadow mapping scompare per alcune zone della mappa: ciò è causato da un controllo dello shader introdotto per evitare un altro bug. Tale bug è realativo al rendering di un’enorme zona d’ombra presente a un lato della scena probabilmente causata da un out-of-bound rispetto alla shadow map.
  • Nella vista con telecamera a bordo della barca, la mappa e il contatore delle boe raccolte vengono in parte nascosti dalla barca stessa.

Lorenzo Vainigli

Appassionato fin da piccolo di computer, tecnologia e informatica. Sono laureato in Informatica all'Università di Bologna e adesso sto studiando per conseguire la laurea magistrale. Ogni tanto mi piace mettere in pratica le mie conoscenze creando qualcosa di nuovo. Qui racconto un po' di me.