前言
近些年,电子竞技行业发展迅速,游戏的品质也是逐年提升。从用户体验的角度来讲,画质渲染及画质特效方面非常重要。任何渲染特效都主要是光影的变幻,从而达到美轮美奂的效果。漫反射是最简单的光照效果,所有的光照特效都是以漫反射为基础的。本篇就来详细介绍,漫反射着色器如何实现。
- 首先我们来看一下什么漫反射
-
漫反射,是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射,这种反射称之为“漫反射”或“漫射”。这种反射的光称为漫射光。很多物体,如植物、墙壁、衣服等,其表面粗看起来似乎是平滑,但用放大镜仔细观察,就会看到其表面是凹凸不平的,所以本来是平行的太阳光被这些表面反射后,弥漫地射向不同方向。
-
以简单人物为例,在Unity中的漫反射是这样的
-
- 关于漫反射的光照模型,常见的是兰伯特(Lambert)和半兰伯特(Half-Lambert)
Lambert的光照公式是(C-light · M-diffuse)max(0,n·I),
即光照颜色 * 漫反射颜色 * max(0,法向量*光照方向)
-
下面以球形2D图像解析一下原理
-
第一步,我们确认公式中的两个方向向量,注意光照方向是指向光的方向,即入射光向量的逆向量,红色光照方向,蓝色顶点法向量。
-
第二步,计算不同顶点两向量的夹角,我们可以得到面向光的部分角度在0-90度之间,背向光的角度在90-180度之间。
- 第三步,Lambert漫反射需要的面向光的部分渲染,背向光的地方不渲染,且渲染部分需要做曲线性的递减。公式中两个向量做点乘计算,即n · I,点乘公式是|n|*|I|*Conθ,其中θ是两个向量的夹角,下面先看一下余弦图。
-
第四步,我们的夹角范围是0-180度,因此我们裁掉后半部分
-
第五步,我们需要0度时颜色最显眼,到90度逐渐递减,余弦前半段得到的结果与我们的预期相符,但90度到180度区间内,漫反射不做任何渲染,即纯黑色代表阴暗面效果,因此得出下图。
-
最终我们在两向量夹角为0°到90°范围内按比例渲染,就形成了最终的漫反射效果。
-
-
使用Lambert进行漫反射渲染,在Shader中有两种写法,一种是逐顶点着色,另一种是逐像素着色。从代码中我们可以看出,逐顶点是在顶点着色器中进行漫反射计算,而逐像素则在片元着色器中进行漫反射计算,下面来看逐顶点代码。
//兰伯特Lambert漫反射(逐顶点) Shader "AlbertShader/VertexDiffuse" { Properties { //漫反射颜色 _DiffuseColor("Color",Color)=(1,1,1,1) } SubShader { Pass { //正向渲染 Tags{ "LightMode"="ForwardBase" } CGPROGRAM//------------------CG语言开始------------------- //声明顶点函数 #pragma vertex vert //声明片段函数 #pragma fragment frag //引入光照函数库 #include "Lighting.cginc" //定义外部属性-漫反射颜色 float4 _DiffuseColor; //顶点输入结构体 struct appdata { //顶点坐标 float4 vertex : POSITION; //顶点法线 float3 normal : NORMAL; }; //顶点输出结构体 struct v2f { //像素坐标 float4 vertex : SV_POSITION; //临时变量:颜色 fixed3 color : COLOR; }; //顶点函数实现 v2f vert (appdata v) { //定义顶点输出结构体对象 v2f o; //顶点坐标转换到屏幕像素坐标 o.vertex = UnityObjectToClipPos(v.vertex); //获取环境光 float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //将顶点法线转换到世界空间下,并做归一化处理 float3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject)); //将光照方向转换到世界空间下,并做归一化处理 float3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //带入公式运算 fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(worldNormal,worldLight)); //结合漫反射和环境光 o.color = ambient + diffuse; //返回结果 return o; } fixed4 frag (v2f i) : SV_Target { //将结果返回 return fixed4(i.color,1.0); } ENDCG//------------------CG语言结束------------------- } } }
-
下面来看逐像素代码
//兰伯特Lambert漫反射(逐像素) Shader "AlbertShader/PixelDiffuse" { Properties { //漫反射颜色 _DiffuseColor("Color",Color)=(1,1,1,1) } SubShader { Pass { //正向渲染 Tags{ "LightMode"="ForwardBase" } CGPROGRAM//------------------CG语言开始------------------- #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" float4 _DiffuseColor; struct appdata { float4 vertex : POSITION; //顶点法线 float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; //世界空间下的顶点法线 fixed3 worldNormal : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); //通过矩阵运算,得到世界空间下的顶点法线 o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject); return o; } fixed4 frag (v2f i) : SV_Target { float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //使用兰伯特光照模型公式 fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(worldNormal,worldLightDir)); fixed3 color = diffuse + ambient; return fixed4(color,1.0); } ENDCG//------------------CG语言结束------------------- } } }
Lambert漫反射效果不错,但背光的一面全是黑色,若镜头在后面照射,则渲染出来的就是一个黑色平面,视觉效果很差,因此,有时我们也常常使用半兰伯特(Half-Lambert)的光照模型。
-
Half-Lambert的公式与Lambert相似,只是人为改动了公式中的渲染比例,半兰伯特没有太多的物理证明,可以理解为测试出来的一种实现方式。公式即(C-light · M-diffuse)(α(n·I)+β),通常情况下α和β的值为0.5。具体代码如下:
//半兰伯特HalfLambert漫反射(逐像素) Shader "Hidden/HalfPixelDiffuse" { Properties { _DiffuseColor("Color",Color)=(1,1,1,1) } SubShader { Pass { Tags{ "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" float4 _DiffuseColor; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; //临时变量:世界法线 fixed3 worldNormal : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); //世界法线 o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject); return o; } fixed4 frag (v2f i) : SV_Target { //环境光 float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //世界法线 fixed3 worldNormal = normalize(i.worldNormal); //世界入射光 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //半兰伯特漫反射公式 fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * (0.5 * (dot(worldNormal,worldLightDir)) + 0.5); fixed3 color = diffuse + ambient; return fixed4(color,1.0); } ENDCG } } }
-
Half-Lambert显的更透亮一点,背光面也不是全黑的,三种Shader的渲染效果对比如下:
结束语
漫反射是光影效果的基础,通过对漫反射底层实现原理,以及对漫反射公式的图像剖析,大家有没有更清晰的认识呢?Shader就是如此奇妙,这就是所谓的数学之美、编程之美。想成为逻辑开发和图形学开发的双料大师吗?一起加油吧!😋😋😋