
снизу фотографии настоящих жуков, сверху — моя реализация
Продолжение предыдущей статьи, на этот раз пишем шейдер.
Иризация
Итак в чем же особенность панциря такого жука? Если смотреть на него под разными углами — он будет изменять свой цвет. Как компакт диск. Такой цвет удобнее всего представлять в цветовой модели HSV, а конкретно нас интересует параметр Hue или цветовой тон, который изменяется вот в таком диапазоне:

Если взять угол между вектором направления взгляда и нормалью к поверхности панциря, то получим число, которое можно использовать в качестве параметра Hue в вышеописанной модели. С дополнительной корректировкой по диапазону можно добиться нужного результата. Так же диапазон можно корректировать через текстуру, об этой карте смещений я писал в предыдущей статье. Примерный алгоритм:
- Для текущего фрагмента определяем вектор направления взгляда eyeDir
- Вычисляем скалярное произведение между вектором направления взгляда и нормалью к поверхности angle=dot(normal, eyeDir)
- Дополнительный шаг для повышения реализма — возводим получившийся угол в квадрат angle=pow(angle, 2.0)
- Используем угол для определения тона, выбираем цветовой диапазон hue=angle*hueRange+hueAverage, где hueRange — диапазон, hueAverage — смещение
- Дополнительно вычисляем смещение для текущего фрагмента из соответствующей текстуры: hue-=shift
- Выбираем оставшиеся два параметра цветовой модели HSV и конвертируем ее в RGB: color=hsv2rgb(hue, saturation, value)
В итоге получаем чистый цвет, который в последствии можно дополнить отражением, затенением и прочими эффектами.
Часть шейдера с этим алгоритмом может выглядит так:
// код конвертации был взят отсюда: http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
vec3 hsv2rgb(float h, float s, float v) {
vec4 k = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(vec3(h)+k.xyz)*6.0-k.www);
return v*mix(k.xxx, clamp(p-k.xxx, 0.0, 1.0), s);
}
const float iridescenceOpacity = 0.6;
const float hsvSaturation = 0.7;
const float hsvValue = 0.8;
const float addIridescenceValue = 0.2;
void main() {
...
float hue = (1.0-pow(dot(normal, eyeDir), 2.0))*hueRange+hueAverage;
hue -= (1.0-dataTex.a)*addIridescenceValue;
vec3 diffuse = hsv2rgb(hue, hsvSaturation, hsvValue)*iridescenceOpacity;
...
}
На этом собственно и заканчивается часть с определением переливающейся части цвета. Остальная часть цвета вычисляется стандартно на основе карт ao, отражений и освещенности. Как видно алгоритм простой и его можно применять практически к любой поверхности и материалу.
Хотя есть один нюанс — у жука на фотографии отражение окружения на панцире размыто. У меня в шейдере для отражений используется cubemap, что бы отражение было размытым можно размыть кубмапу или уменьшить ее разрешение или как сделал я использовать низкий мип-уровень. Так как в GLSL 1.1 нельзя обращаться к конкретному мипу из вершинного шейдера, установить его придется через параметр GL_TEXTURE_BASE_LEVEL функции glTexParameteri:
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 6);
Я использовал шестой уровень мипа, но все зависит от изначального разрешения текстуры и требуемой степени размытия. Честно говоря это не очень хороший способ размытия, так как на гранях кубмапы образуются полосы в виде резкой границы перехода цвета, но так как на жуке ярко выраженная рельефная поверхность — этого не заметно.Кстати как вариант, можно еще использовать сферические гармоники.

резкая граница
Еще вместо конвертирования цветовой модели, можно взять одномерную текстуру и делать выборку по ней, используя в качестве текстурной координаты вычисленный угол между наблюдателем и нормалью. Таким образом меняя текстурку можно менять цвет жука.
Полный шейдер
Вершинный:
Фрагментный:
#version 110
uniform mat4 matrixProj; //матарица проекции
uniform mat4 matrixView; //матрица вида
attribute vec3 vertex;
attribute vec3 normal;
attribute vec3 tangent;
attribute vec2 texCoord;
varying vec2 vTexCoord;
varying mat3 vTbn; //матрица тангент битангент нормаль
varying float zPos; // z-координата
void main() {
vec4 pos = matrixProj*matrixView*vec4(vertex, 1.0);
gl_Position = pos;
vTexCoord = texCoord;
zPos = pos.z;
vec3 bitangent = cross(normal, tangent); // вычисляем битангент
vTbn = mat3(tangent, bitangent, normal); // ...и матрицу
}
Фрагментный:
#version 110
uniform sampler2D texture; //текстура о которой я писал в предыдущей статье
uniform samplerCube envMap; //карта окружения
uniform vec3 light; //позиция источника света
uniform float bright; //общая яркость
uniform float focalDistance; //фокусное расстояние для блура
uniform float focalRange; //область фокуса
uniform vec3 eyeDir; //направление взгляда
uniform float hueAverage; //смещение для цветового тона, фактически начальная величина
uniform float hueRange; //диапазон тона
varying vec2 vTexCoord;
varying mat3 vTbn;
varying float zPos;
const float shadowOpacity = 0.7; //прозрачность затенения
const float envBright = 1.0; //яркость карты окружения
const float envLighting = -0.6;//яркость отражения
const float iridescenceOpacity = 0.6;//влияние иризации на цвет
const float hsvSaturation = 0.7; //насыщенность цвета
const float hsvValue = 0.8;//значение цвета
const float addIridescenceValue = 0.2;//влияние карты смещений на тон
vec3 hsv2rgb(float h, float s, float v) {
vec4 k = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(vec3(h)+k.xyz)*6.0-k.www);
return v*mix(k.xxx, clamp(p-k.xxx, 0.0, 1.0), s);
}
void main() {
vec4 dataTex = texture2D(texture, vTexCoord);
vec3 normal = normalize(vTbn*(vec3(dataTex.rg, 1.0)*2.0-1.0));//вытаскиваем нормаль из текстуры
float lighting = clamp(dot(normal, light), 0.0, 1.0);//вычисляем освещенность
float hue = (1.0-pow(dot(normal, eyeDir), 2.0))*hueRange+hueAverage;//вычисляем цветовой тон
hue -= (1.0-dataTex.a)*addIridescenceValue;//делаем смещение на основе данных из текстуры
vec3 diffuse = hsv2rgb(hue, hsvSaturation, hsvValue)*iridescenceOpacity;//конвертируем в RGB
vec3 env = textureCube(envMap, reflect(-eyeDir, normal)).rgb;//вычисляем отражение
diffuse += env*bright*envBright+envLighting;//корректировка отражения и добавление к основному цвету
float shadow = lighting*dataTex.b*shadowOpacity+1.0-shadowOpacity;//вычисляем затенение на основе освещенности источником цвета и ао из текстуры
gl_FragColor.rgb = shadow*diffuse;//добавляем тени
gl_FragColor.a = clamp((focalDistance+zPos)/focalRange, -1.0, 1.0);//вычисляем и записываем значение размытия
}
Немного про GUI

Мне было интересно попробовать такой способ:
Берется вот такая текстура:

Она достаточно маленькая, хватит 16х16 пикселей. Любой прямоугольный элемент гуи имеет такую сетку:

У этой сетки текстурные координаты задаются таким образом, что используя текстурку выше получается вот такой черно-белый спрайт:

А теперь главное — каждый пиксель этого спрайта — это текстурная координата для одномерной текстуры, которая представляет из себя тему оформления элемента интерфейса. Чем то напоминает distance field. Например, для скриншота выше использовались такие полоски:



У каждой полоски по три строки пикселей — для фона, нажатой кнопки и отпущенной.
Все текстуры должны использоваться с билинейной интерполяцией. Таким образом используя две очень маленькие текстурки можно добиться почти векторного качества элементов интерфейса, на любых DPI. Правда оформление полу��ается во все стороны симметричное, нельзя, например, сделать падающие тени.
Пиксельный шейдер для рендера такого вида интерфейса
#version 110
uniform sampler2D guiMap; //спрайт с гуи-элементом
uniform sampler2D themeMap; //тема оформления
uniform float themeNum;//строка (v-координата текстуры) в теме оформления
varying vec2 vTexCoord;
void main() {
float a = texture2D(guiMap, vTexCoord).r; //находим текстурную координату для темы
gl_FragColor = texture2D(themeMap, vec2(a, themeNum)); //берем цвет
}
Видео
Я добавил к блуру немного шума, чтобы эмитировать эффект film grain. Так же можно заметить недостаток такого эффекта размытия — небольшой ореол вокруг резких границ.
Скринкаст к сожалению получился без вертикальной синхронизации, несмотря на то, что она была включена.
Проблема
Система Kubuntu 12.04, включена вертикальная синхронизация в стандартных настройках. Можно дополнительно включить синхронизацию через nvidia xserver settings. На мониторе у приложений никаких разрывов не наблюдаются, не наблюдаются и во время скринкаста, но если посмотреть полученную запись — видно что там есть разрывы. Видео записываю через avconv:
с параметром vsync игрался по всякому. Какие бы я настройки не выставлял, но при высокой нагрузке, даже при включенной синхронизации во всех настройках — появляются разрывы. Есть подозрение, что косяк в дровах от нвидии. Если кто-то с этим сталкивался и нашел решение проблемы — сообщите, пожалуйста. Прошу прощения за некачественное видео.
avconv -f x11grab -s 1280x720 -i :0.0+2240,185 -c:v libx264 -r 30 -vsync 2 -b 4000k -threads 8 -y 1.mp4с параметром vsync игрался по всякому. Какие бы я настройки не выставлял, но при высокой нагрузке, даже при включенной синхронизации во всех настройках — появляются разрывы. Есть подозрение, что косяк в дровах от нвидии. Если кто-то с этим сталкивался и нашел решение проблемы — сообщите, пожалуйста. Прошу прощения за некачественное видео.
Исходники
Писал на C++ с использованием Qt версии 4.7.* или 4.8.*, тестировал на Kubuntu 12.04 и 14.04, Mac 10.8, видеокартах intel hd 4000, GeForce gtx560, Radeon HD 6750M.
Исходники: github.com/Torvald3d/Beetle
Модель жука (obj): github.com/Torvald3d/Beetle/tree/master/obj
Лицензия Creative Commons, однако в проекте есть HDR карты с этого сайта со всеми вытекающими последствиями, а так же код шейдера fxaa.frag, который был взят отсюда.
Ссылки
- lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl — конвертация цвета HSV модели в RGB
- mew.cx/glsl_quickref.pdf — GLSL 1.1 Quick Reference Guide