Timeglass (Blending)
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.
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:
- 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.
- 0.0f -Minste verdi av u.
- 1.0f - Høyeste verdi av u.
- 3 - avstanden i matrisen mellom X-verdien til første og andre punkt i u-retningen.
- UN - antall antall kontrollpunkter i u-retningen (variabel instansiert tidligere (UN=5)).
- 0.0f - Minste verdi av v.
- 1.0f - Høyeste verdi av v.
- 3*UN - avstanden i matrisen mellom X-verdien til første og andre punkt i v-retningen.
- VN - antall antall kontrollpunkter i v-retningen (variabel instansiert tidligere (VN=5)).
- 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);
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:
- GL_FILL - velger å ha fylte polygoner i nettet.
- 0 - tilsvarer parameter 2 i glMapGrid2 (startpunktet til u-retningen)
- 40 - tilsvarer parameter 1 glMapGrid2 (antall masker i u-retningen)
- 0 - tilsvarer parameter 4 i glMapGrid2 (startpunktet til v-retningen)
- 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".
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.jpg |
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.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.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. |
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). |
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.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:
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:
- boolean needsBottomLowerSand - bestemmer om sanden skal tegnes som Beziersand med topp, eller som tinyLowerHeap.
- boolean trickle - bestemmer om sandstrålen skal tegnes eller ikke
- float lowerT - t-verdien til Beziersanden.
- 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.