Blending
Animasjon
Bezier
Therese Røsholdt / Student 2003
Forklaring av>Blending

Timeglass (Blending)

Hva
ss_running
Et timeglass. Forklaring av blending. Bruk og beregning av Bezier-flater og animasjon

Mitt prosjekt har vært å lage et timeglass med en animasjon som gjør at man kan snu timeglasset og se sanden renne. Jeg utvidet oppgaven raskt til å lage flere typer timeglass. På denne siden har jeg hele tiden brukt den "klassiske" utgaven som eksempel. Kjører du programmet kan du se 2 typer til.

Det var 3 hovedpunkter jeg måtte sette meg inn i for å løse denne oppgaven:

  • Bezierflater, for å kunne modellere glasset og sanden
  • Blending, for å kunne lage glasset gjennomsiktig
  • Animasjon, for å få sanden til å renne fra den ene delen av glasset til den andre

Jeg har derfor delt siden inn i seksjoner for disse temaene, i den rekkefølgen jeg angrep "problemene", men hovedtyngden av prosjektet ligger i blendingen. Koden inneholder mange kommentarer i tillegg til det som står beskrevet på denne siden. Informasjon om strukturen på kommentarene, programmets oppbygning og hva det "gjør og ikke gjør" finner du i avsnittet Programmet og koden.

Som utviklingsverktøy har jeg brukt GL4Java.

Bezierflater

Jeg går ikke inn på å forklare prinsippene bak Bezierkurver og -flater, da det finnes flere moduler som tar for seg dette på en glimrende måte. Nøyer meg med å henvise til modulene Bezier og Figurer og flater, og regner med at du i hvert fall har "skummet" disse før du går videre.

Det jeg vil forklare er hvorda jeg har implementert Bezierflater i mitt prosjekt, og hva slags effekt de forskjellige kallene til OpenGL ga på timeglasset. Dette hjelper deg forhåpentligvis til en øket forståelse av OpenGL's måte å håndtere Bezierflater på. Under seksjonen om Animasjon kommer jeg tilbake til Bezierkurver og beregninger på disse.

I utgangspunktet ville jeg modellere et timeglass med en litt "gammeldags" fasong, med løkformete kurver, og toppen og bunnen rett avskåret, resultatet ble ClassicBulbs.java. (De to andre glassfasongene er bare avarter av det første, kontrollpunktene og noen få andre verdier er justert.)

For å lage bezierflatene til glasset tok jeg utgangspunkt i modulen Påskeegg. Her fant jeg en måte å sette opp kontrollpunktene i matriseform som var oversiktelig og gjorde at det var enklere å se det hele for seg. Istedenfor å sette tallene rett inn i matrisen går man veien om variabler, da blir det ikke fullt så mange verdier å forholde seg til. Jeg gjorde det på samme måten, men forandret verdiene til kontrollpunktene (selvfølgelig) og aksesystemet.

aksesystem

Påskeegget opererte også med en faktor for radiuskompensasjon, dette for å få egget tilnærmet rundt. Glasset hadde enda større krav til å være helt rundt (du finner ut hvorfor hvis du leser om animasjonen), så jeg brukte en faktor til for å "hale" det hele på plass.

Aksesystemet til timeglasset ser du til venstre. Origo ligger midt i timeglasset, de positive aksene er farget; rød X-akse, grønn Y-akse og blå Z-akse. Positiv X-akse peker vannrett til høyre, positiv Y-akse er rett opp og positiv Z-akse er rett mot deg ("ut" av skjermen). I programmets filmeny finner du en mulighet til å "slå av og på" aksesystemet. Der er det også en mulighet til å se linjer mellom kontrollpunktene til en av glassets Bezierflater, (glasset består av 4).

Det aller første som må gjøres for å lage en Bezierflate er altså å sette opp en matrisearray av kontrollpunkter. Matrisene for glasset, og sanden, finner du i subklassen ClassicBulbs.java (og de andre *Bulbs klassene). Koden som følger er metoden drawBulbs() i superklassen Bulbs.java.

bulbsMaterial.enableMaterial(GL_FRONT_AND_BACK);

Aktiviserer det materialet som er valgt for glasset. Materialet ble instansiert samtidig som glasset i metoden init() i HourglassAnimCanvas.java. For mer informasjon om aktivisering av materialer, se seksjonen om Blending.

gl.glMap2f(GL_MAP2_VERTEX_3,
           0.0f, 1.0f, 3, UN, 0.0f, 1.0f, 3*UN, VN, bulbCtrPoints);

Definerer hvordan Bezierflaten skal "tolkes" av OpenGL. Setter verdiene for Bezierflaten, parametrene er som følger:

  1. GL_MAP2_VERTEX_3 - forteller OpenGL at hvert kontrollpunkt oppgitt i matrisen består av X, Y og Z -verdier for punktet. Når OpenGL evaluerer flaten genererer den glVertex3() kommandoer.
  2. 0.0f -Minste verdi av u.
  3. 1.0f - Høyeste verdi av u.
  4. 3 - avstanden i matrisen mellom X-verdien til første og andre punkt i u-retningen.
  5. UN - antall antall kontrollpunkter i u-retningen (variabel instansiert tidligere (UN=5)).
  6. 0.0f - Minste verdi av v.
  7. 1.0f - Høyeste verdi av v.
  8. 3*UN - avstanden i matrisen mellom X-verdien til første og andre punkt i v-retningen.
  9. VN - antall antall kontrollpunkter i v-retningen (variabel instansiert tidligere (VN=5)).
  10. bulbCtroints - selve matrisearrayen med kontrollpunkter (variabel instansiert tidligere).
gl.glEnable(GL_MAP2_VERTEX_3);

Aktiviserer GL_MAP2_VERTEX_3.

gl.glMapGrid2f(40, 0.0f, 1.0f, 30, 0.0f, 1.0f);
ss_mesh1

En Bezierflate består av et rutenett som kan vises fram som punkter, linjer eller fylte polygoner. Man må stille inn antall masker i nettet, både den ene og den andre veien. De 3 første parametrene gjelder u-retningen (X-aksen på glasset). Her har jeg bedt om 40 masker, og at rutenettet i denne retningen skal gå over hele flaten (fra 0 til 1). De 3 siste parametrene er tilsvarende verdier for v-retningen (Y-aksen på glasset).

Dette gir anledning til å lage nett med ulik maskestørrelse for ulike deler av flaten. For enkelhets skyld har jeg brukt et nett på hele flaten, men det hadde vært en ide å lage nettet grovere mot innsnevringen av glasset. Jo flere masker det er i rutenettet, jo lenger tid trenger OpenGL på å beregne og tegne flaten. Jeg har derfor laget rutenettet så grovt som mulig uten at det gikk ut over utseendet på flaten. Her ser du en av timeglassets flater (øverst foran) tegnet med linjer.

gl.glFrontFace(GL_CW);

Hvilken side av flaten som oppfattes som forside av OpenGL kommer an på oppstillingen av matrisene. Mine matriser er satt opp i rekkefølge "med klokka", OpenGL har som defaultinnstilling at de er satt opp "mot klokka" (GL_CCW). Derfor må jeg gjøre om på denne oppfattelsen ved å fortelle OpenGL at forsiden skal være GL_CW ("clockwise").

gl.glPushMatrix();
gl.glEvalMesh2(GL_FILL, 0, 40, 0, 30); // Øverst foran
gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
gl.glEvalMesh2(GL_FILL, 0, 40, 0, 30); // Øverst bak
gl.glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
gl.glEvalMesh2(GL_FILL, 0, 40, 0, 30); // Nederst bak
gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f);
gl.glEvalMesh2(GL_FILL, 0, 40, 0, 30); // Nederst foran
gl.glPopMatrix();

Her tegnes de 4 flatene glasset består av. Metoden glEvalMesh2 "samarbeider" med metoden mapGrid2f, og parametrene er derfor tildels de samme. Først spesifiseres om nettet skal tegnes ut som punkter (GL_POINT), linjer (GL_LINE) eller fylte polygoner (GL_FILL). Så må nettet legges på den allerede spesifiserte flaten. Parametrene er som følger:

  1. GL_FILL - velger å ha fylte polygoner i nettet.
  2. 0 - tilsvarer parameter 2 i glMapGrid2 (startpunktet til u-retningen)
  3. 40 - tilsvarer parameter 1 glMapGrid2 (antall masker i u-retningen)
  4. 0 - tilsvarer parameter 4 i glMapGrid2 (startpunktet til v-retningen)
  5. 30 - tilsvarer parameter 1 glMapGrid2 (antall masker i v-retningen)
gl.glFrontFace(GL_CCW);

Setter hva som skal oppfattes som forside tilbake til defaultsettingen.

bulbsMaterial.disableMaterial();

Det brukte materialet pasifiseres.

På bildene under har jeg satt på linjer mellom kontrollpunktene til flaten. I programmet kan du se dem også for de andre glasstypene. Også dette hjalp min forståelse for Bezierflater betraktelig. Koden for å tegne disse linjene tok jeg fra Børres modul Trampoline, gjorde den bare om for å passe mine matriser. Metoden heter drawBulbCtrPointLines() og du finner den i Bulbs.java. Den kan være kjekk å "putte på" en flate du har, jeg har gjort den litt mer dynamisk enn opprinnelig, så bare du ordner med variabelnavnene UN, VN og bulbCtrPoints, skulle den kunne puttes på "hvor som helst".

ctrpoints3 ctrpoints2

Bezierflatene til glasset var for så vidt ikke noe problem å komme fram til, det var ikke mye av matematikken jeg måtte sette meg inn i for å få til dette. Vanskeligere ble det da jeg skulle bruke Bezierflater til å modellere sanden, og jeg trengte å regne på en av kurvene. Mer om dette i seksjonen om Animasjon.

Glass er sjelden papirtynt, det har en forside og en bakside, og dermed en tykkelse. Jeg forsøkte å gi glasset en tykkelse ved å lage en Bezierflate til, og trekke denne litt innenfor den første. I teorien fungerte det bra, men i praksis fikk jeg store problemer med blendingen for å få det til å se "naturlig" ut. Forsøkte å ordne det hele med cullfacing, men for å få til dette trengs nok en enda dypere forståelse av både cullfacing og blending enn det jeg har klart å tilegne meg i løpet av prosjektet. Slik timeglasset er blitt simuleres glasstykkelse ved at sanden er trukket litt inn fra glasset. Hvis du i programmet ber om glassbunn som støtte er tykkelsen på denne satt lik "tykkelsen" på glasset (avstanden mellom glasset og sanden).

Blending

[Hva er blending?] [Alphaverdier] [Blendingfunksjonen og depthbufferen]
[Måter å blende på] [Min blendingkode]

Hva er blending?

Blending benyttes når man ønsker å modellere et objekt som er helt eller delvis gjennomsiktig. Med andre ord til å modellere objekter av materialer som glass og plastikk, men også til å lage skygger og andre effekter. Det er flere måter å bruke blending på, og det er fullt mulig å komme fram til samme resultat på flere ulike måter.

I denne seksjonen vil jeg forklare noen måter å bruke blending på, fortelle litt om mine erfaringer underveis, og om hvordan jeg har gjort det i mitt prosjekt. Jeg forutsetter at du på forhånd har tilegnet deg litt kunnskap om lys og materialer, for eksempel ved å lese gjennom første halvdel av kap 5, Lighting Red Book Det skader heller ikke om du har litt greie på hvordan depthbufferen virker.

For å illustrere resultatene vil jeg bruke bilder av glasset. For å tydeliggjøre bildene har jeg "sotet" glasset ganske kraftig og tegnet inn noen "solide" kuler, en gullkule i øverste del av beholderen, en sølvkule i nederste og en messingkule midt bak beholderen. Til høyre kan du se bilder av hva jeg ønsket å oppnå (sluttresultatet). Alle bildene kan klikkes på, da får du opp en større "pop-up"-versjon av bildet og kan dermed sammenligne dem bedre.

I korte trekk er det to ting du må "ordne med" for å få et objekt gjennomsiktig, du må angi materialets alphaverdi, og du må aktivisere blendingfunksjonen, (med glEnable(GL_BLEND)), og stille inn denne.

blending1
blending1.jpg
blending1
blending2.jpg

Alphaverdier

Alphaverdi gis som argument sammen med fargene rødt, grønt og blått, (Alphaverdien er altså A'en i RGBA), til OpenGL rutiner som glClearColor(), glColor*(), glLight*() og glMaterial*().
Alphaverdien kan variere mellom 0 og 1, hvor 0 er full gjennomsiktighet (objektet vil ikke synes i det hele tatt), og 1 er absolutt "solid".

Blendingfunksjonen og depthbufferen

Det er ikke mulig å få et objekt gjennomsiktig uten å aktivisere GL_BLEND, dette gjøres med å kalle glEnable(GL_BLEND). Som regel er det også viktig å pasifisere blendingen når du er ferdig med å tegne dem. Funksjonen pasifiseres med å kalle glDisable(GL_BLEND).

I tillegg til å aktivisere/pasifisere må du stille inn blendingen, det vil si å gi beskjed til depthbufferen hvordan fargepixlen som allerede er i depthbufferen skal blandes med fargen på den innkommende pixelen. Til dette benyttes funksjonen glBlendFunc() som tar to konstanter som parametere. Den første konstanten angir hvilke verdier som skal være med fra de nye objektene du tegner (source) , og den andre konstanten angir hvilke verdier som skal være med av de allerede tegnede objektene som ligger i depthbufferen (destination). glBlendFunc() kombinerer så resultatet av disse to. (Defaultkombinasjonen er at de adderes, for å kombinere dem med andre regnearter må subset av OpenGL være implementert). For at det skal være mulig å benytte depthbufferen må den være aktivisert, dette gjøres ved å kalle glEnable(GL_DEPTH_TEST), som regel i init(). Det finnes mange konstanter tilgjengelig som mulige parametere til glBlendFunc(), og kombinasjoner av disse og ulike innstillinger på dybdetestingen gir mange muligheter for "fikse" blendingen.

For å få en liten forståelse av dette: hvis du først kaller glEnable(GL_BLEND) og så kaller glBlendFunc(GL_ONE, GL_ZERO) får det overhodet ingen effekt. Dette betyr "ta alle verdiene til den nye pixelen og addér dem med ingenting av verdiene til den gamle pixelen" og er altså defaultsettingen til OpenGL. (Dette foutsetter at defaultsettingen til dybdetestingen er beholdt (glDepthTest(GL_LESS)). Alle nye objekter blir tegnet uten at noen av de som "ligger bak" synes igjennom.

I mitt prosjekt benytter jeg meg av innstillingen glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), som er en av de vanligst benyttede innstillingene ved blending.

Måter å blende på

Som sagt er det flere måter å bruke blending på. For å finne en blendingsmåte man kan benytte må man stille seg flere spørsmål:

  • Skal scenen være stasjonær eller sees i ulike vinkler?
  • Skal alle eller bare noen objekter i scenen være gjennomsiktige?
  • Vil flere gjennomsiktige objekter overlappe hverandre?

Uansett hva man skal tegne må altså dybdetesting være aktivisert med kallet glEnable(GL_DEPTH_TEST). Dette er gjort i alle eksemplene som følger.

Skal scenen være stasjonær, med bare gjennomsiktige objekter, kan man bare sette i gang ved å kalle glEnable(GL_BLEND) og stille inn blendingsfunksjonen med glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), og så tegne de objektene man ønsker gjennomsiktige med ønsket alphaverdi. Det er viktig at man da tegner det objektet som er bakerst i scenen først, og det som er nærmest sist, ellers vil ikke blendingen bli korrekt.

Skal scenen bestå av både gjennomsiktige og solide objekter, må de solide objektene tegnes før blendingsfunksjonen aktiviseres. Tegnes de etterpå vil de faktisk ikke synes gjennom de gjennomsiktige objektene, se bilde!

blending3
blending3.jpg

Denne måten å gjøre det på var utgangspunktet for glasset, hentet fra modulen Flaske. Det var lett nok å sørge for at de solide objektene ble tegnet først, men jeg fikk snart et annet problem. Glasset består av 4 Bezier-flater som er satt sammen, og selv om jeg kunne tegne dem "bakenifra og forover" i scenen, hjalp ikke det når jeg ikke ville ha scenen stasjonær. Når jeg roterte glasset virket den bakre delen for mørk. Det så ut som beholderen hadde "støpekanter" hvis jeg roterte den litt og det dukket opp noen uforklarlige "sebrastriper". Så jeg beholderen fra baksiden kunne jeg faktisk ikke se forsiden gjennom baksiden! Det foregående bilde gir et lite inntrykk av problemet, men jeg tar med et til. Denne "feilen" preger Farrisflasken også hvis du studerer den nærmere.

blending4 blending4.jpg

Problemet beskrevet ovenfor kommer altså av at scenen ikke skal være stasjonær, jeg vil at brukeren av applikasjonen skal kunne vri og vende på timeglasset så mye han eller hun vil. Jeg lette lenge etter løsningen, men den var besnærende enkel når jeg først oppdaget den.
Som jeg før har nevnt må man aktivisere dybdetesting, og det er i dybdetestingen løsningen ligger. Etter at man har aktivisert blendingsfunksjonen, må depthbufferen settes i "read only"-modus. Dette gjøres ved å kalle glDepthMask(GL_FALSE). Når man bruker blending på denne måten er det fremdeles viktig at de solide objektene i scenen blir tegnet før de gjennomsiktige, ellers vil de solide objektene se ut som de er foran de gjennomsiktige selv om dette ikke er tilfelle (sammenlign bildet til høyre med bildet av sluttresultatet og du vil se at på bildet til høyre virker det som om kulen som er bak beholderen nå ligger foran).

blending5 blending5.jpg

I hvilken rekkefølge de gjennomsiktige objektene nå tegnes spiller ingen rolle. Det er heller ikke så viktig å huske å pasifisere blendingsfunksjonen etter at man er ferdig med å tegne de gjennomsiktige objektene når man bruker glDepthMask(), men det blir til gjengjeld viktig å sette depthbufferen tilbake til normal modus. Dette gjøres ved å kalle glDepthMask(GL_TRUE).
Hvis man glemmer å sette depthbufferen tilbake til normal modus, eller glemmer å pasifisere blendingsfunksjonen når man ikke benytter glDepthMask(), får man et resultat som ligner noe av det du ser på bildet til høyre.

blending6 blending6.jpg

På min leting etter løsninger for ikke-stasjonære scener var jeg "innom" Nehe Productions tutorial. Skal alle objektene i scenen være gjennomsiktige, og ha samme grad av gjennomsiktighet, kan man benytte seg av den fremgangsmåten som er beskrevet i lesson 08, Blending, i Nehe's tutorial. I dette eksempelet kan man slå av og på gjenomsiktighet i hele scenen. Dette gjør de ved først å spesifisere en blendingsfarge (i dette tilfellet helhvit og halvt gjennomsiktig) og å innstille blendingsfunksjonen med glBlendFunc(GL_SRC_ALPHA, GL_ONE) i init(). Når brukeren ber om gjennomsiktighet aktiviseres GL_BLEND og depthbufferen pasifiseres (slås helt av med glDisable(GL_DEPTH_TEST)), og omvendt når brukeren ber om "soliditet".

Dette er, som de skriver i Nehe's tutorial, en slags "juksemåte" å ordne det hele på, skal man ha det litt mer "sofistikert" nytter det ikke å bare pasifisere depthbufferen. Prøvde jeg denne metoden på glasset fikk jeg problemer med kulene på innsiden og utsiden av beholderen. Ble kulene tegnet før beholderen så det ut som den bakre kulen lå foran beholderen, og omvendt hvis kulene ble tegnet etterpå. Jeg prøvde også å skifte ut glBlendFunc(GL_SRC_ALPHA, GL_ONE) med glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) (min "vanlige" innstilling), men dette førte til at alle objektene i scenen ble gjennomsiktige, se bilde.

blending7
blending7.jpg

Min blendingkode

Superklassen Material.java inneholder kun 2 metoder. enableMaterial() er en "tom" metode som alle subklassene må implementere, her aktiviseres da materialets farge og andre egenskaper. enableMaterial() tar et parameter, en konstant som angir hvilken side av Bezierflaten materialet skal være på. disableMaterial() er en defaultmetode som bare stiller glLightModeli() tilbake til opprinnelig verdi. Denne metoden overstyres i subklassene *GlassMaterial.java. (Den eneste forskjellen mellom subklassene er fargen og alphaverdien på materialet).

Metodene blir kalt fra via en "materialfabrikk" i metoden drawBulbs() som er beskrevet i seksjonen om Bezierflater. Her følger implementasjonen av enableMaterial() i BlueGlassMaterial.java.

gl.glEnable(GL_BLEND);
        

Aktiviserer muligheten for å blande farger, slik at det kan vises gjennomsiktig

gl.glDepthMask(GL_FALSE);

Setter depthbufferen i read only modus (slår den IKKE av!).

gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Stiller inn blendingsfunkjonen.

if (sideChoice == GL_FRONT_AND_BACK)
{
   gl.glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,1);
}

Setter lyssettingen. Det første parameteret er en konstant som angir hvilken lysmodell det skal gjøres noe med. Det andre parameteret står for "true" (defaultsetting er 0, "false"). Dette betyr at hvis det er materiale på begge sider av flaten skal begge sider reagere på lyset. Glassmaterialene er det eneste matrialene som blir kalt med sideChoice = GL_FRONT_AND_BACK. Dette er viktig for glasset som består av objekter hvor materialet på "innsiden" synes. Settes ikke denne vil det ikke være mulig å se noen glans fra glassets innside.

float amb[] = {0.20f, 0.50f, 1.0f, 0.1f};
float diff[] = {0.20f, 0.50f, 1.0f, 0.1f};
float spec[] = {1.0f, 1.0f, 1.0f, 1.0f};
float shine = 0.8f; // Mye glans i glasset

gl.glMaterialfv(sideChoice, GL_AMBIENT, amb);
gl.glMaterialfv(sideChoice, GL_DIFFUSE, diff);
gl.glMaterialfv(sideChoice, GL_SPECULAR, spec);
gl.glMaterialf(sideChoice, GL_SHININESS, shine * 128.0f);

Setter farger på "vanlig" måte, men legg merke til den siste verdien i de to første arrayene, dette er alphaverdien. Glasset er satt til å være 90 % gjennomsiktig, og glansen (shine) er satt høy, da jeg nettopp har pusset glassflatene ;-).

Mer kode skal det altså ikke til for å få det fint gjennomsiktig.
Men, som før nevnt, det er viktig å "rydde opp" etterpå. Her følger implementasjonen av disableMaterial(), den er lik for alle glassmaterialene.

gl.glDisable(GL_BLEND);

Pasifiserer muligheten for å blande farger.

gl.glDepthMask(GL_TRUE);

Setter depthbufferen tilbake til read & write modus.

gl.glBlendFunc(GL_ONE, GL_ZERO);

Setter blendingsfunksjonen tilbake til defaultsettingen.

gl.glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 0);

Setter lysmodellen tilbake til defaultsettingen.

Totalt ble det ikke mange linjene kode, men oppsummert vil jeg si om blending som man kan si om sjakk; "A minute to learn, a lifetime to master" ;-D

Animasjon

Problemstilling:
Sanden skal renne så det blir en voksende haug i den nederste delen. Når haugens bunn når glassets vegger skal sanden følge veggen + ha topp. I den øverste delen må sanden følge glassets vegger og minke i takt med med at den øker i den nederste delen. I tillegg må det våre en stråle av sand fra den ene delen til den andre mens sanden renner.

Løsning:
Som nevnt i seksjonen om Bezierflater brukte jeg glassets Bezierflater for å lage den delen av sanden som følger glasset vegger, men trakk dem et "hakk" inn for å imitere tykkelse på glasset. Jeg tar ikke med kode her for hvordan dette er endret, eller for hvordan jeg lager sandens materiale, da dette er forsvinnende likt hva jeg før har beskrevet angående glasset i de tidligere seksjonene. Jeg fant raskt ut at sanden i de to halvdelene av glasset burde behandles i hver sin metode, da sanden skal ha veldig ulik fasong i øverste og nederste del. Dette førte til metodene drawUpperSand() og drawLowerSand(). I tillegg til disse er det en 3. metode som kan tegne sand, drawSandWhileTurning(), som er den metoden som tar seg av å tegne en kule i glasset under selve snuoperasjonen (kommenterer ikke denne ytterligere). Alle metodene for å tegne sand ligger i klassen Bulbs.java. Metodene blir kalt fra metoden drawHourglass() i HourglassAnimCanvas.java.
I canvasets runAnimation()-metode styres hele animasjonen ved at parametrene til drawUpperSand() og drawLowerSand() blir endret, jeg kommer tilbake til dette.
Felles for drawUpperSand() og drawLowerSand() er at de benytter klippeplan for å kontrollere sandnivået, "kutte vekk" den delen av Bezierflatene (kaller dette heretter Beziersanden) som ikke skal synes. De kaller også begge opp metoden getPointOnCurve() som brukes til å plassere klippeplanene, samt regne ut radier for sanden ved klippeplanet.
NB: Alle variabel- og metodenavn som inneholder ordene "upper" eller "lower" har med sanden å gjøre, og indikerer altså om det dreier seg om sanden i øverste eller nederste del. Heretter vil jeg beskrive løsningen for den nederste delen, drawUpperSand() er egentlig bare en forenklet utgave av drawLowerSand().

Sanden tegnes før den "møter veggen" bare som en kon, referer til denne som lowerTinyHeap. Når lowerTinyHeap har vokst seg ut til Beziersanden, slutter lowerTinyHeap å eksistere. I stedet tegnes Beziersanden som klippes i riktig høyde og får "tilsatt" en bunnplate og en topp (også en kon, men ikke lowerTinyHeap).
Radien til Beziersanden ved klippeplanet og radien til konen på toppen må være like slik at disse delene kan "gli i ett" (derfor var det så viktig at glasset ble helt rundt). Jeg trengte derfor en metode som som kunne gi meg radien (X-verdien) til et gitt punkt på Bezierkurven. Jeg ville også trenge Y-verdien til punktet for å kunne plassere klippeplanet på riktig sted. Kurvens X- og Y-kontrollpunkter, og en verdi for hvor på kurven radiusverdien skulle beregnes måtte derfor være input til metoden. Fra modulen Bezier (og Hills bok) hadde jeg en generell beskrivelse av en Bezierkurve:

binom2ny

B er her X-, Y- og Z-verdiene, n er antall kontrollpunkter på kurven og P er kontrollpunktene. t er lengden av kurven i punktet B. Ved hjelp av denne skulle det være mulig å regne ut verdiene til det ønskede punktet. I håp om å slipp å "finne opp hjulet på nytt" søkte jeg på nettet og fant en slik funksjon skrevet i Delphi med OpenGL på Sulaco, i programmet BezierCreator.
Denne metoden tok en 3.grads Bezierkurve (4 kontrollpunkter), i mitt tilfelle var Bezierkurven av 4.grad (5 kontrollpunkter). Ved å bruke Pascals trekant var det enkelt å "utvide" metoden til å ta en 4.grads kurve som input, koeffisientene i formelen ble da 1,4,6,4,1.

Metoden getPointOnCurve() tar imot 5 arrayer, hver bestående av X-, Y- og Z-verdiene til et kontrollpunkt på en Bezierkurve, og en verdi for t-steget. Her er t en verdi mellom 0 og 1 som sier hvor på kurvens lengde punktet skal beregnes, når lengden av kurven er 1. Tar med hele denne metoden her.

public float[] getPointOnCurve(float[] p0, float[] p1, float[] p2,
                               float[] p3, float[] p4, float t)
{
   // Array som skal inneholde X, Y og Z verdiene til
   // det ønskede punktet
   float curvePoint[] = new float [3];

   // Lager noen variabler for å forkorte uttrykk.
   // Dette hjelper for oversikten
   // (og sparer mye utregning senere)
   float omT = 1.0f - t; // (1 - t)
   float omTi2 = omT * omT; // (1 - t)^2
   float omTi3 = omT * omT * omT; // (1 - t)^3
   float omTi4 = omT * omT * omT * omT; // (1 - t)^4
   float ti2 = t * t; // t^2
   float ti3 = t * t * t; // t^3
   float ti4 = t * t * t * t; // t^4

   // Formelen for å finne et punkt på en 4.grads kurve
   /*curvePoint	= p0 * omTi4
		+ p1 * 4 * omTi3 * t
		+ p2 * 6 * omTi2 * ti2
		+ p3 * 4 * omT * ti3
		+ p4 * ti4;*/

   // X-verdiene
   curvePoint[0] = p0[0] * omTi4
		+ p1[0] * 4 * omTi3 * t
		+ p2[0] * 6 * omTi2 * ti2
		+ p3[0] * 4 * omT * ti3
		+ p4[0] * ti4;
   // Y-verdiene
   curvePoint[1] = p0[1] * omTi4
		+ p1[1] * 4 * omTi3 * t
		+ p2[1] * 6 * omTi2 * ti2
		+ p3[1] * 4 * omT * ti3
		+ p4[1] * ti4;
   // Z-verdiene
   curvePoint[2] = p0[2] * omTi4
		+ p1[2] * 4 * omTi3 * t
		+ p2[2] * 6 * omTi2 * ti2
		+ p3[2] * 4 * omT * ti3
		+ p4[2] * ti4;

   // Returnerer arrayen med X, Y og Z verdiene
   // til det ønskede punktet
   return curvePoint;
}

Hadde kurvefunksjonen vært lineær ville t=0.5 vært punktet midt på kurven (alle "t-steg" har lik lengde).
Bezierfunksjonen er ikke-lineær og lenden på "t-stegene" kommer derfor helt an på kurvens kontrollpunkter. "t-stegene" blir av varierende lengde.

Da var det på tide å ta en titt på drawLowerSand(): Metoden tar 4 parmetere:

  1. boolean needsBottomLowerSand - bestemmer om sanden skal tegnes som Beziersand med topp, eller som tinyLowerHeap.
  2. boolean trickle - bestemmer om sandstrålen skal tegnes eller ikke
  3. float lowerT - t-verdien til Beziersanden.
  4. float lowerTinyHeapRadius - radien til tinyLowerHeap.

Her følger "innmaten" i metoden (pseudokode begynner og slutter med ...):

// Henter X og Y -verdiene til lowerT, sender kurven inn
// den "gale" veien for å få lowerT = 1 til å være fullt nedre glass
float lowerTPoint[] = getPointOnCurve( ...kontrollpunktarrayer..., lowerT);

// Y-verdien til lowerT blir brukt til å sette hvor på Y-aksen
// klippeplanet skal være, (er også "grunnplan" for konen på Beziersanden)
float lowerCutoff = - lowerTPoint[1];
System.out.println("  lowerCutoff er: " + lowerCutoff);

// X-verdien er radien til Beziersanden ved klippeplanet
//(er også radien til konen på Beziersanden)
float lowerTPointXValue = lowerTPoint[0];

// Radius for bunnplaten av sandhaugen
float lowerSandBottomDiskRadius;

// Hvis sanden skal gå oppover "veggene" brukes bezierflatene til glasset
// som utganspunkt, så klippes bezierflatene med klippeplanet
if (needsBottomLowerSand)
{
   // Definerer og aktiviserer det nederste klippeplanet, skal vise
   // fram det som er på den negative siden av Y aksen, fra lowerCutoff
   double lowerCutplane[] = {0.0, -1.0, 0.0, (double)lowerCutoff};
   gl.glClipPlane(GL_CLIP_PLANE0, lowerCutplane);
   gl.glEnable(GL_CLIP_PLANE0);

   ...
   Lager og tegner Beziersanden
   ...

   // Pasifiserer brukt klippeplan
   gl.glDisable(GL_CLIP_PLANE0);

   ...
   Tegner toppen av haugen (Beziersanden) (sanden har "møtt veggen")
   ...

   // Setter radien til "bunnplaten" av sandhaugen
   lowerSandBottomDiskRadius = getEndSandRadius();
}
// Hvis sanden ikke har møtt veggen enda
else
{
   // Setter radien til "bunnplaten" av sandhaugen
   lowerSandBottomDiskRadius = lowerTinyHeapRadius;
   ...
   Tegner lowerTinyHeap
   ...
}

// Sanden må få en bunnflate (lowerSandBottomDisk)
// fordi bunnen av glasset kan være laget av glass
...
Tegner bunnplaten
...

// Tegner "stråle" bare når det behøves
if(trickle)
{
	...
	Tegn stråle
	...
}

drawUpperSand() er altså i grunnformen lik drawUpperSand(), men tar bare et parameter, upperT.
Når lowerT blir større stiger sanden i den nederste delen, når upperT blir mindre synker sanden i den øverste delen.

Metoden drawHourglass() skulle være veldig grei å forstå, værsågod:

// ALT SOM SKAL SEES GJENNOM GLASSET MÅ TEGNES FØRST
// Tegner aksene hvis bruker har bedt om det
if (axesClicked)
{
   drawAxes();
}

// Tegner linjer mellom kontrollpunktene hvis bruker har bedt om det
if (ctrPointsClicked)
{
   bulbs.drawBulbCtrPointLines();
}

// Tegner støtten
stand.drawStand();

// Sanden kan bare tegnes hvis timeglassfasongen er klassisk
if(bulbs.toString().equals("Classic"))
{
   // Tegner sanden hvis brukeren vil (og det er "lovlig")
   if (wantSand)
   {
      // Aktiviserer sandens materiale
      // herfra for å slippe mange kall i Bulbs
      bulbs.activateSandMaterial();

      // Hvis glasset ikke er i snufasen skal sanden tegnes ordentlig
      if (!isTurning)
      {
         // Glasset trenger sand i nederste del
         bulbs.drawLowerSand(needsBottomLowerSand,
             trickle, lowerT, lowerTinyHeapRadius);

         // Hvis glasset trenger sand i øverste del
         if (needsUpperSand)
         {
            bulbs.drawUpperSand(upperT);
         }
      }
      // Hvis timeglasset er i snufasen "simuleres" sanden
      else
      {
	     bulbs.drawSandWhileTurning();
      }

      // Pasifiserer sanden materiale
      // herfra for å slippe mange kall i Bulbs
      bulbs.deactivateSandMaterial();
   }
}
// Tegner glasset
bulbs.drawBulbs();

Verktøyet jeg bruker til animasjonen er et GLAnimCanvas, som er en basisklasse for animasjoner i GL4Java. Det dette gir i forhold til et vanlig GLCanvas er en full implementering av JAVA2's multi-threading muligheter.
Om animasjonen skal kjøre bestemmes i display() av den boolske variabelen turnButtonClicked som i utganspunktet er false. Forandres den til true settes det i gang en teller (frameCounter), og så kalles runAnimation(), som tar seg av all inkrementeringen av variabler mellom vært "bilde" i animasjonen. I runAnimation() startes animasjonen når frameCounter'en er 1, med kommandoen start(). Når animasjonen er ferdig (frameCounteren har nådd det antall bilder som skal vises) blir den stoppet, med kommandoen stop(), tellere blir "nullet ut" og turnButtonClicked satt til false.
Ved å bruke GLAnimCanvas slipper jeg å kalle display() "manuelt", start() og stop() tar seg av det, display() blir dermed kalt automatisk. Jeg bruker likevel display() hvis det er noe som skal forandres på grunn av brukerens menyvalg. drawHourglass() blir kalt fra showScene(), som igjen blir kalt fra display().
En annen fordel med GLAnimCanvas er at brukeren kan snu på glasset mens animasjonen kjører, og også bruke menyene. Unntaket er å skifte fasong på glasset, jeg har valgt at dette ikke skal være lovelig under animasjonen.

Jeg tar ikke med koden til runAnimation() her, den er lang fordi det er mange variabler å forholde seg til, og ikke minst fordi det ble en del "last minute"-justeringer. Men den skulle være godt nok kommentert i koden til at det går an å tråkle seg gjennom hvis man vil. Jeg er oppmerksom på at løsningen her kunne vært atskillig mer elegant, men de siste bitene i "animasjonspuslespillet" falt ikke på plass før i løpet av prosjektets siste timer; det ble simpelthen ikke tid til å stramme det opp. Jeg tok sikte på å få til animasjonen for den klassiske glassfasongen. Denne passet ikke så alt for dårlig til den moderne fasongen heller, så disse to fasongene kan sees med sand. Den bisarre typen rakk jeg ikke å få sett på, så denne kan beundres, men dessverre uten sand. I HourglassAnimCanvas.java finner du også mye annen OpenGL-kode som jeg ikke har tatt opp her, også den skulle være godt kommentert.Jeg har ikke tatt med noen bilder som illustrerer animasjonen, kjør heller programmet :-).

Programmet og koden

Jeg bestemte meg tidlig i prosjektet for å benytte meg av Javas objektorientering. Det vil si at jeg har delt opp programmet i en rekke klasser, for å holde de ulike tingene "fra hverandre". La deg ikke "skremme" av antallet klasser, det er bare et fåtall av dem du behøver å se på for å få med deg hva som skjer når det gjelder OpenGL. Jeg har benyttet flere av Javas sterke sider, som arv og polymorfisme. Når det gjelder å instansiere ulike materialer (farger) har jeg benyttet et Design Pattern kalt Factory Method (klassen MaterialFactory). Dette er et Design Pattern som er flittig brukt i Java forøvrig og gjør det veldig enkelt å lage nye instanser av subklasser. Er du mer interessert i dette anbefales boken Patterns in Java av Mark Grand.
Grunnlaget for strukturen hentet jeg fra ett av mine tidligere prosjekter ved HiØ.

For å få en oversikt av strukturen i programmet laget jeg en enkel oversiktsmodell Denne var veldig grei å ha under utviklingen. Etter hvert fikk jeg også laget en API-dokumentasjon og der finner du gode oversikter over klassene og deres metoder. Jeg har forsøkt å gi metoder og variabler selvforklarende navn for å gjøre det enklere å lese koden. I noen av klassene finnes det metodenavn som begynner på "test", dette er metoder som ble brukt til testing under utviklingen. Jeg har beholdt metodene, men kallene til disse er "fnuttet ut" i koden.

I tillegg til API'en, som er på engelsk, er koden rikelig kommentert på norsk, det står mange forklaringer der som ikke er med på denne siden. Du finner mest kommentarer der det dreier seg om OpenGL og mindre kommentarer på den "vanlige" Java-koden. Som en hovedregel (tror det er veldig få unntak) følger de norske kommentarene denne strukturen:
Står en kommentar rett etter en kodelinje gjelder den bare for denne linjen.
Står en kommentar rett over en løkke gjelder den for hele løkken.
Står en kommentar rett over en if-test gjelder den for denne testen, er det et else-statement vil du finne en ny kommentar for den.
Står en kommentar rett over en kodelinje gjelder denne fram til en ny kommentar over en kodelinje.
I tillegg er det noen kommentarer som er skrevet med store bokstaver, jeg tror du skjønner betydningen av disse med en gang du ser dem.

Hva får du så se, og hva kan du gjøre i timeglassprogrammet?
Programmet "styres" ved hjelp av menyer, to kommandoknapper og musen for å rotere scenen.
I menyene kan du velge mellom tre ulike glasstyper (Glassfasonger), ClassicBulbs, ModernBulbs og BizarreBulbs. Disse har hver sin standard støttetype, ClassicStand, ModernStand og BizarreStand, som fås i "messing" eller "ibenholt". I tillegg finnes enkel grønn plate tilgjengelig som støtte for alle glassfasongene, og det er mulig å få dem med glassbunn. Fargen på glasset kan varieres mellom blått, grønt og sotet, mens sanden fås i enten rødt eller sitrongult.
I filmenyen finner du en mulighet til å "slå av og på" aksesystemet til timeglasset og/eller kontrollpunktene til en av glassets Bezierflater. Det er også en mulighet til å "ta vekk" sanden, dette for at det skal være mulig å få sett godt på blendingen (ta en titt på fasongen Bisarr, med blått glass og glassbunn :-)). NB: Den bisarre timeglassfasongen har ikke sand, grunnen til dette finner du i seksjonen om Animasjon.
Kommandoknappen "Originalposisjon" setter timeglasset tilbake til utgangspunktet (åpningsbilde).
Kommandoknappen "Snu timeglass" setter i gang en animasjon av timeglasset, det snus og sanden renner fra øvre til nedre halvdel. NB: Mens timeglasset snus er sanden bare "symbolisert" av en "sandfarget" kule, grunnen til dette finner du i seksjonen om Animasjon.
Musen brukes for å rotere scenen.
Mye av koden for menyer (og det som "hører til" dem) hadde jeg fra tidligere prosjekter ved HiØ. Det å holde på med dette, samt laging av nye glass og støtter, fungerte for meg som ren terapi når jeg slet som mest med "hovedproblemene" mine. Jeg fikk tatt en pause fra tenkingen, men jobbet fremdeles med prosjektet. Anbefales :-D
Det å lage de ulike støttene og glassene ga meg også en god innsikt i å jobbe med med vanlige OpenGL-kall som å pushe og poppe matrix'er, transleringer og rotasjoner (disse kallene er ikke spesielt kommentert på denne siden). Støttene ga meg erfaring med "Quadric'er", og glassene ga meg en bedre ide om "hva som var hva" i matrisene mine.

Mulige utvidelser

Mulighetene for utvidelser er legio ;->.

  • Mulighetene til å utvide med nye timeglassfasonger, støttetyper, glass- og sandfarger er ubegrensede. Slik programmet er nå består for eksempel timeglassfasongene av Bezierflater med 5 x 5 kontrollpunkter. Hva med å lage programmet mer dynamisk så det tar andre kombinasjoner av kontrollpunkter?
  • Animere sanden også mens timeglasset snus. Her blir utfordringen det å "legge lokk på" sanden i den øvre halvdelen av timeglasset.
  • Lage tykkelse på glasset ved å modellere en "innsideflate", gjøre alphaverdier for glasset avhengig av glasstykkelsen.
  • Bruke teksturer. Legge teksturer både på støttene og sanden.
  • Lage bakgrunnsomgivelser. Da kunne blendingen av timeglasset komme enda bedre til sin rett.
  • Legge på slagskygge.
Referanser

Moduler:

  • Bezier-kurven: Bezier (Bezierflater og Animasjon)
  • Påskeegg: Påskeegg(Bezierflater og bruk av clipplane)
  • Farrisflaske: Flaske(utganspunkt for blending)

Internettsider:

  • Nehe, lesson 08, Blending
  • Sulaco, BezierCreator (sidene anbefales varmt, mye flott, og Delphi er ikke så vanskelig å forstå)

Bøker:

  • Red Book, spesielt:
    Kapittel 6: Blending, Antialiasing, Fog and Polygon Offset
  • Hills bok, spesielt:
    Kapittel 11: Curve and Surface Design
  • Patterns in Java, Mark Grand, Wiley:
    Kapittel 5: Creational Patterns

Javakode (gl4java):

HourglassFrame.java
PlainStand.java MaterialFactory.java Material.java
BlueGlassMaterial.java GreenGlassMaterial.java SootyGlassMaterial.java
BrassMaterial.java EbonyMaterial.java GreenMaterial.java
RedSandMaterial.java YellowSandMaterial.java SimpleStand.java
HourglassAnimCanvas.java Bulbs.java ClassicBulbs.java
ModernBulbs.java BizarreBulbs.java Stand.java
ClassicStand.java ModernStand.java BizarreStand.java

Alle disse filene filene zippet: hourglass.zip

API-doc

Oversiktsmodell

Vedlikehold
Skrevet av Therese Røsholdt våren 2003. Kun redaksjonelle endringer Børre Stenseth mars 2003.
(Velkommen) Forklaring av>Blending (Marching Cubes)