In un sistema di materiali basato su grafici, come posso supportare una varietà di tipi di input e output?


11

inserisci qui la descrizione dell'immagine

Sto cercando di avvolgere la mia testa intorno a come sistemi di materiali come questo , questa applicazione. Questi sistemi potenti e intuitivi, simili a grafici, sembrano essere relativamente comuni come metodo per consentire a programmatori e non programmatori di creare rapidamente shader. Tuttavia, dalla mia esperienza relativamente limitata con la programmazione grafica, non sono del tutto sicuro di come funzionino.


Sfondo:

Quindi, quando in precedenza ho programmato semplici sistemi di rendering OpenGL, in genere creo una classe Material che carica, compila e collega shader da file GLSL statici che ho creato manualmente. Di solito creo anche questa classe come un semplice wrapper per l'accesso alle variabili uniformi GLSL. Come semplice esempio, immagina di avere uno shader di vertice di base e uno shader di frammenti, con un Texture2D extra uniforme per passare una trama. La mia classe Materiale caricherà e compilerà semplicemente quei due shader in un materiale, e da quel momento in poi esponerebbe una semplice interfaccia per leggere / scrivere l'uniforme Texture2D di quello shader.

Per rendere questo sistema un po 'più flessibile, di solito lo scrivo in un modo che mi consente di provare a passare uniformi di qualsiasi nome / tipo [es: SetUniform_Vec4 ("AmbientColor", colorVec4); che imposterebbe l'uniforme AmbientColor su un particolare vettore 4d chiamato "colorVec4" se quell'uniforme esiste nel materiale.] .

class Material
{
    private:
       int shaderID;
       string vertShaderPath;
       string fragSahderPath;

       void loadShaderFiles(); //load shaders from files at internal paths.
       void buildMaterial(); //link, compile, buffer with OpenGL, etc.      

    public:
        void SetGenericUniform( string uniformName, int param );
        void SetGenericUniform( string uniformName, float param );
        void SetGenericUniform( string uniformName, vec4 param );
        //overrides for various types, etc...

        int GetUniform( string uniformName );
        float GetUniform( string uniformName );
        vec4 GetUniform( string uniformName );
        //etc...

        //ctor, dtor, etc., omitted for clarity..
}

Questo funziona , ma ci si sente come un cattivo sistema a causa del fatto che il cliente della classe materiale deve uniformi di accesso sulla sola fede - l' utente deve essere in qualche modo a conoscenza delle divise che si trovano in ogni oggetto materiale, perché sono costretti a passali con il loro nome GLSL. Non è un grosso problema quando sono solo 1-2 persone che lavorano con il sistema, ma non posso immaginare che questo sistema si ridimensionerebbe molto bene e prima di fare il mio prossimo tentativo di programmare un sistema di rendering OpenGL, voglio livellare un pochino.


Domanda:

Ecco dove sono finora, quindi ho cercato di studiare come altri motori di rendering gestiscono i loro sistemi materiali.

Questo approccio basato sul nodo è eccezionale e sembra essere un sistema estremamente comune per la creazione di sistemi di materiali facili da usare in motori e strumenti moderni. Da quello che posso dire sono basati su una struttura di dati grafici in cui ogni nodo rappresenta un aspetto shader del tuo materiale e ogni percorso rappresenta una sorta di relazione tra di loro.

Da quello che posso dire, implementare quel tipo di sistema sarebbe una semplice classe MaterialNode con una varietà di sottoclassi (TextureNode, FloatNode, LerpNode, ecc.). Dove ogni sottoclasse MaterialNode avrebbe MaterialConnections.

class MaterialConnection
{
    MatNode_Out * fromNode;
    MatNode_In * toNode;
}

class LerpNode : MaterialNode
{
    MatNode_In x;
    MatNode_In y;
    MatNode_In alpha;

    MatNode_Out result;
}

Questa è l' idea di base , ma sono un po 'incerto su come funzionerebbero alcuni aspetti di questo sistema:

1.) Se osservi le varie "Espressioni materiali" (nodi) utilizzate da Unreal Engine 4 , vedrai che ognuna ha connessioni di input e output di vari tipi. Alcuni nodi output fluttuano, alcuni output vettore2, alcuni output vettore4, ecc. Come posso migliorare i nodi e le connessioni sopra in modo che possano supportare una varietà di tipi di input e output? La sottoclasse di MatNode_Out con MatNode_Out_Float e MatNode_Out_Vec4 (e così via) sarebbe una scelta saggia?

2.) Infine, come si collega questo tipo di sistema agli shader GLSL? Guardando di nuovo UE4 (e in modo simile per gli altri sistemi collegati sopra), l'utente deve infine collegare un nodo di un materiale in un nodo di grandi dimensioni con vari parametri che rappresentano i parametri dello shader (colore di base, metallizzazione, brillantezza, emissività, ecc.) . La mia ipotesi originale era che UE4 avesse una sorta di "master shader" codificato con una varietà di uniformi, e tutto ciò che l'utente fa nel suo "materiale" viene semplicemente passato allo "master shader" quando inserisce i suoi nodi nel " nodo principale ".

Tuttavia, la documentazione UE4 afferma:

"Ogni nodo contiene uno snippet di codice HLSL, designato per eseguire un'attività specifica. Ciò significa che mentre costruisci un Materiale, crei il codice HLSL tramite script visivi."

Se questo è vero, questo sistema genera un vero script shader? Come funziona esattamente?


1
In relazione alla tua domanda: gameangst.com/?p=441
glampert

Risposte:


10

Proverò a rispondere al meglio delle mie conoscenze, con poche conoscenze sul caso specifico di UE4, ma piuttosto sulla tecnica generale.

I materiali basati su grafici sono tanto programmabili quanto scrivere il codice da soli. Semplicemente non sembra per le persone senza background sul codice, rendendolo apparentemente più semplice. Quindi, quando un designer collega un nodo "Aggiungi", fondamentalmente scrive add (valore1, valore2) e collega l'output a qualcos'altro. Questo significa che ogni nodo genererà il codice HLSL, sia una chiamata di funzione o solo semplici istruzioni.

Alla fine, usare il grafico dei materiali è come programmare shader grezzi con una libreria di funzioni predefinite che fanno alcune cose utili comuni, ed è anche ciò che fa UE4. Ha una libreria di codice shader che un compilatore shader prenderà e inietterà nella fonte shader finale quando applicabile.

Nel caso di UE4, se rivendicano la sua conversione in HLSL, presumo che utilizzino uno strumento di conversione in grado di convertire il codice byte HLSL in codice byte GLSL, quindi utilizzabile su piattaforme GL. Ma altre librerie hanno solo più compilatori di shader, che leggeranno il grafico e genereranno direttamente le fonti del linguaggio di shading necessarie.

Il grafico dei materiali è anche un buon modo per astrarre dalle specifiche della piattaforma e concentrarsi su ciò che conta dal punto di vista della direzione artistica. Dal momento che non è legato a una lingua e un livello molto più elevato, è più facile da ottimizzare per la piattaforma di destinazione e iniettare in modo dinamico altri codici come la gestione della luce nello shader.

1) Ora per rispondere alle tue domande in modo più diretto, dovresti avere un approccio basato sui dati per la progettazione di tale sistema. Trova un formato piatto che può essere definito in strutture molto semplici e persino definito in un file di testo. In sostanza, ogni grafico dovrebbe essere un array di nodi, con un tipo, un insieme di input e output, e ciascuno di questi campi dovrebbe avere un link_id locale per assicurarsi che le connessioni del grafico siano inequivocabili. Inoltre, ciascuno di questi campi potrebbe avere una configurazione aggiuntiva rispetto a ciò che il campo supporta (quale intervallo di tipi di dati sono supportati, ad esempio).

Con questo approccio, è possibile definire facilmente il campo di un nodo come (float | double) e lasciarlo dedurre il tipo dalle connessioni, oppure forzare un tipo in esso, senza gerarchie di classi o overengineering. Sta a te progettare questa struttura di dati grafici rigida o flessibile come desideri. Tutto ciò che serve è che abbia abbastanza informazioni in modo che il generatore di codice non abbia ambiguità e quindi potenzialmente gestisca in modo errato ciò che si desidera fare. L'importante è che a livello di struttura di dati di base, tu lo mantenga flessibile e focalizzato sulla risoluzione del compito di definire un materiale da solo.

Quando dico "definisci un materiale" mi riferisco in modo molto specifico alla definizione di una superficie mesh, al di là di ciò che la geometria stessa fornisce. Ciò include l'uso di ulteriori attributi di vertice per configurare l'aspetto della superficie, aggiungere lo spostamento ad essa con una mappa di altezza, disturbare le normali con normali per pixel, cambiare i parametri basati fisicamente, cambiare i BRDF e così via. Non vuoi descrivere nient'altro come HDR, tonemapping, animazioni skinning, gestione della luce o molte altre cose fatte negli shader.

2) Spetta quindi al generatore di shader del renderer attraversare questa struttura di dati e, leggendo le sue informazioni, assemblare un insieme di variabili e collegarle insieme utilizzando funzioni predefinite e iniettando il codice che calcola l'illuminazione e altri effetti. Ricorda solo che gli shader variano non solo da diverse API grafiche, ma anche tra diversi renderer (un rendering differito rispetto a un renderer basato su tile o forward richiede tutti diversi shader per funzionare) e con un sistema materiale come questo, puoi estrarre dai cattivi livello di basso livello e concentrarsi solo sulla descrizione della superficie. "

Per UE4, hanno creato un elenco di cose per quel nodo di output finale che menzioni, che pensano descriva il 99% delle superfici nei giochi vecchi e moderni. Hanno sviluppato questo insieme di parametri per decenni e lo hanno dimostrato con la folle quantità di giochi prodotti finora dal motore Unreal. Quindi starai bene se fai le cose allo stesso modo dell'irreale.

Per concludere, suggerisco un file .material solo per gestire ogni grafico. Durante lo sviluppo, conterrà forse un formato basato su testo per eseguire il debug e quindi essere impacchettato o compilato in binario per il rilascio. Ogni .materiale sarebbe composto da N nodi e N connessioni, un po 'come un database SQL. Ogni nodo avrebbe N campi, con un nome e alcuni flag per i tipi accettati, se il suo input o output, se i tipi sono dedotti, ecc. La struttura dei dati di runtime per contenere il materiale caricato sarebbe altrettanto piatta e semplice, quindi il l'editor può facilmente adattarlo e salvare di nuovo nel file.

E poi lascia il vero sollevamento pesante per l'ultima generazione di shader, che è davvero la parte più difficile da fare. La parte bella è che il tuo materiale rimane agnostico rispetto alla piattaforma di rendering, in teoria funzionerebbe con qualsiasi tecnica di rendering e API fintanto che rappresenterai il materiale nel suo linguaggio di ombreggiatura appropriato.

Fammi sapere se hai bisogno di ulteriori dettagli o eventuali correzioni nella mia risposta, ho perso la visione d'insieme di tutto il testo.


Non posso ringraziarvi abbastanza per aver scritto una risposta così elaborata ed eccellente. Sento di avere una grande idea di dove dovrei andare da qui! Grazie!
MrKatSwordfish

1
Nessun problema amico, sentiti libero di inviarmi un messaggio se hai bisogno di ulteriore aiuto. In realtà sto lavorando a qualcosa di equivalente per i miei strumenti, quindi se vuoi scambiare pensieri, sii mio ospite! Buon pomeriggio: D
Grimshaw,
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.