unity-shader-光照相关


title: unity-shader-光照相关
categories: Unity3d
tags: [unity, shader, 光照模型]
date: 2019-03-19 10:05:18
comments: false

把之前记录到 unity-shader相关 中的笔记抽出单独一个文件记录. 因为光照部分也是比较大的内容.


菲涅尔反射

什么是菲涅耳效应?

简要地说,物体在不同角度观察下,表面的反射率是不一样的。菲涅耳效应模拟的就是物体材质反射率随角度改变的效果。如下图左所示,肥皂泡在边缘处,即视线与表面夹角处比较小时,反射效果更强烈;在肥皂泡中心附近,即视线与表面夹角近乎垂直时,看起来更透明一些。下图右中,池塘远处的水面看上去像镜面一样,近处的水面则更透彻,这同样是由于菲涅耳效应——在不同视角观察下物体材质反射率不同。如果你家里的地板是光滑的大理石材质,或者是打蜡过的木地板的话,可以很容易自己观察到这样的现象。

实际世界的菲涅尔公式非常复杂,我们同样用一些近似公式来计算,如下面提到的Schlick菲涅尔近似公式和Empricial菲涅尔近似公式: ( 参考 : Unity Shader学习笔记(15)立方体纹理、反射、折射、菲涅尔反射 - http://gad.qq.com/article/detail/38332 )

  • Empricial 菲涅尔近似等式

    //resnelBias 菲尼尔偏移系数  
    //fresnelScale 菲尼尔缩放系数  
    //fresnelPower 菲尼尔指数  
    reflectFact = fresnelBias + fresnelScale*pow(1-dot(viewDir,N)),fresnelPower);  //系数:多少光发生折射多少光发生反射

  • Schlick 菲涅尔近似等式:( 这个貌似用的人比较多 )

    /F0是反射系数用于控制菲涅尔的强度
    reflectFact = F0 + (1-F0)*pow(1-dot(viewDir,N),5);

    F0为反射系数,v为视角方向,n为表面法线)

    shader中的使用 ( 参考: Illustrative-Rendering-with-Unity 工程, git : https://github.com/SilangQuan/Illustrative-Rendering-with-Unity )

    //Specular term
    float3 halfVector = normalize(lightDir + viewDir);
    float3 specBase = pow(saturate(dot(halfVector, s.Normal)), _SpecularPower);// Blinn Phong计算镜面反射灯光
    
    // 菲涅尔公式
    float fresnel = 1.0 - dot(viewDir, halfVector);
    fresnel = pow(fresnel, 5.0);
    fresnel = _SpecularFresnel + (1.0 - _SpecularFresnel)*fresnel;
    
    //float3 finalSpec = _SpecularColor * spec* fresnel;;
    float3 finalSpec = specBase * fresnel * _LightColor0.rgb;

光照衰减&阴影

参考 : 笔记十——光照衰减&阴影 - https://zhuanlan.zhihu.com/p/31805436

Unity中的阴影
Unity中可以使一个物体向其他物体投射阴影,以及让一个物体接收其他物体的阴影,从而使场景看起来更加真实。Unity的实时渲染中,使用一种Shadow Map技术,这种技术将摄像机放在光源位置,场景中的阴影区域即为摄像机看不到的区域。阴影映射纹理本质上是一张深度图,记录从光源位置出发,能看到的场景中距离距离它最近的表面位置的深度信息。Unity使用一个额外的Pass专门用于更新光源的阴影映射纹理,该Pass为 LightMode标签设置为 ShadowCaster的Pass。当使用屏幕空间阴影映射时,Unity先调用 LightModeShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。根据光源的阴影映射纹理和深度纹理得到屏幕空间的阴影图,一个物体若想接收其他物体的阴影,则需要在Shader中对阴影进行采样。阴影图是屏幕空间下的,先对表面坐标从模型空间变换到屏幕空间,再对阴影图进行采样。完整的过程为:


让物体接收阴影

物体接受阴影,需要对阴影纹理进行采样和相应的计算,需要用到

#include "AutoLight.cginc"
SHADOW_COORDS()
TRANSFER_SHADOW
SHADOW_ATTENUATION()

这里需要注意的是 这些宏会使用上下文变量来进行相关计算,为了确保宏正确工作,要保证自定义的变量名与宏使用的变量名相匹配,a2f结构体中顶点坐标变量名必须是 vertex ,顶点着色器的输入结构体a2v 必须命名为 v v2f中的顶点位置变量名必须是 pos


前向渲染的两种pass

参考 : 入门精要 中的 9.1.1 前向渲染路径


光照及阴影的计算

可以参考 :

  • TestShader 测试项目中的 Chapter9_Shadow.shader
  • 入门精要 中的 9.1.1 前向渲染路径 会有详细的解释

在 unity 中, 只有 平行光 会产生 阴影投射 , 而 点光源聚光灯 都不会. 所以一般都是以平行光作为主光源, 而其他光源只是作为辅助光.

一般在前向渲染中, 会有两个 pass 去渲染光照.

第一个 pass 是指定 Tags{"LightMode"="ForwardBase"} 用来渲染 主光源漫反射, 高光, 阴影, 同时把 环境光自发光 也丢在这里计算.

第二个 pass ( additional pass ) 是指定 Tags{"LightMode"="ForwardAdd"} 用来渲 除主光源意外的所有光源 () 的 漫反射, 高光 , 因为这个 pass 会叠加在上个 pass 的颜色, 所以不再计算 环境光自发光.

  • 如果不指定 ForwardAdd 会默认是使用 ForwardBase , 则会 覆盖 掉上个 pass 的颜色
  • 这个 pass 需要指定混合模式 Blend One One , 不然会覆盖掉上个 pass 的颜色
  • 测试时可以将第二个 pass 注释点, 看到的效果则是 除主光源外所有的光源 都是无效的.
  • 每个其他光源都会走一次这个 additional pass

所以一个物体所产生的 pass 总量 = 1 ( 主光源 ) + n ( 其他光源 )


阴影的实现 - ShadowMap

参考 :


光照模型

Lambert(兰伯特)

参考 :

主要计算公式

//定义片元shader
fixed4 frag(v2f i) : SV_Target
{
	float atten = LIGHT_ATTENUATION(i); // 很多时候衰减值直接定为 1.0
    //unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _AmbientColor.xyz;
    //归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的
    fixed3 worldNormal = normalize(i.worldNormal);
    //把光照方向归一化
    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
    
    //根据光照模型计算像素的光照信息
    fixed3 lambert = max(0.0, dot(worldNormal, worldLightDir)); // 兰伯特, 背光部偏暗
    // fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5; // 半兰伯特, 背光部会比 兰伯特 亮点, 比较好
    //最终输出颜色为lambert光强*材质diffuse颜色*光颜色
    fixed3 diffuse = lambert * _DiffuseColor.xyz * _LightColor0.xyz * atten + ambient;
    //进行纹理采样
    fixed4 color = tex2D(_MainTex, i.uv);
    return fixed4(diffuse * color.rgb, 1.0);
}

Half Lambert(半兰伯特)

Phong

Blinn Phong

参考 :

相比较Phong模型,Blinn-phong模型只适用N•H替换了V•R,但却获得了明显的提高,它能提供比Phong更柔和、更平滑的高光,而且由于Blinn-phong的光照模型省去了计算反射光线方向向量的两个乘法运算速度上也更快,因此成为很多CG软件中默认的光照渲染方法,同时也被集成到大多数的图形芯片中,在OpenGL和Direct3D渲染管线中,Blinn-Phong就是默认的渲染模型。

Blinn-phong光照模型中,用N•H的值取代了V•R。Blinn-phong光照模型的光强因子为:
(N•H)n
即H 越靠近N,光照越强。
由于这两个光照模型公式基本相同,所以只解释一下N•H:
N与前面相同,是顶点的单位法向量,而H则是入射光L和顶点到视点的单位向量的角平分线单位向量,通常也成为 半向量。其计算方法为:

H = (L + V) / (|L + V|)

主要计算公式

fixed4 frag (v2f i) : SV_Target
{
    // 法线方向
    float3 normalDirection = normalize(i.normalDir);
    // 灯光方向
    float lightDirection = normalize(_WorldSpaceLightPos0.xyz);
    // 灯光颜色
    float3 lightColor = _LightColor0.rgb;
    // 视线方向
    float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
    // 视线方先与法线方向的半向量
    float3 halfDirection = normalize(viewDirection+lightDirection);

    // 计算灯光衰减
    float attenuation = LIGHT_ATTENUATION(i);
    float3 attenColor = attenuation * _LightColor0.xyz;

    // 基于兰伯特模型计算漫反射灯光
    float NdotL = max(0,dot(normalDirection,lightDirection));
    // 方向光
    float3 directionDiffuse = pow(NdotL, _DiffusePower) * attenColor;
    // 环境光  
    float3 inDirectionDiffuse = float3(0,0,0)+UNITY_LIGHTMODEL_AMBIENT.rgb;

    // 基于Blinn Phong计算镜面反射灯光
    float specPow = exp2( _Gloss * 10.0 + 1.0 );
    float3 directionSpecular = attenColor*  _SpecularColor*(pow(max(0,dot(halfDirection,normalDirection)),specPow));

    // 灯光与材质球表面颜色进行作用
    float3 texColor = tex2D(_MainTex, i.uv).rgb;
    float3 diffuseColor = texColor *(directionDiffuse+inDirectionDiffuse);
    float3 specularColor = directionSpecular;
    float4 finalColor = float4(diffuseColor+specularColor,1);
    return finalColor;
}

逐顶点 与 逐像素

参考 :

漫反射光照符合兰伯特定律 : 反射光线的强度 与 表面法线 和 光源方向 之间的夹角的余弦值成正比 .

计算机图形第一定律 : 如果它看起来是对的 , 那么它就是对的 .

逐顶点光照的计算量往往要小于逐像素光照 .

逐顶点光照依赖于线性插值来得到像素光照 , 当光照模型中有非线性的计算的时候 , 逐顶点光照就会出问题 , 例如计算高光反射 .

逐顶点光照会在渲染图元内部对顶点进行插值 , 渲染图元内部的颜色总是暗于顶点处的最高颜色 , 在某些情况下会产生明显的棱角现象 .


光照计算方式

  • 兰伯特 无高光, 参考 : Lambert(兰伯特)

  • 兰伯特 + blin-phong, 参考 : Blinn-phong

  • 有阴影和光照

    fixed4 frag(v2f i):SV_Target{
        float3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
        float3 worldNormal=normalize(i.worldNormal);
        float3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
    
        fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
        fixed3 diffuse=_LightColor0.rgb*_Color.rgb*max(0,dot(worldLightDir,worldNormal));
        fixed3 halfDir=normalize(worldLightDir+worldViewDir);
        fixed3 specularColor=_LightColor0.rgb*_SpecularColor.rgb*pow(saturate(dot(halfDir,worldNormal)),_Gloss);
    
        //片元着色器中计算阴影值
        fixed shadow=SHADOW_ATTENUATION(i);
    
        // UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos); // unity提供的光照衰减
        fixed atten=1.0;
        return fixed4(ambient+(diffuse+specularColor)*atten*shadow,1.0); // 有阴影时的计算方式
    }

总结

  • 无阴影

    最终颜色 = (漫反射系数 x 纹理颜色 x RGB颜色)+ 高光 + 环境光 + 自发光颜色 + [边缘色]
  • 有阴影

    最终颜色 = [(漫反射系数 x 纹理颜色 x RGB颜色)+ 高光] * atten * shadow  + 环境光 + 自发光颜色 + [边缘色]

光照

可以参照 : Unity Shader 入门精要 中的 6.2 标准光照模型

使用实时光要必要的因素

  1. 指定光照模式 Tags{ “LightMode” = “ForwardBase”}
  2. 包含头文件 include “Lighting.cginc”
  3. 主灯光颜色:_LightColor0.rgb

一般的绘制贴图的颜色 (不透明)

fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb; //texColor为纹理采样颜色, _Color 为调色颜色(可有可无)

环境光 ambient

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb;

漫反射 diffuse

计算方式, 主光颜色 * 控制颜色 * 法线向量与光向量 的点积. 参考: Lambert(兰伯特)

高光 specular

计算方式, 主光颜色 * 控制颜色 * (法线向量与观察向量的 点积)的 _Gloss(高光强度) 次方, 参考: [Blinn Phong](#Blinn Phong)

最终颜色

参考: [Blinn Phong](#Blinn Phong) , 里面的shader比较全面

术语

  • 光学不连续性( optical discontinuity)
  • 散射( scattering) 分为 反射( reflection)和 折射( refraction)
  • 吸收( absorption)
  • 漫反射光表示经历 透射( transmission),吸收( absorption)和 散射( scattering) 的光
  • 入射光( Incoming illumination)
  • 辉度( irradiance)
  • 出射光( outgoing light) 出射率( exitance)
  • 粗糙度( roughness,其反义词是 smoothness,光滑度)
  • 半向量 ( halfVector )

cubemap

反射方向的计算

L为入射光(顶点到光源)的单位法向量,N为顶点的单位法向量,R为反射光的单位法向量,V是观察方向。
R=2(N•L)N-L

推导过程:

L在N方向上的投影是|Lcosθ|=cosθ,那么投影矢量N’=Ncosθ
L+S = N’ = Ncosθ
R = L+2S = L+2(Ncosθ-L) = 2Ncosθ-L

简单计算,或者使用Cg的内置函数reflect(),该函数接受参数向量I和法线N,返回I关于N的反射向量。
float3 R = reflect(I,N);

reflect(I, N) 根据入射光方向向量 I,和顶点法向量 N,计算反射光方向向量。其中 I 和 N 必须被归一化,需要非常注意的是,这个 I 是指向顶点的;函数只对三元向量有效

shader

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldNormal = mul(unity_ObjectToWorld, v.normal);
    o.worldViewDir =  normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
    o.worldRef = reflect(-o.worldViewDir,normalize(o.worldNormal)); // 入射光方向向量, 指向顶点 
    return o;
}

samplerCUBE _CubeMap;
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = texCUBE(_CubeMap, i.worldRef);
    return col;
}

半向量

是指 光向量观察向量 之和, 用于 高光部分的计算

float3 halfDir = normalize(lightDir + viewDir);

fixed3 specularColor=_LightColor0.rgb*_SpecularColor.rgb*pow(saturate(dot(halfDir,worldNormal)),_Gloss); // blinn-phong

也于 菲尼尔 反射计算

//Specular term
float3 halfVector = normalize(lightDir + viewDir);
float3 specBase = pow(saturate(dot(halfVector, s.Normal)), _SpecularPower);// *s.Specular * specFresnel;
float fresnel = 1.0 - dot(viewDir, halfVector);
fresnel = pow(fresnel, 5.0);
fresnel = _SpecularFresnel + (1.0 - _SpecularFresnel)*fresnel;
float3 finalSpec = specBase * fresnel * _LightColor0.rgb;

各向异性