unity入门精要之第6 章 Unity 中的基础光照---实现高光反射光照模型

Unity系列文章目录

前言

在6.2.4 节中,我们给出了基本光照模型中高光反射部分的计算公式:
从公式可以看出,要计算高光反射需要知道4 个参数:入射光线的颜色和强度clight,材质的
高光反射系数mspecular,视角方向v 以及反射方向r 。其中,反射方向r 可以由表面法线n 和光源
方向l 计算而得:
rˆ  lˆ  2(nˆ  lˆ)nˆ
上述公式很简单,更幸运的是,Cg 提供了计算反射方向的
函数reflect。
函数:reflect(i, n)
参数:i,入射方向;n,法线方向。可以是float、float2、float3
等类型。
描述:当给定入射方向i 和法线方向n 时,reflect 函数可以
返回反射方向。图6.9 给出了参数和返回值之间的关系。
6.5.1 实践:逐顶点光照
我们首先来看如何实现一个逐顶点的高光反射光照效果。在学习完本节后,我们会得到类似
图6.10 中的效果。
我们需要进行如下准备工作。
(1)在Unity 中新建一个场景。在本书资源中,该场景名为Scene_6_5。在Unity 5.2 中,默
认情况下场景将包含一个摄像机和一个平行光,
并且使用了内置的天空盒子。在Window ->
Lighting -> Skybox 中去掉场景中的天空盒子。
(2)新建一个材质。在本书资源中,该材质
名为SpecularVertexLevelMat。
(3)新建一个Unity Shader。在本书资源中,
该Shader 名为Chapter6-SpecularVertexLevel。把
新的Shader 赋给第2 步中创建的材质。
(4)在场景中创建一个胶囊体,并把第2 步
中的材质赋给该胶囊体。
(5)保存场景。
下面,我们需要编写自己的Shader 来实现一个逐顶点的高光反射效果。打开第3 步中创建的
Chapter6-SpecularVertexLevel,删除所有已有代码,并进行如下修改。
(1)首先,我们需要为这个Shader 起一个名字:
Sh ader “Unity Shaders Book/Chapter 6/Specular Vertex-Level” {
(2)为了在材质面板中能够方便地控制高光反射属性,我们在Shader 的Properties 语义块中
声明了三个属性:
Properties {
_Diffuse (“Diffuse”, Color) = (1, 1, 1, 1)
_Specular (“Specular”, Color) = (1, 1, 1, 1)
_Gloss (“Gloss”, Range(8.0, 256)) = 20
}
其中,新添加的_Specular 用于控制材质的高光反射颜色,而_Gloss 用于控制高光区域的大小。
(3)然后,我们在SubShader 语义块中定义了一个Pass 语义块。这是因为顶点/片元着色器的
代码需要写在Pass 语义块,而非SubShader 语义块中。而且,我们在Pass 的第一行指明了该Pass
的光照模式:
SubShader {
Pass {
Tags { “LightMode”=“ForwardBase” }
▲图6.9 Cg 的reflect 函数
▲图6.10 逐顶点的高光反射光照效果

在这里插入图片描述

在这里插入图片描述

LightMode 标签是Pass 标签中的一种,它用于定义该Pass 在Unity 的光照流水线中的角色,
在第9 章中我们会更加详细地解释它。在这里,我们只需要知道,只有定义了正确的LightMode,
我们才能得到一些Unity 的内置光照变量,例如_LightColor0。
(4)然后,我们使用CGPROGRAM 和ENDCG 来包围CG 代码片,以定义最重要的顶点着色
器和片元着色器代码。首先,我们使用#pragma 指令来告诉Unity,我们定义的顶点着色器和片元
着色器叫什么名字。在本例中,它们的名字分别是vert 和frag:
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
(5)为了使用Unity 内置的一些变量,如_LightColor0,还需要包含进Unity 的内置文件
Lighting.cginc:
#include “Lighting.cginc”
(6)为了在Shader 中使用Properties 语义块中声明的属性,我们需要定义和这些属性类型相
匹配的变量:
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
由于颜色属性的范围在0 到1 之间,因此对于_Diffuse 和_Specular 属性我们可以使用fixed
精度的变量来存储它。而_Gloss 的范围很大,因此我们使用float 精度来存储。
(7)然后,我们定义了顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的
输入结构体):
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
(8)在顶点着色器中,我们计算了包含高光反射的光照模型:
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)_World2Object));
// 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 - mul(_Object2World, v.vertex).xyz);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
o.color = ambient + diffuse + specular;
return o;
}
其中漫反射部分的计算和6.4 节中的代码完全一致。对于高光反射部分,我们首先计算了入
射光线方向关于表面法线的反射方向reflectDir。由于Cg 的reflect 函数的入射方向要求是由光源
指向交点处的,因此我们需要对worldLightDir 取反后再传给reflect 函数。然后,我们通过
_WorldSpaceCameraPos 得到了世界空间中的摄像机位置,再把顶点位置从模型空间变换到世界空
间下,再通过和_WorldSpaceCameraPos 相减即可得到世界空间下的视角方向。
由此,我们已经得到了所有的4 个参数,代入公式即可得到高光反射的光照部分。最后,再
和环境光、漫反射光相加存储到最后的颜色中。
(9)片元着色器的代码非常简单,我们只需要直接返回顶点颜色即可:
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
(10)最后,我们需要把这个Unity Shader 的回调Shader 设置为内置的Specular:
Fallback “Specular”
使用逐顶点的方法得到的高光效果有比较大的问题,我们可以在图6.10 中看出高光部分明显
不平滑。这主要是因为,高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插
值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题。因此,我们就需要
使用逐像素的方法来计算高光反射。
6.5.2 实践:逐像素光照
我们可以使用逐像素光照来得到更加平滑的
高光效果,如图6.11 所示。
首先,我们需要进行如下准备工作。
(1)使用和6.5.1 小节同样的场景。
(2)新建一个材质。在本书资源中,该材质名
为SpecularPixelLevelMat。
(3)新建一个Unity Shader。在本书资源中,
该Shader 名为Chapter6-SpecularPixelLevel。把新的
Shader 赋给第2 步中创建的材质。
(4)把第2 步中创建的材质赋给胶囊体。
打开Chapter6-SpecularPixelLevel,删除已有的
Shader 代码,把上6.5.1 节中的代码粘贴进去,并对顶点着色器和片元着色器进行如下修改。
(1)修改顶点着色器的输出结构体v2f:
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};

在这里插入图片描述

(2)顶点着色器只需要计算世界空间下的法线方向和顶点坐标,并把它们传递给片元着色器
即可:
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)_World2Object);
// Transform the vertex from object space to world space
o.worldPos = mul(_Object2World, v.vertex).xyz;
return o;
}
(3)片元着色器需要计算关键的光照模型:
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
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
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
上面的代码和6.5.1 节中的基本相同,在此不再赘述。
可以看出,按逐像素的方式处理光照可以得到更加平滑的高光效果。至此,我们就实现了一
个完整的Phong 光照模型。
6.5.3 Blinn-Phong 光照模型
在6.5.2 小节中,我们给出了Phong 光照模型在Unity 中的实现,而在6.2.4 节中,我们还提
到了另一种高光反射的实现方法—Blinn 光照模型。回忆一下,Blinn 模型没有使用反射方向,
而是引入一个新的矢量h ,它是通过对视角方向v 和光照方向l 相加后再归一化得到的。即
而Blinn 模型计算高光反射的公式如下:
Blinn-Phong 模型的实现和6.5.2 节中的代码很类似。为此。
(1)仍然使用和6.5.2 节同样的场景。
(2)新建一个材质。在本书资源中,该材质名为BlinnPhongMat。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter6-BlinnPhong。把新的

Shader 赋给第2 步中创建的材质。
(4)把第2 步中创建的材质赋给胶囊体。
打开Chapter6-BlinnPhong,删除已有的Shader 代码,并把6.5.2 节中的Chapter6-Specular
PixelLevel 代码直接粘贴进去。我们只需要修改片元着色器中对高光反射部分的计算代码:
fixed4 frag(v2f i) : SV_Target {

// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
图6.12 给出了逐顶点的高光反射光照、逐像素的高光反射光照(Phong 模型)和Blinn-Phong
高光反射光照的对比结果。

在这里插入图片描述
可以看出,Blinn-Phong 光照模型的高光反射部分看起来更大、更亮一些。在实际渲染中,绝
大多数情况我们都会选择Blinn-Phong 光照模型。需要再次提醒的是,这两种光照模型都是经验
模型,也就是说,我们不应该认为Blinn-Phong 模型是对“正确的”Phong 模型的近似。实际上,
在一些情况下(详见第18 章•基于物理的渲染),Blinn-Phong 模型更符合实验结果。

参考

Unity Shader入门精要
作者:冯乐乐

猜你喜欢

转载自blog.csdn.net/aoxuestudy/article/details/124370180