模拟正式光照环境生成图像时所需了解的相关概念
模拟真实光照环境生成图像的过程主要分为如下3个步骤
- 光源发射光线
- 光线与场景中物体相交,一些光线被吸收,一些光线散射到其他方向
- 摄像机吸收一些光,产生图像
光源
辐照度:光源发射的光的数量
平行光的辐照度:垂直于光的方向的单位面积上单位时间内穿过的能量
吸收
吸收:改变密度和颜色
散射
散射:改变方向
分为2类
- 折射:射到物体内部 漫反射
- 反射:射到外部 高光反射
出射度:出射光线的数量和方向(根据入射光线的数量和方向计算)
着色
根据材质属性和光源信息,使用一个等式(光照模型)计算沿某观察方向的出射度
标准光照模型(Phong光照模型)
只关心直接光照
直接光照:直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光效
基本方法:把进入摄像机的光线分为4部分,各部分使用自己的方法计算贡献度
- 自发光(emissive):给定一个方向,某表面本身会向该方向发射的辐射量
- 高光反射(specular):光线从光源照射到模型表面时,表面会在完全镜面反射方向散射的辐射量
- 漫反射(diffuse):光线从光源照射到模型表面时,该表面会向各方向散射的辐射量
- 环境光(ambient):其他所有间接光照
逐像素和逐顶点
- 片元着色器中计算光照模型:逐像素光照
Phong着色:在面片之间对顶点发现进行插值 - 顶点着色器中计算光效模型:逐顶点光照
高洛德着色:在各顶点计算光照,在渲染图元内进行线性插值,输出成像素颜色
Unity中的光
环境光
在Windows/Lighting/Settings中的AmbientOcclusion中设置
在Shader中通过UNITY_LIGHTMODEL_AMBIENT得到环境光的颜色和强度
自发光
在片元着色器输出最后的颜色之前,将材质的自发光颜色添加到输出颜色上
Unity中的漫反射光照模型
漫反射值 = 光的颜色 * 材质的漫反射系数 * 表面法线与光源方向的点积
最终的颜色 = 环境光值+漫反射值
逐顶点光照
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// 逐顶点的漫反射光照
Shader "Fan/Diffuse Vertex-Level"
{
Properties
{
// 控制材质的漫反射颜色
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
// 顶点/片元着色器的代码要写在Pass中(而非SubShader中)
Pass
{
// 指定该Pass的光照模式(定义该Pass在Unity的光照流水线中的角色)
// 定义了LightMode后,可得到Unity内置的光照变量
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 要使用的Unity内置变量包含在如下内置文件中
#include "Lighting.cginc"
// 与声明的属性相匹配的变量
fixed4 _Diffuse;
struct a2v
{
float4 vertex : POSITION;
// 将模型顶点的法线信息存储到该变量中
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
// 将在顶点着色器中算得的光照颜色存储在该变量中,传递给片元着色器
float3 color : COLOR;
};
v2f vert(a2v v)
{
v2f o;
// 将顶点位置从模型空间转换到裁剪空间
o.pos = UnityObjectToClipPos (v.vertex);
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 将顶点法线转到世界中间中(原来在模型空间中) 归一化处理
fixed3 worldNormal = normalize(mul(v.normal, (float3x3) unity_WorldToObject));
// 将光源方向归一化
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// 计算漫反射光照结果 saturate:将参数截取到范围0~1内
// _LightColor0是内置变量,表示该Pass处理的光源的颜色和强度信息(要定义合适的LightMode标签)
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
o.color = ambient + diffuse;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return fixed4(i.color, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
逐像素光照
更平滑,但是在光照无法到达的区域,模型的外观全黑且无明暗变化
顶点着色器将世界空间下的法线传给片元着色器,在片元着色器中计算漫反射光照模型
半兰伯特模型
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
改善模型背光面明暗一样的问题
对于模型的背光面,兰伯特光照模型的点积结果都会映射到0,而在半兰伯特模型中背光面有明暗变化(不同的点积结果映射到不同值)
该模型无物理依据,仅是视觉加强技术
在片元着色器计算漫反射光照的部分中,用半兰伯特模型代替兰伯特模型
Unity中的高光反射光照模型
高光反射值 = 光的颜色 * 材质的高光反射系数 * 视角方向与反射方向的点积的高光参数次方
高光参数影响高光区域的大小
通过reflect(i,n),根据入射方向和法线方向计算反射方向
逐顶点光照
逐像素光照
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// 逐顶点的高光反射光照
Shader "Fan/Specular Pixel-Level"
{
Properties
{
// 控制材质的漫反射颜色
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
// 控制高光区域大小, 成反比
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
// 顶点/片元着色器的代码要写在Pass中(而非SubShader中)
Pass
{
// 指定该Pass的光照模式(定义该Pass在Unity的光照流水线中的角色)
// 定义了LightMode后,可得到Unity内置的光照变量
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 要使用的Unity内置变量包含在如下内置文件中
#include "Lighting.cginc"
// 与声明的属性相匹配的变量
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
// 将模型顶点的法线信息存储到该变量中
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
// 将顶点位置从模型空间转换到裁剪空间
o.pos = UnityObjectToClipPos (v.vertex);
// 将顶点法线转到世界中间中(原来在模型空间中) 归一化处理
o.worldNormal = normalize(mul(v.normal, (float3x3) unity_WorldToObject));
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 漫反射
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// 高光
// 入射光线方向关于表面法线的反射方向
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// 视角方向:将顶点位置从模型空间变换到世界空间,世界中间中摄像机的位置减去顶点位置
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
顶点着色器计算世界空间下的法线方向和顶点坐标
将高光相关计算移动到片元着色器中,得到的结果更平滑
Blinn-Phong光照模型
// 高光
// 入射光线方向关于表面法线的反射方向
// fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// 视角方向:将顶点位置从模型空间变换到世界空间,世界中间中摄像机的位置减去顶点位置
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 用于替代发射方向的向量
fixed3 halfDir = normalize(worldLightDir + viewDir);
// fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
引入新的向量代替反射方向
点积:法线方向与新向量的点积
新的向量:对视角方向和光照方向相加后的向量归一化
计算简单:仅需一次向量乘法
结果:高光反射部分更大更亮
这是经验模型
上述几种光照模型的实际效果
Unity提供的在计算光照时可用的内置函数
Unity提供了常用计算函数
输入各空间中的顶点位置
- 输出对应空间中该点到摄像机的方向
- 输出世界空间中从该点到光源的光照方向(只能用于前向渲染(Pass的渲染标签中设置),因为只有在前向渲染中光的位置(内置变量_WorldSpaceLightPos0)才能被正确赋值)
将方向向量转到对应空间
未归一化
内置函数输出的方向没有归一化(需开发者主动使用normalize进行归一化)