大家好,我是阿赵。
这里打算给大家介绍一些常用的光照模型的shader实现方法。虽然这些光照模型很多都会在各大引擎内置,一般不需要自己写。但我觉得学习一下,首先对了解渲染原理有帮助,然后对写一些复合效果的shader时,可以比较灵活的控制它的光照效果,也是一件好事情。
一、什么是光照模型
1、模型为什么会有立体感和质感
首先来看一下这三个球体。
1.第一个球,应该不能算球体,因为看起来它并没有立体感,只能看作是一个2D平面上面的一个圆形
2.第二个球,明显看出来,他是一个立体轮廓,可以肯定他是3D的。
3.第三个球,除了能看出来它是立体的以外,我们还可以强烈的感受到,它的表面很光滑,有一种很硬的感觉。
其实这三个球的网格模型是完全一样的,它们能给我们不同的感官上的认知,这主要是因为光影的问题。
首先,物体在受光面和背光面,会形成明暗分割线。这个分割线,有可能是很尖锐,也有可能是很柔和。它赋予了模型亮部和暗部的形状,比如球体,他的分割线是一条弧线,代表了物体表面是曲面。而如果是立方体,那么它的明暗分割线都在各条棱上, 表达了物体边缘的转角。
然后,通过光线在物体表面的反射强度变化,可以模拟到物体表面的光滑度,也就是一般所说的高光。不同的高光形状、高光强度、对比度,可以表达不同表面的不同质感。
2、在引擎里面的光照模型
所谓的光照模型,其实就是通过计算,让物体展现出不同的光照效果的数学模型。
简单的说,一个物体由3种颜色构成:
1.环境光颜色
2.漫反射颜色
3.高光反射颜色
数学模型需要做的事情,就是通过数学方法的计算,求出物体在一个光照空间里面的环境光、漫反射和高光的颜色。
也许很多朋友会觉得,这个所谓的光照模型,好像也没什么存在感?因为平常用游戏引擎,比如Unity3D,新建一个材质球出来,他本身就是有这种立体的效果了。
其实一个shader本身是不会展示光照的效果的,如果你看到模型是立体的,那是因为shader本身调用了引擎内置的光照模型了。比如:
如果你建了一个unity的surface类型的shader,会发现可以直接指定它的光照模型类型。
既然游戏引擎里面已经内置了各种的光照模型,为什么我们还需要自己去学光照模型是怎样写的呢?
我们经常听到的一些光照模型,比如上面的Lambert,或者Blinn-Phong,都是前人所发明的一些比较规范的光照模型。假如我们要做风格化的渲染,一般来说就要在这些标准的光照模型上面做一些修改,比如修改它的漫反射色阶,比如修改它的高光形状和渐变方式,等等。所以如果我们能熟悉光照模型的计算方式,会写光照模型,那么我们能实现的效果就多很多了。
说一下这篇文章的内容,由于标准的光照模型公式,在网上随便搜一下就能有,所以我就不写公式了。先直接写个集合了各种常用光照模型的shader贴上来,然后再逐个说一下不同光照模型之间的区别。
二、常用光照模型示例
Shader "azhao/LightModelTest"
{
Properties
{
_mainColor("MainColor", Color) = (1,1,1,1)
_lightColor("LightColor", Color) = (1,1,1,1)
_gradationMin("gradationMin", Range(0 , 1)) = 0.5
_gradationMax("gradationMax", Range(0 , 1)) = 0.6
_specColor("SpecColor",Color) = (1,1,1,1)
_shininess("shininess", Range(1 , 100)) = 1
_tangentVal("TangentVal", Range(0 , 1)) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float3 tangent : TANGENT;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldTangent : TEXCOORD2;
};
float4 _mainColor;
float4 _lightColor;
float _gradationMin;
float _gradationMax;
float4 _specColor;
float _shininess;
float _tangentVal;
//获取Lambert漫反射值
float GetLambertDiffuse(float3 worldPos, float3 worldNormal)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
return NDotL;
}
//获取HalfLambert漫反射值
float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
float halfVal = NDotL * 0.5 + 0.5;
return halfVal;
}
//获取以Lambert为基础的色阶化漫反射值
float GetLambertGradationDiffuse(float3 worldPos, float3 worldNormal, float min, float max)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
float smoothVal = smoothstep(NDotL, min, max);
return smoothVal;
}
//获取以HalfLambert为基础的色阶化漫反射值
float GetHalfLambertGradationDiffuse(float3 worldPos, float3 worldNormal, float min, float max)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
float halfVal = NDotL * 0.5 + 0.5;
float smoothVal = smoothstep(halfVal, min, max);
return smoothVal;
}
//获取光线反射方向
float3 GetReflectDir(float3 worldPos, float3 worldNormal)
{
float3 worldSpaceLightDir = UnityWorldSpaceLightDir(worldPos);
float3 reflectDir = normalize(reflect((worldSpaceLightDir * -1.0), worldNormal));
return reflectDir;
}
//获取Phong高光
float GetPhongSpec(float3 worldPos, float3 worldNormal)
{
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 reflectDir = GetReflectDir(worldPos, worldNormal);
float specDir = saturate(dot(viewDir, reflectDir));
float specVal = pow(specDir, _shininess);
return specVal;
}
//获取BlinnPhong高光
float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal)
{
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
float specDir = max(dot(normalize(worldNormal), halfDir),0);
float specVal = pow(specDir, _shininess);
return specVal;
}
//获取Anisortropic各向异性高光
float GetAnisortropicSpec(float3 worldPos, float3 worldNormal,float3 worldTangent)
{
float3 binormal = cross(worldNormal, worldTangent);
float3 lerpVal = normalize(lerp((worldNormal + binormal), binormal, _tangentVal));
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
float anistropicVal = dot(lerpVal, halfDir);
float specDir = max(anistropicVal*anistropicVal, 0);
float specVal = pow(specDir, _shininess);
return specVal;
}
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldTangent = UnityObjectToWorldDir(v.tangent);
return o;
}
half4 frag (v2f i) : SV_Target
{
//漫反射模式选择
float diffuseVal = GetLambertDiffuse(i.worldPos,i.worldNormal);//获取Lambert漫反射值
//float diffuseVal = GetHalfLambertDiffuse(i.worldPos,i.worldNormal);//获取HalfLambert漫反射值
//float diffuseVal = GetLambertGradationDiffuse(i.worldPos,i.worldNormal,_gradationMin,_gradationMax);//获取以Lambert为基础的色阶化漫反射值
//float diffuseVal = GetHalfLambertGradationDiffuse(i.worldPos,i.worldNormal,_gradationMin,_gradationMax);//获取以HalfLambert为基础的色阶化漫反射值
//高光模式选择
//float3 specVal = float3(0, 0, 0);//没有高光
//float3 specVal = GetPhongSpec(i.worldPos, i.worldNormal);//获取Phong高光
//float3 specVal = GetBlinnPhongSpec(i.worldPos, i.worldNormal);//获取BlinnPhong高光
float3 specVal = GetAnisortropicSpec(i.worldPos, i.worldNormal,i.worldTangent);//获取Anisortropic各向异性高光
//计算漫反射颜色
float3 diffuseCol = _mainColor * _lightColor*diffuseVal;
//计算高光颜色
float3 specCol = specVal * _specColor* _lightColor;
//最终颜色是环境色+漫反射+高光
half3 finalCol = UNITY_LIGHTMODEL_AMBIENT + diffuseCol+specCol;
return float4(finalCol,1);
}
ENDCG
}
}
}
上面这个shader,通过不同方法的调用,可以组合出多种光照模型结果。各位有兴趣可以把高光和漫反射的部分看一下,修改一下注释,替换不同的光照模型。
1、漫反射
1.Lambert
最简单而且经典的漫反射光照模型Lambert
//获取Lambert漫反射值
float GetLambertDiffuse(float3 worldPos, float3 worldNormal)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
return NDotL;
}
它的计算方式非常的简单,使用顶点的世界法线方向,和灯光的世界空间方向做一个点乘。一般简称为NDotL。通过点乘的几何含义,可以得出,它其实是求出了灯光方向和法线方向的夹角,用这个夹角的大小来模拟物体的表面是否收到光照,和光照的强度。
2.HalfLambert
Lambert的光影对比强度很强,模型看起来比较沉重,于是在Lambert的基础上,出现了Half-Lambert
//获取HalfLambert漫反射值
float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
float halfVal = NDotL * 0.5 + 0.5;
return halfVal;
}
从计算过程可以看出来,Half-Lambert其实是在Lambert的NDotL的基础上,乘以0.5再加上0.5。这样处理之后,模型的光影对比度就变低了,整个模型看起来也变亮了。
3.色阶化
所谓的色阶化,是把原来连续的,柔和的曲面光影变化,变成了分段式的亮面和暗面。一般是使用在卡通渲染之类的风格化渲染上面。
//获取以HalfLambert为基础的色阶化漫反射值
float GetHalfLambertGradationDiffuse(float3 worldPos, float3 worldNormal, float min, float max)
{
float3 lightDir = UnityWorldSpaceLightDir(worldPos);
float NDotL = saturate(dot(worldNormal, lightDir));
float halfVal = NDotL * 0.5 + 0.5;
float smoothVal = smoothstep(halfVal, min, max);
return smoothVal;
}
色阶化的方式有很多种,我这里只是随便举了一种而已。比如常见的是通过step函数或者smoothstep函数来把漫反射颜色分段。也可以是把漫反射的颜色作为UV坐标,来读取一张色阶图,实现颜色的分段。
2、高光
1.Phong
Phong高光
//获取光线反射方向
float3 GetReflectDir(float3 worldPos, float3 worldNormal)
{
float3 worldSpaceLightDir = UnityWorldSpaceLightDir(worldPos);
float3 reflectDir = normalize(reflect((worldSpaceLightDir * -1.0), worldNormal));
return reflectDir;
}
//获取Phong高光
float GetPhongSpec(float3 worldPos, float3 worldNormal)
{
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 reflectDir = GetReflectDir(worldPos, worldNormal);
float specDir = saturate(dot(viewDir, reflectDir));
float specVal = pow(specDir, _shininess);
return specVal;
}
从实现上来看,是通过观察方向和反射方向的点乘来确定高光的方向,一般这个叫做VDotR。
为了控制高光的范围,再对VDotR做一个Power运算。
2.Blinn-Phong
Phone的高光计算比较真实,但存在2个问题:
因为需要计算光线的反射方向,所以他的计算量比较大,消耗性能比较高
Phone的高光比较集中,通过高光来着色效果不是很理想
于是后来Blinn在Phong的基础上修改出了Blinn-Phong。
//获取BlinnPhong高光
float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal)
{
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
float specDir = max(dot(normalize(worldNormal), halfDir),0);
float specVal = pow(specDir, _shininess);
return specVal;
}
从实现上可以看出,这里已经不需要计算光照反射方向了,取而代之的是一个halfDir。这个中间方向,其实是通过观察方向和灯光方向相加得到的。最后还是通过power来控制高光的范围。
Blinn-Phong的计算量比Phong模型少,然后通过halfDir计算的高光范围,会比纯Phong计算的范围要大,边缘会柔和一点,通过Blinn-Phong来对高光着色,效果会比Phong要柔和一些。
3.Anisortropic各向异性
通过对比可以看出,各向异性产生的高光,会沿着物体的表面形状发生变化。各向异性一般是用于头发之类非一个平滑面的物体。
//获取Anisortropic各向异性高光
float GetAnisortropicSpec(float3 worldPos, float3 worldNormal,float3 worldTangent)
{
float3 binormal = cross(worldNormal, worldTangent);
float3 lerpVal = normalize(lerp((worldNormal + binormal), binormal, _tangentVal));
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
float anistropicVal = dot(lerpVal, halfDir);
float specDir = max(anistropicVal*anistropicVal, 0);
float specVal = pow(specDir, _shininess);
return specVal;
}
我这里用的是一个简化版的各向异性计算方式。里面主要用到了一个binormal 的感念。binormal 是通过法线和切线叉乘得到的,通过叉乘的几何含义,我们可以知道,实际上binormal 是一条垂直法线和切线的向量,它和切线可以定义一个和顶点相切的面。一般我们会用到TBN(Tangent,Binormal,Normal)矩阵进行运算。
三、ASE整理
我个人挺喜欢使用ASE编辑器去试shader效果,它出效果的效率很高,可视化和节点管理的功能也很好用。比如刚才手写的那段shader,我使用ASE来连线,并且通过成组的注释分块,可以比较清晰的看出整个shader的结果。不过我个人是不会使用ASE生成的Shader代码的,因为很多细节的把控,我个人觉得还是手写的Shader会控制得比较准确一点,可以省略很多没必要的导入和计算。
这里给大家展示的主要是他编辑和管理的便利性,各位有兴趣可以看看。