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

Симуляція дзеркальних та прозорих поверхонь з допомогою кубічних текстур

Ця стаття описує просту симуляцію дзеркальних і прозорих об'єктів, а також фізичні явища пов'язані з ними. Поверхні дзеркальних об'єктів частково або майже повністю віддзеркалюють зовнішнє середовище, а колір прозорих об'єктів формується з врахування заломлення променів світла при переході від одного середовища в інше. Як досягти інтерактивності при візуалізації таких об'єктів? Для кожного пікселя зображення необхідно дізнатися, що віддзеркалюється і що видно через об'єкт після заломлення променів світла. Методи трасування променів є точними, але вони не є інтерактивними на сучасному обладнанні. Далі буде розглянуто метод мапування зовнішнього середовища. Метод застосовується для симуляції дзеркальних і прозорих об'єктів, а також для створення неконстантного заповнюючого освітлення.
Метод мапування зовнішнього середовища
Метод мапування зовнішнього середовища базується на збереженні середовища навколо об'єкта у текстуру. Для того щоб зберегти панораму навколо об'єкту і вид під та над об'єктом використовуються кубічні текстури. Кубічна текстура складається з шести звичайних двовимірних квадратних текстур. Кожна з шести текстур представляє собою зображення світу вздовж чи проти однієї з головиних осей (XYZ). Текстура називається кубічною, тому що якщо накласти кожну двовимірну текстуру на грань кубу, то вихоходить повноціне 3D середовище. Двовимірні зображення повинні стикуватися, і перехід від одного зображення до іншого не повинен бути помітним.
Вибірка з кубічної текстури проводиться не з допомогою двох текстурних координат, а з допомогою вектора. Уявімо, що початок вектора в центрі координат. Навколо центру координат розташований куб з накладеною на нього кубічною текстурою. Вибірка з текстури проводиться в точці, де вектор перетинає грань куба. Вектор для вибірки не обов'язково повинен бути нормалізованим, але він повинен бути перетвореним з системи координат моделі у систему координат сцени. Вибірку з кубічної текстури з допомогою нормалі моделі можна провести наступним чином:
// В OPENGL
glActiveTexture (GL_TEXTURE0); // активувати перший текстурний слот
glBindTexture (GL_TEXTURE_CUBE_MAP, cubeTexture);// і привязати до ньго текстуру
// шейдер буде використовувати слот 0 як джерело для текстури "environmentTexture"
glUniform1i(glGetUniformLocation(shaderProgram,"environmentTexture"), 0);

// В шейдері
uniform samplerCube environmentTexture; // кубічна текстура
vec3 worldNormal = ModelViewProjectionMat * modelNormal; // перетворення нормалі
vec3 color = texture(environmentTexture, worldNormal); // і вибірка з текстури
Розвернута кубічна текстура
Вибірка з кубічної текстури
При роботі з кубічними текстурами припускається, що всі об'єкти розташовані безкінечно далеко від центру куба. При розрахунку ефектів це призводить до певних неточностей, але для користувача вони є мало помітними. Кубічна текстура ідеально підходить для статичних сцен, а також для сцен, де об'єкти розташовані далеко від дзеркального/прозорого об'єкту. Якщо такий об'єкт рухається, або навколо нього рухаються інші об'єкти, то кубічну текстуру необхідно оновлювати в кожному кадрі. В ідеалі кожен дзеркальний/прозорий об'єкт в сцені повинен мати свою кубічну текстуру, і вона повинна оновлюватися при кожній зміні навколишнього середовища. Aле це призводить до зменшення продуктивності. Тож часто використовують одну кубічну текстуру для усіх об'єктів, і оновлюють її при значних змінах навколишнього середовища.
Завантаження кубічної текстури
Припустимо, що у нас є шість зображень для кубічної текстури. На диску вони збереженні, як env_posx.jpg, env_posy.jpg і тд. З допомогою Qt ці зображення можна завантажити (і створити з них кубічну текстуру) наступним чином:
void loadCubeTexture(const QString path){ // функція завантаження кубічної текстури

    qs pre = path.mid(0,path.lastIndexOf()); // шлях без розширення

    qs ext = path.mid(path.lastIndexOf()+1,3); // розширення зображень

    GLenum cubesides[6] = { // грані кубічної текстури
        GL_TEXTURE_CUBE_MAP_POSITIVE_X,
        GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
        GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
        GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
        GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
    };

    qs cubepaths[6] = { // шлях до зображення для кожної грані
        pre + "_posx." + ext,
        pre + "_negx." + ext,
        pre + "_posy." + ext,
        pre + "_negy." + ext,
        pre + "_posz." + ext,
        pre + "_negz." + ext
    };

    // створити нову кубічну текстуру і встановити її параметри
    glGenTextures(1, &texture);
    glEnable(GL_TEXTURE_CUBE_MAP);
    glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R,GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_GENERATE_MIPMAP, GL_TRUE);

    for(short i=0;i<6;i++){ // для кожної грані кубічної текстури
        QImage imageToConvert;
        if(!imageToConvert.load(cubepaths[i])){ // завантажити зовраження в Qt
            qDebug()<<"Cube face texture loading failed: "+cubepaths[i];
            return;
        }
        // перетворити в OpenGL формат
        QImage GL_formatted_image = QGLWidget::convertToGLFormat(imageToConvert);
        if(GL_formatted_image.isNull()){
            qDebug()<<"Cube face - fail convert to gl format: "+path;
            return;
        }
        // скопіювати Qt зображення в OpenGL
        glTexImage2D (cubesides[i], 0, GL_RGBA, GL_formatted_image.width(),
            GL_formatted_image.height(), 0, GL_RGBA,
            GL_UNSIGNED_BYTE, GL_formatted_image.bits());
    }

    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

}
Створення кубічної текстури

Кубічну текстуру можна зробити з фотографії-панорми. Необхідно накласти панораму на сферу і шість разів провести рендерінг. Камера повинна розташовуватися в центрі сфери, кут зору повинен бути 90 градусів, а відношення сторін кадру – 1 до 1. Для кожного кадру необхідно оновлювати напрям камери, наприклад, вздовж осі X, проти осі X, вздовж осі Y, і тд. Після збереження шести окремих зображень може бути необхідно обробити зображення, що відповідають виду вверх і виду вниз (так як на полюсах сфери в залежності можуть виникати артефакти).

Якщо є наявна повноцінна 3D сцена, то з неї можна просто виготовити кубічну текстуру. Це робиться аналогічно до виготовлення текстури з фотографії-панорами: проводиться рендерінг шести видів сцени, але замість сфери з накладеною текстурою візуалізується сцена. Обробка текстур, що відповідають виду вниз і виду вверх в цьому випадку не є необхідною.

Рендерінг в кубічну текстуру

Для рендерінгу в кубічну текстуру необхідно виділити пам'ять для кубічної текстури, створити фреймбуфер для рендерінгу в текстуру, і провести візуалізацію для кожної сторони кубічної текстури:

GLuint texCube, fb, drb; // кубічна текстура, фреймбуфер і рендербуфер глибини
int size = 256; // розмір кубічної текстури

/////////////////////////////////////////////
// СТВОРИТИ ПУСТУ КУБІЧНУ ТЕКСТУРУ //////
////////////////////////////////////////////////

glGenTextures(1, &texCube);
glBindTexture(GL_TEXTURE_CUBE_MAP, texCube);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// виділити пам'ять під кожну сторону кубічної текстури
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, size,
             size, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA, size,
             size, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// ... аналогічні виклики для інших 4-ох сторін кубічної текстури

///////////////////////////////////////////////
// СТВОРИТИ ФРЕЙМБУФЕР ////////////////////
//////////////////////////////////////////////////

glGenFramebuffers(1, &fb); // згенерувати новий фреймбуфер
glBindFramebuffer(GL_FRAMEBUFFER, fb); // і призначити його активним
glGenRenderbuffers(1, &drb); // згенерувати норвий рендербуфер глибини,
glBindRenderbuffer(GL_RENDERBUFFER, drb);// і встановити його параметри
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, size, size);
//Приєднати одну з граней кубічної текстури до поточного фреймбуфера
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                       GL_TEXTURE_CUBE_MAP_POSITIVE_X, texCube, 0);
//Приєднати рендербуфер глибини до поточного фреймбуфера
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, drb);
// встановити буфер у який буде проводитися малювання
glDrawBuffer(GL_COLOR_ATTACHMENT0);

//перевірити чи новий фреймбуфер не містить помилок
GLenum status;
status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
switch(status){
    case GL_FRAMEBUFFER_COMPLETE:
        break;
    default:
        qDebug()<<"bad framebuffer!";
        break;
}

// повернути фреймбуфер по замовчуванню
glBindFramebuffer(GL_FRAMEBUFFER, 0);

////////////////////////////////////////////
// РЕНДЕР СЦЕНИ В КУБІЧНУ ТЕКСТУРУ //
////////////////////////////////////////////
glViewport(0, 0, size, size); // встановити розмір вьюпорта рівним розміру текстури
// ТУТ: встановити матрицю проекції: перспектива, FOV=90, ASPECT=1
// ТУТ: встановити матрицю виду, орієнтовану вздовж осі X

glBindFramebuffer(GL_FRAMEBUFFER, fb); // встановити рекнедінг в кубічну текстуру
// грань кубічної текстури в яку буде відбуватися рендерінг
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                       GL_TEXTURE_CUBE_MAP_POSITIVE_X, texCube, 0);
renderCurrentScene(); // рендерінг цілої сцени (також має очищувати глибину)

// ТУТ: вибір цілі рендерінку та повторий рендерінг для кожної іншої грані
// кубічної текстури. Також необхідно відповідно оновлювати матрицю виду.


// відновити матрицю проекції
// відновити матрицю виду
// відновити вьюпорт
glBindFramebuffer(GL_FRAMEBUFFER, 0); // відновити фреймбуфер по замовчуванню
Дзеркальні об'єкти
Колір дзеркальних об'єктів складається з кольору матеріалу об'єкта і кольору віддзеркаленого навколишнього середовища. У випадку дзеркала і тп. матеріалів, колір об'єкта майже повністю залежить від навколишнього середовища. В методі мапування навколишнього середовища віддзеркаленний колір розраховується наступним чином. Є нормаль в точці поверхні, і є вектор від камери до точки поверхні (вектор погляду). Розраховуємо відбитий від поверхні промінь з допомогою GLSL функції reflect(I, N), де падаючий вектор I рівний вектору погляду V, а нормаль рівна нормалі N в точці поверхні (детальніше про розрахунок відбитого вектора). Відбитий вектор R відповідає напряму відбитого світла. З допомогою цього вектора проводиться вибірка кольору з кубічної текстури (як показано на малюнку). Далі, віддзеркалений колір можна змішати з кольором об'єкту. Чіткість ефекту дзеркальної поверхні сильно залежить від деталізації об'єкту і зладженості нормалей. Метод не дозволяє відтворити ефект подвійного віддзеркалення, наприклад, коли об'єкт віддзеркалює своє віддзеркалення в іншому об'єкті. Для контролю дзеркальності частин об'єкта можна використовувати спеціальну текстуру (reflectivity map), яка вказує наскільки частина об'єкту є дзеркальною.
vec3 reflectedVector = reflect(view, normal);
vec3 reflectedColor = texture(environmentTexture, normalize(reflectedVector)).rgb;
Дзеркальний об'єкт
Прозорі об'єкти
Колір прозорих об'єктів складається з кольору самого об'єкта та кольору, який видно крізь об'єкт. Для розрахунку кольору, що пройшов крізь об'єкт, необхідно розрахувати заломлений вектор. Це можна зробити з допомогою закону Снеліуса. В GLSL заломлений вектор розраховується з допомогою функції refract(I, N, IOR). Падаючим променем I є напрям від камери до поверхні, а нормаллю N є нормаль поверхні в точці. IOR - відношення показника заломлення світла першого середовища до другого (детальніше про розрахунок заломленого вектора). Якщо досягається повне внутрішнє відбиття, то функція refract() повертає нульовий вектор. Далі, з допомогою заломленого вектора робиться вибірка кольору з кубічної текстури (як показано на малюнку) – це і буде колір, який видно крізь об'єкт. Так як метод мапування навколишнього середовища є всього-лиш простою інтерактивною симуляцією, то буде розраховане тільки одне заломлення вектора при зміні середовища. Більш точні симуляції враховують, що вектор заломлюється, коли входить в об'єкт і повторно заломлюється, коли виходить з об'єкта. Чіткість ефекту сильно залежить від деталізації об'єкта і згладженості його нормалей. Для контролю прозорості об'єкта чи показника заломлення, можна використовувати спеціальну текстуру (refractivity map), яка вказує наскільки частина об'єкта є прозорою.
vec3 refractedVector = refract(view, normal, IOR);
vec3 refractedColor = texture(environmentTexture, normalize(refractedVector)).rgb;
Прозорий об'єкт
Заломлені та відбиті вектори можна розраховувати як у вершинному, так і у фрагментному шейдері. Результати аналогічні до розрахунку світла. Вершинний шейдер дає гіршу якість, але більше швидкість, а фрагментний – кращу якість, але меншу швидкодію. Затрати часу на розрахунки у фрагментному шейдері можуть не виправдати результатів, так як метод мапування зовнішнього середовища є грубою апроксимацією.
Коефіцієнт Френеля
Прозорий об'єкт є прозорішим при малих кутах між нормаллю поверхні і вектором погляду, ніж коли кут між нормаллю поверхні та вектором поглядом є великим. Наприклад, розглянемо поверхню води: якщо дивитися перпендикулярно поверхні, то вода є прозорішою (видно дно річки), але при великому куті між нормаллю поверхні води та вектором погляду, вода все більше віддзеркалює навколишнє середовище (небо). Це повязано з тим, що коли світло досягає межі між двома середовищами, то частина світла відбивається від поверхні, а частина світла заломлюється і проходить крізь поверхню. Коефіціект Френеля використовується для того, щоб дізнатися пропорції віддзеркаленого світла до світла, що проходить крізь об'єкт. Але точний фізичний розрахунок цього числа є складним, і для простої сумуляції ми можемо його апроксимувати. Підійде будь-яка формула, де при малому куті між векторами заломлена складова максимальна, а при великому куті - максимальна дзеркальна складова. Наприклад:
Апроксимація коефіцієнта Френеля
Апроксимація коефіцієнта Френеля 2

Результати для першої та другої формули:

Коефіцієнт Френеля. Білі частини сфери будуть прозорі, а чорні - дзеркальні
Коефіцієнт Френеля в дії
Хроматична дисперсія
Хроматична дисперсія світла є в основі такого ефекту, як розділення призмою білого світла на спектр. Дисперсію світла можна описати наступним чином: хвилі світла з різною частотою заломлюються по різному. Тобто червона складова світла буде заломлюватися більше, а синя складова світла менше. OpenGL використоє систему кольорів RGB, і як наслідок ми маємо тільки три складові світла. Розрахунок ефекту є простим. Необхідно розрахувати три заломлені вектори, кожен з різним відношенням показників заломлення. Потім з допомогою цих векторів проводимо вибірку з кубічної текстури. Отримуємо три різні кольори, які відповідають червоній, зеленій та синій складовій світла. Кожен канал фінального кольору формуємо окремо: vec3 Tr = refract(I, N, IOR_red);
vec3 Tr = refract(I, N, IOR_green);
vec3 Tg = refract(I, N, IOR_blue);
vec3 finalColor;
finalColor.r = texture(u_envTexture, normalize(Tr)).r;
finalColor.g = texture(u_envTexture, normalize(Tg)).g;
finalColor.b = texture(u_envTexture, normalize(Tb)).b;
Приклад: фон є чорно білим, але в результаті дисперсії утворюються нові кольори
Хроматична дисперсія в дії
Хроматична дисперсія з великим відхиленням для зеленої складової світла
Використання кубічної текстури для визначення кольору освітлення
Кубічну текстуру можна використовувати для визначення кольору дифузного чи заповнюючого освітлення. Для цього необхідно зробити вибірку з текстури з допомогою нормалі точки поверхні, і використовувати колір вибірки в розрахунках дифузного чи започнюючого освітлення, як колір та інтенсивність світла з певного напрямку. На наступному малюнку зображений результат змішування дифузного кольору моделі з кольором та інтенсивністю заповнюючого освітлення, яке взяте з кубічної текстури.
Для використання кубічної текстури в якості кольору світла, кубічну текстуру найкраще зменшити до дозміру 8x8 чи 16x16 пікселів. В такому разі текстура не буде містити дрібних деталей з великою частотою і переходи кольору світла будуть плавними. При зменшені кубічної текстури треба враховувати, що пікселі на краях текстури повинні відповідати пікселям на краях суміжних текстур. Для створення зменшених кубічних текстур можна використати програму CubeMapGen від AMD. На останьому зображені показана зменшена кубічна текстура. На ній відсутні дрібні деталі та різкі переходи кольору.
vec3 ambientColor = texture(ambientTexture, normalize(normalVector)).rgb;
Колір та інтенсивність заповнюючого освітлення взяті з кубічної текстури
Зменшена кубічна текстура
  • Вершинний шейдер #version 330

    layout(location = 0) in vec3 i_position;
    layout(location = 1) in vec3 i_normal;

    uniform mat4 u_model_mat;
    uniform mat4 u_viewProj_mat;
    uniform vec3 u_camera_position;

    out vec3 v_directionToCamera;
    out vec3 v_normalVector;

    ///////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////

    void main(void){
       vec4 worldPos = u_model_mat * vec4(i_position,1.0f);
       v_normalVector = normalize(u_model_mat * vec4(i_normal,0.0f));
       v_directionToCamera = normalize(u_camera_position - vec3(worldPos));
       gl_Position = u_viewProj_mat * worldPos;
    }
  • Фрагментрий шейдер #version 330

    layout(location = 0) out vec4 o_FragColor;

    in vec3 v_directionToCamera;
    in vec3 v_normalVector;

    uniform samplerCube u_environmentTexture;

    ///////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////

    void main(void){
       vec3 N = normalize(v_normalVector);
       vec3 V = normalize(-v_directionToCamera);

       vec3 ambientColor = texture(u_environmentTexture, N).rgb;

       //////////////////////////////////////////////////////////////

       vec3 reflectedVector = reflect(V, N);
       vec3 reflectedColor = texture(u_environmentTexture, reflectedVector).rgb;

       //////////////////////////////////////////////////////////////

       float IOR = 0.8f;
       float offset = 0.05f;
       vec3 Tr = refract(V, N, IOR + offset);
       vec3 Tg = refract(V, N, IOR);
       vec3 Tb = refract(V, N, IOR - offset);

       vec3 refractedColor;
       refractedColor.r = texture(u_environmentTexture, Tr).r;
       refractedColor.g = texture(u_environmentTexture, Tg).g;
       refractedColor.b = texture(u_environmentTexture, Tb).b;

       //////////////////////////////////////////////////////////////
       //////////////////////////////////////////////////////////////

       float frenel = clamp(dot(N, V)/-1.0f,0.0f,1.0f);
       if(length(Tb)<0.5f){ // total internal reflection
          frenel = 0.0f;
       }
       frenel = pow( frenel, 1.55f);

       vec3 col = reflectedColor * frenel + refractedColor * (1.0f - frenel);

       //////////////////////////////////////////////////////////////
       //////////////////////////////////////////////////////////////

       o_FragColor = vec4(frenel, 1.0f);
    }



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