Иридисцентный шейдер

Что здесь происходит?

Здесь используются вертексный и фрагментный шейдера, вертексный делает какие-то рассчёты на вертексах геометрии (довольно логично), а получаемые значения впоследствии интерполируются для фрагментов, где к ним применяется фрагментный шейдер (он же пиксельный).

Что такое иридисценция/иридизация/иризация? Да я хз вообще Это явление, при котором материал может менять цвет при изменении угла зрения или угла падения света


Vertex Shader

in vec4 T;  //Получаем вертексный аттрибут T (tangent) с геометрии

out vVert{  //out - значения, которые вертекс шейдер передаст фрагмент
    mat3 tangentToWorld;
    vec4 worldPos;
    vec2 texCoord;
    flat int camIndex;  //flat значит, что интерполировать это при передаче не надо
} oVert;

void main(){
    vec4 worldSpacePos = TDDeform(P);       //TDDeform() - тачевская функция, которая трансформирует P из Object Space в World Space
    oVert.texCoord = uv[0].st;                          //Записываем uv в контейнер для передачи фрагмент шейдеру
    vec3 worldNorm = normalize(TDDeformNorm(N));    //TDDeformNorm() - по сути то же самое, что и TDDeform(), только для тангентов и нормалей
    vec3 worldTan = normalize(TDDeformNorm(T.xyz));
    oVert.tangentToWorld = TDCreateTBNMatrix(worldNorm, worldTan, T.w); //TDCreateTBNMatrix() - собирает TBN матрицу, зачем она нужна расскажу, когда буду разбирать математику из этого шейдера
    oVert.worldPos = worldSpacePos;
    oVert.camIndex = TDCameraIndex();   //передаём в пиксель шейдер индекс камеры, т.к. там его получить не получится
    gl_Position = TDWorldToProj(worldSpacePos); //gl_Position - позиция вертекса в Projection Space, про Tangent, Object, World, Camera и Projection Spaces будет там же
}

Короче, обычный такой вертексный шейдер, ничего необычного, идём дальше


Fragment (Pixel) Shader

uniform float uBumpScale;
uniform float uShadowStrength;
uniform vec3 uShadowColor;
uniform float uShininess;
uniform vec3 uAmbientColor;
uniform float uDiffuse;
uniform float uSpecular;    //баян

uniform sampler2D sNormalMap;
uniform sampler2D sIridescenseMap;
uniform sampler2D sIridescenseOffset;   //sampler2D по сути текстура, из которой мы можем получать значения цвета

in vVert{   //получаем из вертекс шейдера интерполированые значения для текущего пикселя
    mat3 tangentToWorld;
    vec4 worldPos;
    vec2 texCoord;
    flat int camIndex;
} iVert;

out vec4 fragColor;

void main(){
    vec4 col = vec4(0.0f);
    vec3 irDir = vec3(0.0f, 0.0f, 0.0f);
    vec3 diffuseSum = vec3(0.0f);
    vec3 specularSum = vec3(0.0f);

    //мой прадед называл это Normal Mapping
    vec4 normalMap = texture2D(sNormalMap, iVert.texCoord);     //сэмплим значение карты нормалей по uv
    vec3 norm = 2.0f * normalMap.xyz - 1.0f;    //переводим её на русский           (т.к. каждый цвет изображения может быть от 0 до 1, а направления могут быть от -1 до 1, то эти значения от -1 до 1 переводят в 0,1, сейчас мы переводим их обратно в -1,1
    norm.xy = norm.xy * uBumpScale; //ну это все знают
    norm = iVert.tangentToWorld * norm; //короче, карта нормалей показывает направления относительно поверхности нашего объекта, использовать в таком виде их невозможно, для этого трансформим их через TBN матрицу в World Space из Tangent Space
    vec3 oldNorm = iVert.tangentToWorld[2].xyz; //в TBN матрице третий столбец это нормаль, тут я её сохранил отдельно, чтобы меньше печатать
    vec3 normal = normalize(norm);  //нормализуем нормаль, нормальная тема

    vec3 view = normalize(uTDMats[iVert.camIndex].camInverse[3].xyz - iVert.worldPos.xyz);  //смотрим, откуда мы смотрим. uTDMats - контейнер со всяческими матрицами, которые предлагает нам тач, в зависимости от камеры они разные, т.к. они нужны для переходов между спейсами от объекта до проекции, camInverse - обратная матрица камеры, в её четвертом столбце хранится вектор (x,y,z,1), где xyz - её координаты в World Space

    if(!TDFrontFacing(iVert.worldPos.xyz, oldNorm)){
        normal *= -1.0f;
    }

    for(int i = 0; i < TD_NUM_LIGHTS; i++){ //TD_NUM_LIGHTS - количество "ламп" в сцене
        vec3 diffContrib = vec3(0.0f);
        vec3 specContrib = vec3(0.0f);
        TDLighting(diffContrib,     //тачевская функция для лайтинга, считает, сколько света попадёт в глаз
                specContrib,
                i,
                iVert.worldPos.xyz,
                normal,
                uShadowStrength,
                uShadowColor,
                view,
                uShininess);
        specularSum += uSpecular * specContrib;
        diffuseSum += uDiffuse * diffContrib;
        TDLight light = uTDLights[i];   //получаем светильник...
        irDir += normalize(view + normalize(light.position.xyz - iVert.worldPos.xyz)) * (diffContrib);  //...и используем его позицию для изменения переменной, которая потом поможет победить маппинг карты иризации
    }

    irDir = transpose(iVert.tangentToWorld) * (normal - normalize(irDir));  //переводим irDir в пространство поверхности из мира (World -> Tangent)
    vec2 iridPos = 0.5f + 0.5f * irDir.xy;  //помните, что я сказал, про -1_1 и 0_1? тут та же фигня, только наоборот
    vec2 iridOffset = texture2D(sIridescenseOffset, iVert.texCoord).xy; //как же я заебался печатать эту статью, сюда же никто не дочитает, зачем я это делаю
    iridPos += iridOffset;  //а, ну да, есть ещё карта для оффсета координат сэмплера иризации, она по uv накладывается, работает как vector displacement, только 2дшный
    iridPos = mod(iridPos, 1.0f);   //она вылазила за границы текстуры, поэтому я поставил просто буду тайлить текстуру

    col = texture2D(sIridescenseMap, iridPos);  //ахахах, наконец-то, сэмплим карту иризации
    col *= vec4((diffuseSum + specularSum + uTDGeneral.ambientColor.rgb * uAmbientColor.rgb), 1.0f);    //умножаем её на свет, который должен был отскочить нам в глаз
    fragColor = col;    //выводим цвет на рендер
}

  • Есть ещё вариант, где для сэмплинга иризации используется dot(normal, view), но он слишком легок для реализации и менее гибкий, т.к. при вращении вокруг нормали какой-либо точки, цвет в этой точке меняться не будет
  • Если Т будет косячной, то и результат шейдинга тоже будет косячным, это можно увидеть на полюсах сферы, например
  • На строке 60 я умножаю на старую TBN матрицу, без учёта нормал мэппинга. Скорее всего, этог нигде не будет заметно, но правильно было бы найти матрицу World -> Tangent с учётом новой нормали с карты
  • Эта штука должна неплохо выглядеть в виаре, где её можно было бы покрутить в руках

Дополнительно

Iridescense - это то, о чём была эта статья, если кто-то ещё не заметил
Normal Mapping - без бамп мэпов этот материал выглядит не так интересно
Phong - сначала я пытался сделать специальный лайтинг для этого материала, но потом решил, что с некоторыми костылями дополнениями дефолтный TDLighting справится не хуже
Книга про шейдеры, я её толком не читал, но там пишут что-то нормальное. Вроде, она всё ещё в разработке
Вообще классная штука, без которой этого шейдера не было бы


Благодарности

Хочу выразить благодарность команде Derivative за разработку такого софта, как TouchDesigner; текстовому редактору Vim, за невероятно удобное редактирование кода; создателю(-ям) OpenGL за создание OpenGL; Аде Лавлейс за то, что она первая женщина-программист, а не хуе-мое; себе за написание данного шейдера; всем, кто дочитал до этого момента, за то, что они дочитали до того момента, и тем, кто подписан на мой канал в телеграме за меняющуюся циферку там сверху; ресурсу rentry.co за хостинг этой статьи; Ж.Ф.Лиотару за термин "постмодернизм"

Edit

Pub: 05 Dec 2018 21:32 UTC

Edit: 10 Dec 2018 17:09 UTC

Views: 433