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:
|
|
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. |
|
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); 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. |
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.
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)
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)
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 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);
}
/**
* 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);
}
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);
}
Det er imidlertid mye mer å si om teksturer og gjennomsiktighet som ikke omtales videre her.
Kilder:
-
https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/depthFunc
-
https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/blendFunc
-
https://www.shapediver.com/blog/solving-a-common-webgl-issue-transparency-fixed
-
https://medium.com/david-guan/alpha-blending-and-webgl-823d86de00d8
Ingen kommentarer:
Legg inn en kommentar