Всем привет!
Предлагаю разобраться (кому это интересно, естественно) в сабже и совместными усилиями набросать шейдер для PBR в Unity3d. Как утверждает один из разработчиков Unity, на PBR переходит вся игровая индустрия и кинематограф.
Спорить с ним не будем, просто давайте разберемся, что это такое и как его сделать самому. Не для того, "чтобы было", а чтобы знать как это работает "под капотом".
Итак, что подразумевается под термином physically based rendering и каковы основные аспекты этого подхода.
Очевидно из названия, что рендеринг при таком подходе и особенно расчет освещения происходят с учетом реальных физических законов.
Основные аспекты:
-Energy Conservation - имеется ввиду, что объект не может отразить больше света, чем получил
-Everything has specular - все материалы отражают свет
-Everything has Fresnel - для всех материалов необходимо учитывать коэффициент Френеля для расчета отражения
Для художников такой подход облегчает настройку материалов, т.к. количество параметров меньше и они интуитивно понятны.
Ну и, конечно, качество картинки заметно растет. Материалы становятся более естественными и лучше вписываются в окружение, т.к. при расчетах используются кубмапы сгенерированные в этом окружении.
Я набросал на Unity обычный surface-шейдер, с Blinn-Phong моделью освещения, на котором мы и будем ставить эксперименты.
Shader "PBR/PBShader"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Shininess ("Shininess", Range (0.03, 1)) = 0.078125
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf PhysicallyBased
sampler2D _MainTex;
sampler2D _BumpMap;
half _Shininess;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
float2 uv_GlossMap;
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Specular = _Shininess;
o.Gloss = c.a;
}
fixed4 LightingPhysicallyBased(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 h = normalize(lightDir + viewDir);
half diff = max(0, dot(s.Normal, lightDir));
half nh = max(0, dot(s.Normal, h));
half spec = pow(nh, s.Specular * 128.0) * s.Gloss;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * spec * atten;
return c;
}
ENDCG
}
FallBack "Diffuse"
}Все исходники буду выкладывать сюда, пусть люди пользуются)
Давайте разбираться по пунктам. Что такое Energy conservation и как его вписать в наш шейдер?
Bonus
> Что такое Energy conservation
<= 1.0 :)
innuendo
т.е. просто нормализуем diff и spec?
Bonus
> т.е. просто нормализуем diff и spec?
Относительно чего ?
innuendo
Их суммы. Получается хрень) уже попробовал
Просто saturate?
Bonus
> Их суммы
Blinn можно нормализовать через пи - если считать по академической формуле
Bonus
> Что такое Energy conservation и как его вписать в наш шейдер?
конкретно для блинфонга было много обсуждений, я помню часть из них была неправильная, часть с Пи, часть без, и потом вылезали новые статьи , короче хрен помнит уже правильный, но начиналось всё вроде отсюда
http://www.thetenthplanet.de/archives/255
ну и короче где-то в конце концов ты набредаешь на этот вариант
vec3 h = normalize( lightDir - viewDir ); float nh = saturate( dot( h, normal ) ); float spec = pow( nh, specPow ) * (specPow + 2.0)/8.0;
который вполне себе работает.
>Что такое Energy conservation
в контексте спекуляра это значит, что маленький сфокусированный блик яркий, а широкий по всей поверхности рассеянный - слабо видный. типа потому что кол-во энергии одно приходит и распространяется по разной площади, соотвественно чем шире, тем слабее интенсивность.
---
а может это и у меня неправильная)
вот тут ещё есть
http://www.rorydriscoll.com/2009/01/25/energy-conservation-in-games/
(specPow + 8)/(8*Pi)
Тут всё написано
http://en.wikipedia.org/wiki/BRDF
Т.е. диффузный свет никак не связан с сохранением энергии? Это относится только к отраженному свету?
Bonus
> Т.е. диффузный свет никак не связан с сохранением энергии? Это относится только
> к отраженному свету?
Связан. Смотри BRDF
Почитал ссылки, которые дали, покрутил шейдер. Вот к чему пришел:
Все считают фактор нормализации по-разному.
Вот 2 более более-менее формализованных:
1. (specPow + 8)/(8*Pi) - тот что давал Mr F
2. ((specPow + 2)*(specPow + 4)) / ((8*Pi) * (pow(2, -specPow / 2) + specPow))
Визуально разницы нет. Оба дают одинаковый блик, который, как и говорил Mr F
маленький сфокусированный блик яркий, а широкий по всей поверхности рассеянный - слабо видный
Еще я домножил spec на diff, чтобы не было блика, там где нет диффузного света.
Shader "PBR/PBShader"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Shininess ("Shininess", Range (1, 128)) = 30
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf PhysicallyBased
sampler2D _MainTex;
sampler2D _BumpMap;
half _Shininess;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
float2 uv_GlossMap;
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Specular = _Shininess;
o.Gloss = c.a;
}
fixed4 LightingPhysicallyBased(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 h = normalize(lightDir + viewDir);
half diff = max(0, dot(s.Normal, lightDir));
half nh = max(0, dot(s.Normal, h));
half normalizationFactor = ((s.Specular + 8) / 25.1327412287);//25.1327412287 = Pi * 8
//half normalizationFactor = ((s.Specular + 2)*(s.Specular + 4)) / (25.1327412287 * (pow(2, -s.Specular / 2) + s.Specular));
half spec = diff * pow(nh, s.Specular) * s.Gloss * normalizationFactor;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * spec * atten;
return c;
}
ENDCG
}
FallBack "Diffuse"
}Такой спекуляр действительно выглядит интересней.
Как нормализовать диффузную составляющую? Или это не нужно делать?
Разобрался с параметром Roughness.
Он берется из альфы альбедо-текстуры. Это не то же самое, что и Gloss. Gloss теперь не используется.
На основе Roughness вычисляется specPower в пределах от [0.25...2048]
Подсмотрел это в одной из демок
Shader "PBR/PBShader"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_Roughness ("Roughness", Range (0.0, 1.0)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf PhysicallyBased
#define Pi 3.14159265358979323846
sampler2D _MainTex;
sampler2D _BumpMap;
half _Roughness;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
float2 uv_GlossMap;
};
void surf (Input IN, inout SurfaceOutput o)
{
half4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Specular = c.a * _Roughness;
o.Gloss = c.a;
}
fixed4 LightingPhysicallyBased(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 h = normalize(lightDir + viewDir);
half diff = max(0, dot(s.Normal, lightDir));
half nh = max(0, dot(s.Normal, h));
//приводим specPower в промежуток [0.25...2048]
float specPower = exp2(10 * s.Specular + 1) - 1.75;
half normalizationFactor = ((specPower + 8) / 25.1327412287);//25.1327412287 = Pi * 8
half spec = diff * pow(nh, specPower) * normalizationFactor;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * spec * atten;
return c;
}
ENDCG
}
FallBack "Diffuse"
}Вот так работает шейдер сейчас
Справа стандартный BumpSpecular, слева наш.
Bonus
Что-то на energy conservation не похоже.
Это выкрученный на полную Roughness слева и Shininess справа.
Если в нашем шейдере уменьшать Roughness, то блик будет расширяться, но при этом терять интенсивность.
Если в стандартном шейдере уменьшать Shininess, то блик будет расширяться, но интенсивность будет постоянной.
Я так понимаю это и есть сохранение энергии. На статической картинке не знаю как ее показать.
Тема в архиве.