U sklopu teme, napravljen je projekt koji primijenjuje navedene tehnike te prikazuje princip rada Phongovog modela osvjetljavanja. Projekt je dostupan za preuzimanje ovdje.
Za pokretanje projekta potrebno je imati instaliranu Javu, po mogućnosti najnoviju verziju. Preuzetu ZIP datoteku potrebno je raspakirati i pokrenuti izvršnu Phong.jar datoteku.
Projekt je razvijen u alatu Netbeans, a čitav kod izrađenog projekta može se preuzeti ovdje. Kod se nakon preuzimanja i extractanja može pregledati u bilo kojem text editoru, ali najjednosatvnije je svakako preuzeti Netbeans i otvoriti kod pomoću njega.
Pokretanjem primjera otvaraju se četiri prozora, po jedan za svaku komponetnu, te četvrti koji uključuje sve komponente. Prozori su naslagani jedan ispod drugog, kako bi se bolje dočaralo postojanje komponenti. Odvlačenjem prozora pomoću miša otkriva se sljedeći prozor koji sadrži drugu komponentu, itd.
Važno je napomenuti kako se zatvaranjem jednog prozora zatvaraju i svi preostali, kako bi se izbjegnulo nepotrebna potreba za zatvaranjem svakog prozora posebno.
U svakom prozoru nalazi se rotirajuća sfera.
Prva komponenta koja se može vidjeti jest zrcalna komponenta.

Na sferu se upiru tri izvora svjetlosti, čija refleksija daje sjaj i to je jedina vidljiva svjetlost u zrcalnoj komponenti, ostatak sfere je potpuno crn.
Odmicanjem prozora sa zrcalnom svjetlošću u stranu, uočava se sfera prikazana samo s okolnim, odnosno ambijentalnim osvjetljenjem.

Ta okolna komponenta daje sferi zelenu boju, no kad se samo ona prikazuje, nisu vidljivi nikakvi detalji sfere, samo zeleni oblik.
Odmicanjem i tog prozora, može se vidjeti prozor s difuznom komponentom.

Difuzna svjetlost se raspršuje jednako u svim smjerovima, a to osigurava da površine pod većim kutom butu tamnije od onih koje su na izravnom udaru svjetlosti.
Kad se odmakne i taj prozor, dobiva se konačni prikaz koji uključuje sve komponente.

Kad su sve komponente svjetlosti uključene, može se vidjeti kako je riječ o prilično realnom prikazu.
U nastavku će biti izdvojeni neki od najvažnijih dijelova koda koji omogućuju realizaciju Phongovog modela osvjetljavanja.
// Objekti koji ce ispuniti pripadajuce prozore
GrafickiPanel GP =
new GrafickiPanel(OC, prozor, panel.getWidth(), panel.getHeight(), 0.5, 0.5, 0.5);
GrafickiPanel GP1 =
new GrafickiPanel(OC1, prozorDifuzno, panel1.getWidth(), panel1.getHeight(), 0.5, 0.0, 0.0);
GrafickiPanel GP2 =
new GrafickiPanel(OC2, prozorOkolno, panel2.getWidth(), panel2.getHeight(), 0.0, 0.5, 0.0);
GrafickiPanel GP3 =
new GrafickiPanel(OC3, prozorZrcalno, panel3.getWidth(), panel3.getHeight(), 0.0, 0.0, 0.5);
Za svaku se komponentu stvara novi prozor, a ono što je važno je da se kod ispunjavanja prozora odredi koje će svjetlo biti u njemu. To se definira u konstruktoru klase GrafickiPanel u zadnja tri parametra.
public GrafickiPanel(Priprema oc, Prikaz prozor, int duzina, int visina,
double difuzna, double okolna, double zrcalna) {
this.setSize(new Dimension(duzina, visina));
backBufferImage = new BufferedImage(duzina, visina, BufferedImage.TYPE_INT_ARGB);
OC = oc;
this.prozor = prozor;
renderer = new Renderiranje(duzina, visina);
renderer.postaviFaktorOkolnogSvjetla(okolna);
renderer.postaviFaktorDifuznogSvjetla(difuzna);
renderer.postaviFaktorZrcalnogSvjetla(zrcalna);
Vektor3D tmp = new Vektor3D(0.0, 600.0, -600.0);
renderer.dodajSvjetlo(new Osvjetljenje("L1", Color.LIGHT_GRAY, tmp.clone()));
tmp.rotateZ(Math.PI * 2. / 3.);
renderer.dodajSvjetlo(new Osvjetljenje("L2", Color.LIGHT_GRAY, tmp.clone()));
tmp.rotateZ(Math.PI * 2. / 3.);
renderer.dodajSvjetlo(new Osvjetljenje("L3", Color.LIGHT_GRAY, tmp.clone()));
System.out.println("duzina " + this.getWidth() + " visina " + this.getHeight());
}
Prvi od tih parametara određuje intenzitet difuzne svjetlosti, drugi okolne, a treći zrcalne. Kod objekta koji će prikazivati sve svjetlosti, sve se vrijednosti uključuju, dok kod ostalih one koje nisu uključene imaju postavljene vrijednosti 0.
Postavljeni parametri intenziteta koriste se kod renderiranja
for (int i = 0; i < lBroj; i++) {
if (svjetla[i].jeOmoguceno()) {
Color boja = svjetla[i].dohvatiBoju();
Vektor3D tmp = new Vektor3D(nalicje.dohvatiSrediste());
tmp.oduzmi(svjetla[i].dohvatiPoziciju());
tmp.normaliziraj();
// produkt koji se koristi kod racunanja difuznog svjetla
vektorskiProdukt = nalicje.dohvatiNormalu().dohvatiSkalarniProdukt(tmp);
// produkt koji se koristi kod racunanja zrcalnog svjetla
double beta = Math.acos(nalicje.dohvatiNormalu().dohvatiSkalarniProdukt(tmp));
Vektor3D tmp2 = nalicje.dohvatiNormalu().dohvatiVektorskiProdukt(tmp);
Vektor3D R = new Vektor3D(tmp);
R.rotateAV(tmp2, beta * 2);
double vektorskiProdukt2 = Math.pow(obsNormal.dohvatiSkalarniProdukt(R), n);
if (vektorskiProdukt < 0) {
crvena += boja.getRed() * Math.abs(vektorskiProdukt) * faktorDifuznogSvjetla
+ boja.getRed() * Math.abs(vektorskiProdukt2) * faktorZrcalnogSvjetla;
zelena += boja.getGreen() * Math.abs(vektorskiProdukt) * faktorDifuznogSvjetla
+ boja.getGreen() * Math.abs(vektorskiProdukt2) * faktorZrcalnogSvjetla;
plava += boja.getBlue() * Math.abs(vektorskiProdukt) * faktorDifuznogSvjetla
+ boja.getBlue() * Math.abs(vektorskiProdukt2) * faktorZrcalnogSvjetla;
}
}
}
Za svaki se izvor svjetlosti u ovom dijelu prema zadanim formulama računaju intenziteti svake od tri osnovne boje (crvena, zelena, plava) za difuzno i zrcalno osvjetljenje te se dodaju u ukupan intenzitet.
Color boja = nalicje.dohvatiBoju();
crvena += boja.getRed() * FaktorOkolnogSvjetla;
zelena += boja.getGreen() * FaktorOkolnogSvjetla;
plava += boja.getBlue() * FaktorOkolnogSvjetla;
if (crvena > 255) {
crvena = 255;
}
if (zelena > 255) {
zelena = 255;
}
if (plava > 255) {
plava = 255;
}
boja = new Color((int) crvena, (int) zelena, (int) plava);
g2d.setColor(boja);
g2d.fillPolygon(xKoordinate, yKoordinate, 3);
Za okolno osvjetljenje izračun je jednostavniji i jednostavno se dodaje prethodno izračunatim vrijednostima za difuznu i zrcalnu komponentu.
Izračun je time završen i izračunatom se bojom može se popuniti poligon koji se prikazuje. Svaka od tri komponente na ovaj je način uključena u izračun prema zadanim formulama i tako je ostvaren Phongov model osvjetljavanja. Može se primijetiti kako kod prikaza pojedinačnih komponenti, intenzitet ostalih je jednak nuli, što ih jednostavno poništava, tj. miče njihov učinak u jednadžbi, pa ostaje samo ona koja je uključena. To je moguće jer se elementi svake komponente zasebno množe, dok se komponente međusobno zbrajaju. Kod prikaza cjelovitog Phongovog modela jednostavno se sve komponente uključe i formula opet funkcionira.
Od ostalih stvari, može se naglasiti kako funkcionira samo crtanje sfere.
public static Konstrukcija constructSphere(double r, int brojParalela, int brojMeridijana) {
...
for (int i = 0; i < (brojParalela - 2); i++) {
for (int j = 0; j < brojMeridijana - 1; j++) {
int p1 = 1 + i * brojMeridijana + j;
int p2 = 1 + (i + 1) * brojMeridijana + j;
int p3 = 1 + (i + 1) * brojMeridijana + j + 1;
int p4 = 1 + i * brojMeridijana + j + 1;
sfera.addFace(new Ispuna(p1, p2, p3));
sfera.addFace(new Ispuna(p3, p4, p1));
}
}
for (int i = 0; i < (brojParalela - 2); i++) {
int p1 = 1 + i * brojMeridijana + brojMeridijana - 1;
int p2 = 1 + (i + 1) * brojMeridijana + brojMeridijana - 1;
int p3 = 1 + (i + 1) * brojMeridijana + 0;
int p4 = 1 + i * brojMeridijana + 0;
sfera.addFace(new Ispuna(p1, p2, p3));
sfera.addFace(new Ispuna(p3, p4, p1));
}
...
}
Unutar klase konstrukcija, stvara se novi objekt. Parametri su standardni, polumjer, broj paralela i broj meridijana. Kod većeg broja paralela i meridijana, dolazi do pojave raznih neobičnih artefakata, a uzrok tome je taj što Java ne komunicira izravno s grafičkom karticom te u takvim slučajevima izračun postaje vrlo težak za resurse.
U gore navedenoj metodi, izdvojen je najvažniji dio. U petlji se računaju koordinate svakog sjecišta meridijana i paralela. Obje zapravo rade istu stvar, donja je samo za završni meridijan. U jednom se koraku računaju četiri vrha, a kako četiri vrha ne moraju ležati u istoj ravnini, u računalnoj grafici najčešće se koriste trokuti. Premda se ovdje u načelu mogu koristiti i kvadrati, nekako se bolji prikaz dobiva korištenjem trokuta. Zato se u konstrukciju za svaka četiri vrha dodaju po dva komadića slagalice, tj. dva trokuta. To se čini dodavanjem objekta klase Ispuna kao dijelića slagalice, a svi djelići će na kraju činiti sferu.
public Ispuna(int v0, int v1, int v2) {
indeksVrha = new int[3];
indeksVrha[0] = v0;
indeksVrha[1] = v1;
indeksVrha[2] = v2;
objektRoditelj = null;
}
Kao dijelić u konstrukciji, pamte se koordinate vrhova. Kasnije kod renderiranja, ti se vrhovi koriste kako bi se unutar iste ove klase izračunale normale, tj. kako bi se sve na kraju moglo lijepo ispuniti bojom.
public void izracunajNormalu() {
Vektor3D brid0 = new Vektor3D();
Vektor3D brid1 = new Vektor3D();
brid0.become(dohvatiVrh(1));
brid0.oduzmi(dohvatiVrh(0));
brid1.become(dohvatiVrh(2));
brid1.oduzmi(dohvatiVrh(1));
normala = brid1.dohvatiVektorskiProdukt(brid0);
normala.normaliziraj();
izracunataNormala = true;
}
Za izračun se koriste metode definirane u klasi Vektor3d, a radi se o običnom oduzimanju te normaliziranju vektora, što nam daje normale za svaki pojedini dijelić sfere. Krajnji rezultat je pravilno ispunjena i obojana sfera.
Autori: Domagoj Ličanin i Luka Lovrenčić