Modul 4.3 Dybdebuffer, alpha blending

Innledning

Når man lager en 3D-verden med objekter vil enkelte av objektene dekke til andre objekter, helt eller delvis. Objekter kan også være gjennomsiktig i forhold til seg selv og andre objekter. Via pipelinen genereres pikseldata som utgjør et skjermbilde eller en frame. Pikseldata ender opp i frame bufferet som egentlig består av tre ulike deler:

  • Dybdebuffer / z-buffer
    • Indikerer hva som ligger fremst, og bakerst i scenen.
  • Stencil buffer
    • Kan brukes til å utelate piksler ved hjelp av «sjablongverdier» (stencil values). Omtales ikke her.
  • Fargebuffer
    • Inneholder RGBA-verdier for hver enkelt piksel som skal vises på skjermen.

De ulike bufrene er illustrert i figuren under.

Dybde-buffer, stencil-buffer, fargebuffer
Dybde-buffer, stencil-buffer, fargebuffer

Farge- og dybdebuffer/z-buffer

Dybdebufferet, også kalt z-buffer, inneholder en dybdeverdi per piksel. Dette er en verdi i området fra og med 0 til og med 1 som indikerer hva som ligger fremst, og bakerst i scenen. Jo høyere verdi jo lengre «innover» i scenen. Bakgrunnen til scenen vil ha dybdeverdi lik 1.0. Dybdetesten utføres etter at fragmentshaderen er kjørt.

Ved hjelp av dybdebufret avgjøres det om en «innkommende» piksel ligger foran eller bak pikselen som evt. allerede måtte ligge i fargebufret. Ugjennomsiktige objekter kan dermed tegnes i vilkårlig rekkefølge og fortsatt vises korrekt.

Dersom innkommende fragment skal ligge foran eksisterende fragment oppdateres både fargebufferet og dybdebufferet med nye verdier.

Dybdebuffer og fargebuffer
Dybdebuffer og fargebuffer

Et fragment som ikke består dybdetesten forkastes og eksisterende fargeverdi beholdes. Dybdebufferet endres i så fall heller ikke.

Dersom vi ikke aktiverer dybdetest er det rekkefølgen vi kaller drawArrays() på som avgjør hva som blir liggende fremst. Dybdetest er som standard avslått, dvs. vi må eksplisitt aktivere dette. Dette aktiveres vha. følgende kode:

gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);   //Merk: gl.LESS er standard. gl.LEQUAL = Less than or EQUAL.

Selv om dybdetesten er aktivert, vil man i enkelte tilfeller ikke at dybdebufferet skal oppdateres selv om fargebufferet oppdateres. Dette er aktuelt når man tegner gjennomsiktige objekter. Dersom dybdebufferet oppdateres vil det kunne blokkere andre objekter som ligger bak. Dette styres av gl.depthMask(), slik:

gl.depthMask(true);  //Standard. Dybdebuffer oppdateres.
gl.depthMask(false); //Dybdebuffer oppdateres IKKE.  

Anta for eksempel at vi tegner et gjennomsiktig objekt som ligger foran et ugjennomsiktig objekt. Dersom dybdebufferet oppdateres vil det gjennomsiktige objektet kunne blokkere for det ugjennomsiktige objektet som ligger bak. Det samme vil skje dersom vi tegner et gjennomsiktig objekt som ligger bak et annet gjennomsiktig objekt. Dersom dybdebufferet oppdateres vil det første gjennomsiktige objektet kunne blokkere for det andre som ligger bak. Dette vil imidlertid også kunne avhenge av rekkefølgen vi tegner objektene i. Mer om dette lenger ned i notatet.

Eksempel med bruk av dybdetest

Anta at man spesifiserer to ugjennomsiktige trekanter slik at trekant1 (rød) skal ligge foran trekant2(grønn).

Det vil si at spesifiserte Z-koordinatverdier (ikke det samme som z-bufferverdien) til trekant1 er større enn tilsvarende for trekant2. Dersom dybdetest IKKE er aktivert, og vi tegner trekant1 før trekant2, vil trekant2 dekke over trekant1. Her bestemmer rekkefølgen trekantene tegnes på hva som blir liggende øverst (se venstre figur under).

Dersom vi aktiverer dybdetest vil resultatet bli som vist i høyre figur under.

Dybdetest av
Dybdetest av
Dybdetest på
Dybdetest på

Gjennomsiktighet

Gjennomsiktighet simuleres ved å blande innkommende farge på et fragment med eksisterende farge som måtte ligge i fargebufferet. Dersom fargeblanding er aktivert er dette noe som skjer etter dybdetesten. Dybdetesten skjer rett etter at fragmentshaderen har kjørt. Husk at fragmentshaderen kjører for hvert fragment, og at den returnerer en farge som igjen er bestemt av verteksfarger, teksturfarger og/eller beregnet lysfarge. Fargeblanding skjer dermed etter dybdetesten, men før fragmentet ev. ender opp i fargebufferet.

Både dybdetesten og fargeblandingen utføres etter at fragmentshaderen er kjørt, men rekkefølgen mellom dem er viktig: dybdetesten kommer først, fargeblandingen etterpå. Et fragment som forkastes av dybdetesten blir dermed aldri blandet inn i fargebufferet. Dette får konsekvenser for hvordan vi tegner gjennomsiktige objekter, noe vi kommer tilbake til.

Fargeblanding styres av programmereren og kan gjøres på mange måter, bl.a. vha. alpha-verdien i den fargen fragmentshaderen gir fra seg (returnerer). Alpha-verdien kan komme fra verteksfarger, fra en uniform, fra en RGBA-tekstur, eller beregnes på et eller annet vis i fragmentshaderen.

Begreper

I forbindelse med fargeblanding er det to viktige begreper som må forklares:

  • Source fragment: fragmentet som tegnes nå, dvs. det som kommer fra fragmentshaderen. I enkelte sammenhenger brukes også begrepet "innkommende" fragment.
  • Destination fragment: fragmentet som allerede er i frame bufferet.

Aktivere fargeblanding

På samme måte som at dybdetest må aktiveres, må også fargeblanding aktiveres. I tillegg kan vi angi en blandingsfunksjon, som bestemmer hvordan source og destination-fargene skal blandes, f.eks. slik:

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);  

Hvordan fargeblanding utføres bestemmes av blendFunc():

gl.blendFunc(sourceFactor, destinationFactor)

Der sourceFactor er en verdi som multipliseres med sourceColor og destinationFactor multipliseres med destinationColor. Resultatfargen blir da:

finalColor = sourceFactor × sourceColor + destFactor × destColor

Her er finalColor beregnet vha. addisjon (+), som også er standard. Denne kan endres vha.

gl.blendEquation(gl.FUNC_ADD);    //ev. FUNC_SUBTRACT, FUNC_REVERSE_SUBTRACT, MIN eller MAX 

Bruk av gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) er en variant, andre eksempler på bruk av gl.blendFunc() kan være:

gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.blendFunc(gl.ONE, gl.ONE);

Dette er bare noen eksempler. Se developer.mozilla.org: blendFunc() method. for mer informasjon.

Eksempel 1:

Anta at vi bruker:

gl.blendFunc(gl.ONE, gl.ONE);

Dette betyr at:

Source Factor = SF = [SFr, SFg, SFb, SFa] = [1,1,1,1]
Destination Factor = DF = [DFr, DFg, DFb, DFa] = [1,1,1,1]

Anta videre følgende verdier:

Source color = [Rs, Gs, Bs, As] = [1, 0, 0, 1] (innkommende fragmentet, dvs. det som skal tegnes)
Destination color = [Rd, Gd, Bd, Ad] = [0, 1, 0, 1] (fragmentet som allerede ligger i frame bufferet)

Blandingen av source og destination blir da (res=result, s=source, d=destination):

Rres = Rs * SFr + Rd * DFr = 1 * 1 + 0 * 1 = 1
Gres = Gs * SFg + Gd * DFg = 0 * 1 + 1 * 1 = 1
Bres = Bs * SFb + Bd * DFb = 0 * 1 + 0 * 1 = 0
Ares = As * SFa + Ad * DFa = 1 * 1 + 1 * 1 = 2

Merk: Alle resultatkomponentene klippes til området [0,1]. Ares blir derfor 1.0, ikke 2. Alfa lagres i fargebufferet og brukes som destinasjonsalfa i senere blandinger, men vises ikke som farge i seg selv.

Resultatfarge = 1,1,0 (gul)
Eksempel1: Fargeblanding og gjennomsiktighet

Eksempel 2:

Anta at vi bruker følgende verdier på source- og destinationColor:

Source color = [Rs, Gs, Bs, As] = [0, 0, 1, 0.7]
Destination color = [Rd, Gd, Bd, Ad] = [1, 0, 0, 0.7]

Anta videre at vi bruker følgende blandingsfunksjon:

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

Dette gir følgende source- og destinationFactor:

Source Factor = SF = [SFr, SFg, SFb, SFa] = [0.7, 0.7, 0.7, 0.7]
Destination Factor = DF = [DFr, DFg, DFb, DFa] = [0.3, 0.3, 0.3, 0.3]

Siden vi bruker gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) som blandingsfunksjon og alpha-verdien til Source color = 0.7 blir SF lik 0.7 (SRC_ALPHA). Destination factor blir da 1 - 0.7 = 0.3 (ONE_MINUS_SRC_ALPHA).

Blandingen av source og destination blir da:

Rres = Rs * SFr + Rd * DFr = 0 * 0.7 + 1 * 0.3 = 0.3
Gres = Gs * SFg + Gd * DFg = 0 * 0.7 + 0 * 0.3 = 0.0
Bres = Bs * SFb + Bd * DFb = 1 * 0.7 + 0 * 0.3 = 0.7
Ares = As * SFa + Ad * DFa = 0.7 * 0.7 + 0.7 * 0.3 = 0.49 + 0.21 = 0.70

Resultatfarge = 0.3, 0.0, 0.7, 0.7 (lilla)

Eksempel2: Fargeblanding og gjennomsiktighet
Eksempel2: Fargeblanding og gjennomsiktighet

Forhåndsmultiplisert (premultiplied alpha) alfa

I modulene om teksturering lastet vi teksturer med gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true). Det gjør at WebGL multipliserer R, G og B med alfakanalen under opplastingen, som betyr at en texel med verdi (1, 0, 0, 0.5) lagres som (0.5, 0, 0, 0.5). Her er RGB-verdiene multiplisert med alpha-verdien 0,5 (som er alpha-verdien til texel'en).

Er teksturen forhåndsmultiplisert er første del av likninga over allerede utført. Første parameter til gl.blendFunc(...) må derfor settes til gl.ONE, slik:

gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

Med vanlige, ikke-forhåndsmultipliserte verdier bruker vi, som vist over, gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA). Blandingsfargen ble da beregnet slik: source-color * source-alpha + destination-color * destination-factor.

Oppdatere dybdebufferet eller ikke - bruk av gl.depthMask()

Her ser vi på tilfellet der dybdetest og fargeblanding er aktivert og vi ønsker å tegne flere overlappende (delvis) gjennomsiktige 2D-kvadrater. Som standard oppdateres dybdebufferet dersom innkommende fragment har lavere dybdeverdi enn det som allerede ligger i dybdebufferet. Dette kan føre til at gjennomsiktige objekter som ligger bak andre gjennomsiktige objekter ikke vises. Dette kan kontrolleres ved å bruke false som parameter til depthMask(), slik: gl.depthMask(false) (true er standardverdi). Dette indikerer at dybdebufferet IKKE oppdateres, selv om innkommende fragment har lavere dybdeverdi enn det som allerede ligger i dybdebufferet.

La oss se på et eksempel som illustrerer dette. Anta at vi tegner tre (delvis) gjennomsiktige 2D-kvadrater med sidelengder 4x4. Kvadratene A, B og C ligger parallelt med XY-planet og er posisjonert i henholdsvis [0, 0,-2], [1,1,0] og [2, 2, 2].

Anta at kameraet står i [0, 0, -15] og ser mot origo og dermed også mot kvadratene. Se figuren under. Siden vi har plassert kameraet i -15 vil -Z gå "ut av skjermen" og positiv Z går nå innover i skjermen. Ellers er følgende innstillinger er brukt:

  • gl.enable(gl.DEPTH_TEST); //Aktivert dybdetest.
  • gl.enable(gl.BLEND); //Aktivert fargeblanding.
  • gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); //Blandefunksjon.
  • gl.depthMask(true); //Dvs. standardverdi, dybdebuffer oppdateres dersom innkommende har lavere dybdeverdi enn eksisterende dybdeverdi.

Vi tester først med gl.depthMask(true) som vist over (true er standardverdi og man trenger ikke angi dette eksplisitt). La oss anta at A, B og C får dybdeverdier, sett fra kamera, lik henholdsvis 0.4, 0.6 og 0.8.

Dersom kvadratene nå tegnes i rekkefølgen A, B, C, dvs. kvadratet som er nærmest kameraet tegnes først, deretter det som er nest nærmest osv., vil kvadratene fremstå som ugjennomsiktig. Se figuren under.

Forsøker å tegne gjennomsiktige kvadrater med depthMask = true
Forsøker å tegne gjennomsiktige kvadrater med depthMask = true

Forklaring:

Ser på et fragment der alle tre kvadrater overlapper:

  • A (rød) tegnes først. A sin farge blandes med bakgrunnsfargen (hvit). Bakgrunnen har dybdeverdi=1.0. Fargebufferet oppdateres med ny blandingsfarge. Dybdebuffer oppdateres, ny verdi=0.4.
  • B tegnes etter A. B har dybdeverdi 0.6, dvs. bak A (sett fra kamera) og dybdetesten vil dermed forkaste dette fragmentet. Fargen opprettholdes. Ingen endring av dybdebuffer.
  • C har dybdeverdi 0.8, dvs. bak både A og B (sett fra kamera) og dybdetesten vil dermed forkaste også dette fragmentet. Fargen opprettholdes. Ingen endring av dybdebuffer.

Aktivering av dybdetest og depthMask=true gjør at kvadratene vil fremstå som ugjennomsiktige.

Dersom vi nå endrer innstillingene bruker gl.depthMask(false) vil dybdebufferet IKKE oppdateres selv om innkommende har lavere dybdeverdi. Kvadratene vil nå fremstå som gjennomsiktig.

Forklaring:

Ser på et fragment der alle tre kvadrater overlapper.

  • Kameraet står i [0,0,-15].
  • Tegner i rekkefølgen: A, B og C.
  • depthMask = false.

A, B og C har fortsatt dybdeverdiene lik henholdsvis 0.4, 0.6 og 0.8.

  • A tegnes først. A sin farge blandes med bakgrunnsfargen (hvit). Bakgrunnen har dybdeverdi=1.0. Fargebuffer oppdateres med ny blandingsfarge. Dybdebuffer oppdateres IKKE, og har fortsatt verdi 1.0 (tilsvarende bakgrunnen).
  • B tegnes etter A. B har dybdeverdi 0.6. Selv om B ligger bak A, sammenliknes ikke B med A — dybdebufferet står fortsatt på 1.0. Dybdeverdien 0.6 er mindre enn 1.0, så dybdetesten forkaster ikke fragmentet. B sin farge blandes med fargen som allerede ligger i fargebufret. Dybdebuffer oppdateres IKKE, og har fortsatt verdi 1.0.
  • Til slutt tegnes C, med dybdeverdi 0.8. Også C ligger bak, men sammenliknes igjen mot 1.0 og passerer. C sin farge blandes inn. Dybdebuffer oppdateres IKKE.

I dette tilfellet vil kvadratene fremstå som gjennomsiktig. Se figuren under.

Tegne gjennomsiktige kvadrater i rekkefølgen A,B,C med depthMask = false, Kamera=[0,0,-15]
Tegne gjennomsiktige kvadrater i rekkefølgen A,B,C med depthMask = false, Kamera=[0,0,-15]

Figuren under viser de to tilfellene der vi bruker depthMask=true og depthMask=false, kameraet er plassert i [0,0,-15] og vi tegner i rekkefølgen A, B, C.

Tegne gjennomsiktige kvadrater og bruk av depthMask
Tegne gjennomsiktige kvadrater og bruk av depthMask

Sortere objekter basert på avstand fra kamera

Vi ser at tegning med depthMask=false gir en gjennomsiktig effekt, men det blir ikke helt korrekt, man skulle forvente at rødt var dominerende siden det røde kvadratet er nærmest kamera. Dette henger sammen med at rekkefølgen fargene blandes i har betydning. For korrekt resultat bør objektene sorteres basert på objektenes avstand til kameraet. Man kan da alltid tegne objektene fra innerst (dvs. lengst unna kameraet) til ytterst. Dette kan gjøres ved å legge kvadratene, inkludert sin posisjon, i en liste og deretter beregne avstanden vha. kameraets posisjon (en vektorsubtraksjon), sortere listen på kameraavstand, og så bruke denne listen ved tegning. Se kodeeksempel under.

Legg merke til hvordan listen med kvadrater som skal tegnes opprettes. Først laget vi en liste som der hvert objekt i lista representerer et kvadrat med sin posisjon og tilhørende farge. For å også få med avstanden til kameraet til hver av disse kvadratene opprettes en ny, tilsvarende liste, som utvides med attributtet dist. Dette er gjort fordi man ikke kan kalle på distanceFromCamera() med pos som parameter i samme array der pos opprettes. Vi gjør derfor dette i to steg. Det finnes alternative måter å gjøre dette på, men poenget er at vi nå har en liste med objekter som ser slik ut:

 [
  {
    pos: {x:1, y:1, z:0},
    color: {red:0.0, green:1.0, blue:0.0, alpha:0.7},
    dist: 15.07,
  },
  {
    pos: {x:0, y:0, z:-2},
    color: {red:1.0, green:0.0, blue:0.0, alpha:0.7},
    dist: 13,
  },
  {
    pos: {x:2, y:2, z:2},
    color: {red:0.0, green:0.0, blue:1.0, alpha:0.7},
    dist: 17.23,
  }
]

Elementene i den opprinnelige lista (squaresToDraw) kan legges til i vilkårlig rekkefølge. I neste steg sorteres squaresToDrawWithDistanceToCamera basert på avstanden til kameraet (dist).

Elementene i denne lista sorteres så vha.

squaresToDrawWithDistanceToCamera.sort((distFromCam1, distFromCam2) => compare(distFromCam1.dist, distFromCam2.dist));

Funksjonen distanceFromCamera beregner og returnerer avstanden fra gitt kvadrat til kameraet. compare() -funksjonen er knyttet til sort()-funksjonen som kalles på squaresToDrawWithDistanceToCamera-arrayet. sort() er en eksisterende Javascript-funksjon knyttet til arrays. Denne tar som parameter en funksjon (her: compare()) som brukes i forbindelse med sorteringen. Denne tar to parametre, som representerer to elementer i arrayet (f.eks. de to første), og sammenlikner disse verdiene. Funksjonen skal, avhengig av verdiene på elementene, returnere 0, 1 eller -1. Vi kan dermed selv påvirke hvordan sortering skal gjøres. F.eks. stigende eller fallende. Se mer informasjon om sort() her.

Lista er nå sortert basert på dist attributtet. Til slutt tegnes de sorterte kvadratene i rekkefølge, innerst til ytterst:

for (let i = 0; i < squaresToDrawWithDistanceToCamera.length; i++) {
    renderInfo.modelMatrix.setIdentity();
    renderInfo.modelMatrix.translate(squaresToDrawWithDistanceToCamera[i].pos.x, squaresToDrawWithDistanceToCamera[i].pos.y, squaresToDrawWithDistanceToCamera[i].pos.z);
    drawTransparentSquare(renderInfo, camera, squaresToDrawWithDistanceToCamera[i].color);
}  

De tre kvadratene vil nå tegnes korrekt uansett hvilken rekkefølge kvadratene legges til i lista squaresToDraw. Resultatet blir da alltid som vist i figuren under, og er også i dette tilfellet uavhengig av depthMask. Når objektene sorteres som vist, passerer hvert nytt fragment dybdetesten uansett, og resultatet blir derfor det samme enten man bruker gl.depthMask(true) eller gl.depthMask(false). Dette gjelder likevel bare så lenge sorteringen faktisk er korrekt — sorteringa baseres her på avstanden til objektenes sentrum, og den forutsetter at objektene er konvekse og ikke går inn i hverandre. Bruk derfor gl.depthMask(false) som hovedregel. I verste fall blir blandingsfargene litt feil, men objekter forsvinner ikke.

Sortert tegning: kvadratene tegnes C, B, A — bakerst først. Kamera=[0,0,-15]
Sortert tegning: kvadratene tegnes C, B, A — bakerst først. Kamera=[0,0,-15]

Dersom vi nå flytter kameraet til posisjon [0,0,15] vil det blå kvadratet bli liggende nærmest kamera, når kvadratene er sortert basert på kameraavstand. Tegner vi nå kvadratene vil resultatet bli som vist under.

Flytter kameraet til [0,0,15]
Flytter kameraet til [0,0,15]

Her ser vi at det blå kvadratet korrekt ligger foran de andre. Legg også merke til at B og C er forskjøvet (i både x og y) siden vi ser scenen fra motsatt kant (bildet er speilet horisontalt).

Tegne både gjennomsiktige og ikke-gjennomsiktige objekter i en scene

Her gjennomgås et eksempel på en løsning på et spesifikt problem, dvs. hvordan tegne gjennomsiktige og ugjennomsiktige (Eng. opaque) og konvekse 3D-objekter som vekselsvis ligger foran hverandre og som ikke overlapper/går inn i hverandre.

Metoden under er basert på artikkelen Solving A Common WebGL Issue: Transparency [57].

Vi starter med å skille transparente (dvs. delvis gjennomsiktige) objekter fra ugjennomsiktige objekter ved å legge dem i ulike lister.

STEG 1: Tegne først alle ugjennomsiktige objekter, med følgende innstillinger:

  • Aktiver dybdetest (gjøres i clearCanvas()):
    • gl.enable(gl.DEPTH_TEST);
    • gl.depthFunc(gl.LESS) eller gl.depthFunc(gl.LEQUAL);
  • Deaktiver blending:
    • gl.disable(gl.BLEND);
  • Tegn alle ugjennomsiktige objekter (rekkefølgen er ikke viktig).
    • drawOpaqueObjects() //f.eks.

STEG 2: Tegne deretter alle transparente objekter:

  • Sorter de transparente objektene basert på avstand fra kameraet:
    • Sorteringa sikrer korrekt fargeblanding som forklart over.
  • Aktiver blending:
    • gl.enable(gl.BLEND);
    • gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  • Deaktiver depth mask:
    • gl.depthMask(false)
  • Tegn alle gjennomsiktige objekter, i rekkefølge, innerst til ytterst.
    • drawTransparentObjects() //f.eks.
    • For 3D-objekter er sortering alene ikke nok – hvert enkelt objekt må i tillegg tegnes i to omganger vha. culling. Det kommer vi tilbake til under.
  • Slå på depth mask igjen, slik at neste frame blir riktig:
    • gl.depthMask(true) betyr at dybdebufferet igjen oppdateres (true er standardverdien).

Tegne gjennomsiktige 3D-figurer

Dersom man f.eks. tegner kuber som skal ha (delvis) gjennomsiktige vegger vil kubene kunne bli seende merkelig ut. Anta at vi tegner en kube der sidene har ulike farger, som vist under.

Ugjennomsiktig kube
Ugjennomsiktig kube

Anta videre at vi ønsker å gjøre sidene delvis gjennomsiktige, f.eks. alpha=0.7. Tegner vi nå kuben vha. angitt teknikk (deaktivering av skriving til dybdebufferet) blir fargene feil. Resultatet vil bli omtrent som følger dersom vi bruker teknikken over:

Gjennomsiktig kube med feil
Gjennomsiktig kube med feil

Dette ser nesten riktig ut, men ved nærmere ettersyn ser man at det er feil. Grunnen er at kubens sider ikke nødvendigvis tegnes i korrekt rekkefølge. I følge forrige avsnitt måtte vi tegne gjennomsiktige objekter i rekkefølge. Dette må også gjøres for 3D-objekter, og kan oppnås ved å tegne baksidene til kubens polygoner (trekanter) før vi tegner forsidene til kubens polygoner.

Husk at en polygon (trekant) har en forside og en bakside, bestemt av rekkefølgen verteksene oppgis. Disse kan oppgis med eller mot klokka (CW eller CCW). Bruker vi culling på polygonene kan vi først skjule alle forsider for så å tegne kuben. Deretter skjuler vi alle baksider og tegner kuben på nytt. Vi oppnår da det vi ønsker.

Resultatet blir da som vist til høyre i figuren under. De to kubene til venstre viser mellomstegene, dvs. hvordan kuben vil se ut om vi kun tegner baksidene eller kun forsidene. Vi bruker da egentlig samme teknikk som forklart i forrige avsnitt.

Gjennomsiktig kube korrekt tegnet
Gjennomsiktig kube korrekt tegnet

Hvordan løser man dette?

Her utnytter vi det at det er forskjell på et polygons forside og bakside. Tenk deg at vi deler kuben i to:

  • Alle forsider til kubens polygoner og
  • Alle baksider til kubens polygoner

Når vi tegner kuben tegnes først baksidene og deretter forsidene ved hjelp av følgende teknikk:

  • Aktiver culling (gl.enable(gl.CULL_FACE)).
  • Tegner baksidene til objektet først, dvs. cull forsidene.
  • Tegner deretter forsidene, dvs. cull baksidene.

Dette vil da stemme med det vi tidligere sa, nemlig at gjennomsiktige objekter må tegnes i rekkefølge (innerst til ytterst).

Den utvidede oppskriften for STEG 2 (over) blir dermed:

  1. Sorter de transparente objektene på avstand fra kameraet, lengst unna først.
  2. Slå på blending og slå av skriving til dybdebufferet (gl.enable(gl.BLEND), gl.depthMask(false)).
  3. Slå på culling (gl.enable(gl.CULL_FACE)). For hvert objekt i sortert rekkefølge: tegn objektet to ganger – først med gl.cullFace(gl.FRONT), dvs. baksidene, deretter med gl.cullFace(gl.BACK), dvs. forsidene.
  4. Før neste frame (i animasjonsløkka) slås culling av og skriving til dybdebufferet slås på (gl.disable(gl.CULL_FACE), gl.depthMask(true)), slik at tilstanden er tilbake til standardverdiene før neste frame.

Merk: sorteringa håndterer rekkefølgen mellom objektene, mens culling håndterer rekkefølgen innenfor hvert enkelt objekt.

Under er det vist et utsnitt av kode som tegner noen ugjennomsiktige og noen delvis gjennomsiktige kuber. Resultat av koden blir omtrent slik:

Gjennomsiktige og ugjennomsiktige kuber
Gjennomsiktige og ugjennomsiktige kuber

Den viktigste koden:

I funksjonen drawOpaqueObjects tegnes fire kuber slik:

Legg merke til at det brukes et array som holder på de fire kubenes posisjon og farge. Deretter gjennomløpes arrayet vha. for-løkken. I hver runde i løkka settes modelmatrisen før kall på drawOpaqueCube, som tegner kuben (på vanlig måte) med gitt farge og transformasjon. Funksjonen drawOpaqueCube() ser slik ut:

I funksjonen drawTransparentObjects tegnes tre transparente kuber slik:

Avstand fra kamera beregnes slik at lista som inneholder kubenes posisjon (pos) og avstand (dist) fra kamera kan sorteres, som forklart tidligere. Kubene tegnes så i korrekt rekkefølge, dvs. først tegnes kuben som er lengst unna kamera, deretter den som er nest lengst unna osv.

Legg merke til at avstanden til kamera beregnes fra avstanden til sentrum av kubene. Dersom kubene overlapper vil derfor denne metoden kunne feile. Metoden er dermed ikke generell, men vil kunne fungere for ikke-overlappende objekter.

Funksjonen drawTransparentCube ser slik ut. Legg merke til hvordan culling utnyttes for at kubenes gjennomsiktighet skal bli korrekt.

Merk: Kodeeksemplet er ikke komplett, men gir allikevel en indikasjon på rekkefølgen man tegner de ulike delene i.

Det er imidlertid mye mer å si om gjennomsiktighet, buffere, dybdetest og fargeblanding som ikke omtales videre her. Se lenkene under for mer informasjon.

Ingen kommentarer:

Legg inn en kommentar