снизу фотографии настоящих жуков, сверху — моя реализация
Продолжение предыдущей статьи, на этот раз пишем шейдер.
Иризация
Итак в чем же особенность панциря такого жука? Если смотреть на него под разными углами — он будет изменять свой цвет. Как компакт диск. Такой цвет удобнее всего представлять в цветовой модели 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