Introduksjon
Hittil har vi sett hvordan farge kan knyttes til hver enkelt verteks og på den måten fargelegge polygoner/trekanter. Skal man lage et spill eller animasjon med tiltalende grafikk og figurer vil det være svært tungvint å skulle fargelegge polygonene/trekantene til modeller manuelt.
Ved hjelp av teksturer kan vi gjøre våre 3D modeller mer tiltalende ved å ”kle” dem med bilder. I WebGL kan bilder legges på flatene som utgjør 3D-modellene. Man kan for eksempel legge et bilde på et rektangel bestående av to trekanter – slike bilder kalles tekstur.
Prosessen kalles texture mapping («teksturering») og innebærer at fargen til bildets piksler tilordnes fragmentene som genereres i rasteriseringssteget i pipelinen. Pikslene som utgjør teksturbildet kalles texel (texture element) og består av en RGB(A)-verdi.
Ved å kombinere bruk av teksturer, farger og lys kan man lage ulike spesialeffekter som f.eks. blande ulike teksturer, blande en tekstur med en farge filtrert på ulike måter, simulere bølger i vann, generere en «skybox» eller «skydome», diverse partikkeleffekter m.m.
WebGL støtter følgende bildeformater: .jpg, .png.
Teksturkoordinater
Teksturkoordinater, som også kalles st- eller uv-koordinater, spesifiserer et (2D) punkt i teksturen.
En verteks består minimum av en xyz-posisjon. Som vist tidligere kan man også knytte en farge til en verteks i form av en RGBA-verdi. Skal man kle en 3D-modell med en tekstur knyttes en 2D-teksturkoordinat til hver verteks.
Husk at en tekstur er et 2D-objekt som legges på et 3D-modell. Teksturkoordinatene lagres sammen med posisjonsdataene for hver enkelt verteks. Dersom vi har et rektangel, bestående av to trekanter, som vi ønsker dekt med en tekstur (for eksempel en .png) vil verdiene til u og v-koordinatene variere mellom 0 og 1.
Figuren under viser hvordan uv-koordinatsystemet ser ut for to forskjellige 2D teksturer.
I nedre venstre hjørne er uv = 0,0 mens i øvre høyre hjørne er uv = 1,1. Dette gjelder også dersom teksturen ikke er kvadratisk.
WebGL støtter også såkalt «cube map» teksturer. En slik «kubetekstur» består egentlig av 6 enkeltteksturer – en for hver side i en kube. En kubetekstur kan f.eks. brukes til å lage refleksjoner fra omgivelsene i skinnende objekter som f.eks. en skinnende kule. Dette kalles «environment mapping». Kubeteksturer kan også brukes til å lage en såkalt «skybox» som er en teksturert kube som omslutter vår 3D-verden. Teknikken går ut på at innersiden av kuben kles med tekstur, f.eks. blå himmel med skyer. Kamera plasseres inni kuben og når kamera flyttes, flyttes også skyboksen tilsvarende. Man vil dermed aldri nå veggene i kuben uansett hvor mye man beveger kamera som igjen vil gi inntrykk av en «uendelig» horisont.
Bildestørrelser
I de fleste tilfeller bør teksturbilder som brukes i WebGL-applikasjoner ha bredde og høyde, i antall piksler, som tilsvarer 2n x 2m der n og m er positive heltall. Et bilde kan f.eks. være 128 x 256 (27 x 28), 256 x 256, 1025 * 512 osv. piksler stort.
Både standard OpenGL 2.0 og OpenGL ES (& WebGL) har imidlertid støtte for NPOT (non-power-of-two) bilder, dvs. bilder med vilkårlig størrelse. I forbindelse med WebGL er det en del begrensninger og i de fleste tilfeller anbefales det derfor å bruke bildestørrelser der bredde og høyde tilsvarer 2n x 2m piksler.
Steg for å bruke tekstur
Følgende må gjøres for å teksturere en modell/figur:
- Både verteks og fragmentshaderne må endres slik at de kan motta og håndtere teksturdata. Her presenteres oppdaterte shaderfunksjoner:
Oppdatert verteksshader:
<script id="texture-vertex-shader" type="x-shader/x-vertex">
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
attribute vec2 aVertexTextureCoordinate;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying lowp vec2 vTextureCoordinate;
varying lowp vec4 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vTextureCoordinate = aVertexTextureCoordinate;
vColor = aVertexColor;
}
</script>
Oppdatert fragmentshader:
<script id="texture-fragment-shader" type="x-shader/x-fragment">
varying lowp vec4 vColor;
varying lowp vec2 vTextureCoordinate;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vec2(vTextureCoordinate.s, vTextureCoordinate.t));
}
</script>
Legg merke til at verteksshaderen tar i mot posisjon-, farge- og teksturdata for hver verteks. De nye teksturrelaterte elementene er markert og vil bli nærmere forklart etter hvert.
- Last tekstur, klargjør buffer:
- Definere uv-koordinater for hver verteks til modellen som skal tekstureres.
- Laste aktuell bildefil som utgjør teksturen.
- Når bildet er ferdig lastet opprettes et tekstur-objekt vha. gl.createTexture(). Deretter utføres gl.bindTexture() og gl.texImage2D() for å laste teksturen til GPUen/shaderen.
- Utføre gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) for å unngå at bildet vises opp-ned.
- Sette diverse teksturparametre som f.eks. bestemmer hva som skal skje dersom bildet er mindre eller større enn flate som skal tekstureres.
- Opprette et teksturbuffer vha. gl. createBuffer(). Utføre gl.bindBuffer() og gl.bufferData() for å fylle bufret med uv-data (på samme måte som for posisjon og farge).
- Tegning (dvs. i draw()):
- finn referansen til teksturparametret i shaderen vha. gl.vertexAttribPointer() og aktiver dette vha. gl. enableVertexAttribArray().
- Aktiver «teksturenhet», vha. gl.activeTexture(gl.TEXTURE0)
- Opprett et «sampler»-objekt og send til shader.
Teksturering av et rektangel
Her ser vi på hvordan man kan teksturere et enkelt rektangel bestående av to trekanter.
Figuren under viser prosessen:
Et bilde av et tre legges på et rektangel.
Figuren viser opprinnelig .png fil som brukes som tekstur. Til høyre ser vi rektanglet, bestående av to trekanter, som skal «kles» med tekstur. Her er xyz-posisjon og uv-koordinater til hver verteks angitt. Legg merke til at vi ser 3D-koordinatsystemet fra «toppen», rektanglet ligger med andre ord i xz-planet. Rød linje er x-aksen, grønn er y-aksen mens blå linje er z-aksen.
Koden
Bildefilen(e) må lastes ned fra serveren før den kan brukes som tekstur. Slike filer (.png, .jpg e.l.) legges typisk i egen mappe. I eksemplet under er det antatt at .png fila ligger i en egen textures-mappe. Her brukes en egen klasse, ImageLoader, som igjen bruker Javscript-klassen Image, til nedlasting av bilder. Denne er laget slik at man kan laste ned et eller flere bilder i samme operasjon ved å bruke Promise. ImageLoader-klassen har en metode, load(), og ser slik ut:
/**
* Laster et eller flere bilder og kaller på gitt callback-funksjon når alle biler er lastet.
* Bruker Promise.
*/
export class ImageLoader {
load(onLoad, urls) {
const promises = [];
const images = [];
for(let i = 0; i < urls.length; i++) {
promises.push(
//Kaller resolve() etter at hvert enkelt bilde er lastet ned:
new Promise( (resolve, reject) => {
images[i] = new Image();
images[i].src = urls[i]; //HER starter nedlasting.
images[i].onload = () => {
resolve();
};
images[i].onerror = () => {
reject();
};
})
);
}
Promise.all(promises)
.then( () => {
onLoad(images);
})
.catch( () => {
console.log('Feil bildenavn...!');
});
}
}
I main() skjer følgende:
/**
* Et WebGL-program som tegner et teksturert rektangel.
*/
export function main() {
// Oppretter et canvas for WebGL-tegning:
const canvas = new WebGLCanvas('myCanvas', document.body, 960, 640);
// Starter med å laste teksturer:
let imageLoader = new ImageLoader();
let textureUrls = ['./textures/bricks1.png'];
imageLoader.load((textureImages) => {
const textureImage = textureImages[0];
if (isPowerOfTwo1(textureImage.width) && isPowerOfTwo1(textureImage.height)) {
// Fortsetter:
// Hjelpeobjekt som holder på objekter som trengs for rendring:
const renderInfo = {
gl: canvas.gl,
baseShader: initBaseShaders(canvas.gl),
textureShader: initTextureShaders(canvas.gl),
coordBuffers: initCoordBuffers(canvas.gl),
squareBuffers: initSquareTextureAndBuffers(canvas.gl, textureImage),
currentlyPressedKeys: [],
lastTime: 0,
fpsInfo: { // Brukes til å beregne og vise FPS (Frames Per Seconds):
frameCount: 0,
lastTimeStamp: 0
}
};
const camera = new Camera(renderInfo.gl, renderInfo.currentlyPressedKeys);
animate( 0, renderInfo, camera);
} else {
console.log("Feil bildestørrelse");
}
}, textureUrls);
}
function initSquareTextureAndBuffers(gl, textureImage) {
let positions = [
-1, 1, 0,
1, 1, 0,
-1, -1, 0,
1, -1, 0
];
let colors = [
0, 1, 0, 1.0,
0, 1, 0, 1.0,
0, 1, 0, 1.0,
0, 1, 0, 1.0,
];
let textureCoordinates = [
0, 0,
5, 0,
0, 5,
5, 5
];
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
//Texture:
const rectangleTexture = gl.createTexture();
//Teksturbildet er nå lastet fra server, send til GPU:
gl.bindTexture(gl.TEXTURE_2D, rectangleTexture);
//Unngaa at bildet kommer opp-ned:
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); //NB! FOR GJENNOMSIKTIG BAKGRUNN!! Sett også gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
//Laster teksturbildet til GPU/shader:
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage);
//Teksturparametre:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.bindTexture(gl.TEXTURE_2D, null);
const textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
return {
position: positionBuffer,
color: colorBuffer,
texture: textureBuffer,
textureObject: rectangleTexture,
vertexCount: positions.length/3,
};
}
- Kallet til gl. pixelStorei(…) sørger for at bildet vises riktig vei. Kallet på denne har innvirkning på neste kall, som er gl.texImage2D(), og sørger for at bildet snus rundt horisontalaksen. Dette må gjøres siden teksturkoordinatsystemet som brukes av WebGL/OpenGL ES (og Open GL) ikke matcher koordinatsystemet som brukes av Image-klassen. Sett fra WebGL har teksturen «origo» (0,0) i nedre venstre hjørne mens Image-objektet har origo i øvre venstre hjørne.
- Funksjonen gl.texImage2D(...) laster også teksturen til GPUen. Denne funksjonen finnes i flere varianter men den som er brukt her er kanskje den vanligste. Parametrene betyr:
gl.TEXTURE_2D: indikerer at det er en 2D tekstur som benyttes.0: mipmap level. Omtales senere.gl.RGBA: internt format.gl.RGBA: format. Tredje og fjerde argument må alltid ha samme verdi i WebGL. Disse indikerer hva hver texel (piksel) i teksturen består av (her RGBA). Her kan alternativt gl.RGB m.fl. varianter benyttes.gl.UNSIGNED_BYTE: indikerer størrelsen på hver «fargekanal», her brukes en unsigned byte for Rød, en for Grønn osv. Her betyr det at hver texel opptar 4 byte.
textureImage: Bildet som er lastet ned.
Etter at teksturen er sent til GPUen vha. gl.texImage2D() lagres det i GPUen sitt minne. Dette kan være direkte tilknyttet GPUen eller et spesielt område av standard systemminnet som er dedikert GPUen
- Videre settes teksturparametre vha. gl.texParameteri(). I det første kallet, gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST), indikerer vi hvilken farge pikslene (fragmentene) skal få dersom vi forstørrer modellen. Når vi «zoomer» inn modellen betyr det at hver texel dekker flere piksler. Parametret gl_NEAREST indikerer at pikslene får fargen til nærmeste texel. Dette er den enkleste og raskeste metoden. Resultatet er at jo mer man «zoomer» inn på en modell jo mer hakket blir bildet (kalles «pixelation»). Alternativt kan man bruke gl.LINEAR som vil gi en mer diffus modell jo mer man zoomer inn. Tilsvarende gjelder for det andre kallet til gl.texParameteri(). Alternativet til å bruke disse teknikkene er å bruke mipmapping. Se (Anyuru, 2012) for mer info.
- I neste linje kopler vi fra gjeldende tekstur vha. gl.bindTexture(gl.TEXTURE_2D, null).
- I de neste linjene opprettes et bufferobjekt som lastes med uv-data, dvs. aktuelle teksturkoordinater for alle verteksene til rektanglet.
- Til slutt returneres et Javascript-objekt med all relevant informasjon.
Tegne rektanglet
Via draw() og drawSquare() tegnes det teksturerte rektanglet:
/**
* Tegner!
*/
function draw(currentTime, renderInfo, camera) {
clearCanvas(renderInfo.gl);
drawSquare(renderInfo, camera);
}
function drawSquare(renderInfo, camera) {
// Aktiver shader:
renderInfo.gl.useProgram(renderInfo.textureShader.program);
// Kople posisjon og farge-attributtene til tilhørende buffer:
connectPositionAttribute(renderInfo.gl, renderInfo.textureShader, renderInfo.squareBuffers.position);
connectColorAttribute(renderInfo.gl, renderInfo.textureShader, renderInfo.squareBuffers.color);
connectTextureAttribute(renderInfo.gl, renderInfo.textureShader, renderInfo.squareBuffers.texture, renderInfo.squareBuffers.textureObject);
let modelMatrix = new Matrix4();
modelMatrix.setIdentity();
camera.set();
let modelviewMatrix = new Matrix4(camera.viewMatrix.multiply(modelMatrix)); // NB! rekkefølge!
renderInfo.gl.uniformMatrix4fv(renderInfo.textureShader.uniformLocations.modelViewMatrix, false, modelviewMatrix.elements);
renderInfo.gl.uniformMatrix4fv(renderInfo.textureShader.uniformLocations.projectionMatrix, false, camera.projectionMatrix.elements);
renderInfo.gl.drawArrays(renderInfo.gl.TRIANGLE_STRIP, 0, renderInfo.squareBuffers.vertexCount);
}
/**
* Kopler til og aktiverer teksturkoordinat-bufferet.
*/
function connectTextureAttribute(gl, textureShader, textureBuffer, textureObject) {
const numComponents = 2; //NB!
const type = gl.FLOAT;
const normalize = false;
const stride = 0;
const offset = 0;
//Bind til teksturkoordinatparameter i shader:
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.vertexAttribPointer(
textureShader.attribLocations.vertexTextureCoordinate,
numComponents,
type,
normalize,
stride,
offset);
gl.enableVertexAttribArray(textureShader.attribLocations.vertexTextureCoordinate);
//Aktiver teksturenhet (0):
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textureObject);
//Send inn verdi som indikerer hvilken teksturenhet som skal brukes (her 0):
let samplerLoc = gl.getUniformLocation(textureShader.program, textureShader.uniformLocations.sampler);
gl.uniform1i(samplerLoc, 0);
}
Her hentes en referanse til shaderparametret ‘aVertexTextureCoordinate’ som må være definert i verteksshaderen som vist under:
<script id="texture-vertex-shader" type="x-shader/x-vertex">
attribute vec4 aVertexPosition;
attribute vec4 aVertexColor;
attribute vec2 aVertexTextureCoordinate;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying lowp vec2 vTextureCoordinate;
varying lowp vec4 vColor;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
vTextureCoordinate = aVertexTextureCoordinate;
vColor = aVertexColor;
}
</script>
Shaderparametret aktiveres vha. gl.enableVertexAttribArray(). Deretter aktiveres gjeldende tekstur vha. gl.activeTexture(gl.TEXTURE0). Parametret gl.TEXTURE0 koples til aktuell tekstur. Bruk av gl.TEXTURE0 henger også sammen med «sampleren» som initieres og brukes på de neste linjene. uSampler er definert i fragmentshaderen, som ser slik ut:
<script id="texture-fragment-shader" type="x-shader/x-fragment">
varying lowp vec4 vColor;
varying lowp vec2 vTextureCoordinate;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vec2(vTextureCoordinate.s, vTextureCoordinate.t));
}
</script>
Vi ser at verdien 0 sendes til shaderens uSampler-parameter vha. gl.uniform1i(samplerLoc, 0). Denne bestemmer hvilken «teksturenhet» (texture unit) som skal benyttes (her 0). Dersom verdien til uSampler er lik 0 brukes gl.TEXTURE0. Disse to verdiene henger derfor sammen. Vi kan tenke på en «teksturenhet» som en referanse til en tekstur. Ved hjelp av gl.TEXTUREn og samplerverdien kan man blande farger fra flere teksturer – såkalt multiteksturering.
I verteksshaderen settes vTextureCoordinate (en varying) lik innkommende teksturkoordinat. I fragmentshaderen er tilsvarende varying også deklarert. Verdien til denne er interpolert på veien fra verteks- til fragmentshaderen. Verdien til denne brukes til å hente texels, dvs. farge, fra teksturen. Dette gjøres ved hjelp av texture2D() funksjonen i fragmentshaderen.
Blande farge og tekstur
Det er også mulig å blande farger fra teksturen med verteksfarger dersom disse er definert. Dette er neste ferdig implementert i foregående kodeeksempel. Det eneste som mangler er at vi tar i bruk vColor i fragmentshaderen, slik:
<script id="texture-fragment-shader" type="x-shader/x-fragment">
varying lowp vec4 vColor;
varying lowp vec2 vTextureCoordinate;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vec2(vTextureCoordinate.s, vTextureCoordinate.t)) * vColor;
}
</script>
Her multipliseres teksturfarge med fragmentfargen slik at disse fargene blandes. Resultatet tilordnes gl_FragColor. Dette gir (f.eks.) følgende resultat:
Teksturen er blandet med verteksfargene.
Vi ser at opprinnelig tekstur/bilde nå er noe mørkere siden den er blandet med verteksfargene.
Bruk av flere shaderpar
I mange tilfeller er det nødvendig å bruke ulike shaderpar (dvs. en verteks- og en fragmentshader) for ulike modeller som skal tegnes. En teksturert kube er kun avhengig av posisjon og uv-koordinater mens f.eks. tegning av koordinatsystemet krever en shader som tar i mot en posisjon og en farge.Se tilhørende kodeeksempler.
Ingen kommentarer:
Legg inn en kommentar