Olen tänä kesänä lueskellut OpenGL SuperBibleä ja opiskellut ihan ensimmäisiä perusteita siitä, miten 3D-sovelluksia rakennetaan. Ajattelin tiivistää tähän lyhyesti toimintaperiaatteita alkuun pääsemiseen.
Mac OS X:ssä ohjelmoidessa on helpointa aloittaa käyttämällä NSOpenGLView-näkymää. Kyseinen näkymä lisätään normaaliin tapaan Interface Builderissa omaan sovellukseen. Oletusarvoja on syytä heti muuttaa kahdesta propertystä:
Tämän jälkeen on vielä tarpeen tehdä oma luokka, joka periytyy NSOpenGLView-luokasta. Sen nimi voi olla vaikka MyOpenGLView. Periytyminen tehdään normaaliin tapaan luomalla Xcodeen uusi luokka .h- ja .m-tiedostoineen ja määrittelemällä Interface Builderissa lisätyn NSOpenGLView-näkymän luokaksi MyOpenGLView.
Oman luokan .h-tiedoston pitäisi näyttää suunnilleen tältä. Alkuun on lisätty muutama #import-direktiivi, joilla otetaan käyttöön glu- ja glut-apukirjastot.
#import <Cocoa/Cocoa.h>
#import <OpenGL/OpenGL.h>
#import <OpenGL/glu.h>
#import <GLUT/GLUT.h>
@interface MyOpenGLView : NSOpenGLView {
}
@end
Sitten päästään itse asiaan eli lisäämään 3D-toiminnallisuutta varsinaiseen .m-tiedostoon. Ensimmäiseksi kannattaa lisätä prepareOpenGL-metodi, jota kutsutaan automaattisesti sovelluksen käynnistyessä valmistelemaan ympäristö.
Alustuksessa on kolme tärkeää asiaa:
Kameraa määritellessä on mahdollisuus valita myös se, miten päin koordinaatiston x-, y-, ja z-akselit asettuvat ruudulle. Oletuksena x ja y kattavat monitorin pinnan ja z taas liikkuu monitorin "sisään". Omissa kokeiluissani tuntui luonnollisemmalta, että x ja y muodostavat vaakatasoisen "maanpinnan" ja z taas määrittelee objektien korkeuden pystysuunnassa.
- (void)prepareOpenGL {
CGSize size = self.frame.size;
GLfloat aspectRatio = (GLfloat)size.width / (GLfloat)size.height;
// Pehmentämättömät värit ja syvyystestaus.
glShadeModel(GL_FLAT);
glEnable(GL_DEPTH_TEST);
// Asetetaan viewport koko ikkunan kokoiseksi.
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
// Otetaan käyttöön projektiomatriisi.
glMatrixMode(GL_PROJECTION);
// Alustetaan projektio identiteettimatriisilla.
glLoadIdentity();
// Lisätään perspektiivi, jossa silmän näkökenttä on 60 astetta.
gluPerspective(60.0, aspectRatio,
1.0, 500.0); // Näkyvä alue on silmästä etäisyydellä 1.0-500.0.
// Otetaan käyttöön model-matriisi.
glMatrixMode(GL_MODELVIEW);
// Alustetaan maailma identiteetimatriisilla.
glLoadIdentity();
// Asetetaan kamera koordinaatteihin x=50 y=-200 z=100.
gluLookAt(50.0, -200.0, 100.0,
0.0, 0.0, 0.0, // Kamera katsoo origoon (x=0 y=0 z=0)
0.0, 0.0, 1.0); // Ruudulla "ylös" osoittaa z-akselin suuntaan.
}
Kun maailma on luotu, siihen voidaan alkaa piirtää 3D-objekteja. Tämä tapahtuu drawRect-metodissa, jota Mac OS X kutsuu automaattisesti aina, kun ikkunan sisältö pitää päivittää.
Metodin alussa ollaan siinä OpenGL-matriisitilassa, johon viimeksi jäätiin. Tämän vuoksi alkuperäinen matriisitila on syytä tallentaa heti glPushMatrix()-kutsulla pinoon ja palauttaa se lopuksi sieltä glPopMatrix()-kutsulla. Vaihtoehtoisesti voi tietysti myös alustaa matriisin glLoadIdentity()-kutsulla joka kerta piirrettäessä. Tällöin kamerakin pitää määritellä aina uudelleen gluLookAt()-kutsulla.
Piirtämisen alussa on myös tarpeen tyhjentää ruutu aiemmista objekteista. Tämä tehdään määrittelemällä taustaväri ja kutsumalla sitten glClear()-funktiota. Kutsulla on syytä tyhjentää samalla kertaa sekä piirrospuskuri että syvyystestauspuskuri.
- (void)drawRect:(CGRect)rect {
// Alkuperäinen matriisi (kameran sijainti) talteen.
glPushMatrix();
// Ruudun ja syvyyspuskurin tyhjennys.
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Piiretään objektit ruudulle.
// ...
// Alkuperäisen matriisin palautus lopuksi.
glPopMatrix();
// Päivitetään kaksoispuskuri ruudulle.
[self.openGLContext flushBuffer];
}
Edellinen esimerkki vasta tyhjensi ruudun eikä vielä piirtänyt mitään. Objekti piirretään määrittelämällä ensin mitä piirretään (kolmioita, neliöitä, polygoneja, jne.) ja antamalla sitten joukko koordinaatteja.
Tämä esimerkki piirtää vihreän kuution wireframe-moodissa (GL_LINE). Paksummat viivat (glLineWidth) helpottavat ruutukaappausten ottamista.
glColor3f(0.0f, 1.0f, 0.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glLineWidth(5.0);
glBegin(GL_QUADS);
// Top
glVertex3f(40.0, -40.0, 40.0);
glVertex3f(-40.0, -40.0, 40.0);
glVertex3f(-40.0, 40.0, 40.0);
glVertex3f(40.0, 40.0, 40.0);
// Bottom
glVertex3f(40.0, -40.0, -40.0);
glVertex3f(-40.0, -40.0, -40.0);
glVertex3f(-40.0, 40.0, -40.0);
glVertex3f(40.0, 40.0, -40.0);
// Back
glVertex3f(40.0, 40.0, 40.0);
glVertex3f(-40.0, 40.0, 40.0);
glVertex3f(-40.0, 40.0, -40.0);
glVertex3f(40.0, 40.0, -40.0);
// Front
glVertex3f(40.0, -40.0, 40.0);
glVertex3f(-40.0, -40.0, 40.0);
glVertex3f(-40.0, -40.0, -40.0);
glVertex3f(40.0, -40.0, -40.0);
// Right
glVertex3f(40.0, 40.0, -40.0);
glVertex3f(40.0, 40.0, 40.0);
glVertex3f(40.0, -40.0, 40.0);
glVertex3f(40.0, -40.0, -40.0);
// Left
glVertex3f(-40.0, 40.0, -40.0);
glVertex3f(-40.0, 40.0, 40.0);
glVertex3f(-40.0, -40.0, 40.0);
glVertex3f(-40.0, -40.0, -40.0);
glEnd();
Viimeiseksi esimerkiksi otan vielä aiemmin piirretyn kuution pyörittämisen ruudulla z-akselin (pystysuunnan) ympäri. Pyörittely tehdään glRotatef()-funktiolla, joka muuttaa model-matriisia halutun kulman verran. Oletetaan, että käytössä on globaali muuttuja GLfloat zRot, joka määrittelee rotaation asteina (0-360). Lisätään tämä rivi ennen kuution piirtämistä. Huomaa, että rivin täytyy tulla vasta glPushMatrix()-kutsun jälkeen, jotta matriisi ei sekoa.
glRotatef(zRot, 0.0f, 0.0f, 1.0f);
Sitten luodaan vielä prepareOpenGL()-metodin lopussa ajastin, joka päivittää rotaatiota 1/25 sekunnin välein ja piirtää ruudun uudelleen.
[NSTimer scheduledTimerWithTimeInterval:1.0/25.0
target:self selector:@selector(tick:)
userInfo:nil repeats:YES];
Itse ajastusmetodi:
- (void)tick:(NSTimer *)timer {
if (zRot < 360) zRot += 0.5f; else zRot = 0.0f;
[self setNeedsDisplay:YES];
}