WebGL Omnidirectional Shadow Mapping Issue

9

En primer lugar, quiero decir que he leído muchas publicaciones sobre mapeo de sombras usando mapas de profundidad y mapas de cubos y entiendo cómo funcionan y también, tengo experiencia laboral con ellos usando OpenGL, pero tengo un problema al implementar Técnica de mapeo de sombras omnidireccional que utiliza una fuente de luz de un solo punto en mi motor de gráficos 3D llamado "EZ3". Mi motor utiliza WebGL como API de gráficos 3D y JavaScript como lenguaje de programación, esto es para mi tesis de licenciatura en informática.

Básicamente, así es como he implementado mi algoritmo de mapeo de sombras, pero solo me enfocaré en el caso de las luces de punto porque con ellas puedo archivar el mapeo de sombras omnidireccional.

Primero, activo el sacrificio frontal como este:

if (this.state.faceCulling !== Material.FRONT) {
    if (this.state.faceCulling === Material.NONE)
      gl.enable(gl.CULL_FACE);

    gl.cullFace(gl.FRONT);
    this.state.faceCulling = Material.FRONT;
  }

Segundo, creo un programa de profundidad para registrar valores de profundidad para cada cara de mapa de cubos, este es mi código de programa de profundidad en GLSL 1.0:

Sombreador de vértices:

precision highp float;

attribute vec3 position;

uniform mat4 uModelView;
uniform mat4 uProjection;

void main() {
  gl_Position = uProjection * uModelView * vec4(position, 1.0);
}

Fragmento Shader:

precision highp float;

vec4 packDepth(const in float depth) {
  const vec4 bitShift = vec4(256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0);
  const vec4 bitMask = vec4(0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0);
  vec4 res = mod(depth * bitShift * vec4(255), vec4(256)) / vec4(255);
  res -= res.xxyz * bitMask;
  return res;
}

void main() {
  gl_FragData[0] = packDepth(gl_FragCoord.z);
}

Tercero, este es el cuerpo de mi función de JavaScript que "archiva" el mapeo omnidireccional de sombras

program.bind(gl);

  for (i = 0; i < lights.length; i++) {
    light = lights[i];

    // Updates pointlight's projection matrix

    light.updateProjection();

    // Binds point light's depth framebuffer

    light.depthFramebuffer.bind(gl);

    // Updates point light's framebuffer in order to create it 
    // or if it's resolution changes, it'll be created again.

    light.depthFramebuffer.update(gl);

    // Sets viewport dimensions with depth framebuffer's dimensions

    this.viewport(new Vector2(), light.depthFramebuffer.size);

    if (light instanceof PointLight) {

      up = new Vector3();
      view = new Matrix4();
      origin = new Vector3();
      target = new Vector3();

      for (j = 0; j < 6; j++) {

    // Check in which cubemap's face we are ...

        switch (j) {
          case Cubemap.POSITIVE_X:
            target.set(1, 0, 0);
            up.set(0, -1, 0);
            break;
          case Cubemap.NEGATIVE_X:
            target.set(-1, 0, 0);
            up.set(0, -1, 0);
            break;
          case Cubemap.POSITIVE_Y:
            target.set(0, 1, 0);
            up.set(0, 0, 1);
            break;
          case Cubemap.NEGATIVE_Y:
            target.set(0, -1, 0);
            up.set(0, 0, -1);
            break;
          case Cubemap.POSITIVE_Z:
            target.set(0, 0, 1);
            up.set(0, -1, 0);
            break;
          case Cubemap.NEGATIVE_Z:
            target.set(0, 0, -1);
            up.set(0, -1, 0);
            break;
        }

    // Creates a view matrix using target and up vectors according to each face of pointlight's
    // cubemap. Furthermore, I translate it in minus light position in order to place
    // the point light in the world's origin and render each cubemap's face at this 
    // point of view

        view.lookAt(origin, target, up);
        view.mul(new EZ3.Matrix4().translate(light.position.clone().negate()));

    // Flips the Y-coordinate of each cubemap face
    // scaling the projection matrix by (1, -1, 1).

    // This is a perspective projection matrix which has:
    // 90 degress of FOV.
    // 1.0 of aspect ratio.
    // Near clipping plane at 0.01.
    // Far clipping plane at 2000.0.

        projection = light.projection.clone();
        projection.scale(new EZ3.Vector3(1, -1, 1));

    // Attaches a cubemap face to current framebuffer in order to record depth values for the face with this line
    // gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + j, id, 0);

        light.depthFramebuffer.texture.attach(gl, j);

    // Clears current framebuffer's color with these lines:
    // gl.clearColor(1.0,1.0,1.0,1.0);
    // gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        this.clear(color);

    // Renders shadow caster meshes using the depth program

        for (k = 0; k < shadowCasters.length; k++)
          this._renderShadowCaster(shadowCasters[k], program, view, projection);
      }
    } else {
       // Directional light & Spotlight case ...
    }
  }

Cuarto, así es como calculo el mapeo de sombras omnidireccional usando mi mapa de cubos de profundidad en mi sombreador de vértices y sombreador de fragmentos principal:

Sombreador de vértices:

precision highp float;

attribute vec3 position;

uniform mat4 uModel;
uniform mat4 uModelView;
uniform mat4 uProjection;

varying vec3 vPosition;

void main() {
  vPosition = vec3(uModel * vec4(position, 1.0));

  gl_Position = uProjection * uModelView * vec4(position, 1.0);
}

Fragmento Shader:

float unpackDepth(in vec4 color) {
    return dot(color, vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1.0 ));
}

float pointShadow(const in PointLight light, const in samplerCube shadowSampler) {
    vec3 direction = vPosition - light.position;
    float vertexDepth = clamp(length(direction), 0.0, 1.0);
    float shadowMapDepth = unpackDepth(textureCube(shadowSampler, direction));

    return (vertexDepth > shadowMapDepth) ? light.shadowDarkness : 1.0;
}

Finalmente, este es el resultado que estoy obteniendo, mi escena tiene un plano, un cubo y una esfera. Además, la esfera roja brillante es la fuente de luz puntual:

Problema omnidireccional de mapeo de sombras

Como puede ver, parece que el mapa de cubos de framebuffer de profundidad de luz de punto no está haciendo una buena interpolación entre sus caras.

Hasta ahora, no tengo idea de cómo resolver esto.

czapata91
fuente
Esta parecía una buena pregunta: ¿la eliminó porque encontró la solución? Si es así, puede recuperarlo y publicar una respuesta con su solución. Se recomienda responder a su propia pregunta y usted gana reputación tanto por la pregunta como por la respuesta. Además, puede ayudar a alguien que tenga un problema similar en el futuro ...
trichoplax
1
Hola @trichoplax en realidad encontré la solución, compartiré la respuesta con todos los que respondan mi propia pregunta. Honestamente, eliminé mi pregunta porque pensé que a nadie le importa este problema.
czapata91
1
Por cierto, en lugar de editar la pregunta con "RESUELTO" en el título, es preferible aceptar su propia respuesta. (El sitio podría hacerte esperar un día después de publicar para hacer eso; no lo recuerdo.)
Nathan Reed
¡Oye! @NathanReed Cambiaré el título, gracias por eso :)
czapata91

Respuestas:

7

SOLUCIÓN

Después de un par de días, me di cuenta de que estaba calculando mi matriz de proyección usando un ángulo FOV en grados y debería estar en radianes . Hice la conversión y ahora todo funciona muy bien. La interpolación entre caras del mapa de cubos de mi framebuffer de profundidad ahora es perfecta. Por esta razón, es importante manejar el ángulo de cada función trigonométrica en radianes.

Además, me di cuenta de que puede calcular su matriz de vista ya sea como dije en la pregunta y de esta manera:

view.lookAt(position, target.add(position.clone()), up);

Este enfoque significa que su punto de vista se coloca en el centro de la luz puntual y usted simplemente renderiza en cada dirección de su mapa de cubos, pero ¿cuáles son estas direcciones? bueno, estas direcciones se calculan agregando cada objetivo que tengo en el bloque de interruptores (de acuerdo con la cara de cada mapa de cubos) con la posición de su foco .

Además, no es necesario voltear la coordenada Y de la matriz de proyección , en este caso, está bien enviar la matriz de proyección de perspectiva de Pointlight a su sombreador GLSL sin escalarlo en (1, -1, 1) porque estoy trabajando con texturas que no tienen una coordenada Y invertida , creo que debería voltear la coordenada Y de la matriz de proyección de su punto de luz solo si está trabajando con una coordenada Y de una textura invertida , esto para tener un efecto de mapeo de sombras omnidireccional correcto.

Finalmente, dejaré aquí la versión final de mi algoritmo de mapeo de sombras omnidireccional en el lado de la CPU / GPU. En el lado de la CPU, explicaré cada paso que tiene que hacer para calcular un mapa de sombras correcto para la cara de cada mapa de cubos. Por otro lado, en el lado de la GPU, explicaré el sombreador de vértices / fragmentos de mi programa de profundidad y la función de mapeo de sombras omnidireccional en mi sombreador de fragmentos principal, esto para ayudar a alguien que podría estar aprendiendo esta técnica o resolver futuras dudas sobre este algoritmo :

UPC

  // Disable blending and enable front face culling.

  this.state.disable(gl.BLEND);

  this.state.enable(gl.CULL_FACE);
  this.state.cullFace(gl.FRONT);

  // Binds depth program

  program.bind(gl);

  // For each pointlight source do

  for (i = 0; i < lights.length; i++) {
    light = lights[i];

    // Get each pointlight's world position

    position = light.worldPosition();

    // Binds pointlight's depth framebuffer. Besides, in this function,
    // viewport's dimensions are set according to depth framebuffer's dimension.

    light.depthFramebuffer.bind(gl, this.state);

    // Updates point light's framebuffer in order to create it 
    // or if it's resolution have changed, it'll be created again.

    light.depthFramebuffer.update(gl);

    // Check in which cubemap's face we are ...

    for (j = 0; j < 6; j++) {
      switch (j) {
        case Cubemap.POSITIVE_X:
          target.set(1, 0, 0);
          up.set(0, -1, 0);
          break;
        case Cubemap.NEGATIVE_X:
          target.set(-1, 0, 0);
          up.set(0, -1, 0);
          break;
        case Cubemap.POSITIVE_Y:
          target.set(0, 1, 0);
          up.set(0, 0, 1);
          break;
        case Cubemap.NEGATIVE_Y:
          target.set(0, -1, 0);
          up.set(0, 0, -1);
          break;
        case Cubemap.POSITIVE_Z:
          target.set(0, 0, 1);
          up.set(0, -1, 0);
          break;
        case Cubemap.NEGATIVE_Z:
          target.set(0, 0, -1);
          up.set(0, -1, 0);
          break;
      }

      // Creates a view matrix using target and up vectors 
      // according to each face of pointlight's cubemap.

      view.lookAt(position, target.add(position.clone()), up);

      // Attaches cubemap's face to current framebuffer 
      // in order to record depth values in that direction.

      light.depthFramebuffer.texture.attach(gl, j);

      // Clears color & depth buffers of your current framebuffer

      this.clear();

      // Render each shadow caster mesh using your depth program

      for (k = 0; k < meshes.length; k++)
        this._renderMeshDepth(program, meshes[k], view, light.projection);
    }
  }

En la función renderMeshDepth tengo:

  // Computes pointlight's model-view matrix 

  modelView.mul(view, mesh.world);

  // Dispatch each matrix to the GLSL depth program

  program.loadUniformMatrix(gl, 'uModelView', modelView);
  program.loadUniformMatrix(gl, 'uProjection', projection);

  // Renders a mesh using vertex buffer objects (VBO)

  mesh.render(gl, program.attributes, this.state, this.extensions);

GPU

Programa de profundidad del sombreador de vértices:

precision highp float;

attribute vec3 position;

uniform mat4 uModelView;
uniform mat4 uProjection;

void main() {
  gl_Position = uProjection * uModelView * vec4(position, 1.0);
}

Shader de fragmento de programa de profundidad:

precision highp float;

// The pack function distributes fragment's depth precision storing 
// it throughout (R,G,B,A) color channels and not just R color channel 
// as usual in shadow mapping algorithms. This is because I'm working
// with 8-bit textures and one color channel hasn't enough precision 
// to store a depth value.

vec4 pack(const in float depth) {
  const vec4 bitShift = vec4(255.0 * 255.0 * 255.0, 255.0 * 255.0, 255.0, 1.0);
  const vec4 bitMask = vec4(0.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);

  vec4 res = fract(depth * bitShift);
  res -= res.xxyz * bitMask;

  return res;
}

void main() {
  // Packs normalized fragment's Z-Coordinate which is in [0,1] interval.

  gl_FragColor = pack(gl_FragCoord.z);
}

Función de mapeo de sombras omnidireccional en mi sombreador de fragmentos principal:

// Unpacks fragment's Z-Coordinate which was packed 
// on the depth program's fragment shader.

float unpack(in vec4 color) {
   const vec4 bitShift = vec4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0);
   return dot(color, bitShift);
}

// Computes Omnidirectional Shadow Mapping technique using a samplerCube
// vec3 lightPosition is your pointlight's position in world coordinates.
// vec3 vPosition is your vertex's position in world coordinates, in code
// I mean this -> vPosition = vec3(uModel * vec4(position, 1.0));
// where uModel is your World/Model matrix.

float omnidirectionalShadow(in vec3 lightPosition, in float bias, in float darkness, in samplerCube sampler) {
    vec3 direction = vPosition - lightPosition;
    float vertexDepth = clamp(length(direction), 0.0, 1.0);
    float shadowMapDepth = unpack(textureCube(sampler, direction)) + bias;

    return (vertexDepth > shadowMapDepth) ? darkness : 1.0;
}

Aquí tienes un render final del algoritmo

ingrese la descripción de la imagen aquí

Diviértete codificando hermosos gráficos, buena suerte :)

CZ

czapata91
fuente