1.光照模型
光线由光源发射出来会与其他物体相交,相交结果有两个:散射、吸收。散射只改变光线方向,但不改变光线密度和颜色,散射到物体内部称为折射,到物体外部为反射。吸收只改变光线密度和颜色,但不改变光线方向。
光照模型是指根据材质属性(漫反射属性)、光源信息(光源方向、辐射度等),使用公式去计算沿某个观察方向的着色过程,在标准光照中,把进入摄像头的光线4个部分:
- 环境光(ambient)
用于描述其他所有间接光照。环境光非常简单,通常是个全局变量,即场景中所有物体都使用这个环境光:
- 自发光(cmissive)
用于描述给定方向时,表面本身会向发射发射辐射量,如果没有使用全局光照技术,自发光表面不会真的照亮周围物体,而是本身看起来更亮。它的计算很简单,使用该材质的自发光颜色:
- 漫反射(diffuse)
用于描述当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量,漫反射符合兰伯特定律,反射光线强度与表面法线和光源方向的夹角的余弦值乘正比,因此漫反射公式:
Value公司开发《半条命》提出新技术,在Phong光照模型上简单修改,半兰伯特光照模型:
是材质的漫反射颜色。
则是光源颜色和强度。
I是向光源的单位矢量。
n是表面法线。
和绝大部分情况下的值均为0.5:
fixed halfLambert = dot(worldNormal,worldLightDir) * 0.5 + 0.5;
fixed3 color = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
- 高光反射(specular)
用于描述当光线从光源照射到模型时,该表面会在完全反射方向散射多少辐射量,计算高光反射需要知道的信息比较多,如表面法线、视角方向、光源方向、反射方向等,计算反射方向示意图如下:
Phong模型计算高光反射:
Blinn提出简单方法得到类似效果,Blinn模型引入了单位矢量h:
Blinn模型计算高光反射:
是材质光泽度(gloss)也被称为反光度。它用于控制高光区域的亮点有多宽。
是材质的高光反射颜色,控制高光反射的强度和颜色。
则是光源颜色和强度。
h是世界光源方向和视角观察方向相加后归一化的单位向量,具体片段代码如下:
// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir)
// Compute specular term
fiexed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,halfDir)),_Gloss)
标准光照模型只是经验模型,它并不完全符合真实世界中的光照现象。但由于它的易用性、计算速度和效果都比较好,因此仍然被广泛使用。但Phong模型有很多局限性。例如菲涅耳反射。其次Blinn-Phong 模型是各项同性的,当固定视角和光源方向旋转这个表面时,例如拉丝金属、毛发等,反射不会发生任何改变。
2.逐顶点或逐像素光照
给出基本光照模型数学公式后,在哪里计算这些光照模型呢?通常来讲,我们有两种选择:在片元着色器中计算,也被称为逐像素光照(per—pixel lighting);在顶点着色器中计算,也被称为逐顶点光照(per—vertexlighting)。
在逐像素光照中,以每个像素为基础,得到它的法线(对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型计算。这种在面片之间对顶点法线进行插值的技术被称为Phong着色(Phong shading),这不同于我们之前讲到的Phong光照模型。
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal fram object space to world space
o.worldNormal = mul(v.normal,(float3x3)_World20bject);
return o;
}
fixed4 frag(v2f i):SV_Target{
// Get ambient term
fixed3 ambient =UNITY_LIGHTMODEL_AMBIENT.xyz;
// Get the normal in world space
fixed3 worldNormal=normalize(i.worldNormal);
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse =_LightColor0.rgb *_Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz)
// Compute specular term
fiexed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss)
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
在逐顶点光照中,在每个顶点计算光照,然后会在光栅化阶段进行线性插值,最后输出成像素颜色。由于顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。但是逐顶点光照依赖于线性插值来得到像素光照,这会导致宣染图元内部的颜色总是暗于顶点处的最高颜色值,这在某些情况下会产生明显的棱角现象。
v2f vert(a2v v){
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal fram object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World20bject));
// Get the light direction in world space
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse =_LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(_Object2World, v.vertex).xyz)
// Compute specular term
fiexed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss)
o.color= ambient + diffuse + specular;
return o;
}
注:在Pass使用的光照模式:Tags {"LightMode" ="ForwardBase" }
3.Unity的渲染路径
在Unity里,渲染路径决定了光照是如何应用到UnityShader中的。因此要和光源打交道,需要为每个Pass指定它的渲染路径,Unity会通过渲染路径把光源和处理后的光照信息都放在这些数据里,正确选择和设置了渲染路径,Shader光照计算才能被正确执行。
-
前向渲染
对于前向渲染来说,Unity Shader通常会定义Base Pass(Base Pass 可以定义多次,例如双面渲染等情况)以及Additional Pass。Base Pass 仅会执行一次(定义多个Base Pass情况外),而Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行Additional Pass。
每进行完整的前向渲染,需要计算两个缓冲区的的信息(颜色缓冲区、深度缓冲区)。利用深度缓冲来决定片元是否可见,然后决定更新颜色缓冲区中的颜色值。前向渲染路径大致过程的伪代码如下:
Pass{
for(each primitive in this model){
for(each fragment covered by this primitive){
if(failedin depthtest){ //深度测试函数可自定义
//如果没有通过深度测试,说明该片元是不可见的
discard;
}else{
//准备光照相关参数
workLightParams;
//自定义的顶点着色器函数
struct v2f = VertexShading(materialInfo, pos, normalightDir, viewDir, lightParams);
//自定义的片元着色器函数
float4 color= FragmentShading(v2f, materialInfo, pos, normalightDir, viewDir, lightParams);
//更新帧缓冲
writeFrameBuffer(fragment,Color);
}
}
}
}
-
顶点照明渲染
对硬件配置要求最少、运算性能最高,但得到的效果也是最差的,不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。实际上,它仅仅是前向渲染路径子集,也就是说使用顶点照明渲染路径,那么Unity会只填充那些逐顶点相关的光源变量,意味着不可以用一些逐像素光照变量。
-
延迟渲染
当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如在场景的某块区域放置多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,为该区域内的每个物体执行多个Pass来计算不同光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得最终光照。然而每执行Pass都需要重新渲染一遍,但很多计算实际上是重复的。
延迟渲染是古老的渲染方案,但由于上述前向染可能造成的瓶颈问题,近几年又流行起来。延迟渲染还会利用额外的缓冲区,被统称为G缓冲(G-buffer),G缓冲区存储了渲染关心的表面信息(通常指离摄像机最近的表面),例如该表面法线、位置、用于光照计算的材质属性等。
延迟渲染原理主要包含了两个Pass。在第一个Pass中,不进行任何光照计算,仅仅计算哪些片元是可见的,通过深度缓冲技术来实现,当发现片元是可见的,把相关信息存储到G缓冲区中。第二个Pass中,利用G缓冲区各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算,延迟渲染路径大致过程的伪代码如下:
Pass 1{
//
//第一个Pass不进行真正的光照计算,仅仅把光照计算需要的信息存储到G缓冲中
fo(each primitive in this model){
for(each fragment covered by this primitive){
if(failedin depth test){
//如果没有通过深度测试,说明该片元是不可见的
discard;
}else{
//如果该片元可见就把需要的信息存储到G缓冲中
writeGBuffer(materialInfo,posnormal,lightDir,viewDir);
}
}
}
}
Pass 2{
//利用G缓冲中的信息进行真正的光照计算
for(each pixel in thescreen){
if(the pixelisvalid){
//如果该像素是有效的,读取它对应的G缓冲中的信息
readGBuffer(pixel,materialInfor posnormal lightDirviewDir);
//自定义的顶点着色器函数
struct v2f = VertexShading(materialInfo, pos, normalightDir, viewDir, lightParams);
//自定义的片元着色器函数
float4 color= FragmentShading(v2f, materialInfo, pos, normalightDir, viewDir, lightParams);
//更新缓冲
writeFrameBuffer(pixelcolor);
}
}
}
4.Unity的阴影设置
Unity阴影实现原理:(1条消息) 纹理原理和应用_臣定保幼主周全ぃ的博客-CSDN博客
此章节主要介绍阴影设置和内置宏计算光源阴影的作用,阴影设置(MeshRenderer)窗口如下:
其中难理解的是Two Sided选型,可以创建两个Plane相互垂直,然后平行光背面斜照向垂直Plane,观察地面Plane是否存在阴影?经过测试发现On选型是没有阴影生成的。
主要原因是背面剔除后,不存在任何顶点信息,因此不会写入ShadowMap。这种情况有两种解决方案:去掉剔除(Cull off)、使用Two Sided选型。
Cast Shadow开启时,此Mesh就会参与ShadowMap深度写入,另外Shader要添加一个LightMode为ShadowCaster的Pass,需要考虑透明混合和透明测试情况,由于透明度混合关闭深度写入,所以无法产生阴影,需要在每个光源空间下严格按照从后往前顺序进行渲染,这让阴影处理变得非常复杂,而且也会影响性能,虽然使用 dirty trick 可以强制为半透明物体生成阴影。透明测试相对简单,Fallback为Transparent Cutout Vertex-Lit,由于内部使用_Cutoff变量进行透明测试判断,所以创建的Shader需要定义同名变量。透明镂空顶点光照的如下:
Shader "Custom/Transparent/Cutout/VertexLit" {
Properties{
_Color("Main Color", Color) = (1,1,1,1)
_SpecColor("Spec Color", Color) = (1,1,1,0)
_Emission("Emissive Color", Color) = (0,0,0,0)
_Shininess("Shininess", Range(0.1, 1)) = 0.7
_MainTex("Base (RGB) Trans (A)", 2D) = "white" {}
_Cutoff("Alpha cutoff", Range(0,1)) = 0.5
}
SubShader{
Tags {"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
LOD 100
// Non-lightmapped
Pass {
Tags { "LightMode" = "Vertex" }
Alphatest Greater[_Cutoff]
AlphaToMask True
ColorMask RGB
Material {
Diffuse[_Color]
Ambient[_Color]
Shininess[_Shininess]
Specular[_SpecColor]
Emission[_Emission]
}
Lighting On
SeparateSpecular On
SetTexture[_MainTex] {
Combine texture * primary DOUBLE, texture * primary
}
}
// Lightmapped, encoded as dLDR
Pass {
Tags { "LightMode" = "VertexLM" }
Alphatest Greater[_Cutoff]
AlphaToMask True
ColorMask RGB
BindChannels {
Bind "Vertex", vertex
Bind "normal", normal
Bind "texcoord1", texcoord0 // lightmap uses 2nd uv
Bind "texcoord", texcoord1 // main uses 1st uv
}
SetTexture[unity_Lightmap] {
matrix[unity_LightmapMatrix]
constantColor[_Color]
combine texture * constant
}
SetTexture[_MainTex] {
combine texture * previous DOUBLE, texture * primary
}
}
// Lightmapped, encoded as RGBM
Pass {
Tags { "LightMode" = "VertexLMRGBM" }
Alphatest Greater[_Cutoff]
AlphaToMask True
ColorMask RGB
BindChannels {
Bind "Vertex", vertex
Bind "normal", normal
Bind "texcoord1", texcoord0 // lightmap uses 2nd uv
Bind "texcoord1", texcoord1 // unused
Bind "texcoord", texcoord2 // main uses 1st uv
}
SetTexture[unity_Lightmap] {
matrix[unity_LightmapMatrix]
combine texture * texture alpha DOUBLE
}
SetTexture[unity_Lightmap] {
constantColor[_Color]
combine previous * constant
}
SetTexture[_MainTex] {
combine texture * previous QUAD, texture * primary
}
}
// Pass to render object as a shadow caster
Pass {
Name "Caster"
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
struct v2f {
V2F_SHADOW_CASTER;
float2 uv : TEXCOORD1;
};
uniform float4 _MainTex_ST;
v2f vert(appdata_base v)
{
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
uniform sampler2D _MainTex;
uniform fixed _Cutoff;
uniform fixed4 _Color;
float4 frag(v2f i) : SV_Target
{
fixed4 texcol = tex2D(_MainTex, i.uv);
clip(texcol.a* _Color.a - _Cutoff);
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
}
而Receive Shadow决定是否获取阴影系数(阴影纹理进行深度测试,返回0说明处于阴影),最后在漫反射、高光反射乘上阴影系数。Shader代码段如下:
fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
SHADOW_COORDS、TRANSFER_SHADOW和SHADOW_ATTENUATION是计算阴影时的“三剑客”。这些内置宏用于计算光源的阴影,在AutoLight.cginc声明。SHADOW_COORDS在顶点着色器的输出结构体中添加,用于阴影纹理的保存。
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal :TEXCOORDO;
float3 worldPos :TEXCOORD1;
SHADOW_COORDS(2)//纹理序列0、1已被占用,阴影纹理使用TEXCOORD2
};
TRANSFER_SHADOW执行ShadowMap深度保存,SHADOW_ATTENUATION执行深度测试获取阴影系数,在Shader中调用代码如下:
v2f vert(a2v v)
{
v2f o;
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Use shadow coordinates to sample shadow map
fixed shadow = SHADOW_ATTENUATION(i);
}
由于上述宏会使用上下文变量进行相关计算,必须保证a2f结构体中的顶点坐标变量名为vertex,顶点着色器的输出结构体v2f命名为v,且v2f中顶点位置为pos。
UNITY_LIGHT_ATTENUATION是Unity内置的用于计算光照衰减和阴影的宏,用于代替SHADOW_ATTENUATION,具体Shader代码如下:
fixed4 frag (v2f i) : SV_Target {
// UNITY LIGHT ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
5.UnityShader标签、变量、函数
标签名 | 描述 | |||||
Always | 不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照 | |||||
ForwardBase | 用于前向渲染。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和 Lightmaps | |||||
ForwardAdd | 用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源 | |||||
Deferred | 用于延迟渲染。该Pass会渲染G缓冲(G-buffer) | |||||
ShadowCaster | 把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中 | |||||
PrepassBase | 用于遗留的延迟渲染。该Pass会渲染法线和高光反射的指数部分 | |||||
PrepassFinal | 用于遗留的延迟渲染。该 Pass通过合并纹理、光照和自发光来渲染得到最后的颜色 | |||||
Vertex | 用于遗留的顶点照明渲染 |
名 称 | 类 型 | 描 述 |
前向渲染路径的内置变量 | ||
LightColor0 | float4 | 该Pass处理的逐像素光源的颜色 |
LightColor0 | float4 | 该Pass处理的逐像素光源的颜色 |
_WorldSpaceLightPos0 | float4 | _WorldSpaceLightPos0.xyz 是该Pass处理的逐像素光源的位 置。如果该光源是平行光,那么_ _WorldSpaceLightPos0.w是0, 其他光源类型w值为1 |
_LightMatrix0 | float4X4 | 从世界空间到光源空间的变换矩阵。可以用于采样cookie和光 强衰减(attenuation) 纹理 |
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 | float4 | 仅用于Base Pass。前4个非重要的点光源在世界空间中的位置 |
unity_4LightAtten0 | float4 | 仅用于Base Pass。 存储了前4个非重要的点光源的衰减因子 |
unity_LightColor | half4[4] | 仅用于Base Pass。 存储了前4个非重要的点光源的颜色 |
顶点照明渲染路径的内置变量 | ||
unity_LightColor | half4[8] | 光源颜色 |
unity_LightPosition | float4[8] | xyz分量是视角空间中的光源位置。如果光源是平行光,那么分量值为0,其他光源类型z分量值为1 |
unity_LightAtten | half4[8] | 光源衰减因子。如果光源是聚光灯,分量是cos(spotAngle/2),y分量是1/cos(spotAngle/4);如果是其他类型的光源,分量是-l,y分量是1。z分量是衰减的平方,W分量是光源范围开根号的结果 |
unity_SpotDirection | float4[8] | 如果光源是聚光灯的话,值为视角空间的聚光灯的位置;如果是其他类型的光源,值为(0,0,1,0) |
延迟渲染路径的内置变量 | ||
_LightColor | float4 | 光源颜色 |
_LightMatrix0 | float4X4 | 从世界空间到光源空间的变换矩阵。可以用于采样cookie和光强衰减纹理 |
函数名 | 功能 |
实用内置函数 | |
float saturate(float) | 使值范围取到[0,1] |
float3 WorldSpaceViewDir(float4 v) | 输入模型空间顶点位置,返回世界空间从摄像机到该点观察方向,内部使用了UnityWorldSpaceViewDir |
float3 UnityWorldSpaceViewDir(float4 v) | 输入世界空间顶点位置,返回世界空间从摄像机到该点观察方向 |
float3 ObjectSpaceViewDir(float4 v) | 输入模型空间顶点位置,返回模型空间从摄像机到该点观察方向 |
float3 UnityObjectToWorldNormal(float3 norm) | 把法线方向从模型空间转换到世界空间中 |
float3 UnityObjectToWorldDir(in float3 dir) | 把方向矢量从模型空间变换到世界空间中 |
float3 UnityWorldToObjectDir | 把方向矢量从世界空间变换到模型空间中 |
前向渲染内置光照函数 | |
float3 WorldSpaceLightDir (float4 v) | 仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部使用UnityWordSpaceLightDir函数视线。没有被归一化 |
float3 UnityWorldSpaceLightDir (float4 v) | 仅可用于前向渲染中。输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化 |
float3 ObjSpaceLightDir(float4 v) | 仅可用于前向渲染中。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化 |
float3 Shade4PointLight (...) | 仅可用于前向渲染中。计算四个点光源的光照,它的参数是已经打包进矢量的光照数据,通常就是表9.2中的内置变量,如unity_4LightPosX0, unity_4LightPosY0, uity_4Lightposz0. uniy_ighgcColor和uity4Lighttn0等。前向渲染通常会使用这 个函数来计算逐顶点光照 |
顶点渲染内置光照函数 | |
float3 ShadeVertexLights (float4 vertex,float3 normal) | 输入模型空间中的顶点位置和法线,计算四个逐顶点光源的光照以及环境 光。内部实现实际上调用了ShadeVertexLightsFull函数 |
float3 ShadeVertexLightsFull(float4 vertex, float3 normal, int lightCount, bool spotLight) | 输入模型空间中的顶点位置和法线,计算lightCount个光源的光照以及环境光。如果spotLight值为 true,那么这些光源会被当成聚光灯来处理,虽然结果更精确,但计算更加耗时;否则,按点光源处理 |