Problema de mapeamento de sombra omnidirecional do WebGL


9

Antes de tudo, quero dizer que li várias postagens sobre mapeamento de sombra usando mapas de profundidade e cubemaps e entendo como eles funcionam e também tenho experiência de trabalho com eles usando o OpenGL, mas tenho um problema ao implementar Técnica omnidirecional de mapeamento de sombras usando uma fonte de luz de ponto único no meu mecanismo de gráficos 3D chamado "EZ3". Meu mecanismo usa o WebGL como uma API gráfica 3D e JavaScript como linguagem de programação, isto é para a tese de meu bacharelado em Ciência da Computação.

Basicamente, é assim que eu implementei meu algoritmo de mapeamento de sombra, mas vou focar apenas no caso das luzes pontuais porque, com elas, posso arquivar o mapeamento de sombra omnidirecional.

Primeiro, eu ativo o abate frontal assim:

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, eu crio um programa de profundidade para registrar valores de profundidade para cada face do mapa do cubo, este é o meu código de programa de profundidade no GLSL 1.0:

Vertex Shader:

precision highp float;

attribute vec3 position;

uniform mat4 uModelView;
uniform mat4 uProjection;

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

Shader do fragmento:

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);
}

Terceiro, este é o corpo da minha função JavaScript que "arquiva" o mapeamento de sombra omnidirecional

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 ...
    }
  }

Quarto, é assim que eu computo o Mapeamento de Sombra Omnidirecional usando meu mapa de cubos de profundidade no meu principal Vertex Shader & Fragment Shader:

Vertex Shader:

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);
}

Shader do fragmento:

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 é o resultado que estou obtendo, minha cena tem um avião, um cubo e uma esfera. Além disso, a esfera vermelha brilhante é a fonte de luz pontual:

Problema de mapeamento de sombra omnidirecional

Como você pode ver, parece que o mapa de cubos do framebuffer com profundidade de luz pontual não está fazendo uma boa interpolação entre as faces deles.

Até agora, não tenho idéia de como resolver isso.


Essa parecia uma boa pergunta - você a excluiu porque encontrou a solução? Nesse caso, você pode cancelar a exclusão e postar uma resposta com sua solução. É recomendável responder a sua própria pergunta e você ganha reputação tanto pela pergunta quanto pela resposta. Além disso, ele pode ajudar alguém que tem um problema semelhante no futuro ...
Trichoplax

11
Olá @trichoplax Na verdade, eu encontrei a solução, vou compartilhar a resposta com todos que responderem à minha própria pergunta. Honestamente, excluí minha pergunta porque achava que ninguém se importa com esse problema.
czapata91

11
Aliás, em vez de editar a pergunta com "RESOLVIDO" no título, é preferível aceitar sua própria resposta. (O site pode fazer você esperar um dia depois de postar a fazer isso, eu não me lembro.)
Nathan Reed

Ei! @NathanReed eu vou mudar o título, graças a esse respeito :)
czapata91

Respostas:


7

SOLUÇÃO

Depois de alguns dias, percebi que estava computando minha matriz de projeção usando um ângulo de FOV em graus e deveria estar em radianos . Fiz a conversão e agora tudo funciona muito bem. A interpolação entre as faces do mapa de cubos do meu framebuffer de profundidade agora é perfeita. Por esse motivo, é importante lidar com o ângulo de cada função trigonométrica em radianos.

Além disso, percebi que você pode calcular sua matriz de visualizações, como eu disse na pergunta e desta maneira:

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

Essa abordagem significa que seu ponto de vista é colocado no centro do pointlight e você apenas renderiza em cada direção do seu mapa cúbico, mas quais são essas direções? bem, essas instruções são calculadas adicionando cada alvo que eu tenho no bloco de comutação (de acordo com a face de cada mapa do cubo) com a posição do seu foco .

Além disso, não é necessário inverter a coordenada Y da matriz de projeção . Nesse caso, é bom enviar a matriz de projeção em perspectiva do pointlight para o seu shader GLSL sem escalá-lo em (1, -1, 1) porque estou trabalhando com texturas que não têm uma Coordenada Y invertida , acho que você deve inverter a Coordenada Y da matriz de projeção do spotlight apenas se estiver trabalhando com a Coordenada Y de uma textura invertida , para obter um efeito de mapeamento de sombra omnidirecional correto.

Finalmente, deixarei aqui a versão final do meu algoritmo Omnidirectional Shadow Mapping no lado da CPU / GPU. No lado da CPU, explicarei todas as etapas que você deve executar para calcular um mapa de sombras correto para a face de cada cubo. Por outro lado, no lado da GPU, explicarei a função de mapeamento de vértice / fragmento de som e omnidirecional do meu programa de profundidade no meu sombreador de fragmento principal, para ajudar alguém que possa estar aprendendo essa técnica ou para solucionar dúvidas futuras sobre esse algoritmo :

CPU

  // 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);
    }
  }

Na função renderMeshDepth, tenho:

  // 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 profundidade Vertex Shader:

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 do programa de profundidade:

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);
}

Função Omnidirectional Shadow Mapping no meu principal shader fragment:

// 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;
}

Aqui você tem uma renderização final do algoritmo

insira a descrição da imagem aqui

Divirta-se codificando belos gráficos, boa sorte :)

CZ

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.