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ä:

  • Depth: 24-bit (tarvitaan syvyystarkistukseen, joka piilottaa objektien näkymättömissä olevat pinnat)
  • Buffer: [x] Double Buffer (normaali kaksoispuskurointi sulavaa animointia varten)

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

Maailman alustaminen

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:

  • Viewport määrittelee näkyvän ikkunan koon. Sen on syytä olla sama kuin ruudulla näkyvän ikkunan.
  • Projektiomatriisi (GL_PROJECTION) määrittelee, millaisella projektiolla 3D-maailma näytetään kaksiuloitteisella monitorilla. Yleisin on perspektiiviprojektio, joka on ihmiselle luonnollisin.
  • Model-matriisi (GL_MODEL) määrittelee miten 3D-objektit sijoittuvat maailmaan. Se alustetaan yleensä kameran sijainnilla, jonka jälkeen objektit sijoitellaan omille paikoilleen.

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 &quot;ylös&quot; osoittaa z-akselin suuntaan.

}

Maailman piirtäminen

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];

}

Kuution piirtäminen ruudulle

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();

Kuution pyörittely

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];
}