Iluminación y Sombreado

Informática Gráfica

José Ribelles

Departamento de Lenguajes y Sistemas Informáticos

Universitat Jaume I

Llega el momento de añadir iluminación y eso conlleva realizar importantes cambios. Hay que modificar los modelos, el shader, añadir fuentes de luz, materiales, etc. Ve poco a poco, ten paciencia, realiza todas las pruebas que se te vayan ocurriendo, y aclara las dudas con tu profesor.

Última revisión: 19/10/2016

En este shader, se utiliza la normal de cada vértice como valor de color. Para esto, como la normal varía en el rango [-1..1] y un color lo hace en el rango [0..1], a cada normal le hemos de sumar 1 y dividir por 2. Luego la multiplicamos por su matriz de transformación y la normalizamos. El valor final se asigna a la variable colorOut que se interpola para cada fragmento, y en el shader de fragmentos simplemente le asignamos a gl_FragColor el correspondiente valor interpolado. Carga en el navegador coloreaConNormales.html, obtendrás unos resultados similares a estos:

Ejercicio 1 - Incorpora Normales

Hasta ahora las primitivas geométricas que has utilizado estaban formadas únicamente por geometría. Sabemos que para poder aplicar el modelo de iluminación de Phong necesitamos ampliarlas para que además proporcionen la normal de cada vértice. Descarga coloreaConNormales.zip que incluye un nuevo primitivas.js con dicha información. Ábrelo, elije un modelo sencillo, el plano o el cubo por ejemplo, y comprueba qué normal se ha definido para cada vértice. Dibujarlo en papel ayuda bastante.

Nota: Además, hemos aprovechado para aumentar también el número de vértices y triángulos del cono, el cilindro y la esfera. Esto suele ser muy útil a la hora de trabajar con iluminación.

Añadir el atributo de la normal conlleva cambios en el shader de vértices. Hay que añadir dicho atributo y proporcionar la matriz de transformación de la normal, que también debemos definir en el shader de vértices junto a las otras que ya conoces. Observa estos cambios en el siguiente ejemplo:

  // VERTEX SHADER -------------------------------------------------------------

  uniform   mat4 projectionMatrix;

  uniform   mat4 modelViewMatrix;

  uniform   mat3 normalMatrix;

     

  attribute vec3 VertexPosition;

  attribute vec3 VertexNormal;

     

  varying vec3 colorOut;

     

  void main()  {

     

    vec3 N   = (VertexNormal + 1.0) / 2.0; // range [0..1]

    N        = normalize(normalMatrix * N);

     

    colorOut = N;


    gl_Position = projectionMatrix * modelViewMatrix * vec4(VertexPosition,1.0);

     

  }


  // FRAGMENT SHADER -----------------------------------------------------------

  precision mediump float;

  varying vec3 colorOut;


  void main() {

     

    gl_FragColor = vec4(colorOut,1);

     

  }

Vamos ahora con los cambios en el lado de la aplicación. Hay que obtener un índice para cada atributo, y habilitar los vectores correspondientes. También hay que obtener el índice de la matriz de la normal ya que, como ya sabes, la matriz de la normal y la del modelo no tienen por qué coincidir. Así, por ejemplo, la función initShaders puede ser la encargada de realizar estas operaciones. Edita coloreaConNormales.js y observa en dicha función cómo se han realizado estas operaciones:

  program.vertexNormalAttribute = gl.getAttribLocation ( program, "VertexNormal");

  program.normalMatrixIndex     = gl.getUniformLocation( program, "normalMatrix");

  gl.enableVertexAttribArray(program.vertexNormalAttribute);

A la hora de dibujar, para cada primitiva hemos de calcular su matriz de la normal y enviarla al shader (observa la función setShaderNormalMatrix). La calculamos como la traspuesta de la inversa de la matriz de transformación del modelo-vista. Comprueba que en la función getNormalMatrix se realizan las siguientes operaciones:

  var normalMatrix = mat3.create();


  mat3.fromMat4 (normalMatrix, modelViewMatrix);

  mat3.invert   (normalMatrix, normalMatrix);  

  mat3.transpose(normalMatrix, normalMatrix);

Y por último, la función de dibujo de una primitiva especifica cómo se encuentra organizado el vector de vértices (posición y normal), y deberá pintar la superficie de los triángulos en lugar de las líneas que utilizábamos en las prácticas anteriores:

function drawSolid(model) {

    

  gl.bindBuffer (gl.ARRAY_BUFFER, model.idBufferVertices);

  gl.vertexAttribPointer (program.vertexPositionAttribute, 3, gl.FLOAT, false, 2*3*4,   0);

  gl.vertexAttribPointer (program.vertexNormalAttribute,   3, gl.FLOAT, false, 2*3*4, 3*4);

    

  gl.bindBuffer   (gl.ELEMENT_ARRAY_BUFFER, model.idBufferIndices);

  gl.drawElements (gl.TRIANGLES, model.indices.length, gl.UNSIGNED_SHORT, 0);


}

Estudia detenidamente todos los cambios realizados en coloreaConNormales y resuelve las dudas antes de continuar.

Ejercicio 2 - Incorpora el modelo de Iluminación

Añadir el modelo de iluminación de Phong obliga a especificar las características de la fuente de luz y de material de los objetos. Respecto a la fuente de luz, vamos a trabajar con una fuente posicional sin efecto de atenuación, por lo que debemos proporcionar La, Ld, Ls y Lp. Respecto al material debemos especificar Ka, Kd, Ks y alpha. Acude a tus apuntes si no recuerdas qué significan estos parámetros. Descarga iluminacion.zip antes de continuar.

De momento vamos a empezar utilizando un sombreado de Gouraud, por lo que calcularemos la iluminación recibida sólo en los vértices, y dejaremos que la GPU interpole estos valores para cada fragmento. A continuación puedes observar el shader de vértices que incluye los cambios necesarios:

  // VERTEX SHADER -------------------------------------------------------------

  uniform   mat4 projectionMatrix;

  uniform   mat4 modelViewMatrix;

  uniform   mat3 normalMatrix;

     

  attribute vec3 VertexPosition;

  attribute vec3 VertexNormal;

     

  varying   vec3 colorOut;


  struct LightData {

    vec3 Position; // Posición en coordenadas del ojo

    vec3 La;       // Ambiente

    vec3 Ld;       // Difusa

    vec3 Ls;       // Especular

  };

  uniform LightData Light;


  struct MaterialData {

    vec3 Ka;       // Ambiente

    vec3 Kd;       // Difusa

    vec3 Ks;       // Especular

    float alpha;   // Brillo

  };

  uniform MaterialData Material;

     

  vec3 phong (vec3 N, vec3 L, vec3 V) {

     

    vec3  ambient  = Material.Ka * Light.La;

    vec3  diffuse  = vec3(0.0);

    vec3  specular = vec3(0.0);

       

    float NdotL    = dot (N,L);

       

    if (NdotL > 0.0) {

      vec3  R       = reflect(-L, N);;

      float RdotV_n = pow(max(0.0, dot(R,V)), Material.alpha);

          

      diffuse  = NdotL   * (Light.Ld * Material.Kd);

      specular = RdotV_n * (Light.Ls * Material.Ks);

    }

       

    return (ambient + diffuse + specular);


  }

     

  void main()  {

     

    vec3 N         = normalize(normalMatrix * VertexNormal);

    vec4 ecPosition= modelViewMatrix * vec4(VertexPosition,1.0);

    vec3 ec        = vec3(ecPosition);

    vec3 L         = normalize(Light.Position - ec);

    vec3 V         = normalize(-ec);

       

    colorOut       = phong(N,L,V);

       

    gl_Position    = projectionMatrix * ecPosition;

     

  }

Revísalo y aclara las dudas. Sería interesante que te hicieras en papel un pequeño esquema de los diferentes vectores que participan en el modelo de iluminación.

En el lado de la aplicación (iluminacion.js) también hay que obtener un índice de cada una de las variables. Así, por ejemplo, la función initShaders() contendrá el siguiente fragmento de código:

  program.KaIndex       = gl.getUniformLocation( program, "Material.Ka");

  program.KdIndex       = gl.getUniformLocation( program, "Material.Kd");

  program.KsIndex       = gl.getUniformLocation( program, "Material.Ks");

  program.alfaIndex     = gl.getUniformLocation( program, "Material.alpha");


  program.LaIndex       = gl.getUniformLocation( program, "Light.La");

  program.LdIndex       = gl.getUniformLocation( program, "Light.Ld");

  program.LsIndex       = gl.getUniformLocation( program, "Light.Ls");

  program.PositionIndex = gl.getUniformLocation( program, "Light.Position");

Hemos recopilado una pequeña biblioteca de materiales (materiales.js) para que sea más fácil especificar un material. Simplemente llama a la función setShaderMaterial(material) indicando el nombre del material como parámetro.

function setShaderMaterial(material) {


  gl.uniform3fv(program.KaIndex,   material.mat_ambient);

  gl.uniform3fv(program.KdIndex,   material.mat_diffuse);

  gl.uniform3fv(program.KsIndex,   material.mat_specular);

  gl.uniform1f (program.alfaIndex, material.alpha);

  

}

A la hora de dibujar la escena se ha de especificar el material de cada uno de los objetos antes de ordenar su dibujado.

function drawScene() {


  ...

  setShaderMaterial(Jade);

  drawSolid(selectedPrimitive);


}

Respecto a las propiedades de la fuente de luz, en esta práctica solo utilizaremos una única fuente así que sus propiedades las establecemos inicialmente con la función setShaderLight() a la que llamamos desde la función initRendering() .

function setShaderLight() {


  gl.uniform3f(program.LaIndex,       1.0,1.0,1.0);

  gl.uniform3f(program.LdIndex,       1.0,1.0,1.0);

  gl.uniform3f(program.LsIndex,       1.0,1.0,1.0);

  gl.uniform3f(program.PositionIndex, 10.0,10.0,0.0);

  

}

Si no lo has hecho aún, carga iluminacion.html en tu navegador. Pruébalo. Obtendrás resultados similares a los que se muestran en las siguientes imágenes.

Estudia detenidamente todos los cambios realizados y resuelve las dudas antes de continuar.

Ejercicio 3

Añade el modelo de iluminación de Phong a tu escena resultado de la práctica anterior. Añade una única fuente de luz y asigna materiales a las diferentes primitivas utilizadas. Utiliza los materiales de la biblioteca o crea tus propos materiales. Quizá, te sea más fácil entender el resultado si posicionas la fuente de luz en un lugar sencillo como el origen de coordenadas coincidiendo con la cámara del sistema gráfico.

Ejercicio 4 opcional - Modelo de Sombreado de Phong

Implementa el modelo de sombreado de Phong. A continuación tienes el shader. Cámbialo por el tuyo, símplemente susitúyelo. Pruébalo y trata de observar las diferencias con el modelo de sombreado de Gouraud.

  // VERTEX SHADER -------------------------------------------------------------

  uniform   mat4 projectionMatrix;

  uniform   mat4 modelViewMatrix;

  uniform   mat3 normalMatrix;

     

  attribute vec3 VertexPosition;

  attribute vec3 VertexNormal;

     

  varying vec3 N, ec;

     

  void main()  {

       

    N  = normalize(normalMatrix * VertexNormal);

    vec4 ecPosition= modelViewMatrix * vec4(VertexPosition,1.0);

    ec = vec3(ecPosition);

       

    gl_Position = projectionMatrix * ecPosition;

       

  }

    

    

  // FRAGMENT SHADER -----------------------------------------------------------

  precision mediump float;

  

  struct LightData {

    vec3 Position; // Posición en coordenadas del ojo

    vec3 La;       // Ambiente

    vec3 Ld;       // Difusa

    vec3 Ls;       // Especular

  };

  uniform LightData Light;


  struct MaterialData {

    vec3 Ka;       // Ambiente

    vec3 Kd;       // Difusa

    vec3 Ks;       // Especular

    float alpha;   // Brillo

  };

  uniform MaterialData Material;

    

     varying vec3 N, ec;

    

  vec3 phong (vec3 N, vec3 L, vec3 V) {

     

    vec3  ambient  = Material.Ka * Light.La;

    vec3  diffuse  = vec3(0.0);

    vec3  specular = vec3(0.0);

    

    float NdotL    = dot (N,L);

     

    if (NdotL > 0.0) {

      vec3  R       = reflect(-L, N);;

      float RdotV_n = pow(max(0.0, dot(R,V)), Material.alpha);

       

      diffuse  = NdotL   * (Light.Ld * Material.Kd);

      specular = RdotV_n * (Light.Ls * Material.Ks);

    }

     

    return (ambient + diffuse + specular);

  }

    

  void main() {

     

    vec3 n = normalize(N);

    vec3 L = normalize(Light.Position - ec);

    vec3 V = normalize(-ec);

     

    gl_FragColor = vec4(phong(n,L,V),1.0);

     

  }

Ejercicio 5 opcional - Iluminación por ambas caras

Implementa el shader que te permite iluminar por ambas caras los objetos abiertos, como el cilindro, el cono o el plano. A continuación tienes un ejemplo. No hay más pistas. Decide dónde ponerlo. Quizá debas también eliminar alguna línea de tu código. Para ver la diferencia, añade alguna de las primitivas abiertas a tu composición.

  if (gl_FrontFacing)

    gl_FragColor = vec4(phong(n,L,V),1.0);

  else

    gl_FragColor = mix (vec4(1.0,1.0,1.0,1.0), vec4(phong(-n,L,V),1.0), 0.5);

(placeholder)

Ejercicio 7 opcional - Foco de luz (spotlight)

Implementa el shader que te permite iluminar la escena con un foco de luz (spotlight). Esta tarea lleva algo más de trabajo que las dos anteriores ya que además de cambiar tu shader actual, un foco de luz añade tres nuevos parámetros a los que deberás asignarles valor desde la aplicación. Estos parámetros son la dirección del foco, el ángulo de corte conocido como cutoff, y el valor del exponente que simula la atenuación de la luz. A continuación tienes la nueva definición de la estructura LightData, y la nueva función de Phong. Actualiza tu shader. Después, realiza las modificaciones oportunas en la aplicación para suministrar al shader la nueva información.

  struct LightData {

    vec3  Position;  // Posición en coordenadas del ojo

    vec3  La;        // Ambiente

    vec3  Ld;        // Difusa

    vec3  Ls;        // Especular

    vec3  Direction; // Dirección de la luz en coordenadas del ojo

    float Exponent;  // Atenuación

    float Cutoff;    // Ángulo de corte en grados

  };


  vec3 phong (vec3 N, vec3 L, vec3 V) {

     

    vec3  ambient  = Material.Ka * Light.La;

    vec3  diffuse  = vec3(0.0);

    vec3  specular = vec3(0.0);

     

    float NdotL      = dot (N,L);

    float spotFactor = 1.0;

     

    if (NdotL > 0.0) {


      vec3  s      = normalize (Light.Position - ec);

      float angle  = acos(dot(-s, Light.Direction));

      float cutoff = radians(clamp(Light.Cutoff, 0.0, 90.0));


      if (angle < cutoff) {


        spotFactor       = pow (dot(-s,Light.Direction), Light.Exponent);


        vec3  R          = reflect(-L, N);;

        float RdotV_n    = pow(max(0.0, dot(R,V)), Material.alpha);

       

        diffuse  = NdotL   * (Light.Ld * Material.Kd);

        specular = RdotV_n * (Light.Ls * Material.Ks);

      }

    }

     

    return (ambient + spotFactor * (diffuse + specular));

  }

Ejercicio 6 opcional - Luz posicional móvil

Coloca la fuente de luz en el extremo superior del aro más externo, y haz que la fuente de luz gire cuando lo haga el aro. Fíjate en el siguiente vídeo.

Ejercicio 8 opcional - Sombreado Cómic

Implementa un shader que te permita pintar la escena como si de un cómic se tratara.  A continuación tienes la función toonShading que realiza el cálculo del color para cada fragmento. Básicamente, la componente difusa del color de un fragmento se restringe a sólo un número determinado de posibles valores que depende de la variable levels. Añade esta función a tu shader de fragmentos y haz que se llame a ésta en lugar de a la función phong. Observa también que la componente especular se ha eliminado de la ecuación, por lo que la función toonShade solo necesita los vectores N y L..

  vec3 toonShading (vec3 N, vec3 L) {

     

    vec3  ambient     = Material.Ka * Light.La;     

    float NdotL       = max(0.0, dot (N,L));

    float levels      = 3.0;

    float scaleFactor = 1.0 / levels;

       

    vec3 diffuse  = ceil(NdotL * levels) * scaleFactor * (Light.Ld * Material.Kd);

     

    return (ambient + diffuse);

  

  }

    

  void main() {

     

    vec3 n = normalize(N);

    vec3 L = normalize(Light.Position - ec);

  

    gl_FragColor = vec4(toonShading(n,L),1.0);

     

  }

Ejercicio 9 opcional - Niebla

Implementa un shader que simule el efecto de niebla. Mezcla el color de cada fragmento con el color de la niebla dependiendo de la distancia del fragmento a la cámara. Es importante que el color de fondo coincida con el de la niebla. Parte del shader que implementa el modelo de sombreado de Phong. Añade las tres variables que necesitas para implementar el efecto de niebla que son: distancia mínima, distancia máxima y color de la niebla. Si el fragmento está a una distancia menor que la mínima, no se verá afectado por la niebla. Si está a una distancia mayor que la máxima, el fragmento no se verá (se colorea con el color de la niebla). Si está entre la mínima y la máxima distancia, su color dependerá del color proporcionado por el modelo de sombreado y del de la niebla variando el color definitivo de manera lineal. Sin embargo, suele producir mucho mejor resultado utilizar una función exponencial. A continuación tienes el trozo de código que necesitas en tu shader de fragmentos.

  struct FogData {

    float maxDist;

    float minDist;

    vec3  color;

  };

  FogData Fog;

    

  void main() {

     

    Fog.minDist = 10.0;

    Fog.maxDist = 20.0;

    Fog.color   = vec3(0.15, 0.15, 0.15);

     

    vec3 n = normalize(N);

    vec3 L = normalize(Light.Position - ec);

    vec3 V = normalize(-ec);

     

    float dist     = abs (ec.z);


    // lineal

    float fogFactor = (Fog.maxDist - dist) / (Fog. maxDist - Fog.minDist);


    // exponencial

    // float density = 0.1;

    // float fogFactor = exp(-pow(density*dist,2.0));


    fogFactor       = clamp (fogFactor, 0.0, 1.0);

    vec3 phongColor = phong(n,L,V);

    vec3 myColor    = mix (Fog.color, phongColor, fogFactor);


    gl_FragColor    = vec4(myColor,1.0);

     

  }