OpenGL, Maya och specialeffekter Gustav Taxén, gustavt@nada.kth.se Introduktion OpenGL är ett användbart API för att rendera grafik i realtid (om man har det rätta hårdvarustödet). Du har på tidigare kurser (förhoppningsvis) fått lära dig de grundläggande funktionerna i OpenGL: geometrispecifikation, transformationer och texturering. Målsättningen med den här labben är att du ska få en känsla för hur man kan använda 3D-data från Maya i ett OpenGL-program och få en kort introduktion till ett par vanliga realtids-specialeffekter. Eftersom OpenGL saknar möjlighet att importera 3D-data från program som Maya måste man som applikationsprogrammerare själv ordna med detta. Om man har ett välspecificerat 3D-format brukar dock detta mest handla om handarbete, så i den här labben får du tillgång till ett hjälpbibliotek, Wasa, som ordnar detta (och som dessutom har en massa annan användbar funktionalitet). Labben har 6 deluppgifter. Beräknad tidsåtgång är svår att sia om eftersom labben blir olika tidskrävande beroende på om man t.ex. har använt C++ tidigare. Räkna dock med att den tar minst 10-15 timmar att genomföra. Om du vill köra labben på PC hemma behöver du följande: C++-kompilator med DLL-stöd (t.ex., Visual Studio) GLUT, http://www.opengl.org/developers/documentation/glut DevIL, http://www.imagelib.org Boost, http://www.boost.org Wasa, http://www.nada.kth.se/~gustavt/wasa Räkna inte med att du kan få handledning kring PC-relaterade problem!
Innan du börjar Lägg till relevanta moduler Gör module add gcc module add devtools module add glut i ditt shell-fönster. gcc- och devtools-modulerna ger tillsammans tillgång till en kompilator, g++, som vi kommer att använda. GLUT bör du ha kommit i kontakt med tidigare. Om inte, läs igenom dokumentationen på http://www.opengl.org/developers/documentation/glut Kolla din modell Se till att du har exporterat den Maya-modell du byggde i Maya-labben till RTGformat på föreskrivet sätt. Pröva att den funkar att importera till OpenGL m.h.a. Wasa genom att göra följande. Ställ dig i den katalog där din modellfil finns och skriv /info/agi03/opengl/wasa/tests/modelviewer modelfile där modelfile är din modellfil (ska ha extensionen.rtg). Ser modellen konstig ut eller programmet avslutas med ett felmeddelande har du förmodligen inte exporterat den på ett korrekt sätt. Kontakta Pär (paer@nada.kth.se) eller Gustav (gustavt@nada.kth.se) för att få hjälp eller kopiera modellen brood.rtg som finns i katalogen /info/agi03/opengl/models till din labbkatalog tillsammans med texturen som hör till och använd dem istället. Se till att du har lite koll på C++ När du gör labben kommer du att bli tvungen att använda C++. Om du inte kan C++ finns en bra introduktion för Java-programmerare på http://www.cs.brown.edu/courses/cs123/javatoc.shtml. En annan introduktion finns här: http://www.cplusplus.com/doc/tutorial/ En utmärkt C++-bok för nybörjare är Lippman, S., C++ Primer (3:e uppl.), Addison- Wesley. Det finns också en mycket bra C++-FAQ och C++-tips här: http://www.parashift.com/c++-faq-lite/index.html http://cpptips.hyperformix.com/cpptips.html
Du kommer också att behöva använda några funktioner i standardbiblioteket i C++. Det som tidigare kallades Standard Template Library (STL) är numera integrerat i ANSIs C++-standard, som förutom container-klasser också innehåller klasser för I/O, m.m. Information om standardbiblioteket finns bl.a. här: http://www.research.att.com/~bs/3rd_tour2.pdf http://www.msoe.edu/eecs/cese/resources/stl/ http://www.cs.helsinki.fi/u/vihavain/s01/cpp/iostreams.html Läs speciellt igenom informationen som rör string och vector och enkel I/Ohantering. Vill man veta mer om skillnaden mellan "traditionell" C++ och ANSI-C++ kan man gå hit: http://www.langer.camelot.de/papers/ansic++/ansic++whitepaper.htm Wasa utnyttjar ett hjälpbibliotek som heter Boost för att få en säkrare minneshantering (i Boost ingår också en mängd andra väldigt användbara verktyg). Du bör läsa igenom den Boost-dokumentation som rör delade pekare (shared pointers). Informationen finns på följande plats: Gå till http://www.boost.org/ och följ länkarna Documentation memory smart_ptr Documentation shared_ptr. Delade pekare fungerar i stort sett som vanliga pekare, med skillnaden att Boost håller reda på vilka pekare som är aktiva: då alla aktiva pekare gått ur scope avallokeras minnet automatiskt. Detta påminner mycket om Javas minneshantering: i Java skapar man nya klassinstanser med operatorn new, medan minnet avallokeras av Java automatiskt med hjälp av garbage collection. Se till att du har lite koll på Wasa Läs den bifogade Wasa-dokumentationen! Referensinformation i HTML-format finns i katalogen /info/agi03/opengl/wasa/doc. och i katalogen /info/agi03/opengl/wasa/tests finns ett par exempelprogram som använder Wasa. För den som inte kan C++ ger exemplen ger också en inblick i hur C++-kod kan se ut.
Uppgift 1 Få GLUT och OpenGL att fungera under C++ Den första uppgiften är att initiera och använda GLUT i ett C++-program. Gör så här: Skapa en katalog att ha dina labbfiler i, t.ex. ~/agilabb/. Kopiera upp samtliga filer från /info/agi03/opengl/skal/ till din labbkatalog. Öppna filen agi.cc i emacs. Det är i den här filen du kommer att arbeta. Extensionen.cc indikerar att det program du skriver är i C++. (Om du vill arbeta i en fil med ett annat namn eller använda fler källkodsfiler måste du ändra i den Makefile som finns i /info/agi03/opengl/skal.) Som du ser är är filen agi.cc ganska tom. Börja med att göra ett GLUT-skal som öppnar ett fönster och ritar något med hjälp av OpenGL. Utnyttja gärna att du redan har gjort ett sådant program i DGI/DOA/GRIP-kursen. Kompilera ditt program genom att skriva make. Eventuellt kommer du att få några Wasa-relaterade varningar, men dessa behöver du inte bry dig om. Däremot bör du kontrollera alla varningar (och fixa fel!) som uppstått i din egen källkod. (C++kompilatorer varnar normalt mer än C-kompilatorer och hittar ofta fler fel!). Kontrollera att ditt program fungerar. Om du inte använt C++ tidigare är det här ett utmärkt tillfälle att pröva olika typer av C++-konstruktioner. Pröva t.ex. att skapa en ny klass och skapa en delad pekare till en ny klassinstans. Testa att ändra ett värde på en av medlemmarna i klassen (eller anropa en metod) genom att använda operatorn ->. Använd en utskrift i destruktorn för att kontrollera att klassen avallokeras automatiskt av Boost. (I C++ görs normalt utskrifter genom att man skickar strängar och värden till std::cout med <<-operatorn.) Titta också på exempelkoden i /info/agi03/opengl/wasa/tests eller sök efter C++-exempel på nätet.
Uppgift 2 Importera och rita en Maya-modell Nu ska vi använda Wasa för att importera din Maya-modell. Gör så här: Om du inte redan gjort det, läs Wasa-dokumentationen. Se till att ditt program har using namespace wasa; using namespace boost; efter #include-raderna i ditt program. Om du inte vet vad namespaces är, läs igenom introduktionen på http://www-h.eng.cam.ac.uk/help/tpl/languages/c++/namespaces.html Se till att initwasa() finns före raden glutmainloop(). Det är viktigt att Wasa initieras innan du börjar använda det. Det är också viktigt att Wasa initieras efter att OpenGL har initierats. OpenGL initieras när man skapar ett fönster med glutcreatewindow(). Lägg till en global variabel shared_ptr<modeldata> model; model kommer att vara vår pekare till den data som finns i din 3D-fil. Vi behöver komma åt denna data när vi ritar ut modellen i display()-funktionen. Lägg till raderna VertexSetFormat fmt; fmt.wantnormals(); fmt.wanttexcoords(0, 2); model = parse("mymodel.rtg", fmt, true); direkt efter raden initwasa(). Ersätt mymodel.rtg med namnet på din modellfil. Dessa programrader specificerar att Wasa ska läsa normaler och 2Dtexturkoordinater från din modellfil. Om den sista parametern till parse() är satt till true kommer Wasa att skriva ut information om 3D-filen när den parsas. Den första parametern till wanttexcoords() indikerar att det är texturkoordinater för texturenhet 0 som avses. På moderna grafikkort finns oftast mer än en texturenhet så att flera texturer kan kombineras ihop i ett enda renderingspass i OpenGL. Vi kommer bara att använda den första texturenheten i den här labben (och dessutom stöder NADAs Sun-workstations bara en texturenhet än så länge).
I din display()-funktion, se till att du använder och aktiverar ljussättning. Lägg nu till raderna unsigned int i; unsigned int n = model->getnumberofindexsets(); for (i = 0; i < n; i++) { shared_ptr<indexset> iset = model->getindexset(i); Material m = model->getmaterial(i); m.glmaterial(gl_front_and_back); iset->gldrawelements(); } Dessa rader går igenom varje IndexSet (se Wasa-dokumentationen) som finns i modellfilen, hämtar dess materialparametrar och anropar glmaterial() med motsvarande värden. Metoden gldrawelements() aktiverar en s.k. vertex array i OpenGL och ritar primitiverna. I display()-funktionen, se till att OpenGL-kameran är placerad så att den kan se modellen. Det innebär att du måste ta hänsyn till hur stor du byggde modellen i Maya och var du placerade den i förhållande till världsorigo. Glöm inte heller att du kan behöva ändra på klipp-planens position i gluperspective(). Programmet ska nu rita ut din modell. Ändra nu programmet så att det också aktiverar texturering. Använd Wasas Imageklass för att läsa in texturen. Image-klassen stöder formaten JPG, GIF, BMP, DCX, DDS, WAD, ICO, LIF, MDL, PCD, PCX, PIC, PIX, PNM, PSD, PSP, PXR, RAW, SGI, TGA, WAL, och XPM. Information om hur man får tag i bilddata och skickar den till OpenGL finns i Wasa-dokumentationen och i exemplet /info/agi03/opengl/wasa/tests/modelviewer Om det ser ut som om texturen inte "sitter rätt" beror det förmodligen på att texturbildfilen är felvänd i y-led: de flesta bildformat har origo i övre vänstra hörnet, medan OpenGL förutsätter att origo sitter i nedre vänstra hörnet. Image-klassen ger möjlighet att vända bilder efter det att de laddats från fil. Du kan också vända bilden i ett ritprogram, men tänk på att den då också blir felvänd om du skulle vilja gå tillbaka och använda den i Maya.
Uppgift 3 Arbeta med indexerade primitiver Så här långt har vi ett program som ritar ut modelldata, men vi har inte tittat närmare på hur denna data ser ut. När man arbetar med 3D-grafik måste man ofta arbeta med modelldata som inte riktigt passar det projekt man arbetar med, och då måste man manipulera den på olika sätt, t.ex. reducera antalet polygoner eller transformera hörnen. Om du inte redan gjort det, läs Wasa-dokumentationen som rör klassen VertexSet och klasshierarkin där IndexSet är rotklass. Dessa klasser bygger direkt ovanpå den mekanism som oftast används för att specificera geometri i Direct3D och OpenGL. Mekanismen kallas vertex/index buffers i Direct3D och vertex arrays i OpenGL. Hur vertex arrays fungerar i OpenGL finns att läsa på http://www.cs.rit.edu/~ncs/courses/570/userguide/openglonwin-15.html När du tycker att du har koll på hur mekanismen med indexerade primitiver fungerar, gör följande: Spara en kopia av ditt program. Direkt efter att modelldata har lästs in (d.v.s., efter du anropat funktionen parse()), lägg till programkod som stegar igenom varje hörn i din modell. För varje hörn, gör följande: antag att hörnets position är (x, y, z). Uppdatera hörnets position så att den blir (x', y', z') = f(x, y, z) där du väljer någon intressant funktion f(), t.ex. f(x, y, z) = (xy, y, zy). Detta görs enklast genom att du för varje IndexSet i din modell använder metoden getvertexset() för att få tag i hörnen. Sedan kan du stega igenom hörnen med hjälp av metoden getposition3() och ändra positionen med metoden setposition(). Ta reda på hur många hörn som finns i ditt VertexSet med metoden getnumberofvertices(). NOT: Om du transformerar hörnen kommer normalerna att bli felaktiga, så att ljussättningen kan se konstig ut. Det är överkurs att försöka fixa till det men du får gärna försöka om du vill! Kontrollera att programmet ritar ut modellen precis som förut, fast med modifierade hörn enligt din funktion f(x, y, z). Spara en kopia av denna exekverbara fil och motsvarande källkod för redovisningen och arbeta nu vidare med en ny kopia av din backup. Som du förhoppningsvis vet sedan tidigare, kan man använda sig av s.k. back-face culling för att effektivisera utritning av polygonmodeller. Det innebär att man med hjälp av hörnens ordning tar reda på om polygonen är riktad mot eller från kameran. Aktivera nu back-face culling i ditt program genom att använda kommandot glenable(gl_cull_face). Kompilera om och försök avgöra om programmet tycks gå fortare (du kan använda Wasas WTimer-klass om du vill mäta skillnaden).
En förutsättning för att backface culling ska fungera är att hörnen konsekvent är specificerade i motsols ordning. För att testa denna princip ska du nu ändra ordningen på den första triangeln i din modell så att den hamnar i medsols ordning. Gör så här: RTG-parsen skapar data av typen TriangleList som är en subklass av IndexSet. Du kan förutsätta att alla IndexSet som parse() returnerar är av denna typ (man kan kontrollera genom att anropa metoden getgeomtype()). För att komma åt funktionaliteten i TriangleList via din IndexSet-pekare måste du genomföra en s.k. dynamisk typecast. Detta påminner mycket om hur man i Java använder operatorn instanceof för att avgöra om det är OK att göra en downcast. Lägg till följande rader i ditt program efter parse()-raden: shared_ptr<indexset> iset = model->getindexset(0); shared_ptr<trianglelist> tlist; tlist = shared_dynamic_cast<trianglelist>(iset); Du kan nu komma åt TriangleList-metoderna via pekaren tlist, men kolla först att tlist är skilt från 0 - annars misslyckades din typecast! (NOT: Om du kompilerar programmet med Visual C++ måste du aktivera RTTI RunTime Type Information för att dynamisk casting ska funka.) Motsvarande kod i Java skulle se ut ungefär så här: IndexSet iset = model.getindexset(0); TriangleList tlist; if (iset instanceof TriangleList) { tlist = (TriangleList)iset; } Hämta de tre index som motsvarar triangel 0 med hjälp av metoden gettriangle(). Ändra ordning på dem så att de hamnar i "motsatt riktning" genom att använda metoden settriangle(). Gör motsvarande operation för triangel 1 och 2. Kompilera om och kör programmet. Kontrollera att tre trianglar tycks ha "försvunnit". Ta bort aktiveringen av back-face culling och kontrollera att de syns igen. Spara undan en kopia av din exekverbara fil och källkod för redovisningen och arbeta vidare med en ny kopia av din backup.
Uppgift 4 Alternativ belysningsmodell Resten av labben handlar om hur man kan använda OpenGL kreativt för att åstadkomma ett par olika specialeffekter. Först ska vi använda oss av s.k. multipass rendering för att åstadkomma en mer avancerad belysningsmodell än den som finns i OpenGL. Sedan ska vi skapa illusionen av att modellen speglas i ett golvplan. Till sist ska vi utnyttja s.k. environment mapping för att få modellen att se spegelblank ut. För en ljuskälla L, och för var och en av komponenterna R, G, och B, har OpenGLs belysningsmodell för ett hörn formen (något förenklat) Color = A + D + S A = A L k A D = max(l N, 0) D L k D S = max(h N, 0) shininess S L k S där A L, D L och S L är ljuskällans värden för GL_AMBIENT, GL_DIFFUSE resp. GL_SPECULAR. k A, k D och k S är motsvarande värden för materialet. L är riktningen mot ljuskällan, N är normalen i hörnet och H är summan av riktningsvektorn mot ljuskällan och vektorn som pekar mot kamerapositionen (alla vektorer är normerade). Texturer appliceras på följande sätt: för en primitiv, säg en triangel, beräknas först färgen i varje hörn med hjälp av ljusättningsmodellen. Dessa färger interpoleras sedan linjärt över triangeln. Om man har aktiverat texturering och angett GL_MODULATE till funktionen gltexenv() kommer färgen i varje pixel att multipliceras med texturens färg då interpoleringen görs. Detta gör att också s.k. specular highlights multipliceras med texturens färg. Men ofta vill man att highlighten ska ligga "ovanpå" ytdetaljerna för att den ska synas bättre. I den vänstra bilden nedan används vanlig texturering: highlighten "försvinner" in i texturen. I den högra ligger highlighten "ovanpå" texturen. Vi vill alltså applicera specular-komponenten efter det att modellen texturerats. Ett sätt att göra detta är att rita ut modellen två gånger och summera resultaten. Gör så här:
Aktivera texturering och ambient-kompnenten i materialet. Kolla Wasadokumentationen för Material-klassen för att se hur du får tag i materialdata. Eftersom din modell har textur har Maya-exporten satt diffuse-komponenten till 0, så ändra den till (1, 1, 1, 1) genom att anropa den OpenGLs glmaterialfv()- funktion på vanligt sätt. Rita ut modellen. Efter första utritningen, ändra djuptestet så att det testar GL_EQUAL istället för GL_LESS. Varför det är nödvändigt att göra så? Förklara här: Stäng av texturering ambient+diffuse-komponenterna i materialet. Hämta specularkomponenten från Wasa-materialet och aktivera den. Aktivera färgblandning och välj blandningsfaktorer så att inkommande fragmentfärg adderas med den fragmentfärg som finns i färgbufferten. Rita ut modellen igen. Återställ djuptestet och stäng av färgblandning. Om du ritat en "specular map" när du gjorde Maya-labben, applicera den på andra renderingspasset, d.v.s. modulera specular-komponenten med en annan textur än diffuse-komponenten. Som ett alternativ till en egen specular-textur kan du använda filen /info/agi03/opengl/models/specularmap.jpg. Man kan sammanfatta OpenGLs klassiska belysningsmodell enligt C = T(A + D + S) där C är färg, T är texturfärg, A är ambient ljus, D matt ljus och S är speglande ljus. Beskriv på motsvarande sätt den belysningsmodell du just har implementerat:
Uppgift 5 - Spegelbilder Nu ska du använda OpenGLs s.k. stencilbuffert tillsammans med färgblandning för att åstadkomma en spegelbildseffekt. Bilderna nedan illustrerar hur det kan se ut. Du ska lägga till ett golvplan under din modell och modellen ska speglas i golvplanet. Det ska gå flytta kameran så att man kan se golvet från sidan; detta ska inte förstöra illusionen av spegling (därav behovet av stenciltest). Golvet ska ha en textur. För att åstadkomma spegelbildseffekten behöver du bygga upp bilden i flera lager: 1. Rita in golvpolygonen i stencilbufferten. 2. Rita ut spegelbilden, d.v.s. din modell upp-och-nervänd. 3. Rita ut golvpolygonen med färgblandning. 4. Rita ut modellen rättvänd. Tänk på att hantera djuptest och stenciltest rätt när du ritar ut dina lager, annars kan det hända att vissa lager inte syns. Ta en kopia av din källkod och den exekverbara filen för redovisning innan du fortsätter.
Uppgift 6 Environment mapping Till sist ska du använda s.k. environment mapping för att få din modell att se spegelblank ut. Det finns en sådan environment map i filen /info/agi03/opengl/models/envmap.jpg Låt programmet vara som tidigare, men ändra i din alternativa ljussättningsmodell så att specular-komponenten istället skapas med environment mapping, d.v.s. rendera modellen med enbart diffust ljus och texturering i första passet och addera ett andra pass där du modulerar diffuse-färgen med data från din environment map (och, förstås, aktiverar environment mapping). Om resultatet blir för ljust kan du skala ner bilddata i din environment map med Image-metoden scale(). Mer information om environment mapping i OpenGL finns här: http://www.opengl.org/developers/code/sig99/advanced99/notes/ node176.html#scene:spheremap