//老师在这里简单的简述了一下渲染流水线,我在之前的图形学笔记中很详细的讲述了渲染流水线的过程,便不再赘述
//因为老师很详细的将几种常见的光照模型都讲了一遍,但都是美术方向,前面的案例也都缺少代码,所以我结合shader入门精要一同再将这几种光照模型复习一遍
//由于并不是严格按照入门精要的顺序进行,所以杂糅了入门紧要的很多章节,整体路线是根据庄懂老师的课进行。
//这次笔记涵盖了庄懂技术美术入门课视频1-11节的内容
//前置知识,熟悉渲染管线,向量,点乘意义。了解MVP变换
一、 黑话——向量
二、黑话——点乘
//再详细的笔记就是图形那边,很详细的讲了向量和数学运算
三、简单的标准shader模板:
Shader "0000000000" { //这行代码是Shader的路径名
Properties { //此处是生命外部材质面板参数
}
SubShader {
Tags {
"RenderType"="Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert//哪个函数包含了顶点着色器的代码
#pragma fragment frag//哪个函数包含了片元着色器的代码
#pragma vertex name
#pragma fragmen name//更通用的表示方式
/*其中 name 就是我们指定的函数名,这两个函数的名字不一定是 vert frag,
它们可以是任意自定义的合法函数名,但我们 般使用 vert frag 来定义这两个函数,
因为它们很直观。*/
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//参数定义
struct VertexInput { //输入结构
float4 vertex : POSITION;
};
struct VertexOutput { //输出结构
float4 pos : SV_POSITION;
};
VertexOutput vert (VertexInput v) { //顶点Shader
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos( v.vertex );
return o;
}
/*顶点着色器的输入和输出都是通过定义结构体变量对数据进行传递,
当需要传递的数据不止一个时,需要使用定义结构体变量对多个数据进行传递。
当顶点着色器的返回数据为一个结构时,方法的返回值指定需要返回的结构体类型,
方法的参数列表后无需SV:POSITION语义,
因为此时输出的数据不仅仅只是模型裁剪空间的顶点坐标了。
*/
float4 frag(VertexOutput i) : COLOR { //像素Shader
float3 emissive = float3(0,1,1);
float3 finalColor = emissive;
return fixed4(finalColor,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
vert 函数的输入v包含顶点位置,通过POSITION语义指定
SV_POSITION 顶点着色器的输出是裁剪空间中顶点坐标
SV_Target 将用户的输出颜色存储到一个渲染目标中,这里会输出到默认的帧缓存
POSITION 和 SV_POSITION 均为CG/HLSL中的语义,语义告诉系统输入哪些值,输出哪些值,输出到哪里
获得更多模型数据TEXCOORD0:使用模型第一套纹理坐标填充texcoord变量,纹理坐标可以用来访问纹理
NORMAL:获得法线方向,用于计算光照
COLOR0语义告诉Unity,color用于存储颜色信息
//填充到这些语义中的数据来自于模型的Mesh Render组件,每帧调用DrawCall时,Mesh Render组件会将负责渲染的模型的数据发给Unity。
四、光照模型
着色是根据材质属性(高光反射属性,漫反射属性等)、光源信息(如光源方向、辐照度等),使用等式去计算沿某个观察方向的出射度的过程。该等式即 光照模型(Lighting Model) 。
光照模型中包含不同的部分来计算光线经过物体表面后不同的方向
高光反射部分:表示光线在物体表面如何被反射
漫反射部分:表示光线在物体表面如何被折射,吸收和散射出表面
1、标准光照模型
• 自发光(emissive)部分,本书使用Cemissive来表示。这个部分用千描述当给定一 个方向时, 一个表面本身会向该方向发射多少辐射量。需要注意的是,如果没有使用全局光照(global illumination)技术, 这些自发光的表面并不会真的照亮周围的物体, 而是它本身看起来更 亮了而已。
• 高光反射(specular)部分, 本书使用 Cspecular 来表示。 这个部分用千描述当光线从光源照 射到模型表面时, 该表面会在完全镜面反射方向散射多少辐射量。高光反射需要知道的信息比较多,有法线方向( n )、入射方向( i ),反射方向( r ),视角方向( v )等。反射方向( r )可由入射方向( i )和法线方向( n )计算出来:
就可 以利 Phong 模型来计算高光 反射的部分
为负
Blinn模型提出一个简单的修改方法得到类似效果,避免计算反射方向,引入一个新的矢量,对入射方向和视角方向求和后归一化得到:
使用法线方向和新的矢量进行计算而不是反射方向和视角方向
Blinn 模型的公式如下:
• 漫反射(diffuse)部分, 本书使用 Cdiffuse来表示。 这个部分用于描述, 当光线从光源照射 到模型表面时, 该表面会向每个方向散射多少辐射量。漫反射光照复合Lambert定律:反射光线强度与表面法线和光源之间夹角余弦成正比
• 环境光(ambient)部分, 本书使用 Cambiem 来表示。 它用于描述其他所有的间接光照
- 环境光使用全局变量,场景中所有物体都使用这个环境光。
C(ambient)=g(ambient)
光照计算可以在顶点着色器中计算,称为逐顶点光照,也可以在片元着色器中计算,称为逐像素光照。 标准光照模型仅仅是一个经验模型,并不完全符合真实世界中的光照现象。这种模型的局限性在于:
- 重要的物理现象无法表现出来,如菲涅尔反射
- 该模型各项同性,固定视角和光源方向旋转表面时,反射不会发生任何改变。而有些表面是具有各项异性的,如拉丝金属等
不过由于易用性、计算速度和得到效果都比较好,被广泛应用。
//来自shader入门精要
在课程中庄懂(老师真名叫啥不知道)老师通过下边的经验光照模型分别讲授了全部的BRDF简化光照模型
2、光源漫反射
实现漫反射模型——lambert模型
lambert模型公式
为了防止点积结果为负值,我们需要使用 max 操作,而 CG 提供了这样的函数 。在本例中, 使用 的另一个函数可以达到同样的目的,即 saturate 函数。
SahderForge中重要向量的运算
逐顶点的漫反射模型
Shader "Unlit/逐顶点" { //这行代码是Shader的路径名
Properties{
_Diffuse("DiffuseColor",Color) = (1.0,1.0,1.0,1.0)
//此处是生命外部材质面板参数
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"定义光照模式,只有正确定光照模式,才能得到一些Unity内置光照变量
}
CGPROGRAM
#pragma vertex vert//哪个函数包含了顶点着色器的代码
#pragma fragment frag//哪个函数包含了片元着色器的代码
#include "Lighting.cginc"
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//参数定义
fixed4 _Diffuse;
struct VertexInput { //输入结构
float4 vertex : POSITION;
float3 normal:NORMAL;
};
struct VertexOutput { //输出结构
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
VertexOutput vert(VertexInput v) { //顶点Shader
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//通过模型到世界的转置逆矩阵计算得到世界空间内的顶点法向方向(v.normal存储的是模型空间内的顶点法线方向)
fixed3 nDirWS = UnityObjectToWorldNormal(v.normal);
//得到世界空间内的光线方向
fixed3 lDirWS = normalize(_WorldSpaceLightPos0.xyz);
//根据Lambert定律计算漫反射 saturate函数将所得矢量或标量的值限定在[0,1]之间
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(nDirWS, lDirWS));
o.color = diffuse + ambient;
return o;
}
float4 frag(VertexOutput i) : COLOR { //像素Shader
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
逐像素的漫反射模型
Shader "Unlit/逐像素"
{ //这行代码是Shader的路径名
Properties{
_Diffuse("DiffuseColor",Color) = (1.0,1.0,1.0,1.0)
//此处是生命外部材质面板参数
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"定义光照模式,只有正确定光照模式,才能得到一些Unity内置光照变量
}
CGPROGRAM
#pragma vertex vert//哪个函数包含了顶点着色器的代码
#pragma fragment frag//哪个函数包含了片元着色器的代码
#include "Lighting.cginc"
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//参数定义
fixed4 _Diffuse;
struct VertexInput { //输入结构
float4 vertex : POSITION;
float3 normal:NORMAL;
};
struct VertexOutput { //输出结构
float4 pos : SV_POSITION;
float3 nDirWS:TEXCOORD0;
};
VertexOutput vert(VertexInput v) { //顶点Shader
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.nDirWS = UnityObjectToWorldNormal(v.normal); //变换法线信息,OS>WS
return o;
}
float4 frag(VertexOutput i) : COLOR { //像素Shader
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 nDirWS = normalize(i.nDirWS);通过模型到世界的转置逆矩阵计算得到世界空间内的顶点法向方向(v.normal存储的是模型空间内的顶点法线方向)
float3 lDirWS = normalize(_WorldSpaceLightPos0.xyz);//得到世界空间内的光线方向
//根据Lambert定律计算漫反射 saturate函数将所得矢量或标量的值限定在[0,1]之间
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(nDirWS, lDirWS));
fixed3 color = diffuse + ambient;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
左侧为逐顶点实现效果,右侧为逐像素实现效果。可以看到,在逐顶点效果中,模型下部背光面与向光面的交界处会出现不平滑的锯齿过渡。而逐像素则交界处相对平滑。但以上两种均存在在光线无法到达的区域,模型外观通常全黑,没有明暗变化,使背光区域看起来像平面,为此提出一项改进技术——Half Lambert光照模型
实现漫反射模型——Half lambert模型
可以看出 与原兰伯特模型相比,半兰伯特光照模型没有使用 max 操作来防止 的点积 为负值 而是对其结果进行了一个 倍的缩放再加上一个p-jc 小的偏移。绝大多数情况下, α和β值均为 0.5 即公式为
这样在原有兰伯特基础上,背光面取值为负的顶点在半兰伯特下,有了对应的到[0,0.5]的值,半兰伯特并没有物理基础,仅仅只是视觉加强技术而已。
SahderForge中Half Lambert光照模型的实现
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(nDirWS, lDirWS)); //最后相乘的数值,是lambert,只要将其改变为half lambet即可 fixed halfLambert = dot(nDirWS, lDirWS)*0.5 + 0.5;
简单的sss效果
在Shaderforge里面,只要计算出一个兰伯特,和一个可以变化的一维向量用Append合并成一个二维向量,然后采样一张SSSlut的贴图就可以实现简单的SSS次表面散射效果。
与RampTex不同的是,RampTex是恒定U轴或者V轴变化的,而SSSLut是U轴和V轴都有采样变化的,所以用半兰伯特和一个可以变化的一维数据合并成二维数据。用半兰伯特作为U坐标,另外一个输入的值作为V坐标,去采样我们的SSSLut图,最后输出结果。
3、光源高光反射(镜面反射)
实现高光反射模型——phong模型
基础公式
逐顶点高光反射
Shader "Custom/Chapter6_SpecularVertexLevel" {
Properties{
_Diffuse("DiffuseColor",Color)=(1,1,1,1)
_Specular("SpecualrColor",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f {
float4 pos:SV_POSITION;
fixed3 color : COLOR;
};
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
//通过CG提供的reflect(i,n)提供的函数计算反射方向
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
//计算世界空间下的观察方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-mul(_Object2World,v.vertex).xyz);
//计算高光反射部分
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(viewDir, reflectDir)), _Gloss);
o.color=ambient+diffuse+specular;
return o;
}
fixed4 frag(v2f i) :SV_Target{
return fixed4(i.color,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
逐顶点的高光反射在高光部分出现非常明显的锯齿不连续现象,由于高光计算部分pow()是非线性,因此顶点插值后的结果不理想。
//以下shader都默认为逐像素光照
逐像素高光反射
Shader "Unlit/Pong"{
Properties{
//Properties段定义参数格式:_名称("面板标签",类型(参数,可无))=默认值 如下:
_MainCol("颜色", color) = (1.0, 1.0, 1.0, 1.0) //定义一个颜色参数,可开放给外部面板调试
//变量名:_MainCol; 面板标签:颜色; 类型:color 颜色类型无参数; 默认值( 1.0, 1.0, 1.0, 1.0); 为白色
_SpecularPow("高光次幂" , range(1, 90)) = 30 //定义一个次幂参数,是一个1-90的滑块开放给外部面板调试。
//变量名:_SpecularPow; 面板标签:高光次幂; 类型:range 参数1-90区间; 默认值:30;
_Specular("SpecualrColor",Color) = (1,1,1,1)
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
//修饰字:在vertexInput前先声明参数。不然我们无法使用参数来进行计算,和上方Properties开放给外部的参数也无法使用
// uniform 共享于vert(顶点shader),frag(像素shader)
// attibute 仅用于vert(顶点shader)
// varying 用于vert(顶点shader), frag传数据(像素shader)
//VertexOutput前的参数声明,要与Properties定义的参数对应,并且要选取合适的数据类型
uniform float3 _MainCol; //声明我们在Properties种定义好的颜色信息,因为没有透明度所以这里用float3即可,贴图可以使用float4
uniform float _SpecularPow;//声明在Properties定义好的在1-90区间的滑轨参数,这里用float即可
fixed4 _Specular;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //将模型顶点信息输入
float3 normal : NORMAL; //将模型法线信息输入,法线一般float3足够,只用到RGB值。
};
//输出结构
struct VertexOutput {
float4 posCS : SV_POSITION; //裁剪空间(可以理解为屏幕空间)顶点位置
float3 nDirWS : TEXCOORD0; //世界空间法线方向
float4 posWS : TEXCOORD1; //世界空间顶点位置
};
//输入结构>>>顶点Shader>>>输出结构
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.posCS = UnityObjectToClipPos(v.vertex); //变换顶点信息,OS>CS
o.posWS = mul(unity_ObjectToWorld, v.vertex); //变换顶点信息,OS>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); //变换法线信息,OS>WS
return o; //将输出结构里的信息 输出 这里输出的前缀是O.xxx
}
//输出出结构>>>像素Shader
float4 frag(VertexOutput i) : COLOR {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//准备向量
float3 nDir = i.nDirWS; // 获取nDir法线向量 这里的前缀改变,变为 i.XXX
float3 lDir = normalize(_WorldSpaceLightPos0.xyz); // 获取lDir光向量 这里是U3D系统内置的一个主光源。normalize是对光源进行一个归一化,这样能比较正确的计算一些数值
float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS); // 用摄像机位置减去物体的点的位置就得到了观察方向,并且因为是向量,所以做一次归一化处理。
float3 rDir = normalize(reflect(-lDir, nDir));//归一化很重要
//准备点积结果
float ndotr = dot(rDir, vDir);
//光照模型
float Phong = pow(saturate(max(0.0, ndotr)), _SpecularPow);
float3 finalRGB = ambient + _LightColor0.rgb*_MainCol +_LightColor0.rgb * _Specular.rgb * Phong;
//返回结果
return float4(finalRGB ,1.0); // 因为finalRGB是声明的3维数据,所以我们只要再返回颜色中写一次就可以了最后追加一个A通道即可,最终输出颜色输出最终颜色
}
ENDCG
}
}
FallBack "Specular"
}
//最原始phong模型
两种不正确的表现方式
左侧为反射部分算法未归一化导致;右侧逐顶点的高光反射在高光部分出现非常明显的锯齿不连续现象,由于高光计算部分pow()是非线性,因此顶点插值后的结果不理想。
左侧为逐像素高光效果,高光边缘连续无明显锯齿现象,为正确显示的phong模型
实现高光反射模型——Blinn-Phong模型
逐像素高光反射
Shader "Unlit/bilnn-phong"
{
Properties{
//Properties段定义参数格式:_名称("面板标签",类型(参数,可无))=默认值 如下:
_MainCol("颜色", color) = (1.0, 1.0, 1.0, 1.0) //定义一个颜色参数,可开放给外部面板调试
//变量名:_MainCol; 面板标签:颜色; 类型:color 颜色类型无参数; 默认值( 1.0, 1.0, 1.0, 1.0); 为白色
_Specular("SpecualrColor",Color) = (1,1,1,1)
_SpecularPow("高光次幂" , range(1, 90)) = 30 //定义一个次幂参数,是一个1-90的滑块开放给外部面板调试。
//变量名:_SpecularPow; 面板标签:高光次幂; 类型:range 参数1-90区间; 默认值:30;
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
fixed4 _Specular;
uniform float3 _MainCol; //声明我们在Properties种定义好的颜色信息,因为没有透明度所以这里用float3即可,贴图可以使用float4
uniform float _SpecularPow;//声明在Properties定义好的在1-90区间的滑轨参数,这里用float即可
//输入结构
struct VertexInput {
float4 vertex : POSITION; //将模型顶点信息输入
float3 normal : NORMAL; //将模型法线信息输入,法线一般float3足够,只用到RGB值。
};
//输出结构
struct VertexOutput {
float4 posCS : SV_POSITION; //裁剪空间(可以理解为屏幕空间)顶点位置
float3 nDirWS : TEXCOORD0; //世界空间法线方向
float4 posWS : TEXCOORD1; //世界空间顶点位置
};
//输入结构>>>顶点Shader>>>输出结构
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.posCS = UnityObjectToClipPos(v.vertex); //变换顶点信息,OS>CS
o.posWS = mul(unity_ObjectToWorld, v.vertex); //变换顶点信息,OS>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); //变换法线信息,OS>WS
return o; //将输出结构里的信息 输出 这里输出的前缀是O.xxx
}
//输出出结构>>>像素Shader
float4 frag(VertexOutput i) : COLOR {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//准备向量
float3 nDir = i.nDirWS; // 获取nDir法线向量 这里的前缀改变,变为 i.XXX
float3 lDir = normalize(_WorldSpaceLightPos0.xyz); // 获取lDir光向量 这里是U3D系统内置的一个主光源。normalize是对光源进行一个归一化,这样能比较正确的计算一些数值
float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS); // 用摄像机位置减去物体的点的位置就得到了观察方向,并且因为是向量,所以做一次归一化处理。
float3 hDir = normalize(vDir + lDir); // 半角向量 = 观察方向 + 光向量 然后把得到的结果进行一次归一化,防止信息出现负数
//准备点积结果
float ndoth = dot(nDir, hDir); // 法线向量点积半角向量,获得了布林冯光照模型的前置计算数据
//光照模型
float BlinnPhong = pow(saturate(max(0.0, ndoth)), _SpecularPow); // 用上面的ndoth点积结果,截断负值,让值的范围为正数,之后用POW来控制高光的范围和显示效果,用一开始声明的_SpecularPow来作为次幂的数值,最后声明为布林冯光照模型
float3 finalRGB = _LightColor0.rgb * _MainCol+ _Specular.rgb*_LightColor0.rgb*BlinnPhong; //返回结果
return float4(finalRGB ,1.0); // 因为finalRGB是声明的3维数据,所以我们只要再返回颜色中写一次就可以了最后追加一个A通道即可,最终输出颜色输出最终颜色
}
ENDCG
}
}
FallBack "Specular"
经过对比可以发现,Phong和Blinn-Phong两种光照模型实际上会有一些小小的差别,Blinn-Phong的高光范围相比Phong就比较大一些。在高光的分布上,两者也有一些不同。Phong在边缘处和真实度上的处理比Blinn-Phong要好一些。
但是在性能优化上面Blinn-Phong要比Phong要好一些,所以大多时候更多的是使用Blinn-Phong光照模型。
//一些向量
4、环境漫反射
三色环境光
//Shaderforge中实现三色环境光
实现原理,通过mask遮罩不同区域,使不同区域只能受到设置的光照
下图为代码整体实现思路
Shader "Unlit/3ColAmbient"{
Properties {
//定义贴图参数方法:_XXX ("面板标签", 2d) = "white" {}
_Occlusion ("环境光遮蔽", 2d) ="White" {} //向控制面板声明环境光遮蔽贴图
//"white"{} 代表缺省纹理为纯白贴图额,还有其他"black"{} "gray"{}等等
_EnvUpCol ("朝上环境光色", color) = (1.0, 1.0, 1.0, 1.0) //向控制面板声明朝上环境光颜色控制
_EnvSideCol ("侧面周围环境光颜色", color) = (0.5, 0.5, 0.5, 1.0) //向控制面板声明侧面周围环境光颜色控制
_EnvDownCol ("朝下环境光颜色", color) = (0, 0, 0, 1.0) //向控制面板声明朝下环境光颜色控制
}
SubShader {
Tags {
"RenderType"="Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
uniform float3 _EnvUpCol; //输入朝上环境光颜色参数
uniform float3 _EnvSideCol; //输入侧边周围环境光颜色参数
uniform float3 _EnvDownCol; //输入朝下环境光颜色参数
uniform sampler2D _Occlusion; //输入环境光遮蔽贴图
//输入结构
struct VertexInput {
float4 vertex : POSITION; //将模型顶点信息输入
float3 normal : NORMAL; //将模型法线信息输入,法线一般float3足够,只用到RGB值。
float2 uv0 : TEXCOORD0; //将UV信息输入。 在U3D中可以有4套UV
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //由模型顶点信息换算来的顶点屏幕位置
float3 nDirWS : TEXCOORD0; //由模型法线信息换算来的世界空间法线信息
float2 uv :TEXCOORD1; //追加UV信息用于像素Shader采样贴图,因为当前项目只有一套UV所以这里不做区分采样。
};
//输入结构>>>顶点Shader>>>输出结构
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos( v.vertex ); //变换顶点信息,并将其传入输出结构
o.nDirWS = UnityObjectToWorldNormal(v.normal); //变换法线信息,并将其传入输出结构
o.uv = v.uv0; //输出结构的“uv”就等于输入结构的“uv0”
return o; //将输出结构里的信息 输出 这里输出的前缀是O.xxx
}
//输出出结构>>>像素Shader
float4 frag(VertexOutput i) : COLOR {
// 准备向量
float3 nDir = i.nDirWS; // 获取nDir
// 计算各部位遮罩
float UpMask = max(0, nDir.g); // 获取向量的G通道去除负数,作为模型上部遮罩。
float DownMask = max(0, -nDir.g); // 取向量G通道的负值,去除负数,作为模型的下部遮罩
float SideMask = 1.0 - UpMask - DownMask; // 用1减去上部遮罩和下部遮罩,就得到了侧面周围遮罩。
// 混合环境色
float3 EnvCol = _EnvUpCol * UpMask + _EnvDownCol * DownMask + _EnvSideCol * SideMask; //混合三个方向的遮罩并分别乘以已经生命好的颜色
// 采样环境光遮蔽贴图
float Occlusion = tex2D(_Occlusion, i.uv); //采样一张环境光遮蔽贴图把贴图额塞到UV里面。
//计算环境光照
float3 EnvLighting = EnvCol * Occlusion; // 最后把环境光和环境光遮蔽相乘得到最后想要的效果
return float4(EnvLighting ,1.0); //最后输出最终颜色
}
ENDCG
}
}
FallBack "Diffuse"
}
效果
5、环境镜面反射//这里包含了入门精要的第十章第一节(立方体纹理)
Matcap
Matcap用的不多,因为移动的话容易穿帮,所以一般用在比较静态的物体用来模仿环境高光反射的情况下使用。或者是游戏里的物体展示或者人物展示的情况下使用。
Shader "Unlit/matcap"
{
Properties{
_Matcap("Matcap", 2D) = "gray" {}
_EnvSpecInt("环境镜面反射强度", Range(0, 5)) = 1
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
uniform sampler2D _Matcap;
uniform float _EnvSpecInt;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv0 : TEXCOORD0; //uv信息
float3 normal : NORMAL; //法线信息
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //屏幕顶点位置
float2 uv0 : TEXCOORD0; //uv信息
float4 posWS : TEXCOORD1; //世界顶点位置
float3 nDirWS: TEXCOORD2; //世界法线方向
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; // 新建一个输出结构
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0; // 传递UV信息
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 顶点位置 OS>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 法线方向 OS>WS
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR{
//准备向量
float3 nDir = i.nDirWS;
float3 nDirVS = mul(UNITY_MATRIX_V, float4(nDir, 0.0)); //观察空间的法线向量,用于计算MatcapUV
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //世界空间下的观察向量
//准备中间变量
float2 MatcapUV = nDirVS.rg * 0.5 + 0.5; //采样Matcap到观察空间下的法线向量中的r与g通道去采样一张Rampmap,把通道中的-1到1映射到0到1所以乘以0.5再加0.5
//光照模型
float3 matcap = tex2D(_Matcap, MatcapUV); //这里是把上面声明的_Matcap采样到matcapUV中
float3 envSpecLighting = matcap * _EnvSpecInt; //最后的环境高光反射
return float4(envSpecLighting, 1.0); //输出 返回,返回一个值输出,这里单纯输出了一个颜色.
}
ENDCG
}
}
FallBack "Diffuse"
}
实现效果
cubemap
立方体纹理
立方体纹理(CubeMap)是 环境映射(Enviroment Mapping)的一种实现方式。立方体纹理包含6张图像,每个面表示沿着世界空间下的轴向(上、下、左、右、前、后) 观察所得的图像。对立方体纹理的采样需要提供三维纹理坐标,表示在世界空间下的一个3D方向,方向矢量从立方体中心出发,向外部延伸就会和立方体的6个纹理之一发生相交,采样结果由交点计算而来。
Cubemap其实就是一张全景图。就是在一个球的中心点,去扫描周围的所有的场景信息,映射到我们的Cubemap上。
Cubemap由来是因为早期的Cubemap储存方式,早期的Cubemap并不是储存在一张图里面,而是储存在6张图里面。因为正好是6张图组成的类似立方体的盒子,所以就叫Cubemap。
但是合并成一张贴图的Cubemap其实采样的时候还是拆分了上下前后左右6个面,所以本质上叫Cubemap并没有什么问题。
立方体纹理在实时渲染中最常见的应用是天空盒子以及环境映射。
天空盒子的创建通过Unity自带的Skybox/6 Sided材质创建,需要对应6张纹理
立方体纹理用于环境映射,模拟类似金属反射周围环境的效果,这个过程首先需要获取到指定位置的立方体纹理,再应用到反射和折射计算。
得到cubemap有几种实现方法
庄懂老师课程主要讲述了第一种方法
观察方向(vDir),并不是摄像机看向物体顶点的方向,而是顶点到摄像机的方向。因为Cubemap是把整个环境的信息映射到一张贴图里,并且目前作用是用作物体表面的反射,反射是并不需要拿视方向,我们需要拿视方向到模型顶点然后反弹的方向。所以这里我们把观察方向取了个反然后点乘我们的从切线空间转置过来的法线方向,这样我们就拿到了观察方向的反弹方向。、
//这里代码为单纯的cubemap,不添加其余任何效果
Shader "Unlit/cubemap"
{ Properties{
_Cubemap("HDR环境贴图", Cube) = "_Skybox" {} //cubemap贴图使用特殊格式Cube
_CubemapMip("HDR环境贴图Mip", Range(0, 7)) = 0 //HDR环境贴图的Mipmap的等级
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
uniform samplerCUBE _Cubemap; //输入CubeMap贴图的参数
uniform float _CubemapMip; //输入Cubemap的Mipmap的参数
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float3 normal : NORMAL; //法线信息
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //屏幕顶点位置
float4 posWS : TEXCOORD1; //世界顶点位置
float3 nDirWS : TEXCOORD2; // 世界法线方向
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 让顶点位置信息从模型空间转换到世界空间 OS>>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 法线方向从模型空间转换到世界空间 OS>>WS
return o; // 将输出结构 输出
}
//像素Shader
float4 frag(VertexOutput i) : COLOR{
//准备向量
float3 nDirWS = i.nDirWS; //世界空间下的法线向量,用于计算计算nDirVS
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //世界空间下的观察向量
float3 vrDirWS = reflect(-vDirWS, nDirWS); // 世界空间下的观察方向的反射方向用于采样Cubemap
// 光照模型
float3 var_Cubemap = texCUBElod(_Cubemap, float4(vrDirWS, _CubemapMip)).rgb;
//Cubemap采样,Cubemap使用的是Float4,XYZ轴就是世界空间下的观察方向的反射,W轴是Mipmap的等级(可以理解为LOD)然后以这4维坐标去采样我们的Cubemap贴图
float3 envSpecLighting = var_Cubemap ; //最后计算环境高光反射,Cubemap
return float4(envSpecLighting, 1.0); //输出 返回,返回一个值输出,这里单纯输出了一个颜色.
}
ENDCG
}
}
FallBack "Diffuse"
}
cubemap效果和cubemap贴图(HDR)
在unity内置的skybox,sahder
获取指定位置的立方体纹理
但在理想情况下,我们希望根据物体在场景中位置的不同,生成它们各自 不同的立方体纹理。这时,我们就可以在 nity 中使用脚本来创建。这是通过利用 nity 提供的 Camera.RenderToCubemap 函数来实现的 Camera.RenderToCubemap 函数可以把从任意位置观 察到的场景图像存储到 张图像中,从而创建出该位置上对应的立方休纹理。
public class RenderCubeMapWizard : ScriptableWizard
{
public Transform renderFromPosition;
public Cubemap cubemap;
void OnWizardUpdate()
{
helpString = "Select transform to render from and cubemap to render into";
isValid=(renderFromPosition!=null)&&(cubemap!=null);
}
void OnWizardCreate()
{
GameObject go=new GameObject("CubemapCamera");
go.AddComponent<Camera>();
go.transform.position = renderFromPosition.position;
go.GetComponent<Camera>().RenderToCubemap(cubemap);
DestroyImmediate(go);
}
[MenuItem("GameObject/Render into Cubemap")]
static void RenderCubemap()
{
ScriptableWizard.DisplayWizard<RenderCubeMapWizard>("Render cubemap", "Render");
}
}
需要添加 “using UnityEditor”命名空间,点击“Render”脚本执行后会将对应点的立方体纹理渲染到指定的立方体纹理中。
反射计算
反射效果通过入射光线方向和表面法线得到反射方向,再利用反射方向对立方体纹理进行采样,得到反射效果。入射光线为观察方向的反方向,主要使用CG函数中的reflect函数。
整体思路与算法是与庄懂老师的算法是一致的。但在sahder入门精要中在原有基础上添加了控制反射强度和反射颜色的模块
Shader "Custom/Reflection" {
Properties{
_Cubemap("HDR环境贴图", Cube) = "_Skybox" {} //cubemap贴图使用特殊格式Cube
_CubemapMip("HDR环境贴图Mip", Range(0, 7)) = 0 //HDR环境贴图的Mipmap的等级
_Color("Color",Color) = (1,1,1,1)
_ReflectionColor("ReflectionColor",Color) = (1,1,1,1)
_ReflectionAmount("ReflectionAmount",Range(0,1)) = 1
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
uniform samplerCUBE _Cubemap; //输入CubeMap贴图的参数
uniform float _CubemapMip; //输入Cubemap的Mipmap的参数
fixed4 _Color;
fixed4 _ReflectionColor;
float _ReflectionAmount;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float3 normal : NORMAL; //法线信息,用作之后法线采样与TBN矩阵的计算
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //屏幕顶点位置
float4 posWS : TEXCOORD1; //世界顶点位置
float3 nDirWS : TEXCOORD2; // 世界法线方向
float3 vDirWS:TEXCOORD3;
float3 ReDirWS:TEXCOORD4;
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 让顶点位置信息从模型空间转换到世界空间 OS>>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 法线方向从模型空间转换到世界空间 OS>>WS
o.vDirWS = UnityWorldSpaceViewDir(o.posWS);
return o; // 将输出结构 输出
}
//像素Shader
float4 frag(VertexOutput i) : COLOR{
//准备向量
float3 nDirWS = i.nDirWS; //世界空间下的法线向量,用于计算计算nDirVS 计算Fresnel
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //世界空间下的观察向量 用于计算Fresnel
float3 vrDirWS = reflect(-vDirWS, nDirWS); // 世界空间下的观察方向的反射方向用于采样Cubemap
float3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS));
// 光照模型
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//基础光
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(dot(nDirWS, lDirWS), 0);//lambert
float3 var_Cubemap = texCUBElod(_Cubemap, float4(vrDirWS, _CubemapMip)).rgb * _ReflectionColor.rgb;
//Cubemap采样,Cubemap使用的是Float4,XYZ轴就是世界空间下的观察方向的反射,W轴是Mipmap的等级(可以理解为LOD)然后以这4维坐标去采样我们的Cubemap贴图
fixed3 color = ambient + lerp(diffuse, var_Cubemap, _ReflectionAmount) ;
return float4(color, 1.0); //输出 返回,返回一个值输出,这里单纯输出了一个颜色.
}
ENDCG
}
}
FallBack "Diffuse"
}
最终效果,在原有基础上,可以控制反射强度和反射颜色
折射计算
折射计算和反射计算类似,先计算出折射方向,再向生成立方体纹理进行采样,主要用到CG函数中的refract函数,传入三个参数,入射方向,法线方向,折射率比值(入射到反射方向)值得注意的是 传入的方向参数必须是归一化之后的方向。
Shader "Custom/Refraction" {
Properties{
_Cubemap("HDR环境贴图", Cube) = "_Skybox" {} //cubemap贴图使用特殊格式Cube
_CubemapMip("HDR环境贴图Mip", Range(0, 7)) = 0 //HDR环境贴图的Mipmap的等级
_Color("Color",Color) = (1,1,1,1)
_RefractionColor("ReflectionColor",Color) = (1,1,1,1)
_RefractionAmount("ReflectionAmount",Range(0,1)) = 1
_RefractionRatio("Refraction Ratio",Range(0.1,1)) = 0.5
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
uniform samplerCUBE _Cubemap; //输入CubeMap贴图的参数
uniform float _CubemapMip; //输入Cubemap的Mipmap的参数
fixed4 _Color;
fixed4 _RefractionColor;
float _RefractionAmount;
float _RefractionRatio;
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float3 normal : NORMAL; //法线信息,用作之后法线采样与TBN矩阵的计算
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //屏幕顶点位置
float4 posWS : TEXCOORD1; //世界顶点位置
float3 nDirWS : TEXCOORD2; // 世界法线方向
float3 vDirWS:TEXCOORD3;
float3 ReDirWS:TEXCOORD4;
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos(v.vertex);
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 让顶点位置信息从模型空间转换到世界空间 OS>>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 法线方向从模型空间转换到世界空间 OS>>WS
o.vDirWS = UnityWorldSpaceViewDir(o.posWS);
return o; // 将输出结构 输出
}
//像素Shader
float4 frag(VertexOutput i) : COLOR{
//准备向量
float3 nDirWS = i.nDirWS; //世界空间下的法线向量,用于计算计算nDirVS 计算Fresnel
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //世界空间下的观察向量 用于计算Fresnel
float3 vrDirWS = refract(-normalize(vDirWS), normalize( nDirWS),_RefractionRatio); // 世界空间下的观察方向的反射方向用于采样Cubemap
float3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS));
// 光照模型
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//基础光
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(dot(nDirWS, lDirWS), 0);//lambert
float3 var_Cubemap = texCUBElod(_Cubemap, float4(vrDirWS, _CubemapMip)).rgb * _RefractionColor.rgb;
//Cubemap采样,Cubemap使用的是Float4,XYZ轴就是世界空间下的观察方向的反射,W轴是Mipmap的等级(可以理解为LOD)然后以这4维坐标去采样我们的Cubemap贴图
fixed3 color = ambient + lerp(diffuse, var_Cubemap, _RefractionAmount);
return float4(color, 1.0); //输出 返回,返回一个值输出,这里单纯输出了一个颜色.
}
ENDCG
}
}
FallBack "Diffuse"
}
菲涅耳反射 (Fresnel reflection)
菲涅耳反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反 射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在 定的比率关系,这 个比率关系可以通过菲涅耳等式进行计算。一个经常使用的例子是,当你站在湖边,直接低头看 脚边的水面时,你会发现水几乎是透明的,你可以直接看到水底的小鱼和石子;但是,当你抬头 看远处的水面时,会发现几乎看不到水下的情 ,而只能看到水面反射的环境。
菲涅尔需要法线向量(nDir)与观察方向(vDir),由于观察方向是从顶点指向摄像机与光向量光源照射在模型顶点处,从模型顶点处到光源的方向。所以法向量点积观察方向会造成球模型呈现出一种类似于兰伯特的效果。然后把法向量点积观察向量的积取反。相当于黑白反向,那么我们就得到了球模型中间暗,边缘亮的效果。
用ShaderFroge来实现菲涅尔:
在UE中实现
shader
Shader "Unlit/fersnel"
{
Properties{
_FresnelPow("FresnelPow", Range(0, 5)) = 1
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
uniform float _FresnelPow; //输入菲涅尔次幂参数
//输入结构
struct VertexInput {
float4 vertex : POSITION; //顶点信息
float2 uv0 : TEXCOORD0; //uv信息
float3 normal : NORMAL; //法线信息
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; //屏幕顶点位置
float2 uv0 : TEXCOORD0; //uv信息
float4 posWS : TEXCOORD1; //世界顶点位置
float3 nDirWS : TEXCOORD2; // 世界法线方向
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0; //传递uv信息
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 让顶点位置信息从模型空间转换到世界空间 OS>>WS
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 法线方向从模型空间转换到世界空间 OS>>WS
return o; // 将输出结构 输出
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
//准备向量
float3 nDirWS = normalize(i.nDirWS); //世界空间下的法线向量,用于计算计算nDirVS 计算Fresnel
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //世界空间下的观察向量 用于计算Fresnel
// 准备中间变量
float vdotn = dot(vDirWS, nDirWS); //菲涅尔的中间变量,世界空间下的法线向量点乘世界空间下的观察向量
// 光照模型
float fresnel = pow(max(0.0, 1.0 - vdotn), _FresnelPow); //计算菲涅尔
float3 envSpecLighting = fresnel ; //最后计算
return float4(envSpecLighting, 1.0); //输出 返回,返回一个值输出,这里单纯输出了一个颜色.
}
ENDCG
}
}
FallBack "Diffuse"
}
最后效果
6、投影
unity的ShaderForge中已经给我们内置了投影节点,直接使用即可。
在代码中实现投影:
首先我们如果要使用unity投影必须要声明两个库的文件:
CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "AutoLight.cginc" // 使用Unity投影必须包含这两个库文件 #include "Lighting.cginc" // 使用Unity投影必须包含这两个库文件 #pragma multi_compile_fwdbase_fullshadows #pragma target 3.0
然后我们在输出结构里直接使用投影坐标信息,然后传入到顶点Shader中调用。最后到像素Shader中最终计算即可:
//输出结构 struct VertexOutput { float4 pos : SV_POSITION; //模型顶点信息换算到屏幕顶点位置,与投影无关 LIGHTING_COORDS(0, 1) //投影坐标信息, unity中已经封装,直接拿来用即可 //其中括号中的(0,1);0;1代表着分别占用了TEXCOORD1与TEXCOORD2 }; //顶点Shader VertexOutput vert (VertexInput v) { VertexOutput o = (VertexOutput)0; //新建的输出结构,与投影无关 o.pos = UnityObjectToClipPos( v.vertex ); //变换顶点信息 将其塞给输出结构,与投影无关 TRANSFER_VERTEX_TO_FRAGMENT(o) //顶点Shader中必须调用unity中已经封装好的方法 return o; } //像素Shader float4 frag(VertexOutput i) : COLOR { float Shadow = LIGHT_ATTENUATION(i); //像素Sahder中获取投影信息同样通过Unity提供的方法 return float4(Shadow, Shadow, Shadow,1.0); //输出 返回,返回一个值输出,这里单纯输出了一个颜色. } ENDCG
五、整合——传统经验光照模型
OldSchoolPro
//法线部分见入门精要第七章
经验光照模型ShaderForge
传统经验光照模型代码
Shader "Unlit/oldschoolpro"
{
Properties{
// 材质面板排版
_MainTex("MainTex", 2D) = "white"{} // RGBA四通道,RGB是基本颜色贴图 A通道是环境光遮蔽贴图
_NormTex("NormTex", 2D) = "bump"{} // 法线贴图的默认值一定要是Bump
_SpecTex("SpecTex", 2D) = "gray" {} // RGB是高光颜色贴图,A通道是高光次幂贴图
_EmitTex("EmitTex", 2d) = "black" {} // 自发光默认颜色是黑色
_Cubemap("Cubemap", cube) = "_Skybox" {} // Cubemap的默认值一定要是"_Skybox"
_MainCol("MainCol", Color) = (0.5, 0.5, 0.5, 1.0)
_EnvDiffInt("EnvDiffInt", Range(0, 1)) = 0.2
_EnvUpCol("EnvUpCol", Color) = (1.0, 1.0, 1.0, 1.0)
_EnvSideCol("EnvSideCol", Color) = (0.5, 0.5, 0.5, 1.0)
_EnvDownCol("EnvDownCol", Color) = (0.0, 0.0, 0.0, 0.0)
_SpecPow("SpecPow", Range(1, 90)) = 30
_EnvSpecInt("EnvSpecInt", Range(0, 5)) = 0.2
_FresnelPow("FresnelPow", Range(0, 5)) = 1
_CubemapMip("CubemapMip", Range(1, 7)) = 0
_EmitInt("EmitInt", range(1, 10)) = 1
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 追加投影相关包含文件
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
// Texture
uniform sampler2D _MainTex;
uniform sampler2D _NormTex;
uniform sampler2D _SpecTex;
uniform sampler2D _EmitTex;
uniform samplerCUBE _Cubemap;
// Diffuse
uniform float3 _MainCol;
uniform float _EnvDiffInt;
uniform float3 _EnvUpCol;
uniform float3 _EnvSideCol;
uniform float3 _EnvDownCol;
// Specular
uniform float _SpecPow;
uniform float _EnvSpecInt;
uniform float _FresnelPow;
uniform float _CubemapMip;
// Emission
uniform float _EmitInt;
//输入结构
struct VertexInput {
float4 vertex : POSITION; // 顶点信息
float2 uv0 : TEXCOORD0; // UV信息
float4 normal : NORMAL; // 法线信息(法线是4维的,第四维是副切线轴方向)
float4 tangent : TANGENT; // 切线信息(包含了切线和副切线)
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 屏幕空间顶点位置
float2 uv0 : TEXCOORD0; // uv0
float4 posWS : TEXCOORD1; // 世界空间顶点位置
float3 nDirWS : TEXCOORD2; // 世界空间法线方向
float3 tDirWS : TEXCOORD3; // 世界空间切线方向
float3 bDirWS : TEXCOORD4; // 世界空间副切线方向
LIGHTING_COORDS(5,6) // u3d自带的投影相关的函数直接调用即可
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; // 新建输出结构
o.pos = UnityObjectToClipPos(v.vertex); // 从模型空间转换到裁剪空间的顶点位置
o.uv0 = v.uv0; // 传递UV
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 从模型空间转换到世界空间的顶点位置
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 从模型空间转换到世界空间的法线方向
o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz); // 从模型空间转换到世界空间的切线方向
o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w); // 从模型空间转换到世界空间的副切线方向
TRANSFER_VERTEX_TO_FRAGMENT(o) // u3d自带的投影相关函数
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
// 向量准备
float3 nDirTS = UnpackNormal(tex2D(_NormTex, i.uv0)).rgb; // 切线空间法线方向,与TBN矩阵一起转换出世界空间法线方向,采样了导入进来的法线贴图塞入到模型的UV中。UnpackNormal是对贴图的解码
float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS); // TBN矩阵
float3 nDirWS = normalize(mul(nDirTS, TBN)); // 世界空间法线方向,用作采样法线贴图。利用TBN矩阵与切线空间法线向量相乘得出,记得结果做归一化(去掉负值)
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); // 世界空间观察方向,用作计算观察方向的反射方向。 由摄像机位置减去需要看的顶点的位置结果做一次归一化。
float3 vrDirWS = reflect(-vDirWS, nDirWS); // 世界空间观察方向的反射方向,用于计算菲涅尔。观察方向的反方向与法线的方向的反射得出
float3 lDirWS = _WorldSpaceLightPos0.xyz; // 世界空间光照方向,用作计算光照方向的反射方向,与兰伯特光照模型
float3 lrDirWS = reflect(-lDirWS, nDirWS); // 世界空间光照反射方向,用作计算冯光照模型。由光照方向的反方向与法线方向的反射得出
// 中间量准备
float ndotl = dot(nDirWS, lDirWS); // 世界空间法线方向点乘世界空间光照方向,结果是兰伯特光照模型要用的中间量。
float vdotr = dot(vDirWS, lrDirWS); // 世界空间观察方向点乘世界空间光照反射方向,结果是冯光照模型要用的中间量。
float vdotn = dot(vDirWS, nDirWS); // 世界空间观察方向点乘世界空间法线方向,结果是菲涅尔要用的中间量
// 纹理采样
float4 var_MainTex = tex2D(_MainTex, i.uv0); // 基本颜色贴图采样,采样后塞入到模型的UV中。因为除了RGB基本颜色外,A通道塞了AO贴图,所以这里是float4
float4 var_SpecTex = tex2D(_SpecTex, i.uv0); // 高光贴图采样,采样后塞入到模型的UV中。因为除了RGB是高光贴图外,A通道塞了高光次幂,所以这里是float4
float3 var_EmitTex = tex2D(_EmitTex, i.uv0).rgb; // 自发光贴图采样,采样后塞入到模型的UV中
float3 var_Cubemap = texCUBElod(_Cubemap, float4(vrDirWS, lerp(_CubemapMip, 1.0, var_SpecTex.a))).rgb;
// 立方体环境贴图采样。 首先线性插值了立方体环境贴图的mipmap等级和mipmap的第一个等级,然后用高光贴图的A通道也就是高光次幂(可以理解为光滑度,光滑度越亮(接近1))做插值。做这个的为的就是,模型越光滑,那么反射的立方体环境贴图就越清晰.越粗糙,反射的就越模糊。用这个值去采样立方体贴图
// 光照模型
// 光源漫反射
float3 baseCol = var_MainTex.rgb * _MainCol; // 模型基的颜色。由基本颜色贴图的RGB通道采样,然后乘以光源漫反射的颜色。
float lambert = max(0.0, ndotl); // 兰伯特光照模型,由法线方向点乘光照方向把结果去除负值得出
// 光源镜面反射
float specCol = var_SpecTex.rgb; // 高光颜色。由高光贴图的RGB通道采样得出。
float specPow = lerp(1, _SpecPow, var_SpecTex.a); // 高光次幂, 高光次幂由 1和高光次幂值用高光贴图的A通道的高光次幂贴图插值得出,这样可以用贴图的划定好的高光次幂值来控制高光的反射数值状态。
float phong = pow(max(0.0, vdotr), specPow); // 冯光照模型, 由观察方向点乘光照反射方向去除负值。把值用高光次幂的值做power控制得出。这样就可以用贴图来控制高光的数值状态。
// 光照投影
float shadow = LIGHT_ATTENUATION(i); //光照部分阴影,由U3D自带函数得出
// 光源反射混合
float3 dirLighting = (baseCol * lambert + specCol * phong) * _LightColor0 * shadow; //光源部分混合,由基本颜色乘以兰伯特光照模型相加高光颜色乘以冯光照模型得出的值再乘以光的颜色再乘以阴影
// 环境漫反射
float upMask = max(0.0, nDirWS.g); // 环境光上部遮罩,用作三环境色计算, 由世界空间法线方向的Y方向(G通道)然后去除负值得出
float downMask = max(0.0, -nDirWS.g); // 环境光下部遮罩,用作三环境色计算, 由世界空间法线方向的Y方向的反方向去除负值得出
float sideMask = 1.0 - upMask - downMask; // 环境光的侧面方向,用作三环境色计算,由 1 减去环境光的上和下部遮罩得出。
float3 envCol = _EnvUpCol * upMask + // 环境光的上部颜色乘以上部遮罩
_EnvSideCol * sideMask + // 环境光的侧面颜色乘以侧面遮罩
_EnvDownCol * downMask; // 环境光的下部颜色乘以下部遮罩最后的值相加
float3 envDiff = envCol * baseCol * _EnvDiffInt; // 环境光漫反射混合,由环境光三色和模型基本色相乘,再用环境光漫反射强度控制得出。
// 环境镜面反射
float fresnel = pow(max(0.0, 1.0 - vdotn), _FresnelPow); // 菲涅尔现象,由 1 减去观察方向点乘世界空间法线方向的值,然后用菲涅尔次幂控制菲涅尔的范围。
float3 envSpec = var_Cubemap * fresnel * _EnvSpecInt; // 环境高光混合,由立方体环境贴图与菲涅尔和环境镜面反射的强度相乘的值得出。
// 环境光遮挡
float occlusion = var_MainTex.a; //环境光遮挡贴图采样。由基本颜色贴图的A通道采样得出。
// 环境反射混合
float3 envLighting = (envDiff + envSpec) * occlusion; // 最后环境光的混合
// 自发光
float3 emission = var_EmitTex * _EmitInt; //自发光, 由自发光贴图采样与自发光强度控制得出。
// 最终混合
float3 finalRGB = dirLighting + envLighting + emission; //最终颜色输出是由光源光照与环境光照和自发光相加得出。
// 返回值
return float4(finalRGB, 1.0); //输出最终颜色
}
ENDCG
}
}
FallBack "Diffuse"
}
如有看客更多内容请到https://www.yuque.com/u29039111/ftisl0