Ejercicio 8: Iluminación


El ejercicio

El objetivo de este ejercicio es crear una escena que muestre el uso de distintos tipos de iluminación en WebGL. Para esto, se modificara el ejemplo realizado para el ejercicio 6, ya que este presenta una geometría en movimiento. Crearemos una iluminación direccional, una de punto y una de foco, permitiéndole al usuario alternar entre ellas por medio de botones.

Luz direccional

La luz direccional es el modelo de iluminación mas sencillo, en este asumimos que la luz de la escena esta impactando todas las superficies de la geometría desde la misma dirección, en forma paralela. Este tipo de iluminación simula una fuente de luz muy lejos de la geometría, por ejemplo, la luz del sol. Para esta situación, calculamos el producto punto entre el vector normal y la dirección inversa de la luz, si ambos vectores son unitarios esto nos da un valor entre -1 y 1. Si ambos vectores tienen la misma dirección, el producto punto es 1, si en cambio son posiciones opuestas es -1, y las posiciones intermedias nos dan interpolaciones. Este resultado lo utilizaremos como factor para multiplicar el color de la superficie, lo cual representa la cantidad de color de la luz que esta rebotando de la superficie y llegando a la cámara. Si la luz esta en dirección opuesta, nada del color rebotara.

A diferencia de los ejercicios anteriores, en este se requiere el uso de los vectores normales en los vértices. Cada vértice debe tener un vector normal asociado, y ya que la orientación del vértice varia con el tiempo, de igual forma lo realiza las normales. Por esto, los vectores normales van a hacer parte del grafo de escena.

Tenemos que realizar cambios en el vertex shader de forma que ahora espere nuestro vector normal, y pasar este vector al fragment shader después de orientarlo correctamente.

attribute vec4 a_position;
attribute vec3 a_normal;

uniform mat4 u_worldViewProjection;
uniform mat4 u_world;

varying vec3 v_normal;

void main() {
  gl_Position = u_worldViewProjection * a_position;

  v_normal = mat3(u_world) * a_normal;
}

El fragment shader será el encargado de realizar todo el trabajo. Este recibirá el vector normal del vertex shader y recibirá ahora un vector unitario que representa la dirección de la luz. Primero normalizamos el vector normal, posteriormente se calcula el producto punto anteriormente mencionado y por ultimo se multiplica el color por el resultado. Para este ejemplo, se tomo en cuenta una luz ambiental, esta representa una luz que permean la geometría desde todas las direcciones, por lo tanto la orientación de la superficie no importa. En este caso, creamos un vector con un factor especifico y lo sumamos al color final. En la practica, esto nos ayude a que la superficie no quede en la oscuridad total, lo cual nunca ocurre en la realidad.

precision mediump float;

varying vec3 v_normal;

uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;

void main() {
  vec3 normal = normalize(v_normal);
  float light = dot(normal, u_reverseLightDirection);

  float ambientLightfactor = 0.3;
  vec3 ambientLight = vec3(ambientLightfactor, ambientLightfactor, ambientLightfactor);

  gl_FragColor = u_color;
  gl_FragColor.rgb *= light + ambientLight;
}

Podemos ver en el resultado que la superficie que esta en la dirección de la luz tiene un color mas vivo, y este se va opacando según la superficie rota.

Luz de punto

Este modelo , a diferencia del anterior, las superficies no son iluminadas desde la misma dirección, en cambio son iluminadas desde un punto especifico del espacio 3D.

Para esto caso, el vertex shader necesita recibir la posición de la luz y este calculara la dirección de la superficie hacia la luz y la pasara al fragment shader.

    attribute vec4 a_position;
    attribute vec3 a_normal;

    uniform vec3 u_lightWorldPosition;

    uniform mat4 u_world;
    uniform mat4 u_worldViewProjection;
    uniform mat4 u_worldInverseTranspose;

    varying vec3 v_normal;

    varying vec3 v_surfaceToLight;

    void main() {
      gl_Position = u_worldViewProjection * a_position;

      v_normal = mat3(u_worldInverseTranspose) * a_normal;

      vec3 surfaceWorldPosition = (u_world * a_position).xyz;
      v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
    }

En el fragment shader, los únicos cambios a realizar es que ya no obtendrá la dirección de la luz, en cambio la dirección desde la superficie hasta la luz, y realizaremos el producto punto entre esta dirección y la normal.

precision mediump float;

varying vec3 v_normal;
uniform vec3 v_surfaceToLight;

uniform vec4 u_color;

void main() {
  vec3 normal = normalize(v_normal);
  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
  float light = dot(normal, surfaceToLightDirection );

  float ambientLightfactor = 0.3;
  vec3 ambientLight = vec3(ambientLightfactor, ambientLightfactor, ambientLightfactor);

  gl_FragColor = u_color;
  gl_FragColor.rgb *= light + ambientLight;
}

Podemos ver el resultado al ver la geometria pasar por la luz.

Luz de foco

Este modelo es muy similar al anterior, solo que en vez de que el punto emane luz en todas las direcciones, vamos a limitar esto a una dirección especifica y un rango especifico a su alrededor.

Para realizar esto, solo tenemos que modificar el fragment shader de forma que reciba un parámetro de la dirección de la luz y el limite de esta. Calcularemos el producto punto entre la dirección de la superficie a la luz y el negativo de la dirección de la luz, esto nos dará un factor que podemos utilizar para determinar si la superficie se encuentra dentro del rango del spotlight. Ponemos un condicional, de forma que la iluminación solo se calcula para las superficies dentro del rango.

precision mediump float;

varying vec3 v_normal;
varying vec3 v_surfaceToLight;

uniform vec4 u_color;
uniform vec3 u_lightDirection;
uniform float u_limit;

void main() {
  vec3 normal = normalize(v_normal);
  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);

  float light = 0.0;

  float dotFromDirection = dot(surfaceToLightDirection, -u_lightDirection);
  if (dotFromDirection >= u_limit) {
    light = dot(normal, surfaceToLightDirection);
  }

  gl_FragColor = u_color;
  gl_FragColor.rgb *= light;
}

Podemos ver el resultado, la dirección de la luz y el limite se manifiestan en un circulo especifico donde los objetos se ven iluminados, de lo contrario aparecen negros.

Resultado final

El demo del ejercicio se puede ver en este fiddle.

Tiempo dedicado

Para este ejercicio, había planeado un tiempo de 5 horas, lo cual fue una buena estimación del tiempo que me tomo realizar el ejercicio.

results matching ""

    No results matching ""