Creare Videogiochi - Game Developer

Versione completa: Tutorial 10 - Shaders
Al momento stai visualizzando i contenuti in una versione ridotta. Visualizza la versione completa e formattata.
Tutorial 10: Shaders

[Immagine: 010shot.jpg]

Questo tutorial spiega come utilizzare gli shaders per D3D8, D3D9, OpenGL e Cg con Irrlicht e come creare nuovi materiali. Viene anche spiegato come disabilitare la generazione delle mipmaps durante il caricamento delle texture e come utilizzare i nodi di scena di tipo testo (etichette con del testo).
In questo tutorial non verrà spiegato come funzionano gli shaders. Se volete approfondire gli shaders consiglio di leggere la documentazione delle D3D, OpenGL e del linguaggio Cg, cercate tutorials o leggete un libro specifico.
Come negli altri tutorial, per prima cosa includiamo gli header ed indichiamo al linker la libreria giusta:
Codice PHP:
#include <irrlicht.h>
#include <iostream>
#include "driverChoice.h"

using namespace irr;

#ifdef _MSC_VER
#pragma comment(lib, "Irrlicht.lib")
#endif 
Poiché vogliamo utilizzare determinati shaders dobbiamo preimpostare alcuni dati per il loro utilizzo affinché possano elaborare correttamente i colori da produrre. In questo esempio useremo un semplice vertex shader che calcolerà il colore dei vertici in base alla posizione della camera. Per questo allo shader dovremo passare alcune informazioni: la 'matrice mondo' inversa per la trasformazione delle normali, la 'matrice di taglio' per la trasformazione della posizione, la posizione della camera, le coordinate mondo per la posizione dell'oggetto per il calcolo dell'angolo della luce e il colore della luce. Per fornire ad ogni frame tutti questi dati allo shader dobbiamo derivare una classe ad hoc dall'interfaccia IShaderConstantSetCallBack e sovrascrivere il solo metodo OnSetConstants(). Questo metodo verrà richiamato ogni volta che viene impostato il materiale. Il metodo setVertexShaderConstant() dell'interfaccia IMaterialRendererServices è utilizzato per passare le costanti come parametro allo shader. Se l'utente decidesse di utilizzare un High Level shader language come HLSL invece di uno di tipo Assembler (non più usati dalle DX9 in poi), dovrete impostare la variabile del nome indicandolo direttamente come parametro tra apici anziché passare il suo indirizzo.
Codice PHP:
IrrlichtDevicedevice 0;
bool UseHighLevelShaders false;
bool UseCgShaders false;

class 
MyShaderCallBack : public video::IShaderConstantSetCallBack
{
public:

    
virtual void OnSetConstants(video::IMaterialRendererServicesservices,
            
s32 userData)
    {
        
video::IVideoDriverdriver services->getVideoDriver();

        
// set inverted world matrix
        // if we are using highlevel shaders (the user can select this when
        // starting the program), we must set the constants by name.

        
core::matrix4 invWorld driver->getTransform(video::ETS_WORLD);
        
invWorld.makeInverse();

        if (
UseHighLevelShaders)
            
services->setVertexShaderConstant("mInvWorld"invWorld.pointer(), 16);
        else
            
services->setVertexShaderConstant(invWorld.pointer(), 04);

        
// set clip matrix

        
core::matrix4 worldViewProj;
        
worldViewProj driver->getTransform(video::ETS_PROJECTION);
        
worldViewProj *= driver->getTransform(video::ETS_VIEW);
        
worldViewProj *= driver->getTransform(video::ETS_WORLD);

        if (
UseHighLevelShaders)
            
services->setVertexShaderConstant("mWorldViewProj"worldViewProj.pointer(), 16);
        else
            
services->setVertexShaderConstant(worldViewProj.pointer(), 44);

        
// set camera position

        
core::vector3df pos device->getSceneManager()->
            
getActiveCamera()->getAbsolutePosition();

        if (
UseHighLevelShaders)
            
services->setVertexShaderConstant("mLightPos"reinterpret_cast<f32*>(&pos), 3);
        else
            
services->setVertexShaderConstant(reinterpret_cast<f32*>(&pos), 81);

        
// set light color

        
video::SColorf col(0.0f,1.0f,1.0f,0.0f);

        if (
UseHighLevelShaders)
            
services->setVertexShaderConstant("mLightColor",
                    
reinterpret_cast<f32*>(&col), 4);
        else
            
services->setVertexShaderConstant(reinterpret_cast<f32*>(&col), 91);

        
// set transposed world matrix

        
core::matrix4 world driver->getTransform(video::ETS_WORLD);
        
world world.getTransposed();

        if (
UseHighLevelShaders)
        {
            
services->setVertexShaderConstant("mTransWorld"world.pointer(), 16);

            
// set texture, for textures you can use both an int and a float setPixelShaderConstant interfaces (You need it only for an OpenGL driver).
            
s32 TextureLayerID 0;
            if (
UseHighLevelShaders)
                
services->setPixelShaderConstant("myTexture", &TextureLayerID1);
        }
        else
            
services->setVertexShaderConstant(world.pointer(), 104);
    }
}; 
Le prossime linee di codice avviano l'engine come nella maggior parte dei tutorial fino a qui visti. In più stavolta chiederemo all'utente se vuole usare gli shaders di tipo HLSL nel caso il driver selezionato li supporti.
Codice PHP:
int main()
{
    
// ask user for driver
    
video::E_DRIVER_TYPE driverType=driverChoiceConsole();
    if (
driverType==video::EDT_COUNT)
        return 
1;

    
// ask the user if we should use high level shaders for this example
    
if (driverType == video::EDT_DIRECT3D9 ||
         
driverType == video::EDT_OPENGL)
    {
        
char i;
        
printf("Please press 'y' if you want to use high level shaders.\n");
        
std::cin >> i;
        if (
== 'y')
        {
            
UseHighLevelShaders true;
            
printf("Please press 'y' if you want to use Cg shaders.\n");
            
std::cin >> i;
            if (
== 'y')
                
UseCgShaders true;
        }
    }

    
// create device
    
device createDevice(driverTypecore::dimension2d<u32>(640480));

    if (
device == 0)
        return 
1// could not create selected driver.

    
video::IVideoDriverdriver device->getVideoDriver();
    
scene::ISceneManagersmgr device->getSceneManager();
    
gui::IGUIEnvironmentgui device->getGUIEnvironment();

    
// Make sure we don't try Cg without support for it
    
if (UseCgShaders && !driver->queryFeature(video::EVDF_CG))
    {
        
printf("Warning: No Cg support, disabling.\n");
        
UseCgShaders=false;
    } 
Ora la parte più interessante. Se utilizziamo le Direct3D, dovremo caricare i rispettivi vertex e pixel shader, se usiamo le OpenGL caricheremo l'ARB fragment ed i vertex script. Ho scritto i corrispondenti programmi nei files d3d8.ps, d3d8.vs, d3d9.ps, d3d9.vs, opengl.ps e opengl.vs. Dobbiamo solo indicare i nomi giusti. Lo facciamo nel comando switch che segue. Notare, non è necessario scrivere gli shaders dentro un file di testo, come nell'esempio. E' sempre possibile scrivere gli shaders direttamente come stringhe dentro il sorgente cpp e poi passarle tramite addShaderMaterial() invece del addShaderMaterialFromFiles().
Codice PHP:
io::path vsFileName// filename for the vertex shader
    
io::path psFileName// filename for the pixel shader

    
switch(driverType)
    {
    case 
video::EDT_DIRECT3D8:
        
psFileName "../../media/d3d8.psh";
        
vsFileName "../../media/d3d8.vsh";
        break;
    case 
video::EDT_DIRECT3D9:
        if (
UseHighLevelShaders)
        {
            
// Cg can also handle this syntax
            
psFileName "../../media/d3d9.hlsl";
            
vsFileName psFileName// both shaders are in the same file
        
}
        else
        {
            
psFileName "../../media/d3d9.psh";
            
vsFileName "../../media/d3d9.vsh";
        }
        break;

    case 
video::EDT_OPENGL:
        if (
UseHighLevelShaders)
        {
            if (!
UseCgShaders)
            {
                
psFileName "../../media/opengl.frag";
                
vsFileName "../../media/opengl.vert";
            }
            else
            {
                
// Use HLSL syntax for Cg
                
psFileName "../../media/d3d9.hlsl";
                
vsFileName psFileName// both shaders are in the same file
            
}
        }
        else
        {
            
psFileName "../../media/opengl.psh";
            
vsFileName "../../media/opengl.vsh";
        }
        break;
    } 
Andiamo a verificare se l'hardware ed il driver selezionato sono in grado di eseguire gli shader che stiamo usando. Se non lo fossero andremo a svuotare la stringa del nome file dello shader. Nel nostro caso non sarebbe necessario ma comunque è utile per l'esempio in se: infatti potrebbe capitare che il nostro hardware sia capace solo di gestire i vertex shaders ma non i pixel shaders, allora dovremmo creare un materiale ad hoc che utilizzi solo i vertex shader e nessun pixel shader, in ogni caso se avessimo comunque creato questi materiali e l'engine avesse visto che l'hardware non poteva processarli, l'engine stesso non avrebbe creato alcun materiale. In questo esempio dovremmo vedere di certo il vertex shader in azione anche senza il pixel shader (oramai l'hardware odierno li gestisce entrambi).
Codice PHP:
if (!driver->queryFeature(video::EVDF_PIXEL_SHADER_1_1) &&
        !
driver->queryFeature(video::EVDF_ARB_FRAGMENT_PROGRAM_1))
    {
        
device->getLogger()->log("WARNING: Pixel shaders disabled "\
            
"because of missing driver/hardware support.");
        
psFileName "";
    }

    if (!
driver->queryFeature(video::EVDF_VERTEX_SHADER_1_1) &&
        !
driver->queryFeature(video::EVDF_ARB_VERTEX_PROGRAM_1))
    {
        
device->getLogger()->log("WARNING: Vertex shaders disabled "\
            
"because of missing driver/hardware support.");
        
vsFileName "";
    } 
Ora creiamo i materiali. Come forse avete capito dai precedenti tutorial, un tipo di materiale in Irrlicht viene impostato semplicemente cambiando il valore MaterialType nella struct SMaterial. Si tratta di un valore da 32 bit value, come video::EMT_SOLID. A noi serve quindi andare a creare un nuovo valore da immettere nell'engine per il nostro nuovo materiale. Per farlo andiamo a prenderci un puntatore dal IGPUProgrammingServices e richiamiamo addShaderMaterialFromFiles(), che ci ritorna il valore come sequenza di 32 bit. E' tutto.
I parametri da passare a questo metodo sono i seguenti: Primo, il nome del file che contiene il codice di ciascun shader. Se invece volete usare addShaderMaterial(), non vi serve il nome del file, potete scrivere il codice direttamente in una stringa e passarla come parametro. Il parametro che segue è un puntatore alla interfaccia IShaderConstantSetCallBack che avevamo derivato all'inizio del tutorial. Se non volete passare alcuna costante allo shader, azzerate il parametro. L'ultimo parametro dice all'engine quale materiale deve usare come materiale base.
Per dimostrarlo, creiamo due materiali ciascuno con un differente materiale base, uno con EMT_SOLID l'altro con EMT_TRANSPARENT_ADD_COLOR.
Codice PHP:
// create materials

    
video::IGPUProgrammingServicesgpu driver->getGPUProgrammingServices();
    
s32 newMaterialType1 0;
    
s32 newMaterialType2 0;

    if (
gpu)
    {
        
MyShaderCallBackmc = new MyShaderCallBack();

        
// create the shaders depending on if the user wanted high level
        // or low level shaders:

        
if (UseHighLevelShaders)
        {
            
// Choose the desired shader type. Default is the native
            // shader type for the driver, for Cg pass the special
            // enum value EGSL_CG
            
const video::E_GPU_SHADING_LANGUAGE shadingLanguage =
                
UseCgShaders video::EGSL_CG:video::EGSL_DEFAULT;

            
// create material from high level shaders (hlsl, glsl or cg)

            
newMaterialType1 gpu->addHighLevelShaderMaterialFromFiles(
                
vsFileName"vertexMain"video::EVST_VS_1_1,
                
psFileName"pixelMain"video::EPST_PS_1_1,
                
mcvideo::EMT_SOLID0shadingLanguage);

            
newMaterialType2 gpu->addHighLevelShaderMaterialFromFiles(
                
vsFileName"vertexMain"video::EVST_VS_1_1,
                
psFileName"pixelMain"video::EPST_PS_1_1,
                
mcvideo::EMT_TRANSPARENT_ADD_COLORshadingLanguage);
        }
        else
        {
            
// create material from low level shaders (asm or arb_asm)

            
newMaterialType1 gpu->addShaderMaterialFromFiles(vsFileName,
                
psFileNamemcvideo::EMT_SOLID);

            
newMaterialType2 gpu->addShaderMaterialFromFiles(vsFileName,
                
psFileNamemcvideo::EMT_TRANSPARENT_ADD_COLOR);
        }

        
mc->drop();
    } 
Ora è tempo di testare i nostri materiali. Creiamoci dei cubi ed impostiamo i materiali appena creati. In più aggiungiamo un nodo di scena di tipo text sui cubi (delle etichette) ed un rotation animator per renderli più interessanti e dare un po' di dinamicità.
Codice PHP:
// create test scene node 1, with the new created material type 1

    
scene::ISceneNodenode smgr->addCubeSceneNode(50);
    
node->setPosition(core::vector3df(0,0,0));
    
node->setMaterialTexture(0driver->getTexture("../../media/wall.bmp"));
    
node->setMaterialFlag(video::EMF_LIGHTINGfalse);
    
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType1);

    
smgr->addTextSceneNode(gui->getBuiltInFont(),
            
L"PS & VS & EMT_SOLID",
            
video::SColor(255,255,255,255), node);

    
scene::ISceneNodeAnimatoranim smgr->createRotationAnimator(
            
core::vector3df(0,0.3f,0));
    
node->addAnimator(anim);
    
anim->drop(); 
Facciamo lo stesso per il secondo cubo ma ovviamente usiamo il secondo materiale.
Codice PHP:
// create test scene node 2, with the new created material type 2

    
node smgr->addCubeSceneNode(50);
    
node->setPosition(core::vector3df(0,-10,50));
    
node->setMaterialTexture(0driver->getTexture("../../media/wall.bmp"));
    
node->setMaterialFlag(video::EMF_LIGHTINGfalse);
    
node->setMaterialFlag(video::EMF_BLEND_OPERATIONtrue);
    
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType2);

    
smgr->addTextSceneNode(gui->getBuiltInFont(),
            
L"PS & VS & EMT_TRANSPARENT",
            
video::SColor(255,255,255,255), node);

    
anim smgr->createRotationAnimator(core::vector3df(0,0.3f,0));
    
node->addAnimator(anim);
    
anim->drop(); 
Quindi aggiungiamo un terzo cubo senza alcun shader, per poterli comparare.
Codice PHP:
// add a scene node with no shader

    
node smgr->addCubeSceneNode(50);
    
node->setPosition(core::vector3df(0,50,25));
    
node->setMaterialTexture(0driver->getTexture("../../media/wall.bmp"));
    
node->setMaterialFlag(video::EMF_LIGHTINGfalse);
    
smgr->addTextSceneNode(gui->getBuiltInFont(), L"NO SHADER",
        
video::SColor(255,255,255,255), node); 
Per ultimo, aggiungiamo una skybox ed una camera controllata dall'utente. Per la texture della skybox, andiamo a disabilitare la generazione della mipmap, perché non ci serve.
Codice PHP:
// add a nice skybox

    
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPSfalse);

    
smgr->addSkyBoxSceneNode(
        
driver->getTexture("../../media/irrlicht2_up.jpg"),
        
driver->getTexture("../../media/irrlicht2_dn.jpg"),
        
driver->getTexture("../../media/irrlicht2_lf.jpg"),
        
driver->getTexture("../../media/irrlicht2_rt.jpg"),
        
driver->getTexture("../../media/irrlicht2_ft.jpg"),
        
driver->getTexture("../../media/irrlicht2_bk.jpg"));

    
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPStrue);

    
// add a camera and disable the mouse cursor

    
scene::ICameraSceneNodecam smgr->addCameraSceneNodeFPS();
    
cam->setPosition(core::vector3df(-100,50,100));
    
cam->setTarget(core::vector3df(0,0,0));
    
device->getCursorControl()->setVisible(false); 
Ora disegniamo. Questo è tutto.
Codice PHP:
int lastFPS = -1;

    while(
device->run())
        if (
device->isWindowActive())
    {
        
driver->beginScene(truetruevideo::SColor(255,0,0,0));
        
smgr->drawAll();
        
driver->endScene();

        
int fps driver->getFPS();

        if (
lastFPS != fps)
        {
            
core::stringw str L"Irrlicht Engine - Vertex and pixel shader example [";
            
str += driver->getName();
            
str += "] FPS:";
            
str += fps;

            
device->setWindowCaption(str.c_str());
            
lastFPS fps;
        }
    }

    
device->drop();

    return 
0;

Compiliamo e lanciamo il programma, spero vi siate divertiti con il vostro nuovo piccolo programma e la scrittura degli shader Smile.

Versione pdf scaricabile da QUI
Programmazione degli Shader.

Come vago approfondimento diciamo che gli shader sono essenzialmente di due tipi.

I vertex shader sono comandi interpretati direttamente dalle schede grafiche che lavorano sui vertici delle geometrie nelle nostre scene, tipicamente sono usati per modificarne le coordinate, il colore o l'illuminazione. tipici esempi sono gli shader che generano l'ondulazione di una bandiera o di una superficie d'acqua.

I pixel shader sono invece più 'fini' poiché lavorano sui singoli pixel in uscita dal rendering, infatti sono tipicamente usati per effetti come il bump map, il fresnel, la diffrazione o per generare ombre. Per questo sono considerati più potenti ma anche più pesanti.

Introdotti a partire dalle DirectX8 con lo shader level1 bisognava programmarli tramite un linguaggio simile all'Assembler (nel tutorial che è datato infatti viene menzionata questa possibilità).

Si sono susseguite le versione level2 e level3 fino alle DX9, poi con le DX10 è arrivata la level4 e con el DX11 la level5. Grosso modo vengono sempre aumentati il numero di istruzioni compilabili (128.. 512.. dalla level4 sono illimitate) il numero di registri e la complessità delle strutture interne e delle istruzioni per gestirle (quasi delle macro).

OpenGL ha seguito un percorso analogo e parallelo. I pixel shader sono anche detti fragment shader. La numerazione delle versioni agli inizi era meno lineare rispetto alle DirectX, si parte dalla versione 1 con le OpenGL2.0, la versione 1.50 con le OpenGL3.2; poi dalla versione 4 si sono allineati a quella delle OpenGL. Oggi abbiamo gli shader GLSL 4.50 e OpenGL 4.5.

Subito dopo il linguaggio simil-assembly ognuno ha proposto un proprio linguaggio vagamente basato sul C. Per le OpenGL abbiamo il GLSL, per le DirectX abbiamo HLSL. Fino al 2012 Nvidia supportava un proprio linguaggio il Cg che offriva anche tools con la particolarità di generare un output compatibile per i due linguaggi precedenti.

La programmazione degli shader è considerata un'arte a se, spesso si trovano richieste specifiche di programmatori di solo shader.

Un GLSL vertex shader che volesse scalare i vertici sulla x e y potrebbe essere questo:

Codice PHP:
void main(void)
{
   
vec4 a gl_Vertex;
   
a.a.0.5;
   
a.a.0.5;
   
gl_Position gl_ModelViewProjectionMatrix a;


Il trucco sta nella gl_Position che è una struttura 4D che indica la posizione finale del vertex e nel gl_Vertex che invece è l'attributo contenente la posizione attuale del vertex. Lo salviamo un un vettore di 4 elementi (x,y,z,w), scaliamo riducendo della metà le coordinate e le riportiamo con la matrice gl_ModelViewProjectionMatrix che sarebbe la concatenazione del modelview e della matrice di proiezione (comunque è una struttura deprecata, prendete tutto col beneficio d'inventario perché dalla versione 4 del GLSL ha cambiato molte strutture).

Invece se volessimo impostare a verde un pixel con un pixel shader GLSL poco prima che venga renderizzato in 2d basterebbe agire sul valore del gl_FragColor.
Codice PHP:
void main (void)  
{     
   
gl_FragColor vec4(0.01.00.01.0);  


Inutile ripetere che si tratta di un aspetto molto avanzato, i comuni engine offrono già una serie di shader preimpostati (e anche più sicuri) e anche un linguaggio proprietario con cui garantire la portabilità rispetto alla libreria di rendering adottata. Per cimentarsi nella loro programmazione consiglio di essere sempre aggiornatissimi e di leggere parecchi tutorial e le risorse ufficiali di DX e OpenGL, vi garantisco che sarà una esperienza piuttosto snervante. Angel
Thanks a lot