Modul 4.3 Dybdebuffer, alpha blending

Framebuffer, dybdebuffer og fargebuffer

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:

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

Jc96aGHdER_EAbj5nYK2ErCk-UCpvtcLmh9gBeZ7OLnXKArSsHFQM61XUjglPzZjlmuDTOdnHho4UpPsLhjTQYEQw0sdKDVoWQWzyHuKqfy_w_MuLugxsuZrlLZuqOwx_X_rBrNhKsF2zrfYBvazZ97SIAlc0HxXikMOcj3lUIOonRop6BMbZBcio2glj7xklqnveQ

Dybdebuffer / z-buffer

Dybdebufferet inneholder en dybdeverdi per fragment. 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. 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. 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.

rSCHrbwhpz-um71tDQGl7a4bfQM40ioEJJoFY7WTFh6u5dBD94MSt0dV58l4S4uo4XCNsrq25IH_FgZ0kUCo2xXY05z2dWOA6Gyx2850SOt5DatHzWr7JKMKTz46FrGxXFKX14kK39EzKtcszYFRVUW0moVJD69igtqQJg6LP2TYFLwJS5UgaBKGtn8aY54Qct69aw

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);

Man ønsker som regel å slå på dybdetest, men det kan være unntak. I enkelte tilfeller ønsker vi ikke at dybdebufferet skal oppdateres selv om vi endrer fargebufferet. Dette styres av deptMask, slik:

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

Eksempel:

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

Det vil si at spesifiserte Z-verdier (ikke bland denne Z-verdien med 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.

Et bilde som inneholder tekst, utendørsobjekt Automatisk generert beskrivelse      Et bilde som inneholder tekst, paraply, tilbehør, utendørsobjekt Automatisk generert beskrivelse

Gjennomsiktighet

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

Fargeblanding styres av programmereren og kan gjøres på mange måter, bl.a. vha. ALPHA-verdien til fragmentfargen.

Fargeblanding må aktiveres og alpha-verdi angis per verteks. I tillegg kan vi angi en blandingsfunksjon (depthFunction), 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);

Andre eksempler:

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

Dette er bare noen eksempler. Se https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/blendFunc for mer informasjon.

Begreper:

Source fragment: fragmentet som tegnes nå, dvs. det som kommer fra fragmentshaderen.

Destination fragment: fragmentet som allerede er i frame bufferet.

Fargeblanding gjøres vha. følgende funksjon: 

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_ADD eller FUNC_SUBTRACT

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] (fragmentet som tegnes)
Destination color = [Rd, Gd, Bd, Ad] = [0, 1, 0, 1] (fragmentet som ligger i frame bufferet)

Blandingen av source og destination blir da:

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: Alpha-verdien gir ikke farge i seg selv, men brukes til å angi hvordan fargene skal blandes. Den er derfor ikke en del av resultatfargen.

Resultatfarge = 1,1,0 (gul)

gydL87ryn0NOmdfJzbsyHmEqY9RflnVIECtcQjGeV11uQKXJ7gDFmzUxgETlBx1Vgqy0D4WkWJunE_7Da3IjxPYEgPfvDnlw5vawK3Vph-PfMBLsecTS7_IrbzdjHqDWwycFNH6y8gk8TyaD46CVqGskBEBIUd0geYPgWTP9cS7v2ljmfUvTAUAzyZPd220FSh26tw

Eksempel 2:

Anta at vi bruker følgende verdier:

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

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

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]

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.3 * 0.3 = 0.58

Resultatfarge = 0.3, 0.0, 0.7 (lilla)

FvLjE8kyzpqYAMlWXqrndJB6gqU8pfjhlzIdOLofgFKI5DVFzYp9GOTj-DcevVItE4SAW0_1AhXtZnlFct7P0eEx8QuAaREulDdhtwcCT0yze7f0J911IuN8qJtspjvjbExu_wl7ZIrSi9bl0QLQksK85QlrQrlX2bRePpHlF_7GXsM954O6C8pVS4eXNACAFCBs7w


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" skrevet av M. Oppitz (2019) og dialog med ChatGPT.

Start med å skille transparente (dvs. delvis gjennomsiktige) objekter fra ugjennomsiktige objekter, legg dem f.eks. 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.deptFunc(gl.LESS) eller gl.deptFunc(gl.LEQUAL);
  • Deaktiver blending:
    • gl.disable(gl.BLEND);
  • Tegn alle ugjennomsiktige objekter (rekkefølgen er ikke viktig).
    • renderOpaqueObjects() //f.eks.

STEG 2: Tegne deretter alle transparente objekter:

  • Sorter de transparente objektene basert på avstand fra kameraet.
    • Sorteringa sikrer korrekt fargeblanding siden fargene til transparente objekter må blandes med bakgrunnen, inkludert objekter som måtte ligge bak.
  • Aktiver blending:
    • gl.enable(gl.BLEND) 
    • gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  • Deaktiver depth mask: 
    • gl.depthMask(false), som betyr at dybdebufferet IKKE oppdaters selv om fragment overskrives.
      • Dette for å hindre transparente objekter fra å blokkere andre transparente objekter som ev. skal ligge bak.
  • Tegn alle gjennomsiktige objekter, i rekkefølge, innerst til ytterst.
    • renderTransparentObjects() //f.eks.
  • Aktiver depth mask igjen slik at ugjennomsiktige objekter tegnes korrekt: 
    • gl.depthMask(true), betyr at dybdebufferet oppdaters dersom fragment overskrives (true er standardverdi).

Dette fungerer stort sett fint, men dersom man f.eks. tegner kuber som skal ha gjennomsiktige vegger vil kubene bli seende merkelig ut. Anta at vi tegner en kube der sidene har ulike farger, som vist under.












Samme kube sett fra ulike vinkler.

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






Dette ser ikke helt rett ut. Grunnen er at kubens sider ikke nødvendigvis tegnes i korrekt rekkfø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 klokkal (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.








Hvordan løser man dette?

Her er det viktig å minne om at et polygon/trekant har en forside og en bakside, bestemt av rekkefølgen verteksene oppgis. Rekkefølgen verkeksene til et polygon/trekant oppgis i er enten Clock Wise (CW) eller Counter Clock Wise (CCW). Dette sammen med culling kan dermed utnyttes her.

Tenk deg at vi deler kuben i to:

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

Tenger vi nå først baksidene og deretter forsidene ved hjelp av følgende teknikk:

  • Aktiver backside culling
  • Tenger først 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).

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


















Tegner ugjennomsiktige og transparente kuber om hverandre.

Den viktigste koden: 

function draw(currentTime, renderInfo, camera) {
clearCanvas(renderInfo.gl);

// Tegner koordinatsystemet:
drawCoord(renderInfo, camera);

//STEG 1: Tegner alle UGJENNOMSIKTIGE (OPAQUE) objekter først:
//* Deaktiverer blending:
renderInfo.gl.disable(renderInfo.gl.BLEND);
//* Tegner:
drawOpaqueObjects(renderInfo, camera);

//STEG 2: Tegner alle GJENNOMSIKTIGE objekter, i rekkefølgen innerst til ytterst:
//* Enabler blending:
renderInfo.gl.enable(renderInfo.gl.BLEND);
renderInfo.gl.blendFunc(renderInfo.gl.SRC_ALPHA, renderInfo.gl.ONE_MINUS_SRC_ALPHA);
//* Slår AV depthMask (endrer dermed ikke DEPTH-BUFFER):
renderInfo.gl.depthMask(false);
//* Tegner:
drawTransparentObjects(renderInfo, camera);
//* Slår PÅ depthMask (dybdebufferet oppdateres):
renderInfo.gl.depthMask(true);
}

I funksjonen drawOpaqueObjects tegnes fire kuber slik:

function drawOpaqueObjects(renderInfo, camera) {
let modelMatrix = new Matrix4();
// Liste med ønskede posisjoner og farger for ikke-gjennomsiktige objekter/kuber:
let cubesToDraw = [];
cubesToDraw.push(
{pos: {x: 1, y: 0, z: 9}, color: {r: 0.2, g: 0.2, b: 0.2, a: 1.0}},
{pos: {x: 1, y: 0, z: 3}, color: {r: 1.0, g: 0.5, b: 0.0, a: 1.0}},
{pos: {x: 1, y: 0, z: -3}, color: {r: 0.5, g: 0.0, b: 0.5, a: 1.0}},
{pos: {x: 1, y: 0, z: -9}, color: {r: 0.0, g: 0.0, b: 1.0, a: 1.0}},
);

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

Legg merke til at det brukes et array som holder på de fire kubenes posisjon og farge. Deretter gjenomlø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:

function drawOpaqueCube(renderInfo, camera, modelMatrix, color) {
// Aktiver shader:
renderInfo.gl.useProgram(renderInfo.cubeShader.program);

let color1 = [color.r, color.g, color.b, color.a];
connectColorUniform(renderInfo.gl, renderInfo.cubeShader, color1);

// Kople posisjon og farge-attributtene til tilhørende buffer:
connectPositionAttribute(renderInfo.gl, renderInfo.cubeShader, renderInfo.cubeBuffers.position);

// Lager en kopi for å ikke påvirke kameramatrisene:
let viewMatrix = new Matrix4(camera.viewMatrix);
let modelviewMatrix = viewMatrix.multiply(modelMatrix); // NB! rekkefølge!
renderInfo.gl.uniformMatrix4fv(renderInfo.cubeShader.uniformLocations.modelViewMatrix, false, modelviewMatrix.elements);
renderInfo.gl.uniformMatrix4fv(renderInfo.cubeShader.uniformLocations.projectionMatrix, false, camera.projectionMatrix.elements);

renderInfo.gl.drawArrays(renderInfo.gl.TRIANGLES, 0, renderInfo.cubeBuffers.vertexCount);
}
I funksjonen drawTransparentObjects tegnes tre transparente kuber slik:

/**
* Tegner gjennomsiktige kuber. Disse må tegnes i riktig rekkefølge.
* MERK: Bruker også et annet shaderpar for å tegne disse (som bruker verteksfarger):
*/
function drawTransparentObjects(renderInfo, camera) {
let modelMatrix = new Matrix4();

// Liste med ønskede posisjoner for transparente objekter/kuber:
let positions = [];
positions.push(
{x: 0, y: 0, z: 6},
{x: 0, y: 0, z: -6},
{x: 0, y: 0, z: 0}
);
// Liste som inneholder kubenes posisjon (pos) og avstand til kamera (dist):
let cubesToDraw = [];
cubesToDraw.push(
{pos: positions[0], dist: distanceFromCamera(camera, positions[0])},
{pos: positions[1], dist: distanceFromCamera(camera, positions[1])},
{pos: positions[2], dist: distanceFromCamera(camera, positions[2])},
);

// Sorterer transparente objekter basert på avstanden fra kamera.
// Merk: Bruker sentrum av objektet, som ikke nødvendigvis alltid blir helt korrekt.
cubesToDraw.sort((distFromCam1, distFromCam2) => compare(distFromCam1.dist, distFromCam2.dist));

// Tegner de sorterte objektene i rekkefølge, innerst til ytterst:
for (let i = 0; i < cubesToDraw.length; i++) {
modelMatrix.setIdentity();
modelMatrix.translate(cubesToDraw[i].pos.x, cubesToDraw[i].pos.y, cubesToDraw[i].pos.z);
drawTransparentCube(renderInfo, camera, modelMatrix);
}
}

/**
* Funksjonen sammenlikner to nærliggende verdier i arrayet som sorteres.
* Returnerer 1, -1 eller 0 avhengig av sammenlikningen.
*/
function compare( dist1, dist2 ) {
if (dist1 < dist2 ){
return 1;
}
if ( dist1 > dist2 ){
return -1;
}
return 0;
}

/**
* Merk: ** betyr eksponent. Eks. 2 ** 3 = 2 * 2 * 2 = 8
*/
function distanceFromCamera(camera, positions) {
let x = positions.x;
let y = positions.y;
let z = positions.z;
return Math.sqrt((camera.camPosX - x) ** 2 + (camera.camPosY - y) ** 2 + (camera.camPosZ - z) ** 2);
}
Legg merke til hvordan kubenes avstand fra kamera beregnes slik at lista som inneholder kubenes posisjon (pos) og avstand (dist) fra kamera kan sorteres. Lista sorteres på avstanden til kamera slik at de tegnes i korrekt rekkefølge, dvs. først tegnes kuben som er lengst unna kamera, deretter den som er nest lengst unna osv.  Funksjonen distanceFromCamera beregner og returnerer avstanden fra gitt kubeposisjon til kameraet. compare() - funksjonen er knyttet til sort()-funksjonen som kalles på cubesToDraw-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 nærliggende 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

Legg merke til at av avstanden til kamera bergnes 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.
function drawTransparentCube(renderInfo, camera, modelMatrix) {
// Aktiver shader:
renderInfo.gl.useProgram(renderInfo.transparentCubeShader.program);

// Kople posisjon og farge-attributtene til tilhørende buffer:
connectPositionAttribute(renderInfo.gl, renderInfo.transparentCubeShader, renderInfo.cubeBuffers.position);
connectColorAttribute(renderInfo.gl, renderInfo.transparentCubeShader, renderInfo.cubeBuffers.color);

// Lager en kopi for å ikke påvirke kameramatrisene:
let viewMatrix = new Matrix4(camera.viewMatrix);
let modelviewMatrix = viewMatrix.multiply(modelMatrix); // NB! rekkefølge!
renderInfo.gl.uniformMatrix4fv(renderInfo.transparentCubeShader.uniformLocations.modelViewMatrix, false, modelviewMatrix.elements);
renderInfo.gl.uniformMatrix4fv(renderInfo.transparentCubeShader.uniformLocations.projectionMatrix, false, camera.projectionMatrix.elements);

// Bruker culling for korrekt blending:
renderInfo.gl.frontFace(renderInfo.gl.CCW); // Angir vertekser CCW.
renderInfo.gl.enable(renderInfo.gl.CULL_FACE); // Aktiverer culling.

//Tegner baksidene først:
renderInfo.gl.cullFace(renderInfo.gl.FRONT); // Skjuler forsider.
renderInfo.gl.drawArrays(renderInfo.gl.TRIANGLES, 0, renderInfo.cubeBuffers.vertexCount);

//Tegner deretter forsidene:
renderInfo.gl.cullFace(renderInfo.gl.BACK); // Skjuler baksider.
renderInfo.gl.drawArrays(renderInfo.gl.TRIANGLES, 0, renderInfo.cubeBuffers.vertexCount);

//Dette blir feil fordi kube er transparent. Vi må tegne baksidene først, og deretter forsidene:
//renderInfo.gl.drawArrays(renderInfo.gl.TRIANGLES, 0, renderInfo.cubeBuffers.vertexCount);
}
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 teksturer og gjennomsiktighet som ikke omtales videre her.

Kilder:

Ingen kommentarer:

Legg inn en kommentar