Tutorial 11 - Per-Pixel Lighting - Chip - 17-08-2015
Tutorial 11: Per-Pixel Lighting
In questo tutorial mostreremo l'utilizzo di uno dei materiali shader più complessi e preimpostati in Irrlicht: Il Per pixel lighted surfaces (illuminazione delle superfici a livello di pixel) utilizzando le normal maps e il parallax mapping (non traduco perché ormai sono di uso comune ndt1). Mostreremo anche l'ultilizzo dell'effetto nebbia e come creare sistemi particellari in movimento. E comunque niente panico: non vi serve alcuna esperienza (che comunque non fa mai male ndt) con la programmazione degli shaders per usare i materiali in Irrlicht.
Come prima cosa includiamo l'header e le solite cose necessarie come abbiamo fatto fino ad ora in tutti i precedenti tutorials.
Codice PHP: #include <irrlicht.h> #include "driverChoice.h"
using namespace irr;
#ifdef _MSC_VER #pragma comment(lib, "Irrlicht.lib") #endif
Per questo esempio ci serve un event receiver, per rendere l'utente in grado di switchare tra i tre tipi di materiali (Diffuse, BumpMap, Parallax). Oltre all'event receiver creiamo anche una piccola finestra con la GUI che mostri quale materiale è usato. Non c'è niente di speciale in questa classe, volendo potete saltarne la lettura (cosa che vi sconsiglio di fare ndt).
Codice PHP: class MyEventReceiver : public IEventReceiver { public:
MyEventReceiver(scene::ISceneNode* room,scene::ISceneNode* earth, gui::IGUIEnvironment* env, video::IVideoDriver* driver) { // store pointer to room so we can change its drawing mode Room = room; Earth = earth; Driver = driver;
// set a nicer font gui::IGUISkin* skin = env->getSkin(); gui::IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp"); if (font) skin->setFont(font);
// add window and listbox gui::IGUIWindow* window = env->addWindow( core::rect<s32>(460,375,630,470), false, L"Use 'E' + 'R' to change");
ListBox = env->addListBox( core::rect<s32>(2,22,165,88), window);
ListBox->addItem(L"Diffuse"); ListBox->addItem(L"Bump mapping"); ListBox->addItem(L"Parallax mapping"); ListBox->setSelected(1);
// create problem text ProblemText = env->addStaticText( L"Your hardware or this renderer is not able to use the "\ L"needed shaders for this material. Using fall back materials.", core::rect<s32>(150,20,470,80));
ProblemText->setOverrideColor(video::SColor(100,255,255,255));
// set start material (prefer parallax mapping if available) video::IMaterialRenderer* renderer = Driver->getMaterialRenderer(video::EMT_PARALLAX_MAP_SOLID); if (renderer && renderer->getRenderCapability() == 0) ListBox->setSelected(2);
// set the material which is selected in the listbox setMaterial(); }
bool OnEvent(const SEvent& event) { // check if user presses the key 'E' or 'R' if (event.EventType == irr::EET_KEY_INPUT_EVENT && !event.KeyInput.PressedDown && Room && ListBox) { // change selected item in listbox
int sel = ListBox->getSelected(); if (event.KeyInput.Key == irr::KEY_KEY_R) ++sel; else if (event.KeyInput.Key == irr::KEY_KEY_E) --sel; else return false;
if (sel > 2) sel = 0; if (sel < 0) sel = 2; ListBox->setSelected(sel);
// set the material which is selected in the listbox setMaterial(); }
return false; }
private:
// sets the material of the room mesh the the one set in the // list box. void setMaterial() { video::E_MATERIAL_TYPE type = video::EMT_SOLID;
// change material setting switch(ListBox->getSelected()) { case 0: type = video::EMT_SOLID; break; case 1: type = video::EMT_NORMAL_MAP_SOLID; break; case 2: type = video::EMT_PARALLAX_MAP_SOLID; break; }
Room->setMaterialType(type);
// change material setting switch(ListBox->getSelected()) { case 0: type = video::EMT_TRANSPARENT_VERTEX_ALPHA; break; case 1: type = video::EMT_NORMAL_MAP_TRANSPARENT_VERTEX_ALPHA; break; case 2: type = video::EMT_PARALLAX_MAP_TRANSPARENT_VERTEX_ALPHA; break; }
Earth->setMaterialType(type);
Nel caso i materiali non venissero processati correttamente, ci servirebbe aggiungere un messaggio di warning. Non c'è problema, i materiali in Irrlicht hanno un materiale per il fallback, se falliscono vengono attivati come sostitutivi, ma è giusto indicare all'utente che con un hardware migliore potrebbe ottenere una qualità migliore. Semplicemente controlliamo se il materiale può essere gestito al massimo della qualità con questo hardware. In questo caso la classe che usiamo IMaterialRenderer::getRenderCapability() ritornerebbe 0.
Codice PHP: video::IMaterialRenderer* renderer = Driver->getMaterialRenderer(type);
// display some problem text when problem if (!renderer || renderer->getRenderCapability() != 0) ProblemText->setVisible(true); else ProblemText->setVisible(false); }
private:
gui::IGUIStaticText* ProblemText; gui::IGUIListBox* ListBox;
scene::ISceneNode* Room; scene::ISceneNode* Earth; video::IVideoDriver* Driver; };
Ora arriva il divertimento. Creiamo il device di Irrlicht Device ed iniziamo il rendering.
Codice PHP: int main() { // ask user for driver video::E_DRIVER_TYPE driverType=driverChoiceConsole(); if (driverType==video::EDT_COUNT) return 1;
// create device IrrlichtDevice* device = createDevice(driverType, core::dimension2d<u32>(640, 480));
if (device == 0) return 1; // could not create selected driver.
Prima di iniziare con la parte interessante dobbiamo però fare alcune cose: registrare i puntatori alle parti più importanti dell'engine (video driver, scene manager, ambiente gui) per evitarci di dover scrivere troppe derivazioni nel codice, aggiungiamo un logo di irrlicht alla finestra ed una camera controllata dall'utente di tipo FPS. Dobbiamo anche indicare ad Irrlicht che deve registrare tutte le texture in 32 bit. Questo è necessario sopratutto per il parallax mapping che richiede texture a 32 bit.
Codice PHP: video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager(); gui::IGUIEnvironment* env = device->getGUIEnvironment();
driver->setTextureCreationFlag(video::ETCF_ALWAYS_32_BIT, true); // add irrlicht logo env->addImage(driver->getTexture("../../media/irrlichtlogo3.png"), core::position2d<s32>(10,10)); // add camera scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS(); camera->setPosition(core::vector3df(-200,200,-200));
// disable mouse cursor device->getCursorControl()->setVisible(false);
Poiché vogliamo che la scena sembri un po' più spaventosa aggiungiamo un po' di nebbia. Questo si fa richiamando IVideoDriver:etFog(). Qui possiamo impostare molti valori per la nebbia. In questo esempio useremo la pixel fog, perché è l'ideale per il tipo di materiali che andremo ad usare nell'esempio. Vorrei che notaste che impostiamo a 'true' il flag EMF_FOG_ENABLE per ogni materiale di ciascun nodo della scena che vogliamo venga colpito dalla nebbia.
Codice PHP: driver->setFog(video::SColor(0,138,125,81), video::EFT_FOG_LINEAR, 250, 1000, .003f, true, false);
Per mostrare qualcosa di interessante carichiamo una mesh da file .3ds di una piccola stanza che ho medellato con Anim8or. Si tratta della stessa stanza usata nel tutorial specialFX. Forse vi ricorderete che sono un pessimo modellatore infatti nel modello avevo sbagliato il texture mapping, ma possiamo riparare attraverso il metodo IMeshManipulator::makePlanarTextureMapping().
Codice PHP: scene::IAnimatedMesh* roomMesh = smgr->getMesh("../../media/room.3ds"); scene::ISceneNode* room = 0; scene::ISceneNode* earth = 0;
if (roomMesh) { // The Room mesh doesn't have proper Texture Mapping on the // floor, so we can recreate them on runtime smgr->getMeshManipulator()->makePlanarTextureMapping( roomMesh->getMesh(0), 0.003f);
Ora la prima cosa eccitante: Se la mesh è stata caricata correttamente dobbiamo texturizzarla. Poiché vogliamo che la stanza abbia un bel aspetto con un buon materiale, non possiamo limitarci ad applicare la texture e basta. Invece di caricare la sola texture come al solito, andiamo anche a caricare una mappa di altezze (height map) che è una semplice texture a scala di grigi. Da questa height map, andiamo a crearci una normal map che impostiamo come seconda texture nella stanza. Se già abbiamo una normal map ovviamente possiamo impostarla direttamente, è solo che non ne ho trovata una adatta per questa texture. La normal map texture viene generata attraverso il metodo makeNormalMapTexture del VideoDriver. Il secondo parametro indica l'altezza della height map (cioè tra il punto più scuro e il punto più chiaro della mappa c'è un salto di 9 punti). Se aumentate il valore, la mappa avrà un aspetto più roccioso, con più scanalatura.
Codice PHP: video::ITexture* normalMap = driver->getTexture("../../media/rockwall_height.bmp");
if (normalMap) driver->makeNormalMapTexture(normalMap, 9.0f);
La Normal Map e la displacement map/height map nel canale alpha (credo ci sia un errore di formattazione nel tutorial ndt)
Codice PHP: video::ITexture* normalMap = driver->getTexture(“../../media/rockwall_NRM.tga”);
Ma impostare i colori e la normal map non è tutto. Il materiale che stiamo usando richiede altre informazioni per ogni singolo vertex come le tangenti e le binormali. E visto che siamo troppo pigri per calcolarcele, lasciamo che sia Irrlicht a farlo per noi. Ecco perché andiamo a richiamare IMeshManipulator::createMeshWithTangents(). Che crea una copia della nostra mesh con le tangenti e le binormali a partire dalla nostra mesh (la prima e unica di indice 0 ndt). Fatto questo semplicemente creiamo un nodo di scena di tipo mesh standard con questa mesh appena copiata, impostiamo il colore e la normal map e aggiustiamo altri settaggi del materiale. Notare che anche questa volta andiamo ad attivare il flag EMF_FOG_ENABLE per rendere la mesh sensibili rispetto alla nebbia.
Codice PHP: scene::IMesh* tangentMesh = smgr->getMeshManipulator()-> createMeshWithTangents(roomMesh->getMesh(0));
room = smgr->addMeshSceneNode(tangentMesh); room->setMaterialTexture(0, driver->getTexture("../../media/rockwall.jpg")); room->setMaterialTexture(1, normalMap);
// Stones don't glitter.. room->getMaterial(0).SpecularColor.set(0,0,0,0); room->getMaterial(0).Shininess = 0.f;
room->setMaterialFlag(video::EMF_FOG_ENABLE, true); room->setMaterialType(video::EMT_PARALLAX_MAP_SOLID); // adjust height for parallax effect room->getMaterial(0).MaterialTypeParam = 1.f / 64.f;
// drop mesh because we created it with a create.. call. tangentMesh->drop(); }
Dopo che abbiamo creato la nostra stanza con l'effetto di illuminazione per pixel da shader, andiamo a creare una sfera con lo stesso materiale ma stavolta lo renderemo trasparente. Inoltre, visto che la sfera ci ricorda qualcosa di un pianeta a noi familiare, la facciamo ruotare. La procedura è simile a quella già vista. La differenza è che stavolta carichiamo una mesh da un file .x che già contiene una mappatura dei colori che quindi non necessita di essere impostata manualmente. Ma la sfera è un po' piccola per le nostre necessità, quindi la scaliamo di un fattore 50.
Codice PHP: // add earth sphere
scene::IAnimatedMesh* earthMesh = smgr->getMesh("../../media/earth.x"); if (earthMesh) { //perform various task with the mesh manipulator scene::IMeshManipulator *manipulator = smgr->getMeshManipulator();
// create mesh copy with tangent informations from original earth.x mesh scene::IMesh* tangentSphereMesh = manipulator->createMeshWithTangents(earthMesh->getMesh(0));
// set the alpha value of all vertices to 200 manipulator->setVertexColorAlpha(tangentSphereMesh, 200);
// scale the mesh by factor 50 core::matrix4 m; m.setScale ( core::vector3df(50,50,50) ); manipulator->transform( tangentSphereMesh, m );
earth = smgr->addMeshSceneNode(tangentSphereMesh);
earth->setPosition(core::vector3df(-70,130,45));
// load heightmap, create normal map from it and set it video::ITexture* earthNormalMap = driver->getTexture("../../media/earthbump.jpg"); if (earthNormalMap) { driver->makeNormalMapTexture(earthNormalMap, 20.0f); earth->setMaterialTexture(1, earthNormalMap); earth->setMaterialType(video::EMT_NORMAL_MAP_TRANSPARENT_VERTEX_ALPHA); } // adjust material settings earth->setMaterialFlag(video::EMF_FOG_ENABLE, true); // add rotation animator scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator(core::vector3df(0,0.1f,0)); earth->addAnimator(anim); anim->drop(); // drop mesh because we created it with a create.. call. tangentSphereMesh->drop(); }
I materiali per l'illuminazione dei pixel ovviamente mostrano il meglio di se quando ci sono in giro luce dinamiche che si muovono. Quindi aggiungiamone un pò. E siccome le luci in movimento non sono granché da sole, allora ci aggiungiamo una bella billboard (uno sprite 3d) ed un intero sistema particellare che ne segue una. Cominciamo con la prima luce che sarà rossa e avrà soltanto una billboard attaccata.
Codice PHP: // add light 1 (more green) scene::ILightSceneNode* light1 = smgr->addLightSceneNode(0, core::vector3df(0,0,0), video::SColorf(0.5f, 1.0f, 0.5f, 0.0f), 800.0f);
light1->setDebugDataVisible ( scene::EDS_BBOX );
// add fly circle animator to light 1 scene::ISceneNodeAnimator* anim = smgr->createFlyCircleAnimator (core::vector3df(50,300,0),190.0f, -0.003f); light1->addAnimator(anim); anim->drop();
// attach billboard to the light scene::IBillboardSceneNode* bill = smgr->addBillboardSceneNode(light1, core::dimension2d<f32>(60, 60));
bill->setMaterialFlag(video::EMF_LIGHTING, false); bill->setMaterialFlag(video::EMF_ZWRITE_ENABLE, false); bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR); bill->setMaterialTexture(0, driver->getTexture("../../media/particlegreen.jpg"));
Ora facciamo lo stesso con la seconda luce. La differenza è che stavolta ci attacchiamo anche un sistema particellare. E siccome la luce si muove anche le particelle si muoveranno con essa. Se volete sapere di più sui sistemi particellari date un'occhiata al tutorial specialFx. Avrete notato che non abbiamo aggiunto più di due luci, la ragione è semplice: lo shader di questi materiali è stato scritto con una versione piuttosto bassa la ps1.1 e la vs1.1, che non consentono l'uso di più di due luci alla volta. Potreste comunque aggiungere una terza luce alla scena ma questa non verrebbe utilizzata dallo shader del muro comunque. Naturalmente questo cambierà non appena in futuro le nuove versioni di Irrlicht avranno pixel/vertex shader più moderni e potenti (ad oggi con la versione Irrlicht 1.8.1 sono supportati gli shader HLSL fino alle DX11 tramite un'estensione ad hoc ed la OpenGL GLSL 4.0 mentre gli shader per DX8 sono stati dismessi ndt).
Codice PHP: // add light 2 (red) scene::ISceneNode* light2 = smgr->addLightSceneNode(0, core::vector3df(0,0,0), video::SColorf(1.0f, 0.2f, 0.2f, 0.0f), 800.0f);
// add fly circle animator to light 2 anim = smgr->createFlyCircleAnimator(core::vector3df(0,150,0), 200.0f, 0.001f, core::vector3df(0.2f, 0.9f, 0.f)); light2->addAnimator(anim); anim->drop();
// attach billboard to light bill = smgr->addBillboardSceneNode(light2, core::dimension2d<f32>(120, 120)); bill->setMaterialFlag(video::EMF_LIGHTING, false); bill->setMaterialFlag(video::EMF_ZWRITE_ENABLE, false); bill->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR); bill->setMaterialTexture(0, driver->getTexture("../../media/particlered.bmp"));
// add particle system scene::IParticleSystemSceneNode* ps = smgr->addParticleSystemSceneNode(false, light2);
// create and set emitter scene::IParticleEmitter* em = ps->createBoxEmitter( core::aabbox3d<f32>(-3,0,-3,3,1,3), core::vector3df(0.0f,0.03f,0.0f), 80,100, video::SColor(10,255,255,255), video::SColor(10,255,255,255), 400,1100); em->setMinStartSize(core::dimension2d<f32>(30.0f, 40.0f)); em->setMaxStartSize(core::dimension2d<f32>(30.0f, 40.0f));
ps->setEmitter(em); em->drop();
// create and set affector scene::IParticleAffector* paf = ps->createFadeOutParticleAffector(); ps->addAffector(paf); paf->drop();
// adjust some material settings ps->setMaterialFlag(video::EMF_LIGHTING, false); ps->setMaterialFlag(video::EMF_ZWRITE_ENABLE, false); ps->setMaterialTexture(0, driver->getTexture("../../media/fireball.bmp")); ps->setMaterialType(video::EMT_TRANSPARENT_ADD_COLOR);
MyEventReceiver receiver(room, earth, env, driver); device->setEventReceiver(&receiver);
Finalmente disegniamo e questo è tutto.
Codice PHP: int lastFPS = -1;
while(device->run()) if (device->isWindowActive()) { driver->beginScene(true, true, 0); smgr->drawAll(); env->drawAll(); driver->endScene(); int fps = driver->getFPS();
if (lastFPS != fps) { core::stringw str = L"Per pixel lighting example - Irrlicht Engine ["; str += driver->getName(); str += "] FPS:"; str += fps;
device->setWindowCaption(str.c_str()); lastFPS = fps; } } device->drop(); return 0; }
1 ndt (nota del traduttore)
Versione in pdf scaricabile da QUI
|