Ejercicio 9: Sombras


El ejercicio

El objetivo de este ejercicio es modificar el ejercicio anterior añadiendole sombras.

Creación de mapa de sombras

Crearemos un mapa de sombras de la escena por cada frame. Para esto, se procesara la escena desde el punto de vista de la luz y se guardara la información de distancia en una textura que después se utilizara. Ya que debemos realizar una pasada para crear este mapa, se debe crear shaders para este proceso. En este caso, el vertex shader se mantendrá igual que el hemos realizado en clases pasadas:

uniform mat4 mProj;
uniform mat4 mView;
uniform mat4 mWorld;

attribute vec3 a_position;

varying vec3 fPos;

void main()
{
    fPos = (mWorld * vec4(a_position, 1.0)).xyz;

    gl_Position = mProj * mView * vec4(fPos, 1.0);
}

En el vertex shader, obtenemos la posición del vértice y las matrices de proyección, vista y mundo. Calculamos entonces la posición respecto a la proyección y vista y pasamos esta información al fragment shader. Cabe mencionar que en este caso no nos interesa las normales de los vértices, ya que no estamos calculando iluminación.

En el fragment shader estan los mayores cambios:

uniform vec3 pointLightPosition;

varying vec3 fPos;

void main()
{
    vec3 fromLightToFrag = (fPos - pointLightPosition);

    float lightFragDist =length(fromLightToFrag);

    gl_FragColor = vec4(lightFragDist, lightFragDist, lightFragDist, 1.0);
}

Al fragment shader le pasamos la posición calculada en el vertex shader y la posición de la luz en el mundo. Con esto, calculamos la distancia entre la luz y el fragmento, lo cual guardaremos en el color de la textura.

Podemos ahora modificar los shaders que van a ser utilizados para renderizar la escena 3D. Para el vertex shader, vemos que la única diferencia es que ahora tenemos en cuenta las normales de los vértices, ya que vamos a realizar la iluminación.

uniform mat4 mProj;
uniform mat4 mView;
uniform mat4 mWorld;

attribute vec3 a_position;
attribute vec3 a_normal;

varying vec3 fPos;
varying vec3 fNorm;

void main()
{
    fPos = (mWorld * vec4(a_position, 1.0)).xyz;
    fNorm = (mWorld * vec4(a_normal, 0.0)).xyz;

    gl_Position = mProj * mView * vec4(fPos, 1.0);
}

En el fragment shader realizaremos el calculo de las sombras. Debemos la textura creada anteriormente al fragment shader por medio de un sampler. En este shader calcularemos de nuevo la distancia entre el fragmento y la luz, y se comparara con el valor guardado en la textura. Si el valor almacenado en la textura es menor que la distancia calculada del fragmento, indica que este fragmento estaba detrás de un objeto desde el punto de vista de la luz, por lo tanto este se encuentra en la sombra. Para esto, realizamos la comparación y se encuentra esta situación no se realiza la iluminación, solo se tiene en cuenta la luz ambiental.

uniform vec3 pointLightPosition;
uniform vec4 meshColor;

uniform samplerCube lightShadowMap;

varying vec3 fPos;
varying vec3 fNorm;

void main()
{
    vec3 toLightNormal = normalize(pointLightPosition - fPos);

    float fromLightToFrag = length(fPos - pointLightPosition);

    float shadowMapValue = textureCube(lightShadowMap, -toLightNormal).r;

    float lightIntensity = 0.15;
    if (shadowMapValue >= fromLightToFrag) {
        lightIntensity += 0.75 * max(dot(fNorm, toLightNormal), 0.0);
    }

    gl_FragColor = vec4(meshColor.rgb * lightIntensity, meshColor.a);
}

Con nuestros shaders modificados, ahora tenemos que implementar el código para crear la textura del mapa de sombras. Ya que lo realizaremos para un luz tipo punto, vamos a crear 6 mapas y guardarlas en una textura de tipo gl.TEXTURE_CUBE_MAP, normalmente utilizada para Skybox. De esta forma, tendremos la proyección de las sombras creadas por la luz para cada dirección del espacio.

Primero creamos la textura y configuramos sus parametros.

var shadowMapCube = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, shadowMapCube);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);

Creamos un frameBuffer donde almacenaremos la textura y realizamos el binding.

var shadowMapFramebuffer =gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMapFramebuffer);

var shadowMapRenderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, shadowMapRenderbuffer);
gl.renderbufferStorage(
    gl.RENDERBUFFER, gl.DEPTH_COMPONENT16,
    textureSize, textureSize
);

gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);

Ya que vamos a crear el mapa de sombras para las seis direcciones del espacio. Debemos realizar 6 pasos a la escena utilizando cámaras que estén en la posición de la luz y apuntando en las direcciones especificas (x positivo, x negativo, y positivo, etc.). Inicializamos las cámaras:

//Camaras para el shadow map
  var shadowCameras = [];
  var shadowAspect = 1.0;
  var shadowFieldOfViewRadians = degToRad(90);
  var nearFar = [1, 1000];
  var projectionMatrix = m4.perspective(shadowFieldOfViewRadians, shadowAspect, nearFar[0], nearFar[1]);
  for (var i = 0; i< 6; i++) {
     var position = pointLightPosition;
     var target = m4.addVectors(pointLightPosition, [1, 0, 0]);
     var up = [0, 1, 0];
     switch (i) {
       case 1:
         target = m4.addVectors(pointLightPosition, [-1, 0, 0]);
         break;
       case 2:
         target = m4.addVectors(pointLightPosition, [0, 1, 0]);
         up = [0, 0, 1];
         break;
       case 3:
         target = m4.addVectors(pointLightPosition, [0, -1, 0]);
         up = [0, 0, -1];
         break;
       case 4:
         target = m4.addVectors(pointLightPosition, [0, 0, 1]);
         break;
       case 5:
         target = m4.addVectors(pointLightPosition, [0, 0, -1]);
         break;         
     }
     var cameraMatrix = m4.lookAt(position, target, up);
     var viewMatrix = m4.inverse(cameraMatrix);
     var camera = {
       world: cameraMatrix,
       view: viewMatrix,
       proj: projectionMatrix
     };
     shadowCameras.push(camera);   
  }

Y por cada frame, realizamos las seis pasadas, utilizando el programa con los shaders creados:

//Generar shadow map
gl.useProgram(shadowMapProgramInfo.program);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, shadowMapCube);
gl.bindFramebuffer(gl.FRAMEBUFFER, shadowMapFramebuffer);
gl.bindRenderbuffer(gl.RENDERBUFFER, shadowMapRenderbuffer);

gl.viewport(0, 0, textureSize, textureSize);

gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);

for (var i = 0; i < shadowCameras.length; i++) {

        for(var k = 0; k<objects.length; k++) {
          objects[i].drawInfo.uniforms.mWorld = objects[i].worldMatrix;
          objects[i].drawInfo.uniforms.mView = shadowCameras[i].view;
          objects[i].drawInfo.uniforms.mProj = shadowCameras[i].proj;
          objects[i].drawInfo.uniforms.pointLightPosition = pointLightPosition;
        }
        // Set framebuffer destination
        gl.framebufferTexture2D(
          gl.FRAMEBUFFER,
          gl.COLOR_ATTACHMENT0,
          gl.TEXTURE_CUBE_MAP_POSITIVE_X + i,
          shadowMapCube,
          0
        );
        gl.framebufferRenderbuffer(
          gl.FRAMEBUFFER,
          gl.DEPTH_ATTACHMENT,
          gl.RENDERBUFFER,
          shadowMapRenderbuffer
        );

        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        objectsToDraw.forEach(object => {

          var bufferInfo = object.bufferInfo;

          webglUtils.setBuffersAndAttributes(gl, shadowMapProgramInfo, bufferInfo);
          webglUtils.setUniforms(shadowMapProgramInfo, object.uniforms);

          gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements);  

        });
    }
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);

Por ultimo, utilizando el otro programa de shaders, y pasando la textura creada como parámetro, realizamos la renderización de la escena.

Resultado final

Aunque si generamos el mapa de sombras, puede que las cámaras utilizadas para este estén mal configuradas, ya que como se puede ver en ll demo del ejercicio, este fiddle, las sombras no parecen estar orientadas correctamente y aparecen en lugares donde no deberían. Por mas que cambie la configuración de las cámaras, estos errores no mejoran.

Tiempo dedicado

Para este ejercicio, había planeado un tiempo de 4 horas, lo cual, debido a los problemas encontrados, no fue suficiente. Tomando al rededor de 6 horas.

results matching ""

    No results matching ""