OpenGL ES 2.0 kehityksen aloittaminen

OpenGL ES 2.0 on ollut speksinä olemassa vuodesta 2007 ja tarjolla laitteissakin jo tovin. 3D-grafiikkaan nojaavia mobiileja menestystarinoita ei ihan Angry Birds -mittakaavassa ole näkynyt mutta laitteiden alati parantuessa ja käyttökokemusten karttuessa 3D-renderöinti saattaa hyvinkin nousta keskeisempään rooliin mobiilimaailmassakin. Eivätkä ohjelmoitavan OpenGL ES 2.0 shader pipelinen mahdollisuudet grafiikan piirtämiseen lopu!

Tässä artikkelissa käydään läpi muutama asia jotka on tiedostettava – ja ehkä jopa osattava – aloittaessaan OpenGL ES 2.0 kehitystä. Varsinaisena kohdeyleisönä ovat ne joilla on edes jonkinlainen käsitys 3D-grafiikan periaatteista ja ovat tehneet joitain asioita vanhanmallisella fixed pipeline OpenGL:llä (kuten OpenGL ES 1.1 tai desktop maailman OpenGL 1.x) mutta tarjonnee teksti hieman lohtua aivan vasta-alkajallekin. Ensin käydään läpi uudet asiat jotka tulevat vastaan kun siirrytään fixed function renderöinnistä programmable pipelinen maailmaan ja lopuksi rakennetaan yksinkertainen 3D-softa iOS:lle käyttäen Xcoden OpenGL ES wizardia.

Matriisit

3D-grafiikan kulmakivi, kuvausmatriisit, olivat sisäänrakennettuina vanhassa OpenGL:ssä glPushMatrix()/glPopMatrix()/glRotate() jne myötä. Programmable pipelinessä matriisipinoa ei ole tarjolla ja matriisioperaatiot on toteutettava itse. Käytännössä kaikki alustat tarjoavat omat matriisikirjastonsa tähän (QMatrix4x4 Qt:ssä, android.opengl.Matrix Androidille sekä iOS 5.0 alkaen GLKMatrix4 iPhonelle) tarkoitukseen. Jos affiinit kuvaukset eivät ole tuttuja, aika oppia ne on nyt! Kuvausmatriisien hallinta on käytännössä ehdoton vaatimus modernin OpenGL:n taitamiselle.

Shaderit

Matriisien ohella shaderit ovat suurin muutos. Fixed pipeline hoiti käyttäjän puolesta geometrian transformoinnin ja pikselien piirtämisen; programmable pipelinessä kaikki tehdään itse. Tämä mahdollistaa valtavan paljon enemmän kontrollia siihen mitä ollaan tekemässä: vanha OpenGL käytännössä vaan konfiguroitiin (uploadattiin geometria ja tekstuurit, asetettiin valot ja transformaatiot) ja sen jälkeen OpenGL hoiti loput; lopputulokseen ei voinut vaikuttaa kauheasti. Kaikki tämä on nyt toisin ja ainakin ne, joilla on softwarerenderöintitaustaa, iloinnevat tästä! Shaderin kirjoittajalla on täysi valta (ja toisaalta myös vastuu) piirtää geometria juuri sillä tavalla kuin huvittaa.

Shaderit ovat käytännössä pienia C-kieltä muistuttavia OpenGL Shader Language (“GLSL”) ohjelmia joita ajetaan erittäin tehokkaissa shader prosessoreissa näytönohjaimella. GLSL:n eri versiot ovat keskenään hieman erilaisia; versio 1.20 on yhteensopiva OpenGL ES 2.0:n kanssa. OpenGL ES 2.0:ssa näitä shadereita on kahdenlaisia; ns. Vertex Shaderit sekä Fragment Shaderit. Rutkasti tiivistäen shaderit toimivat seuraavasti: vertex shader -ohjelmaa kutsutaan (ajurin/raudan toimesta) kerran per polygonin vertex. Vertex shader hoitaa geometrian transformoinnin ja voi laskea lisää asioita kuten valaistuksia, distance fogia jne. Vertex shaderin laskutoimitusten lopputulokset (outputit) interpoloidaan yli piirrettävänä olevan polygonin ja tarjoillaan inputteina fragment shaderille “varying” muuttujina. Fragment shader laskee näiden avulla lopullisen piirrettävän pikselin (“fragmentin”) väriarvon. Tämä on fragment shaderin ainoa output. Nyrkkisääntönä, tee kaikki laskenta minkä voit vertex shaderissa ja säilytä fragment shader mahdollisimman yksinkertaisena.

Esimerkki Vertex Shaderista:

// "Attribuutteja" jotka lähetetään shaderille OpenGL:stä
// automaattisesti jahka ne on enabloitu
attribute vec4 position; // transformoimaton "object space" koordinaatti
attribute vec3 normal; // transformoimaton "object space" vertex normaali
attribute vec2 tex_coords; // tekstuurikoordinaatit u,v

// "Uniformeja"; OpenGL:stä manuaalisesti lähetettyjä arvoja jotka ovat
// samat koko piirrettävälle geometrialle
uniform mat4 modelViewProjectionMatrix;
uniform mat3 normalMatrix;

// Fragment shaderin outputit
varying lowp float ndotVarying;
varying mediump vec2 texCoordsVarying;

// Vakio
const vec3 lightPosition = vec3(0.0, 0.0, 1.0);

void main()
{
  // Lasketaan valaistusarvo pinnan normaalin ja valon suunnan välisestä
  // kulmasta kosinilla (dot()) joka rajataan väliin 0..1
  vec3 eyeNormal = normalize(normalMatrix * normal);
  float ndot = max(0.0, dot(eyeNormal, normalize(lightPosition)));

  // Valmistellaan outputit; näiden interpoloidut arvot päätyvät
  // fragment shaderille inputeiksi
  ndotVarying = ndot;
  texCoordsVarying = tex_coords;

  // Geometrian transformointi. gl_Position on varattu sana output
  // muuttujalle johon tämä informaatio kirjoitetaan.
  gl_Position = modelViewProjectionMatrix * position;
}

Esimerkki Fragment Shaderista:

uniform lowp sampler2D texture; // OpenGL:stä passattu "handle" tekstuuriin

// Shaderin inputit; nämä laskettiin fragment shaderissa, interpoloitiin
// raudan toimesta ja ovat nyt tarjolla lopullisen pikselin värin laskemiseksi
varying lowp float ndotVarying;
varying mediump vec2 texCoordsVarying;

void main()
{
  // Tämän shaderin tapauksessa lopullinen pikselin väri lasketaan seuraavasti:
  // sampletaan tekstuurista oikealta kohdalta pikseli ja kerrotaan se
  // valoisuusarvolla (0..1). Lopputulos on kauniisti Lambert/flat shadeutuva
  // pinta jossa valo paistaa katselusuunnasta.
  highp vec4 color = texture2D(texture, texCoordsVarying) * ndotVarying;

  // gl_FragColor on (kuten kaikki gl_ prefiksin GLSL nimet) varattu sana
  // muuttujalle johon lopullinen fragment shaderin output kirjoitetaan.
  gl_FragColor = color;
}

Muut asiat

Koska meillä on shaderit ja niiden myötä valta piirtää miten haluamme, monia vanhan OpenGL:n kutsuja ei tarvita enää mihinkaan. Esimerkkeinä vaikkapa glEnableClientState() ja glEnable(GL_TEXTURE_2D). Lisäksi GLUT-kirjastoa ei ole tarjolla suoraan; matriisikirjaston onkin kyettävä tarjoaamaan toiminnallisuus paitsi rotaatioiden/translaatioiden myös perspektiivimatriisien luomiseksi. Ideaalinen tila tietysti on että kehittäjä tietää miten glFrustum() toimii. Shadereihin liittyvät attributet ja uniformit pähkinänkuoressa: attributet ovat mekanismi tuoda geometriaan liittyvä data vertex shaderille. Niitä hallitaan glBindAttribLocation() / glEnableVertexAttribArray() / glVertexAttribPointer() kutsuilla. Uniformit taas ovat kehittäjän shadereilleen syöttämää dataa jotka ovat yhtenäisiä (tästä nimi..) läpi koko kappaleen (tai sen osan, tai usean kappaleen) geometrian, esimerkiksi transformaatiomatriisit, tekstuurit jne. Uniformeja hallitaan glGetUniformLocation() / glUniform*() kutsuilla.

Toteutus: OpenGL ES 2.0 “Hello, World”

Uusi Xcode ja iOS 5.0 ovat tehneet tämän helpoksi; Xcodesta löytyy projektinluontiwizard jolla syntyy toiminnallisuudeltaan täydellinen OpenGL ES 2.0 sovellus shadereineen kaikkineen. Wizardin luoma toteutus piirtää sinisen ja punaisen laatikon jotka pyörivät toistensa sekä omien keskipisteidensä ympäri. Toinen laatikko piirretään GLSL:llä, toinen GLKitin avulla. GLKit on hyvin laaja ja ominaisuusrikas OpenGL-apukirjasto joka
esiteltiin iOS 5.0 päivityksen myötä. Wizardin/GLKitin käytössä on puolensa; toisaalta pääsee nopeasti liikkeelle toimivalla rungolla, mutta toisaalta se myös tarjoaa kehittäjälle mahdollisuuden olla oikeasti oppimatta mitä konepellin alla tapahtuu.

Tässä harjoituksessa luodaan wizardilla oletusprojekti ja muokataan sita siten
että sininen kuutio piirretään tekstuurimapattynä väritetyn sijaan.
Valitaan Xcode:sta File > New > New Project.. ja valitaan GLKit wizard templaatiksi:

Korvataksemme alkuperäisen kovakoodatun värityksen tekstuurilla, on tehtävä
jokunen asia:

  1. Vertex attribuutteihin lisätään tekstuurikoordinaatit; muuttuja gCubeVertexData[] sisältämään taulukkoon lisätään u,v arvot ja muutetaan sitä käyttäviä osia koodissa sopivasti. (Tähän liittyen, omasta mielestäni on paljon elegantimpaa käyttää C:n structia kuin float-arrayta vertex attributejen kuvaamiseen).
  2. Lisätään attribute ATTRIB_TEXCOORD ja uniform UNIFORM_TEXTURE sekä niiden vaatimat muutokset muualle koodiin.
  3. Ladataan tekstuuri kuvatiedostosta ja uploadataan se OpenGL:n puskureihin;kuvan lataaminen tehdaan UIImage:lla ja byte datan ekstraktoimiseen käytetään seuraavaa apukirjastoa: UIImage-Conversion (Huom., GLKitin mukana voi hyvinkin olla jouhevampikin tapa tehdä tämä).
  4. Muutetaan shaderien koodi tukemaan tekstuuria; katso Shader-esimerkit yllä!

Kuvakaappaukset ennen ja jälkeen muutosten:

Wizardin generoima koodi; kuutiot piirretään pelkin värein
Wizardin generoima koodi; kuutiot piirretään pelkin värein
Muokattu koodi; toinen kuutio piirretään teksturoituna
Muokattu koodi; toinen kuutio piirretään teksturoituna

 

 

 

 

 

 

 

 

 

 

Esimerkin lähdekoodi Xcode 4.2 projektina (598kB)

 

Loppusanat

OpenGL -osaaminen on jossain määrin harvinaista mobiilikehittäjien joukossa, luultavasti lähinnä siksi että sen sovellusalueet ovat olleen rajalliset. Asiaa ei auta korkeahko oppimiskynnys. Sen tarjoamat mahdollisuudet ovat kumminkin valtavat; shadereissa voidaan suorittaa mitä tahansa laskentaa rinnakkain CPUn corejen kanssa – mahdollistaen vaikkapa reaaliaikaisen signaalinkäsittelyn sijoittamisen pois itse suorittimelta.

Tämä artikkeli aloittaa 3D grafiikka / OpenGL ES 2.0 -juttusarjan; seuraavalla kerralla katsotaankin sitten jotain mielekkäämpää kuin ihan perusteita! Kysymyksia, palautetta, muuta sanottavaa? Jätä kommentti!

Lisää luettavaa

WikiBooks: Modern OpenGL Introduction

Lighthouse GLSL Tutorials

 

4 thoughts on “OpenGL ES 2.0 kehityksen aloittaminen”

  1. Varsin pätevä artikkeli, go Matti!

    Olisit tietty voinut kirjoittaa suoraan enkuksi, aiheeseen riittäisi varmaan kiinnostusta laajemminkin.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>