Problem mit der omnidirektionalen Schattenzuordnung von WebGL

9

Zunächst möchte ich sagen, dass ich viel über Schattenmapping mit Tiefenkarten und Cubemaps gelesen habe und verstehe, wie sie funktionieren. Außerdem habe ich Berufserfahrung mit OpenGL, aber ich habe ein Problem bei der Implementierung Omnidirektionale Schattenkartierungstechnik unter Verwendung einer Einzelpunktlichtquelle in meiner 3D-Grafik-Engine namens "EZ3". Meine Engine verwendet WebGL als 3D-Grafik-API und JavaScript als Programmiersprache. Dies ist für meine Bachelorarbeit in Informatik.

Grundsätzlich habe ich meinen Schattenzuordnungsalgorithmus so implementiert, aber ich werde mich nur auf Punktlichter konzentrieren, da ich mit ihnen omnidirektionale Schattenzuordnungen archivieren kann.

Zuerst aktiv ich Front-Face-Culling wie folgt:

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

Zweitens erstelle ich ein Tiefenprogramm, um Tiefenwerte für jede Cubemap-Fläche aufzuzeichnen. Dies ist mein Tiefenprogrammcode in 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);
}

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

Drittens ist dies der Körper meiner JavaScript-Funktion, der die omnidirektionale Schattenzuordnung "archiviert"

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

Viertens berechne ich auf diese Weise die omnidirektionale Schattenzuordnung mithilfe meiner Tiefen-Cubemap in meinem Haupt-Vertex-Shader und 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);
}

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

Schließlich ist dies das Ergebnis, das ich bekomme, meine Szene hat eine Ebene, einen Würfel und eine Kugel. Außerdem ist die rote helle Kugel die Punktlichtquelle:

Problem mit der omnidirektionalen Schattenzuordnung

Wie Sie sehen können, scheint mir die Cubemap des Framebuffers mit Punktlichttiefe keine gute Interpolation zwischen ihren Gesichtern zu bewirken.

Bis jetzt habe ich keine Ahnung, wie ich das lösen soll.

czapata91
quelle
Dies schien eine gute Frage zu sein - haben Sie sie gelöscht, weil Sie die Lösung gefunden haben? Wenn ja, können Sie es wiederherstellen und eine Antwort mit Ihrer Lösung veröffentlichen. Die Beantwortung Ihrer eigenen Frage wird empfohlen und Sie gewinnen sowohl für die Frage als auch für die Antwort einen guten Ruf. Außerdem kann es jemand anderem helfen, der in Zukunft ein ähnliches Problem hat ...
Trichoplax
1
Hallo @trichoplax, eigentlich habe ich die Lösung gefunden. Ich werde die Antwort mit allen teilen, die meine eigene Frage beantworten. Ehrlich gesagt habe ich meine Frage gelöscht, weil ich dachte, dass sich niemand um dieses Problem kümmert.
czapata91
1
Übrigens, anstatt die Frage mit "SOLVED" im Titel zu bearbeiten, ist es vorzuziehen, nur Ihre eigene Antwort zu akzeptieren. (Die Seite könnte Sie einen Tag nach dem Posten warten lassen, um das zu tun; ich erinnere mich nicht.)
Nathan Reed
Hallo! @ NathanReed Ich werde den Titel ändern, danke dafür :)
czapata91

Antworten:

7

LÖSUNG

Nach ein paar Tagen wurde mir klar, dass ich meine Projektionsmatrix mit einem FOV-Winkel in Grad berechnete und es im Bogenmaß sein sollte . Ich habe die Konvertierung gemacht und jetzt funktioniert alles super. Die Interpolation zwischen den Gesichtern der Cubemap meines Tiefen-Framebuffers ist jetzt perfekt. Aus diesem Grund ist es wichtig, den Winkel jeder einzelnen trigonometrischen Funktion im Bogenmaß zu behandeln.

Außerdem wurde mir klar, dass Sie Ihre Ansichtsmatrix entweder wie in der Frage gesagt und auf folgende Weise berechnen können:

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

Dieser Ansatz bedeutet, dass Ihr Blickwinkel in der Mitte des Punktlichts platziert ist und Sie nur in jede Richtung Ihrer Cubemap rendern. Aber in welche Richtungen? Nun , diese Richtungen werden berechnet, indem jedes Ziel, das ich im Schaltblock habe (entsprechend dem Gesicht jeder Cubemap), mit der Position Ihres Punktlichts addiert wird .

Darüber hinaus ist es nicht erforderlich, die Y-Koordinate der Projektionsmatrix umzudrehen. In diesem Fall ist es in Ordnung, die perspektivische Projektionsmatrix von pointlight an Ihren GLSL-Shader zu senden, ohne sie um (1, -1, 1) zu skalieren, da ich mit arbeite Texturen ohne umgedrehte Y-Koordinate . Ich denke, Sie sollten die Y-Koordinate der Projektionsmatrix Ihres Punktlichts nur umdrehen, wenn Sie mit der Y-Koordinate einer umgedrehten Textur arbeiten , um einen korrekten omnidirektionalen Schattenabbildungseffekt zu erzielen.

Schließlich werde ich hier die endgültige Version meines omnidirektionalen Shadow Mapping-Algorithmus auf der CPU / GPU-Seite belassen. Auf der CPU-Seite erkläre ich jeden Schritt, den Sie ausführen müssen, um eine korrekte Schattenkarte für das Gesicht jeder Cubemap zu berechnen. Auf der GPU-Seite werde ich andererseits die Vertex / Fragment-Shader- und omnidirektionale Shadow-Mapping-Funktion meines Tiefenprogramms in meinem Hauptfragment-Shader erläutern, um jemandem zu helfen, der diese Technik erlernen oder zukünftige Zweifel an diesem Algorithmus lösen könnte ::

Zentralprozessor

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

In der Funktion renderMeshDepth habe ich:

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

Tiefenprogramm Vertex Shader:

precision highp float;

attribute vec3 position;

uniform mat4 uModelView;
uniform mat4 uProjection;

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

Tiefenprogramm Fragment Shader:

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

Omnidirektionale Schattenzuordnungsfunktion in meinem Hauptfragment-Shader:

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

Hier haben Sie ein endgültiges Rendering des Algorithmus

Geben Sie hier die Bildbeschreibung ein

Viel Spaß beim Codieren schöner Grafiken, viel Glück :)

CZ

czapata91
quelle