目录
前言
在 Unity Shader 入门笔记(二)和 Unity Shader 入门笔记(三)中学习了Shader程序的基础用法,现在是时候写一个标准光照模型,作为实践Unity Shader的起点,不过在写光照模型Shader之前让我们先来了解什么是光照模型。
一、光照模型
1. 简介
当光照射到物体表面时,物体对光会发生反射、透射、吸收、衍射、折射、和干涉,其中被物体吸收的部分转化为热、反射、透射的光进入人的视觉系统,使我们能看见物体。为模拟这一现象,我们建立一些数学模型来替代复杂的物理模型,这些模型就称为明暗效应模型或者光照明模型。
2. 发展历程
1967年,Wylie等人第一次在显示物体时加进光照效果。Wylie认为,物体表面上一点的光强,与该点到光源的距离成反比。
1970年,Bouknight提出第一个光反射模型,指出物体表面朝向是确定物体表面上一点光强的主要因素,用Lambert漫反射定律计算物体表面上各多边形的光强,对光照射不到的地方,用环境光代替。
1971年,Gourand提出的基于“漫反射模型与插值”思想的Gourand模型。对多面体模型,用漫反射模型计算多边形顶点的光亮度,再用增量法插值计算。
1975年,Bui Tuong Phong(裴祥风)提出图形学中第一个有影响的简单光照明模型。模型虽然只是一个经验模型,但是其真实度已达到可以接受的程度。在Phong光照模型的基础之上,相继出现了Goud明暗处理和Phong明暗处理两个增量式光照模型。
1980年,Whitted提出了一个光透射模型:Whitted模型,并第一次给出光线跟踪算法的范例,实现了模型它综合考虑了光的反射、折射透射、阴影等。从本质上来说,whitted模型也只是一种经验模型,它仅考虑特定方向上的环境入射光对被照射点的光亮度共线,因而它仅能模拟理想的镜面反射和规则的投射效果。
1982年,Cook和Torrance为了克服Phong模型的缺点,提出了一个基于物理光学的表面反射模型—Cook-Torrance模型,“使得模型中反射光的位置和分布与实际情况非常接近,因而用它绘制的图形具有很好的质感。
1983年,Hall和Greenbert在Whitted基础上此进一步给出Hall光透射模型,考虑了漫透射和规则透射光。改进了whitted中投射高光效果,并再环境光中加入距离衰减因子,使之能够更好的模拟物体表面的透射特性。
1986年,Kajiya统一了以前所有的光照模型,Kajiya首先提出使类似于随机采样的蒙特卡罗(Monte Carlo)方法求解绘制方程的光线追踪算法(Raytracing), 通过对到达图像平面上的光线路径进行采样,然后估计它们对最终图像的贡献来生成图像。
3. 局部光照模型和全局光照模型
3.1. 局部光照模型
局部光照模型(Local llumination Model)是场景中的光源直接照射到物体表面时,计算产生的反射或折射光的数学模型。
早期游戏引擎中往往只使用一个光照模型,这个光照模型被称为标准光照模型。标准光照模型只关心直接光照,也就是那些直接从光源发射出来照射到物体表面后经过物体表面一次反射直接进入摄像机的光线,基本方法是把进入到摄像机的光线分为4个部分,每个部分使用一种方法计算它的贡献度,如下:
- 自发光(Emissive):描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量。
- 环境光(Ambient):描述标准模型中使用环境光来近似模拟间接光照。
- 高光反射(Specular):描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量。
注意:在硬件实现时,如果摄像机和光源距离模型足够近,Blinn模型会快于Phong模型,此时可以认为视角方向和光照方向是一个定值,所以半程向量是一个常量,反之,如果不是定值,Phong模型反而更快。
- 漫反射(Diffuse):描述当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量。
从图片可以看出,漫反射光照符合兰伯特定律:漫反射光线的强度与表面法线和光源方向之间的夹角的余弦成正比。那么在法线和光照方向均为单位向量的情况下漫反射部分计算如下:
3.2. 全局光照模型
全局光照(Global Illumination),简称 GI,或被称为Indirect Illumination;间接光照,是指既考虑场景中直接来自光源的光照(Direct Light)又考虑经过场景中其他物体反射后的光照(Indirect Light)的一种渲染技术。使用全局光照能够有效地增强场景的真实感。
即可以理解为:全局光照 = 直接光照(Direct Light) + 间接光照(Indirect Light)
全局光照涉及的知识非常多,本文主要记录标准光照模型,所以暂不展开深入学习,现阶段仅做了解。
二、Unity Shader实现标准光照模型
结合上文可知,标准光照模型 = 环境光 + 高光反射 + 漫反射,下文的Shader程序也是如此实现,主要实现了逐顶点的光照和逐像素光照。
1. 逐顶点的光照模型
1.1. 实现效果如下:
1.2. 实现代码参考:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "My Shader/PhongLightModelVertex"
{
Properties
{
_Diffuse("Diffuse", Color) = (1.0, 1.0, 1.0, 1.0)
_Specular("Specular", Color) = (1.0, 1.0, 1.0, 1.0)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Diffuse; //声明材质漫反射颜色
fixed4 _Specular; //声明材质高光反射颜色
float _Gloss; //声明材质高光反射范围
struct appdata
{
float4 vertex : POSITION; //获取到模型顶点信息
float3 normal :NORMAL; //获取到模型顶点法线信息,用于计算漫反射
};
struct v2f
{
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
v2f vert (appdata v)
{
v2f o;
//计算模型顶点在裁剪空间上的位置
o.pos = UnityObjectToClipPos(v.vertex);
//获得环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
//计算漫反射
fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal)); //将模型空间法线转换到世界空间中
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //获取光照方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
//计算高光反射
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
//实现思路一:基于Phong模型的计算方式
// fixed3 reflectDir = normalize(reflect(-worldLight, worldNormal)); //反射公式
// fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
//实现思路二:基于BilinPhong模型的计算方式
fixed3 halfDir = normalize(worldLight + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
//光照模型
o.color = ambient + diffuse + specular;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(i.color, 1.0);
}
ENDCG
}
}
}
2. 逐像素的光照
2.1. 实现效果:
2.2. 实现代码参考:
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "My Shader/PhongLightModelFragment"
{
Properties
{
_Diffuse("Diffuse", Color) = (1.0, 1.0, 1.0, 1.0)
_Specular("Specular", Color) = (1.0, 1.0, 1.0, 1.0)
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Diffuse; //声明材质漫反射颜色
fixed4 _Specular; //声明材质高光反射颜色
float _Gloss; //声明材质高光反射范围
struct appdata
{
float4 vertex : POSITION; //获取到模型顶点信息
float3 normal : NORMAL; //获取到模型顶点法线信息,用于计算漫反射
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0; //模型世界空间下的模型法线
float3 worldPos : TEXCOORD1; //模型世界空间下的模型顶点
};
v2f vert (appdata v)
{
v2f o;
//计算模型顶点在裁剪空间上的位置
o.pos = UnityObjectToClipPos(v.vertex);
//模型空间法线转换到世界空间中
o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
//模型空间坐标点转换到世界空间中
o.worldPos = UnityObjectToWorldDir(v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//获得环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
//计算漫反射
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //获取光照方向
//半兰伯特公式
fixed3 halfLambert = dot(i.worldNormal, worldLight) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
//计算高光反射
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
fixed3 halfDir = normalize(worldLight + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal, halfDir)), _Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color, 1.0);
}
ENDCG
}
}
}
三、参考链接
四、总结
本文主要学习了标准光照模型的数学模型和shader实现,后面的大多数代码实现都是依赖此光照模型实现的,值得注意的是我们在实现时需要将法线、光照、视角方向都转为单位向量,并都处于同一坐标空间下才能得到正确结果。现在已经有基础的光照模型了,下一篇文章将会学习纹理的相关概念,并为模型实现更多的细节和效果。
关于光照相关知识补充,可以看参考链接,如果你也有较好的资料分享,欢迎在评论区留言。最后,非常感谢您能看到这里,如果该文章对你有所帮助和启发,也欢迎您能点赞👍支持,如果有任何疑问和优化建议也非常欢迎各位大佬能够在评论区友好交流,一起学习进步。