Випуск всенаправленого тіньового картографування WebGL


9

Перш за все, я хочу сказати, що я читав багато публікацій про тіньове картографування за допомогою карт глибини та кубічних карт, і розумію, як вони працюють, а також, маю досвід роботи з ними за допомогою OpenGL, але, у мене є проблема реалізації Техніка всенаправленого тіньового картографування за допомогою одноточкового джерела світла в моєму 3D-графічному двигуні під назвою "EZ3". Мій двигун використовує WebGL як 3D графічний API та JavaScript як мову програмування, це для моєї дипломної роботи з інформатики.

В основному саме так я реалізував свій алгоритм тіньового відображення, але зосередитимусь лише на випадку точкових світильників, тому що за допомогою них я можу архівувати всенаправлене тіньове відображення.

По-перше, я активно відкидаю передне обличчя так:

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

По-друге, я створюю програму глибини для того, щоб записувати значення глибини для кожного обличчя кубічної карти, це мій код програми глибини в GLSL 1.0:

Вершина шейдера:

precision highp float;

attribute vec3 position;

uniform mat4 uModelView;
uniform mat4 uProjection;

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

Фрагмент Шейдер:

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

По-третє, це тіло моєї функції JavaScript, яке "архівує" всенаправлене тіньове відображення

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

По-четверте, саме так я обчислюю всенаправлене тіньове картографування за допомогою моєї кубічної карти глибини в головному шейдері вершин і фрагментів:

Вершина шейдера:

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

Фрагмент Шейдер:

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

Нарешті, це результат, який я отримую, моя сцена має площину, куб і кулю. Крім того, червона яскрава сфера є точковим джерелом світла:

Випуск всенаправленого тіньового картографування

Як бачите, мені здається, що в кутовій карті глибинного світла фреймбуфера вона не робить хорошої інтерполяції серед їхніх облич.

До сих пір я не знаю, як це вирішити.


Це здавалося гарним питанням - ви його видалили, оскільки знайшли рішення? Якщо це так, ви можете скасувати її та опублікувати відповідь зі своїм рішенням. Відповідь на власне запитання заохочується, і ви отримуєте репутацію як питання, так і відповіді. Плюс це може допомогти комусь, хто має подібну проблему в майбутньому ...
trichoplax

1
Привіт @trichoplax насправді я знайшов рішення, я поділюсь відповіддю з усіма, хто відповідає на моє власне питання. Чесно кажучи, я видалив своє запитання, бо думав, що ніхто не переймається цим питанням.
czapata91

1
BTW, замість того, щоб редагувати питання із заголовком "РОШЕНО", бажано просто прийняти власну відповідь. (Сайт може змусити вас почекати день після публікації, щоб зробити це; я не пам’ятаю.)
Nathan Reed

Гей! @NathanReed Я поміняю заголовок, спасибі про це :)
czapata91

Відповіді:


7

РІШЕННЯ

Через пару днів я зрозумів, що я обчислював свою проекційну матрицю, використовуючи кут FOV в градусах, і це повинно бути в радіанах . Я здійснив перетворення і зараз все працює чудово. Інтерполяція між гранями кубічної карти мого кадрового буфера зараз ідеальна. З цієї причини важливо керувати кутом кожної тригонометричної функції в радіанах.

Крім того, я зрозумів, що ви можете обчислити свою матрицю перегляду або як я вже сказав у запитанні, і таким чином:

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

Такий підхід означає, що ваша точка зору розміщується в центрі точкового світла, і ви просто подаєте в кожному напрямку вашої кутової карти, але які ці напрямки? добре, ці вказівки обчислюються, додаючи кожну ціль, яку я перебуваю в блоці комутаторів (відповідно до обличчя кожної кубічної карти) з позицією вашого прожектора .

Крім того, не потрібно перевертати Y-координату проекційної матриці. У цьому випадку це нормально перспективна проекційна матриця диспетчеризації на ваш шейдер GLSL, не масштабуючи її на (1, -1, 1), тому що я працюю з текстури, які не мають перевернутої Y-координати , я думаю, вам слід перевернути Y-координату проекційної матриці вашої точки, лише якщо ви працюєте з Y-координатою перевернутої текстури , це для того, щоб мати правильний ефект всенаправленого тіньового відображення.

Нарешті, я залишу тут остаточну версію мого всенаправленого алгоритму Shadow Mapping на стороні CPU / GPU. На стороні процесора я поясню кожен крок, який ви повинні зробити, щоб обчислити правильну тіньову карту для обличчя кожної кубічної карти. З іншого боку в GPU, я поясню функцію шейдера вершини / фрагмента моєї програми та всенаправлену функцію відображення тіні в моєму головному шейдері фрагмента, це для того, щоб допомогти тому, хто міг вивчити цю техніку, або вирішити майбутні сумніви щодо цього алгоритму :

ЦП

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

У функції renderMeshDepth я:

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

Вершина шейдера програми глибинної програми:

precision highp float;

attribute vec3 position;

uniform mat4 uModelView;
uniform mat4 uProjection;

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

Фрагмент шейдера глибинної програми:

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

Функція всенаправленого тіньового картографування в моєму головному фрагменті шейдера:

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

Тут ви маєте остаточне відображення алгоритму

введіть тут опис зображення

Приємно кодуючи гарну графіку, удачі :)

CZ

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.