sunLoadingImage
whowedImag
decoration left 1
decoration left 2
transhome
transprojects
transgallery
transarticles
decoration rigth
English
Українська
Show/Hide search bar
black cat logo variable logo
[27 Сер 2012]

Розмиття кубічної текстури

В цьому уроці показано як з допомогою шейдера розмити кубічну текстуру. Розмита кубічна текстура може знадобитися для освітлення базованого на зображеннях (image-bases lighting), в методі environmental mapping для симуляції дзеркальних і прозорих поверхонь та в інших ефектах.
Розмиття кубічної текстури не є таким простим, як розмиття двовимірної текстури. Потрібно враховувати текселі з різних граней шести текстур, які входять до кубічної текстури. В image-based lighting для розрахунку дифузної карти освітлення потрібно для кожного текселя вихідної розмитої текстури розрахувати середнє зважене значення з усіх текселів вхідної текстури, які знаходяться у півкулі, яка орієнтована вздовж нормалі. Для карти відбитого освітлення може знадобитися розмиття з врахуванням тільки невеликої кількості текселів, які знаходяться в конусі, орієнтованому вздовж нормалі. На зображенні показано, що мається на увазі:
Напрями семплінгу для розрахунку розмитого кольору тексeля
  • Сфера - усі напрямки для яких потрібно розрахувати зважений колір (розмита cubemap)
  • Зелені стрілки - базовий напрям для якого робиться розмиття в текселі
  • Конус/півкуля - напрямки, які використовуються для знаходження розмитого значення
  • Куб - вхідна кубічна текстура
  • В більшості випадків недоречно обробляти усі текселі вхідної кубічної текстури, що потрапляють у конус/півкулю. Припустимо, що вхідна текстура має ширину в 1024 текселі. Тоді для розмиття з врахуванням цілої півкулі потрібно зробити більше трьох міліонів вибірок з текстури для кожного вихідного текселя. При цьому якщо вихідна текстура має ширину всього лиш 32 текселі, то потрібно повторити операцію розмиття шість тисяч разів, що в сумі дасть 18 міліардів вибірок з кубічної текстури. Це може зайняти доволі багато часу. Якщо точність не є обовязковою, то можна враховувати лише малу частину текселів, які рівномірно розподілені в конусі/півкулі. Звичайно, такий семплінг може пропустити малі і дуже яскраві джерела світла, які можуть істотно змінити вміст кубічної текстури, якщо б вони були враховані.
    Напрями в яких необхідно зробити вибірку з початкової кубічної текстури можна зберегти в двовимірній текстурі (схожій до карти нормалей). Передавши текстуру в фрагментний шейдер, можна з допомогою кожного напрямку провести вибірку з вхідної кубічної текстури, і потім знайти середнє значення з повернених значень. Напрямки в текстурі зберігаються в просторі дотичних і можуть бути використані будь-яким фрагментом. Але для семплінгу кубічної текстури необхідно перетворити напрямок у простір сцени. Для того щоб перетворити напрям з простору дотичних у простір сцени необхідно сформувати матрицю повороту з нормалі, дотичної та бідотичної. Дотична і бідотична повинні бути передані в вершинний шейдер як атрибути вершини.
    Генерація текстури з напрямками
    Для того, щоб згенерувати текстуру з напрямками, можна спроектувати рівномірно розміщені точки двовимірної сітки на півкулю. Для цього потрібно перевести картезіанські двовимірні координати в сферичну систему координат, а потім сферичні координати в тривимірні картезіанські.
    Проекція двовимірних координат на одиничному квадраті на півкулю
    На зображені показано, як інтерпретується двовимірний квадрат. Спочатку двовимірні координати переводяться з меж [0, 1] в межі [-1, 1].
    Сферичні координати задаються через трійку значень r, theta і phi. Радіус r у нашому випадку рівний 1, так як ми працюємо з нормалізованими напрямками.
    Кут theta - кут між перпендикулярним до поверхні напрямком Z і шуканим напрямком. Для визначення theta, двовимірний простір ділиться на чотири сектора вздовж осей X, Y, -X і -Y. В кожному секторі проекція точки на вісь сектора інтерпретується як sin(theta).
    Кут phi - кут навколо осі Z починаючи від осі X. Кут визначається через функцію atan2(x, y).

    Коли визначені сферичні координати для точки на двовимірному квадраті, сферичні координати переводяться у тривимірний простір за формулами:
  • x = cos(phi) * sin(theta);
  • y = sin(phi) * sin(theta);
  • z = cos(theta);
  • Текстура з напрямами
    Це і є шуканий напрямок, який відповідає точці на одиничному квадраті. Наступні функції виконують це перетворення.
    Перетворення 2D картезіанських координат в сферичні координати
    Функція для для знаходження напрямку вибірки в півкулі з точки на одиничному квадраті:
    glm::vec3 mapToHemisphere(const glm::vec2 & point, float maxVertAngle = M_PI/2)
    {
       // точка на двовимірному квадраті [0, 1] для якої потрібно знайти напрям
       glm::vec2 in = point;

       // перевід координат точки в межі [-1, 1]
       in = in * 2.0f - 1.0f;

       // перпендикулярний напрямок
       if(in.x == 0 && in.y ==0)
       {
          // в просторі дотичних перпендикулар орієнтований вздовж Z
          return glm::vec3(0, 0, 1);
       }

       // розрахунок відхилення від перпендикуляра в межах [0, 1]
       float sinTheta;
       if(in.y > -in.x) // над лінією y=-x
       {
          if(in.y < in.x) // під лінією y=x
          {
             sinTheta = in.x;
          }
          else // над лінією y = x
          {
             sinTheta = in.y;
          }
       }
       else // під лінією y=-x
       {
          if(in.y > in.x) // над лінією y=x
          {
             sinTheta = -in.x;
          }
          else // під лінією y = x
          {
             sinTheta = -in.y;
          }
       }

       // визначити кут theta - відхилення від вертикальної осі
       float theta = asinf(sinTheta);
       // масштабування кута. По замовчуванню theta лежить в межі [0, M_PI/2]
       theta *= maxVertAngle/(M_PI/2);

       // нормалізований напрям в 2D
       in = glm::normalize(in);
       // визначити кут навколо вертикальної осі
       float phi = atan2(in.y, in.x);

       // визначити напрям переводячи сферичні координати в картезіанські
       glm::vec3 out;
       out.x = cos(phi) * sin(theta);
       out.y = sin(phi) * sin(theta);
       out.z = cos(theta);
       return out;
    }
    Функція створює та наповнює текстуру напрямками в просторі дотичних:
    std::unique_ptr getHemisphereNormalsTexture(int size, float hemiAngle)
    {
       // виділення памяті для даних текстури
       int components = 3;
       std::unique_ptr data(new GLubyte[components * size * size]);

       // зсув в пів текселя
       float base = 0.5 / size;
       // розрахувати напрям для кожного текселя в текстурі
       for(int x=0; x    {
          // позиція на 2D квадраті вздовж осі X
          float xx = float(x)/size;
          for(int y=0; y       {
             // позиція на 2D квадраті вздовж осі Y
             float yy = float(y)/size;

             // дістати напрям вибірки для текселів
             glm::vec3 res = MathUtilities::mapToHemisphereComplex(glm::vec2(xx + base,
                      yy + base), hemiAngle);

             // розміщення пікселя в текстурі
             int offset = x * components + y * size * components;

             // записати напрям в текстуру, перевівши компоненту в межі [0, 255]
             data[offset++] = (res.x + 1.0f)/2.0f * 255;
             data[offset++] = (res.y + 1.0f)/2.0f * 255;
             data[offset++] = (res.z + 1.0f)/2.0f * 255;
          }
       }

       // створити OpenGL текстуру і заповнити її даними
       std::unique_ptr tex(new Texture2D());
       tex->setup(size, size, GL_RGB, GL_RGB, GL_UNSIGNED_BYTE, data.get());
       return std::move(tex);
    }
    Шейдер для розмиття кубічної текстури
    Потрібно зробити наступні налаштування перед рендерінгом:
  • Проводиться рендерінг в вихідну кубічну текстуру (розмиту).
  • Об'єкт, що рендериться - сфера.
  • Камера розміщена всередині сфери. Fov = 90 градусів. Aspect ratio = 1.
  • Рендерінг проводиться шість разів. Кожного разу камера орієнтована вздовж іншої осі.
  • В шейдер передається текстура, яку необхідно розмити, а також текстура з напрямами.
  • Вершинний шейдер просто передає нормаль та дотичну у фрагментний шейдер.
    В фрагментному шейдері кожен напрям з текстури напрямків перетворюється з простору дотичних у простір сцени. Проводиться семплінг кубічної текстури через напрям в координатах сцени. Колір додається до змінної-акумулятора. Після проведення усіх семплів визначається середній колір, який записується в поточний тексель розмитої текстури (в яку проводиться рендерінг).
    Вершинний шейдер для розмиття кубічної текстури:
    #version 330

    // атрибути
    layout(location = 0) in vec3 i_position; // xyz - position
    layout(location = 1) in vec3 i_normal; // xyz - normal
    layout(location = 3) in vec4 i_tangent; // xyz - tangent, w - handedness

    // матриці
    uniform mat4 u_modelViewProjectionMat;
    uniform mat3 u_normalMat;

    // дані для фрагментного шейдера
    out vec3 o_worldNormal;
    out vec3 o_worldTangent;
    out float o_handedness;

    void main(void)
    {
       // позиція вершини в екранних координатах
       gl_Position = u_modelViewProjectionMat * vec4(i_position, 1);

       // перетворити нормаль та дотичну в світовий простір
       o_worldNormal = normalize(u_normalMat * i_normal);
       o_worldTangent = normalize(u_normalMat * i_tangent.xyz);
       o_handedness = i_tangent.w;
    }
    Фрагментний шейдер для розмиття кубічної текстури:
    #version 330

    // дані з вершинного шейдера
    in vec3 o_worldNormal;
    in vec3 o_worldTangent;
    in float o_handedness;

    // текстура, яку треба розмити
    layout(location = 0) uniform samplerCube u_colorTexture;
    // текстура з напрямами для розмивання
    layout(location = 1) uniform sampler2D u_normalsTexture;

    // розмір текстури з напрямами
    uniform vec2 u_normalTextureDimensions;

    // колір у фреймбуфер
    out vec4 resultingColor;

    void main(void)
    {
       // знормалізувати нормаль та дотичну після інтерполяції
       vec3 N = normalize(o_worldNormal);
       vec3 T = normalize(o_worldTangent);

       // відтворити ортогональність дотичної
       T = T - N * dot(N, T);
       // розрахувати бідотичну
       vec3 B = cross(N, T) * o_handedness;

       // зсув розміром в тексель
       vec2 texStep = vec2(1.0, 1.0) / u_normalTextureDimensions;
       // зсув розміром з пів текселя
       vec2 texOffset = vec2(0.5, 0.5) / u_normalTextureDimensions;

       // матриця для трансформації з простору дотичних в простір сцени
       mat3 toWorldSpace = mat3(T, B, N);

       // змінні для акумуляції кольору
       vec3 accumulatedColor = vec3(0, 0, 0);
       float accumulatedWeight = 0;

       // для кожного напряму в текстурі з напрямами
       for(float x=0; x < u_normalTextureDimensions.x; x+=1)
       {
          for(float y=0; y < u_normalTextureDimensions.y; y+=1)
          {
             // текстурні координати для вибірки з текстури напрямків
             vec2 texCoords = texOffset + texStep * vec2(x, y);

             // напрямок з текстури напрямків (простір дотичних)
             vec3 NT = normalize(texture(u_normalsTexture, texCoords).rgb * 2.0 - 1.0);

             // перетворення напряму в простір сцени
             vec3 NW = normalize(toWorldSpace * NT);

             // вибірка кольору через знайдений напрям
             vec3 sampleColor = texture(u_colorTexture, NW).rgb;

             // для дифузної карти - зваження впливу через напрям на базову нормаль
             float weight = dot(NW, N);

             // додати та зважити колір
             accumulatedColor += sampleColor * weight;
             accumulatedWeight += weight;
          }
       }

       // зберегти середній колір
       resultingColor.xyz = accumulatedColor / accumulatedWeight;
       resultingColor.a = 1;
    }



    Sun and Black Cat- Ігор Дихта (igor dykhta email) © 2007-2014