Iluminando una escena con mapeo de sombras usando WebGL
Utilice el cursor para cambiar el ángulo de la fuente de luz. Utilice el botón izquierdo del mouse (y manténgalo presionado) para girar el modelo. A la izquierda: cambiar el color. A la derecha: eligir el modelo.
Si quiere probarlo, aquí está el link de descarga de la carpeta .zip que contiene los archivos que hacen funcionar este ejemplo. Los archivos contenidos en esta carpeta son:
Contenido: - Los modelos 3D - Entendiendo el mapeo de sombras en WebGL - Configuración - El loop principal - La función shadowMapRender - La función normalRender - Los shaders de vértices (mapa de sombras) - Los shaders de fragmentos (mapa de sombras) - Los shaders de vértices (dibujado normal) - Los shaders de fragmentos (dibujado normal)
Las partes más importantes de este ejemplo de WebGL son: dibujar en la textura que se va a utilizar como mapa de sombras, dibujar la escena y escribir un shader que va a utilizar el mapa de sombras, este shader también va a utilizar la información de los vectores normales de los vértices del modelo, esto para lograr el efecto de iluminación difusa y de iluminación especular.
Los modelos 3D.
El primer modelo, el modelo de la cabeza de mono, es un modelo que se puede encontrar en Blender, un software
de gráficos de computadora 3D.
El nombre del modelo es Suzanne, puede aprender más al respecto
aquí.
El segundo modelo, el modelo del conejo, es un muy famoso escaneo 3D de una estatuilla de
cerámica de un conejo desarrollado en la Universidad de Stanford. Puede aprender más al respecto
aquí.
El tercer modelo, el modelo de la tetera, es también un modelo 3D muy famoso,
conocido comúnmente como la tetera de Utah. Más información
aquí.
Estos 3 modelos se incluyen en la carpeta .zip que puede descargar al principio de esta página.
Hay tres archivos OBJ dentro de la carpeta, uno para cada modelo, puede verlos en cualquier
software que pueda abrir archivos de OBJ.
Entendiendo el mapeo de sombras en WebGL.
Si quiere una buena explicación del concepto de mapeo de sombras, consulte este artículo:
http://www.opengl-tutorial.org/es/intermediate-tutorials/tutorial-16-shadow-mapping/
La información en ese artículo es un buen recurso que utilicé para entender el mapeo de sombras de
mejor manera. Ese artículo explica la teoría del mapeo de sombras y explica cómo implementarlo en
OpenGL, que es similar a cómo se hace en WebGL.
Cuando se dibuja un mapa de sombras, la escena tiene que ser dibujada desde el punto de vista
de la fuente de luz, es decir, la cámara se coloca donde está la fuente de luz y esta se dirige
a la escena a la que se le quieren añadir sombras.
Lo que se busca es saber qué tan lejos de la fuente de luz está cada punto en la escena. Esto
se representa con la profundidad de cada fragmento, y esa es la información que se almacena en
el mapa de sombras. Para obtener y utilizar esta información dibujamos a una textura que va a
almacenar el valor de profundidad en cada píxel.
En el dibujado final vamos a usar esta textura para hacer algunas comparaciones. Comparamos la
profundidad de los fragmentos que se dibujan en ese momento con la profundidad de los píxeles
en la textura que se dibujaron con anterioridad. Si el fragmento actual está más alejado de la
luz que el píxel con el que se compara (lo que significa que el valor de profundidad del fragmento
es mayor que el del píxel en la textura), eso significa que el fragmento está cubierto o está en sombra.
En este ejemplo vamos a utilizar la extensión de WebGL denominada WEBGL_depth_texture.
Puede aprender más sobre esta extensión
aquí.
Nota: Ya que es una extensión, es posible que no esté disponible en todos los dispositivos.
Es posible lograr todo lo que se muestra en este tutorial sin usar esta extensión, usando una
textura común como el frame buffer en su lugar, la diferencia será que una textura
común solo permite colores de 8 bits, esto significa que la única diferencia real será que
la resolución de la información de profundidad será menor, pero el proceso sigue siendo el
mismo y puede entregar resultados similares si se realiza correctamente.
Al usar esta extensión podemos dibujar a una textura que va a tener sólo la información
de profundidad de la escena. A continuación se presentan algunos ejemplos de cómo el dibujado
final se compara con el mapa de sombra. A la izquierda está el dibujado final y a la derecha
está su correspondiente mapa de sombras. Así es como se ve cuando la fuente de luz está directamente
por encima de los modelos:
El mapa de sombras es de color rojo porque cuando usamos WEBGL_depth_texture la información de
profundidad se almacena en el canal rojo de la textura. Preste atención a esto cuando revise el código.
Como puede ver, los lugares más oscuros en la textura son los que están más cerca de la fuente
de luz (más cerca, en este caso, significa que el valor de profundidad es menor, el valor más
pequeño posible es 0, lo que haría que el píxel se viera negro). Es por esto que puede ver la
silueta del modelo en el mapa de sombras. Esa silueta es de color rojo más oscuro porque el modelo
está siempre más cerca de la luz que el suelo.
Configuración:
La variable canvas se define, esta se utilizará para hacer referencia al elemento Canvas en el cual dibujaremos. Además, se establece explícitamente que el contenido del Canvas cambiará constantemente, 60 FPS al usar requestAnimationFrame.
canvas = document.getElementById( "gl-canvas" ); canvas.style.willChange = 'contents';
La variable divCanvas es el contenedor del Canvas, este elemento tiene la interfaz de usuario dentro de él.
divCanvas = document.getElementById("divCanvas");
Se declaran las variables que indicarán la posición del mouse en relación con el canvas y divCanvas.
rect = canvas.getBoundingClientRect(); rectLeft = rect.left; rectTop = rect.top; rectLeftA = rectLeft; rectTopA = rectTop; winScrollXA = window.scrollX; winScrollYA = window.scrollY; rectDivCanvas = divCanvas.getBoundingClientRect(); rectLeftDivCanvas = rectDivCanvas.left; rectTopDivCanvas = rectDivCanvas.top; rectLeftADivCanvas = rectLeftDivCanvas; rectTopADivCanvas = rectTopDivCanvas; winScrollXADivCanvas = window.scrollX; winScrollYADivCanvas = window.scrollY;
Una variable llamada gl será el contexto de WebGL. Si el dispositivo o el navegador no permiten el uso de WebGL, se muestra un mensaje. El clearColor del buffer de color se establece explícitamente, este es el color de fondo predeterminado del Canvas. Habilitamos DEPTH_TEST y CULL_FACE, esto se hace porque los modelos son modelos sólidos, esto asegura un efecto agradable al dibujar.
gl = WebGLUtils.setupWebGL(canvas); if (!gl) { alert("WebGL isn't available. WebGL no esta disponible"); } gl.clearColor( 0.9, 0.9, 1.0, 1.0 ); gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
Un buffer de vértices y uno de índices se crean para almacenar la información de cada uno de los modelos y también del piso.
bufVMonkey = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, bufVMonkey); gl.bufferData(gl.ARRAY_BUFFER, verMonkey, gl.STATIC_DRAW); bufIMonkey = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufIMonkey); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indMonkey, gl.STATIC_DRAW); bufVBunny = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, bufVBunny); gl.bufferData(gl.ARRAY_BUFFER, verBunny, gl.STATIC_DRAW); bufIBunny = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufIBunny); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indBunny, gl.STATIC_DRAW); bufVTeapot = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, bufVTeapot); gl.bufferData(gl.ARRAY_BUFFER, verTeapot, gl.STATIC_DRAW); bufITeapot = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufITeapot); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indTeapot, gl.STATIC_DRAW); bufVFloor = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, bufVFloor); gl.bufferData(gl.ARRAY_BUFFER, verFloor, gl.STATIC_DRAW); bufIFloor = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufIFloor); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indFloor, gl.STATIC_DRAW);
Estos arreglos almacenan la información de los vértices y de los índices de los 3 modelos, estos ayudarán a cambiar entre los modelos más fácilmente.
arrBufVModel = [bufVMonkey, bufVBunny, bufVTeapot]; arrBufIModel = [bufIMonkey, bufIBunny, bufITeapot]; arrVerModel = [verMonkey, verBunny, verTeapot]; arrIndModel = [indMonkey, indBunny, indTeapot];
Necesitamos configurar un shader para el modelo y un shader para el piso. Ya que vamos a necesitar un mapa de sombras, hay un shader específico para cuando se dibuja el mapa de sombras y otro para el dibujado final.
proModel = initShaders(gl, "progModelVer", "progModelFra"); proFloor = initShaders(gl, "progFloorVer", "progFloorFra"); proShadowMapModel = initShaders(gl, "progShadowMapModelVer", "progShadowMapModelFra"); proShadowMapFloor = initShaders(gl, "progShadowMapFloorVer", "progShadowMapFloorFra");
Aquí es donde obtendremos los punteros de los atributos que se van a utilizar en cada shader. Es importante notar que, para el modelo, necesitamos el atributo "normals", este atributo nos permitirá lograr la iluminación difusa y especular en el modelo.
vLModel = gl.getAttribLocation(proModel, "vertices"); gl.enableVertexAttribArray(vLModel); nLModel = gl.getAttribLocation(proModel, "normals"); vLFloor = gl.getAttribLocation(proFloor, "vertices"); gl.enableVertexAttribArray(vLFloor); vLShadowMapModel = gl.getAttribLocation(proShadowMapModel, "vertices"); gl.enableVertexAttribArray(vLShadowMapModel); vLShadowMapFloor = gl.getAttribLocation(proShadowMapFloor, "vertices"); gl.enableVertexAttribArray(vLShadowMapFloor);
Se obtiene el puntero de las uniformes que se utilizan en los shader de vértices; "sc" es la escala, "rot" es la rotación y "t" es la traslación.
vUModel0 = gl.getUniformLocation(proModel, "sc"); vUModel1 = gl.getUniformLocation(proModel, "rot"); vUModel2 = gl.getUniformLocation(proModel, "t"); vUFloor0 = gl.getUniformLocation(proFloor, "sc"); vUFloor1 = gl.getUniformLocation(proFloor, "rot"); vUFloor2 = gl.getUniformLocation(proFloor, "t"); vUShadowMapModel0 = gl.getUniformLocation(proShadowMapModel, "sc"); vUShadowMapModel1 = gl.getUniformLocation(proShadowMapModel, "rot"); vUShadowMapModel2 = gl.getUniformLocation(proShadowMapModel, "t"); vUShadowMapFloor0 = gl.getUniformLocation(proShadowMapFloor, "sc"); vUShadowMapFloor1 = gl.getUniformLocation(proShadowMapFloor, "rot"); vUShadowMapFloor2 = gl.getUniformLocation(proShadowMapFloor, "t");
Se obtiene el puntero de las uniformes que se utilizan en los shader de fragmentos.
fUModel0 = gl.getUniformLocation(proModel, "anguFoco"); fUModel1 = gl.getUniformLocation(proModel, "color"); fUModel2 = gl.getUniformLocation(proModel, "shadowMapOK"); fUFloor0 = gl.getUniformLocation(proFloor, "shadowMapOK");
Se obtienen algunos otros punteros de uniformes que serán usados en los shader de vértices. En este caso son las matrices que se van a utilizar.
rotMatrixModelL = gl.getUniformLocation(proModel, "rotMatrix"); perspMatrixModelL = gl.getUniformLocation(proModel, "perspMatrix"); orthoMatrixModelL = gl.getUniformLocation(proModel, "orthoMatrix"); rotMatrixShadowMapModelL = gl.getUniformLocation(proShadowMapModel, "rotMatrix"); orthoMatrixShadowMapModelL = gl.getUniformLocation(proShadowMapModel, "orthoMatrix"); perspMatrixFloorL = gl.getUniformLocation(proFloor, "perspMatrix"); orthoMatrixFloorL = gl.getUniformLocation(proFloor, "orthoMatrix"); orthoMatrixShadowMapFloorL = gl.getUniformLocation(proShadowMapFloor, "orthoMatrix");
El último puntero de uniformes que necesitamos es el que tendrá la textura del mapa de sombras.
texShadowMapModelL = gl.getUniformLocation(proModel, "shadowMapTexture"); texShadowMapFloorL = gl.getUniformLocation(proFloor, "shadowMapTexture");
Se añaden algunos "event listeners" que ayudan a darle control a las acciones que suceden dentro de canvas.
document.addEventListener("scroll", scrollFunc); canvas.addEventListener("mousemove", mouseMoved); canvas.addEventListener("mouseup", clickUpCanvas); canvas.addEventListener("mousedown", clickDownCanvas); canvas.addEventListener("mouseout", mouseOutCanvas);
Estos "event listeners" se usan para la interfaz de usuario, específicamente para cambiar el color del modelo.
divCanvas.addEventListener("mousemove", mouseMovedDivCanvas); divCanvas.addEventListener("mouseup", clickUpDivCanvas); divCanvas.addEventListener("mousedown", clickDownDivCanvas); divCanvas.addEventListener("mouseout", mouseOutDivCanvas); red.addEventListener("mousedown", clickDownRed); red.addEventListener("mouseup", clickUpRed); green.addEventListener("mousedown", clickDownGreen); green.addEventListener("mouseup", clickUpGreen); blue.addEventListener("mousedown", clickDownBlue); blue.addEventListener("mouseup", clickUpBlue);
Esta sección del código sumamente importante, aquí es donde establecemos las cosas que son necesarias para poder hacer el mapa de sombras. Primero, empezamos por obtener la extensión, necesitamos comprobar el valor de depthTextureExt porque si es null no podemos usar esta extensión, en este caso lo único que podemos hacer es alertar al usuario sobre ello. Pero, si no es null, hay algunas cosas que deben hacerse. Se necesita un “frame buffer” (un frame buffer en WebGL es un lugar donde se dibuja) y también se necesita una textura asociada con él. Preste especial atención a los parámetros de texImage2D. Como se puede ver, especificamos que la textura almacena el DEPTH_COMPONENT (la componente de profundidad) y se almacena en un UNSIGNED_SHORT, es decir, 16 bits, esto es importante porque nos da más resolución para trabajar, haciendo posible el mapa de sombras.
depthTextureExt = gl.getExtension('WEBGL_depth_texture'); if(depthTextureExt !== null){ shadowMapFramebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMapFramebuffer); shadowMapTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, shadowMapTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, 1024, 1024, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, shadowMapTexture, 0); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); }else{ alertaShadowMap = document.getElementById("alertaShadowMap"); var alertaSM = "WEBGL_depth_texture is not available, shadows will not be shown." alertaShadowMap.innerHTML = alertaSM; }
Lo último que debemos hacer es crear tanto la matriz de perspectiva como la matriz ortográfica.
var fieldOfView = 45.0; var nearFrustum = 0.1; var farFrustum = 1000.0; var fovFactor = nearFrustum * (Math.sin(((fieldOfView / 2) / 180) * Math.PI)); pMatrix = glFrustum(-fovFactor, fovFactor, -fovFactor, fovFactor, nearFrustum, farFrustum); pMatrix0T = [pMatrix[0][0], pMatrix[1][0], pMatrix[2][0], pMatrix[3][0], pMatrix[0][1], pMatrix[1][1], pMatrix[2][1], pMatrix[3][1], pMatrix[0][2], pMatrix[1][2], pMatrix[2][2], pMatrix[3][2], pMatrix[0][3], pMatrix[1][3], pMatrix[2][3], pMatrix[3][3]]; pMOrtho = glOrtho( - 3.2, 3.2, - 3.2, 3.2, - 10, 10); pMatrix0TOrtho = [pMOrtho[0][0], pMOrtho[1][0], pMOrtho[2][0], pMOrtho[3][0], pMOrtho[0][1], pMOrtho[1][1], pMOrtho[2][1], pMOrtho[3][1], pMOrtho[0][2], pMOrtho[1][2], pMOrtho[2][2], pMOrtho[3][2], pMOrtho[0][3], pMOrtho[1][3], pMOrtho[2][3], pMOrtho[3][3]];
El loop principal:
Esta función se llama repetidamente para lograr la animación. La función rotMatrix actualiza la matriz que se utiliza para girar el modelo cuando se hace click en el Canvas. La función updateMatrices se utiliza para calcular los nuevos valores tanto de la matriz de perspectiva como de la matriz ortográfica. La función sunPositionUpdate es una función muy simple que se encarga de cambiar la posición del indicador de la posición del sol cuando se mueve el mouse sobre el Canvas. Como se puede ver, la variable depthTextureExt se verifica para ver si su valor no es igual a null, si no es así, la función shadowMapRender se llama para dibujar el mapa de sombras. Después de dibujar el mapa de sombras podemos finalmente dibujar la escena final con la función normalRender.
function loop() { rotMatrix(); updateMatrices(); sunPositionUpdate(); if(depthTextureExt !== null){ shadowMapRender(); } normalRender(); requestAnimationFrame(loop); }
La función shadowMapRender:
Esta función comienza vinculando el shadowMapFramebuffer como el “frame buffer”, esto le dice al programa que todo lo que se dibuje posteriormente tiene que ser hecho en este buffer. Establecemos el tamaño del viewport para que sea de 1024X1024 píxeles, esto para que coincida con la resolución de la textura. Debido a que sólo necesitamos la profundidad, sólo se reinicia el DEPTH_BUFFER_BIT.
gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMapFramebuffer); gl.viewport( 0, 0, 1024, 1024 ); gl.clear(gl.DEPTH_BUFFER_BIT);
Configurando el programa para usar proShadowMapModel.
gl.useProgram(proShadowMapModel);
Se vincula el buffer de vértices para usar la información de los vértices del modelo, como puede ver, elegimos de uno de los tres modelos de arrBufVModel, el modelo que va a ser utilizado está determinado por el valor de la variable llamada model.
gl.bindBuffer( gl.ARRAY_BUFFER, arrBufVModel[model] ); gl.vertexAttribPointer( vLShadowMapModel, 3, gl.FLOAT, false, 4*(8), 0 );
Configurando los valores de las uniformes, el modelo necesita de una matriz que se usará para hacerlo rotar y de otra que se usará para la vista ortográfica.
gl.uniformMatrix3fv( rotMatrixShadowMapModelL, false, rotMatrixAux ); gl.uniformMatrix4fv( orthoMatrixShadowMapModelL, false, orthoMatrix ); gl.uniform3fv(vUShadowMapModel0, [1,1,1]); gl.uniform3fv(vUShadowMapModel1, [0,0,0]); gl.uniform3fv(vUShadowMapModel2, [0.0,1.7,0.0]);
Finalmente se puede dibujar, de nuevo, el valor de la variable model es el que indica que modelo se usará.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, arrBufIModel[model]); gl.drawElements(4, arrIndModel[model].length, gl.UNSIGNED_SHORT, 0);
Ahora toca el turno de dibujar el piso, en este caso, se usa el programa proShadowMapFloor.
gl.useProgram(proShadowMapFloor);
Vinculando el buffer de vértices del piso.
gl.bindBuffer( gl.ARRAY_BUFFER, bufVFloor ); gl.vertexAttribPointer( vLShadowMapFloor, 3, gl.FLOAT, false, 4*(8), 0 );
Se configuran los valores de las uniformes, el piso solo necesita de la matriz de vista ortográfica.
gl.uniformMatrix4fv( orthoMatrixShadowMapFloorL, false, orthoMatrix ); gl.uniform3fv(vUShadowMapFloor0, [3.5,2,1]); gl.uniform3fv(vUShadowMapFloor1, [-90,0,0]); gl.uniform3fv(vUShadowMapFloor2, [0,0,0]);
La última cosa que es necesaria es vincular el buffer de índices del piso y luego ya puede dibujarse.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufIFloor); gl.drawElements(4, indFloor.length, gl.UNSIGNED_SHORT, 0);
La función normalRender:
Esta función comienza haciendo que el "frame buffer" sea el que se usa por defecto, este es el que se presenta al usuario. Esto se logra poniendo su valor a null. También, se necesita cambiar la resolución del viewport, ahora no estamos dibujando en la textura sino que en el Canvas, así que que la resolución de este debe ser de 512X512 píxeles. A diferencia del mapa de sombras, en esta ocasión si vamos a usar el buffer de color, además del de profundidad, así que ambos se reinician al inicio del dibujado.
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport( 0, 0, 512, 512 ); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
Configurando el programa para usar proModel.
gl.useProgram(proModel);
Vinculando el buffer de vértices para usar la información perteneciente al modelo, de nuevo, el valor de la variable model es la que nos indica que modelo se usará. Esta vez también se necesita la información de la normal de los vértices, esta es la que nos ayudará a crear el efecto de iluminación difusa y especular.
gl.enableVertexAttribArray( nLModel ); gl.bindBuffer( gl.ARRAY_BUFFER, arrBufVModel[model] ); gl.vertexAttribPointer( vLModel, 3, gl.FLOAT, false, 4*(8), 0 ); gl.vertexAttribPointer( nLModel, 3, gl.FLOAT, false, 4*(8), 4*5 );
Se configuran los valores de las uniformes, se necesitan 3 matrices esta vez, una que describe la rotación del modelo y las otras dos son para calcular la vista de perspectiva y la vista ortográfica.
gl.uniformMatrix3fv( rotMatrixModelL, false, rotMatrixAux ); gl.uniformMatrix4fv( perspMatrixModelL, false, perspMatrix ); gl.uniformMatrix4fv( orthoMatrixModelL, false, orthoMatrix ); gl.uniform3fv(vUModel0, [1,1,1]); gl.uniform3fv(vUModel1, [0,0,0]); gl.uniform3fv(vUModel2, [0.0,1.7,0.0]); gl.uniform1f(fUModel0, mouseX/512); gl.uniform3fv(fUModel1, [1-((redY-70)/399), 1-((greenY-70)/399), 1-((blueY-70)/399)]);
En esta sección del código es en donde se utiliza el mapa de sombras que se dibujó en la función shadowMapRender, se revisa el valor de depthTextureExt para ver si no es igual a null, si lo fuera, esto significaría que no se puede usar la extensión, este chequeo se realiza para prevenir errores.
if(depthTextureExt !== null){ gl.uniform1i(texShadowMapModelL, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, shadowMapTexture); } if(depthTextureExt !== null){ gl.uniform1f(fUModel2, 1.0); }else{ gl.uniform1f(fUModel2, 0.0); }
La última cosa que se necesita para el modelo es vincular el buffer de índices para por fin dibujarlo.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, arrBufIModel[model]); gl.drawElements(4, arrIndModel[model].length, gl.UNSIGNED_SHORT, 0);
Aquí es donde se empieza con el dibujado del piso, se configura el programa para usar proFloor.
gl.useProgram(proFloor);
Se vincula el buffer de vértices del piso.
gl.bindBuffer( gl.ARRAY_BUFFER, bufVFloor ); gl.vertexAttribPointer( vLFloor, 3, gl.FLOAT, false, 4*(8), 0 );
Configurando los valores de las uniformes, el piso solo necesita la matriz de perspectiva y la de vista ortográfica.
gl.uniformMatrix4fv( perspMatrixFloorL, false, perspMatrix ); gl.uniformMatrix4fv( orthoMatrixFloorL, false, orthoMatrix ); gl.uniform3fv(vUFloor0, [3.5,2,1]); gl.uniform3fv(vUFloor1, [-90,0,0]); gl.uniform3fv(vUFloor2, [0,0,0]);
Esto es muy similar a lo se hace cuando se dibuja al modelo, se configura la textura para usar el mapa de sombras. De nuevo, nos aseguramos que el valor de depthTextureExt no sea null para evitar errores.
if(depthTextureExt !== null){ gl.uniform1i(texShadowMapFloorL, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, shadowMapTexture); } if(depthTextureExt !== null){ gl.uniform1f(fUFloor0, 1.0); }else{ gl.uniform1f(fUFloor0, 0.0); }
Y finalmente podemos vincular el buffer de índices y dibujar el piso.
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufIFloor); gl.drawElements(4, indFloor.length, gl.UNSIGNED_SHORT, 0);
Los shaders de vértices (mapa de sombras):
El shader de vértices del modelo: este shader aplica algunas transformaciones a los vértices del modelo.
attribute vec4 vertices; mat4 scalem(float x, float y, float z){ mat4 scale = mat4( x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0 ); return scale; } mat4 translate(float x, float y, float z){ mat4 trans = mat4( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0 ); return trans; } uniform mat4 orthoMatrix; uniform mat3 rotMatrix; uniform vec3 sc; uniform vec3 rot; uniform vec3 t; void main() { vec4 verticesa = vertices; mat4 mFinal = translate(t.x, t.y, t.z) * mat4(rotMatrix) * scalem(sc.x, sc.y, sc.z); verticesa = mFinal*verticesa; verticesa = orthoMatrix*verticesa; gl_Position = verticesa; }
El shader de vértices del piso: de forma similar al shader de vértices del modelo, este shader aplica algunas transformaciones a los vértices.
attribute vec4 vertices; mat4 rotarX(float theta){ float angulo = radians( theta ); float c = cos( angulo ); float s = sin( angulo ); mat4 rx = mat4( 1.0, 0.0, 0.0, 0.0, 0.0, c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0 ); return rx; } mat4 scalem(float x, float y, float z){ mat4 scale = mat4( x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0 ); return scale; } mat4 translate(float x, float y, float z){ mat4 trans = mat4( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0 ); return trans; } uniform mat4 orthoMatrix; uniform vec3 sc; uniform vec3 rot; uniform vec3 t; void main() { mat4 mFinal = translate(t.x, t.y, t.z) * rotarX(rot.x) * scalem(sc.x, sc.y, sc.z); gl_Position = orthoMatrix * mFinal * vertices; }
Los shaders de fragmentos (mapa de sombras):
El shader de fragmentos del modelo: dado a que usamos la extensión WEBGL_depth_texture no necesitamos hacer nada dentro del shader de fragmentos, el valor de la profundidad se escribe por defecto en la textura.
precision mediump float; void main() { }
El shader de fragmentos del piso:
precision mediump float; void main() { }
Los shaders de vértices (dibujado normal):
El shader de vértices del modelo: en este shader aplicamos algunas transformaciones a los vértices y a las normales de los vértices, note que el valor que es enviado a gl_Position requiere una transformación con la matriz de perspectiva. Se usan algunos registros variables, a través de el registro normalsav mandamos el valor de las normales transformadas. Los registros variables ww y posOrtho son muy importantes para calcular las sombras ya que son utilizados para hacer algunas comparaciones en el shader de fragmentos.
attribute vec4 vertices; attribute vec3 normals; mat4 scalem(float x, float y, float z){ mat4 scale = mat4( x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0 ); return scale; } mat4 translate(float x, float y, float z){ mat4 trans = mat4( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0 ); return trans; } uniform mat4 perspMatrix; uniform mat4 orthoMatrix; uniform mat3 rotMatrix; uniform vec3 sc; uniform vec3 rot; uniform vec3 t; varying float yv; varying vec2 pos; varying float norv; varying float specv; varying vec3 verticesav; varying vec3 normalsav; varying float ww; varying vec4 posOrtho; void main() { vec4 verticesa = vertices; vec3 normalsa = normals; mat4 mFinal = translate(t.x, t.y, t.z) * mat4(rotMatrix) * scalem(sc.x, sc.y, sc.z); normalsa = mat3(mFinal)*normalsa; verticesa = mFinal*verticesa; verticesav = verticesa.xyz; normalsav = normalsa.xyz; ww = verticesa.w; gl_Position = perspMatrix*verticesa; posOrtho = orthoMatrix*verticesa; }
El shader de vértices del piso: este es muy similar al del modelo, pero es un poco más simple, el valor que se envía a gl_Position requiere de la transformación con la matriz de perspectiva. Solo se necesita un registro variable: posOrtho. Este será usado para calcular la sombra en el shader de fragmentos.
attribute vec4 vertices; mat4 rotarX(float theta){ float angulo = radians( theta ); float c = cos( angulo ); float s = sin( angulo ); mat4 rx = mat4( 1.0, 0.0, 0.0, 0.0, 0.0, c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0 ); return rx; } mat4 scalem(float x, float y, float z){ mat4 scale = mat4( x, 0.0, 0.0, 0.0, 0.0, y, 0.0, 0.0, 0.0, 0.0, z, 0.0, 0.0, 0.0, 0.0, 1.0 ); return scale; } mat4 translate(float x, float y, float z){ mat4 trans = mat4( 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0 ); return trans; } uniform mat4 perspMatrix; uniform mat4 orthoMatrix; uniform vec3 sc; uniform vec3 rot; uniform vec3 t; varying vec4 posOrtho; void main() { mat4 mFinal = translate(t.x, t.y, t.z) * rotarX(rot.x) * scalem(sc.x, sc.y, sc.z); gl_Position = perspMatrix * mFinal * vertices; posOrtho = orthoMatrix * mFinal * vertices; }
Los shaders de fragmentos (dibujado normal):
El shader de fragmentos del modelo: primero necesitamos algunos cálculos que van a ser necesarios para crear el efecto de iluminación especular y el efecto de iluminación difusa.
//the light source vec3 source = vec3(10000.0*cos(radians(135.0 - 90.0*anguSource)), 10000.0*sin(radians(135.0 - 90.0*anguSource)), 0.0); vec3 sourceD = vec3(source.x-verticesav.x, source.y-verticesav.y, source.z-verticesav.z); vec3 sourceN = normalize(sourceD); float sourceDot = dot(normalsav.xyz, sourceN.xyz); float sourceAngu = acos(sourceDot); //the eye vec3 eye = vec3(0.0, 0.0, 10000.0); vec3 eyeD = vec3(eye.x-verticesav.x, eye.y-verticesav.y, eye.z-verticesav.z); vec3 eyeN = normalize(eyeD); float eyeDot = dot(normalsav.xyz, eyeN.xyz); float eyeAngu = acos(eyeDot); //light that bounces from the floor vec3 bounce = vec3(0.0, -10.0, 10.0); vec3 bounceD = vec3(bounce.x-verticesav.x, bounce.y-verticesav.y, bounce.z-verticesav.z); vec3 bounceN = normalize(bounceD); float bounceDot = dot(normalsav.xyz, bounceN.xyz);
Aquí es donde se calcula la componente especular de la iluminación, esta componente es el resultado de la relación entre el ángulo de incidencia de la luz y de donde se posiciona el ojo, debido a que estos cálculos se hacen de fragmento a fragmento el efecto resultante es muy suave.
//specular component calculations vec3 sourceXEye = cross(sourceN.xyz, eyeN.xyz); float sourceEyeDot = dot(normalsav.xyz, sourceXEye.xyz); float sourceEyeAngu = acos(sourceEyeDot); float difAngu = sourceAngu-eyeAngu; float difFOAngu = (3.1416/2.0)-sourceEyeAngu; float dista = sqrt(difAngu*difAngu + difFOAngu*difFOAngu); float specularComp = 0.0; float anguFact = 0.218165; if(dista<=anguFact){ specularComp = min(2.0*((anguFact-dista)/anguFact), 1.0); }
Aquí, la sombra es calculada. Algo que es importante notar es que estos cálculos sólo ocurren si shadowMapOK es igual a 1, en caso que no lo fuera esto nos dice que la extensión no está disponible, por lo que el mapa de sombras no fue dibujado, esto se hace para evitar errores. El mapa de sombras es muestreado y comparado 4 veces y luego esos valores se mezclan juntos, esto asegura que los bordes de la sombra sean más suaves, dando un efecto más agradable.
//shadow calculations float shadow = 1.0; if(shadowMapOK == 1.0){ if(sourceDot>0.1){ vec3 projCoords = posOrtho.xyz/posOrtho.w; projCoords = projCoords * 0.5 + 0.5; float depth = projCoords.z - (4.0/1024.0); vec2 texelSize = vec2(1.0/1024.0, 1.0/1024.0); vec2 pixelPos = projCoords.xy/texelSize + vec2(0.5); vec2 fracPart = fract(pixelPos); vec2 startTexel = (pixelPos - fracPart) * texelSize; float shadowDepth1 = texture2D(shadowMapTexture, startTexel).r; float blTexel = step(depth, shadowDepth1); float shadowDepth2 = texture2D(shadowMapTexture, startTexel+vec2(texelSize.x, 0.0)).r; float brTexel = step(depth, shadowDepth2); float shadowDepth3 = texture2D(shadowMapTexture, startTexel+vec2(0.0, texelSize.y)).r; float tlTexel = step(depth, shadowDepth3); float shadowDepth4 = texture2D(shadowMapTexture, startTexel+texelSize).r; float trTexel = step(depth, shadowDepth4); float mixA = mix(blTexel, tlTexel, fracPart.y); float mixB = mix(brTexel, trTexel, fracPart.y); shadow = mix(mixA, mixB, fracPart.x); } }
En esta sección del código se obtiene el color de la componente difusa, como se puede ver, la componente difusa no es sólo el resultado de la fuente de luz principal, sino que tomamos en cuenta que un poco de la luz rebota desde el suelo hasta el modelo. Esto da un efecto más realista porque en el mundo real la luz rebota de un objeto a otro, así que, es lógico que algo de la luz rebote en el suelo, iluminando el modelo desde abajo.
//diffuse component calculations float diffuseSource = max(sourceDot, 0.1); diffuseSource = diffuseSource/0.1; diffuseSource = (1.0 - shadow) + (shadow * diffuseSource); float diffuseBounce = max(bounceDot, 0.7); diffuseBounce = diffuseBounce/0.7; float factDiffuse = min( ( 0.5*((diffuseSource - 1.0)/9.0) + 0.5) * diffuseBounce, 1.0 ); vec3 diffuseColor = vec3(color.r*factDiffuse, color.g*factDiffuse, color.b*factDiffuse);
Y finalmente, podemos obtener el color resultante del modelo, teniendo en cuenta la contribución de la iluminación difusa, iluminación especular y la sombra.
//final color gl_FragColor = vec4(diffuseColor.r + (1.0-diffuseColor.r)*specularComp*shadow, diffuseColor.g + (1.0-diffuseColor.g)*specularComp*shadow, diffuseColor.b + (1.0-diffuseColor.b)*specularComp*shadow, 1.0);
El shader de fragmentos del piso: lo único que se necesita en este shader es calcular la sombra que se va a proyectar sobre el suelo. El proceso es muy similar a lo que se hizo en el shader de fragmentos del modelo.
vec3 projCoords = posOrtho.xyz/posOrtho.w; projCoords = projCoords * 0.5 + 0.5; float depth = projCoords.z; vec2 texelSize = vec2(1.0/1024.0, 1.0/1024.0); vec2 pixelPos = projCoords.xy/texelSize + vec2(0.5); vec2 fracPart = fract(pixelPos); vec2 startTexel = (pixelPos - fracPart) * texelSize; float shadow = 1.0; if(shadowMapOK == 1.0){ float shadowDepth1 = texture2D(shadowMapTexture, startTexel).r; float blTexel = step(depth-0.0025, shadowDepth1); float shadowDepth2 = texture2D(shadowMapTexture, startTexel + vec2(texelSize.x, 0.0)).r; float brTexel = step(depth-0.0025, shadowDepth2); float shadowDepth3 = texture2D(shadowMapTexture, startTexel + vec2(0.0, texelSize.y)).r; float tlTexel = step(depth-0.0025, shadowDepth3); float shadowDepth4 = texture2D(shadowMapTexture, startTexel + texelSize).r; float trTexel = step(depth-0.0025, shadowDepth4); float mixA = mix(blTexel, tlTexel, fracPart.y); float mixB = mix(brTexel, trTexel, fracPart.y); shadow = mix(mixA, mixB, fracPart.x); } gl_FragColor = vec4(0.5+0.5*shadow, 0.5+0.5*shadow, 0.5+0.5*shadow, 1.0);
Esos son todos los detalles importantes que hacen que este experimento funcione.
Espero que este ejemplo le diera algunas herramientas para agregar iluminación a
cualquier escena. También, espero que este ejemplo le dé una mejor comprensión de cómo usar WebGL en general.
Asegúrese de probarlo por sí mismo utilizando el enlace de descarga para los archivos que
se proporciona al principio de esta página. Le invito a cambiar algunos valores en el
código para ver qué pasa, puede intentar cambiar la resolución del mapa de sombras para
ver cómo esto cambia la calidad de la sombra resultante, o tal vez algo un poco más
técnicamente desafiante como cambiar la intensidad o el color de la fuente de luz.
Recuerde, todo en este tutorial se puede lograr utilizando una textura común en lugar de la
extensión "depth texture". Le recomiendo que pruebe modificar este tutorial para que se use una
textura común para almacenar la información de profundidad, esto aumentará la
compatibilidad, ya que algunos dispositivos no pueden usar esta extensión.
Siga experimentando y gracias por leer.