Rotolando il mio grafico della scena


23

Ciao Game Development SE!

Mi sto facendo strada in OpenGL con la speranza di creare un motore di gioco semplice e molto leggero. Considero il progetto come un'esperienza di apprendimento che alla fine potrebbe far guadagnare un po 'di soldi, ma sarà comunque divertente.

Finora ho usato GLFW per ottenere un I / O di base, una finestra (con un tasto a schermo intero F11 davvero fantastico) e ovviamente un contesto OpenGL. Ho anche usato GLEW per esporre il resto delle estensioni di OpenGL perché sto usando Windows e voglio usare tutto OpenGL 3.0+.

Il che mi porta al grafico della scena. In breve, vorrei fare il mio. Questa decisione è arrivata dopo aver esaminato OSG e aver letto alcuni articoli su come il concetto di grafico di una scena sia diventato contorto, piegato e rotto. Uno di questi articoli ha descritto come i grafici delle scene si sono sviluppati come ...

Quindi abbiamo aggiunto tutte queste cose extra, come appendere gli ornamenti su un albero di Natale, tranne per il fatto che alcuni degli ornamenti sono deliziose bistecche succose e alcune sono intere mucche vive.

Seguendo l'analogia, mi piacerebbe la bistecca, la carne di quello che dovrebbe essere un grafico di scena, senza dover legare pile di codice extra o intere mucche.

Quindi, con questo in mente, mi trovo a chiedermi esattamente quale dovrebbe essere un grafico di scena e come dovrebbe essere implementato un semplice grafico di scena? Ecco cosa ho finora ...

Un genitore, albero di n-figli o DAG che ...

  • Dovrebbe tenere traccia delle trasformazioni degli oggetti di gioco (posizione, rotazione, scala)
  • Dovrebbe contenere stati di rendering per ottimizzazioni
  • Dovrebbe fornire un mezzo per eliminare gli oggetti che non si trovano all'interno del frustum della vista

Con le seguenti proprietà ...

  • Tutti i nodi devono essere trattati come renderizzabili (anche se non vengono visualizzati) Ciò significa che ...

    • Dovrebbero avere tutti i metodi cull (), state () e draw () (restituisce 0 se non visibile)
    • cull () chiama ricorsivamente cull () su tutti i child, generando così una mesh cull completa per l'intero nodo e tutti i child. Un altro metodo, hasChanged () potrebbe consentire alle cosiddette mesh statiche di non dover calcolare la loro geometria di abbattimento per ogni frame. Ciò funzionerebbe in modo tale che se un nodo nel sottoalbero è cambiato, viene ricostruita tutta la geometria fino alla radice.
  • Gli stati di rendering saranno mantenuti in una semplice enumerazione, ogni nodo selezionerà da questa enumerazione un set di stati OpenGL che richiede e quello stato verrà impostato prima che draw () sia chiamato su quel nodo. Ciò consente il batch, tutti i nodi di un determinato set di stati verranno resi insieme, quindi verrà impostato il set di stati successivo e così via.

  • Nessun nodo dovrebbe contenere direttamente i dati di geometria / shader / trama, invece i nodi dovrebbero puntare a oggetti condivisi (forse gestiti da alcuni oggetti singleton come un gestore risorse).

  • I grafici di scena dovrebbero essere in grado di fare riferimento ad altri grafici di scena (magari utilizzando un nodo proxy) per consentire situazioni come questa , consentendo in tal modo a modelli / oggetti complessi multi-mesh di essere copiati attorno al grafico di scena senza aggiungere una tonnellata di dati.

Spero di ottenere un feedback prezioso sul mio progetto attuale. Manca la funzionalità? Esiste un modo / modello di progettazione decisamente migliore? Mi sto perdendo un concetto più ampio che sarà necessario includere in questo progetto per un gioco 3D piuttosto semplice? Eccetera.

Grazie, -Cody

Risposte:


15

Il concetto

Fondamentalmente, un grafico di scena non è altro che un grafico aciclico bi-diretto che serve a rappresentare un insieme strutturato di relazioni spaziali gerarchicamente.

I motori in natura tendono ad includere altre chicche nel grafico della scena, come notato. Se la vedi come la carne o la mucca probabilmente dipende dalla tua esperienza con i motori e le librerie là fuori.

Mantenerlo leggero

Preferisco lo stile Unity3D di avere il tuo nodo grafico di scena (che al suo centro è una struttura topologica piuttosto che una struttura spaziale / topografica) include intrinsecamente parametri e funzionalità spaziali. Nel mio motore, i miei nodi sono persino più leggeri di Unity3D, dove ereditano molti membri inutili di spazzatura da superclassi / interfacce implementate: ecco cosa ho - più leggero che puoi ottenere:

  • membri del puntatore padre / figlio.
  • pre-trasforma i membri dei parametri spaziali: posizione xyz, inclinazione, imbardata e rollio.
  • una matrice di trasformazione; le matrici in una catena gerarchica possono moltiplicarsi molto rapidamente e facilmente camminando ricorsivamente su / giù dall'albero, dandoti le trasformazioni spaziali gerarchiche che sono la caratteristica principale di un grafico di scena;
  • un updateLocal()metodo che aggiorna solo le matrici di trasformazione di questo nodo
  • un updateAll()metodo che aggiorna questa e tutte le matrici di trasformazione dei nodi discendenti

... Includo anche la logica delle equazioni di movimento e quindi i membri di velocità / accelerazione (lineari e angolari) nella mia classe di nodi. Puoi rinunciare a questo e gestirlo nel tuo controller principale invece se lo desideri. Ma questo è tutto - molto leggero davvero. Ricorda, potresti averli su migliaia di entità. Quindi, come hai suggerito, tienilo leggero.

Costruire Gerarchie

Cosa dici di un grafico di scena che fa riferimento ad altri grafici di scena ... Sto aspettando la battuta finale? Certo che lo fanno. Questo è il loro uso principale. È possibile aggiungere qualsiasi nodo a qualsiasi altro nodo e le trasformazioni dovrebbero avvenire automaticamente nello spazio locale della nuova trasformazione. Tutto quello che stai facendo è cambiare un puntatore, non è come se stessi copiando i dati! Modificando un puntatore, si ottiene quindi un grafico della scena più profondo. Se usare i proxy rende le cose più efficienti, allora non ne ho mai visto la necessità.

Evita la logica relativa al rendering

Dimentica il rendering mentre scrivi la classe del nodo del grafico della scena, o confonderai le cose per te stesso. Tutto ciò che conta è che tu abbia un modello di dati - che si tratti o meno del grafico della scena - e che alcuni renderer ispezioneranno quel modello di dati e renderizzeranno gli oggetti nel mondo di conseguenza, sia che siano in 1, 2 , 3 o 7 dimensioni. Il punto che sto sottolineando è: non contaminare il grafico della scena con la logica di rendering. Un grafico di scena riguarda la topologia e la topografia, ovvero la connettività e le caratteristiche spaziali. Questi sono il vero stato della simulazione ed esistono anche in assenza di rendering (che può assumere qualsiasi forma sotto il sole da una vista in prima persona a un grafico statistico a una descrizione testuale). I nodi non puntano a oggetti correlati al rendering, tuttavia potrebbe essere vero il contrario. Considera anche questo: Non tutti i nodi del grafico della scena nell'intero albero saranno renderizzabili. Molti saranno solo contenitori. Quindi perché allocare memoria anche per un puntatore a render-object? Anche un membro puntatore che non viene mai utilizzato, sta ancora occupando memoria. Quindi invertire la direzione del puntatore: l'istanza relativa al rendering fa riferimento al modello di dati (che potrebbe essere o includere il nodo del grafico della scena), NON viceversa. E se vuoi un modo semplice per scorrere l'elenco dei controller e ottenere l'accesso alla vista correlata, usa un dizionario / tabella hash, che si avvicina al tempo di accesso in lettura O (1). In questo modo non c'è contaminazione e la tua logica di simulazione non si preoccupa dei renderer presenti, il che rende i tuoi giorni e le tue notti di programmazione Quindi perché allocare memoria anche per un puntatore a render-object? Anche un membro puntatore che non viene mai utilizzato, sta ancora occupando memoria. Quindi invertire la direzione del puntatore: l'istanza relativa al rendering fa riferimento al modello di dati (che potrebbe essere o includere il nodo del grafico della scena), NON viceversa. E se vuoi un modo semplice per scorrere l'elenco dei controller e ottenere l'accesso alla vista correlata, usa un dizionario / tabella hash, che si avvicina al tempo di accesso in lettura O (1). In questo modo non c'è contaminazione e la tua logica di simulazione non si preoccupa dei renderer presenti, il che rende i tuoi giorni e le tue notti di programmazione Quindi perché allocare memoria anche per un puntatore a render-object? Anche un membro puntatore che non viene mai utilizzato, sta ancora occupando memoria. Quindi invertire la direzione del puntatore: l'istanza relativa al rendering fa riferimento al modello di dati (che potrebbe essere o includere il nodo del grafico della scena), NON viceversa. E se vuoi un modo semplice per scorrere l'elenco dei controller ma ottenere l'accesso alla vista correlata, usa un dizionario / tabella hash, che si avvicina a O (1) tempo di accesso in lettura. In questo modo non c'è contaminazione e la tua logica di simulazione non si preoccupa dei renderer presenti, il che rende i tuoi giorni e le tue notti di programmazione E se vuoi un modo semplice per scorrere l'elenco dei controller ma ottenere l'accesso alla vista correlata, usa un dizionario / tabella hash, che si avvicina a O (1) tempo di accesso in lettura. In questo modo non c'è contaminazione e la tua logica di simulazione non si preoccupa dei renderer presenti, il che rende i tuoi giorni e le tue notti di programmazione E se vuoi un modo semplice per scorrere l'elenco dei controller ma ottenere l'accesso alla vista correlata, usa un dizionario / tabella hash, che si avvicina a O (1) tempo di accesso in lettura. In questo modo non c'è contaminazione e la tua logica di simulazione non si preoccupa dei renderer presenti, il che rende i tuoi giorni e le tue notti di programmazionemondi più facili.

Per quanto riguarda l'abbattimento, fare riferimento a quanto sopra. L'abbattimento dell'area di interesse è un concetto logico di simulazione. Cioè, non elabori il mondo al di fuori di quest'area (solitamente inscatolata, circolare o sferica). Ciò avviene nel controller principale / loop di gioco, prima che avvenga il rendering. D'altra parte, l'abbattimento del frustum è puramente correlato al rendering. Quindi dimentica di abbattere subito. Non ha nulla a che fare con i grafici delle scene e, concentrandoti su di essa, oscurerai il vero scopo di ciò che stai cercando di ottenere.

Una nota finale ...

Ho la forte sensazione che tu provenga da uno sfondo Flash (in particolare AS3), dati tutti i dettagli sul rendering inclusi qui. Sì, il paradigma Flash Stage / DisplayObject include tutta la logica di rendering come parte dello scenario. Ma Flash fa molte ipotesi che non vuoi necessariamente fare. Per un motore di gioco completo, è meglio non mescolare i due, per motivi di prestazioni, convenienza e controllo della complessità del codice attraverso un SoC adeguato .


1
Grazie Nick. In realtà sono un animatore 3D (vero 3D non flash) diventato programmatore, quindi tendo a pensare in termini di grafica. Se ciò non è abbastanza male, ho iniziato con Java e mi sono fatto leva sulla mentalità "tutto deve essere un oggetto" instillata in quella lingua. Mi hai convinto che il grafico della scena dovrebbe essere separato dal codice di rendering e di abbattimento, ora i miei ingranaggi girano su esattamente come dovrebbe essere realizzato. Sto pensando di trattare il renderer come se fosse un proprio sistema distinto che fa riferimento al grafico della scena per trasformare i dati, ecc.
Cody Smith,

1
@CodySmith, felice di aver aiutato. Spina senza vergogna, ma mantengo un framework che riguarda tutto SoC / MVC. In tal modo, sono venuto a dura prova con il campo più tradizionale del settore, che insiste sul fatto che tutto dovrebbe trovarsi in un oggetto monolitico centrale. Ma anche loro lo direbbero generalmente: mantieni il rendering separato dal grafico della scena. SoC / SRP è qualcosa che non posso sottolineare abbastanza: non mescolare mai più logica in una singola classe di quanto tu abbia bisogno. Sostenerei anche complesse catene di ereditarietà OO sulla logica mista nella stessa classe, se mi metti una pistola in testa!
Ingegnere

No, mi piace il concetto. E hai ragione, questa è la prima menzione di SoC che ho visto ovunque negli anni in cui ho letto del game design. Grazie ancora.
Cody Smith,

@CodySmith Pensiero veloce mentre navighi di nuovo su questo. In generale è bene mantenere le cose disaccoppiate. Per vari tipi di oggetti controller di modello nel tuo codebase sottoposti a rendering, tuttavia, va bene conservare le raccolte di Renderables (che è un'interfaccia o una classe astratta) internamente a quegli oggetti core del controller di modello. Buoni esempi di questo sono entità o elementi dell'interfaccia utente. In questo modo è possibile accedere rapidamente solo ai renderer pertinenti a quel particolare oggetto principale, senza specifiche di implementazione che contaminerebbero la classe di entità, quindi l'uso di interfacce.
Ingegnere

@CodySmith Il vantaggio è evidente con le entità, ad esempio. avere rappresentazioni sia nella vista del mondo che su una minimappa. Da qui la collezione. In alternativa, è possibile consentire un solo slot di rendering per ciascun oggetto controller di modello, internamente a tale oggetto. Ma mantieni l'interfaccia generale! Nessun dettaglio - solo Renderer.
Ingegnere
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.