UnityShader入门精要笔记1——顶点/片元着色器结构与BRDF(基本光照模型)

BRDF(基本光照模型)

基本光照模型一共有四种。分别为环境光自发光漫反射高光反射,我们先来实现漫反射

实现漫反射

光线强度的计算

在开始实现漫反射之前,我们需要了解一个概念:辐射度

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

下图使用兰伯特模型进行漫反射光计算(并且只进行了漫反射光计算),正面对着光线方向的部分最亮,然后逐渐变淡,

兰伯特模型
反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比
(也就是使用我们刚刚的“辐射度”作为漫反射光的强度)

在这里插入图片描述

好现在开始写Shader

我们按书里的流程写一个顶点/片元着色器来实现漫反射

新建Shader

首先新建一个Unity Shader,把原有代码全部删除,然后给shader起个名字

Shader "Diffuse-Lambert" {

}

添加一个Properties语义块

为Shader添加一个Properties 语义块,声明我们需要的属性。

Properties 语义块是材质和Unity Shader的桥梁,它包含了一系列属性,这些属性会出现在检查器窗口的材质面板中。【candycat】

扫描二维码关注公众号,回复: 10606139 查看本文章
Shader "Diffuse-Lambert" {
	Properties {
	//_Color 为属性标识符,我们会在稍后的Shader编写中使用这个名字
	//"Color Tint" 为在检查器窗口中显示的属性名称
	//Color 为属性的类型
	//等号右边的是属性的默认值,在这里四个1代表为“白色”
	
		_Color ("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}
}

此时在Unity中随意新建一个材质,并把我们的Diffuse-Lambert着色器拖到材质上,就可以在检查器面板看到我们刚刚声明的属性了
在这里插入图片描述

添加SubShader和Pass。

每一个Unity Shader可以定义多个SubShader,但最少要有一个。当Unity需要加载这个UnityShader时,Unity会扫描所有的SubShader语义块,并选出当前显卡能够支持的第一个SubShader运行。【candycat】

Shader "RefShader" {
	//属性
	Properties {
	}
	//显卡A使用的子着色器
	SubShader {
	}
	//显卡B使用的子着色器
	SubShader {
	}
}

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		//如果支持的话,当前显卡会使用这个子着色器
		//不支持就靠 Fallback了
	}
	
}

SubShader中定义了一系列Pass以及可选的状态和标签,每个Pass定义了一次完整的渲染流程,所以我们应该尽量使用最小数目的Pass。

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			//在这里会进行一次完整的渲染
		}
	}
	
}

在Pass中,我们设置标签LightModeForwardBase(向前渲染),这是为了Unity能够按向前渲染路径的方式为我们正确提供各个光照变量

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
		}
	}
	
}

使用CG/HLSL语言来编写顶点/片元着色器

我们使用 CGPROGRAM作为CG/HLSL语言的开始符,而ENDCG是结束符。
在CG开始后,我们先来申明vert函数(顶点着色器,逐顶点渲染)和frag函数(片元着色器,逐片元渲染)

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
			
			CGPROGRAM //CG开始
			#pragma vertex vert
			#pragma fragment frag

			//app to vert,顶点着色器的输入类型,获取当前顶点的信息(由程序给出的)
			struct a2v {
			};
			
			//vert to frag,片元着色器的输入类型,获取当前片元的信息(由顶点着色器输出的数据再插值得到的)
			struct v2f {
			};
			
			//顶点着色器,逐顶点运行,输入a2v是当前顶点的信息,输出v2f给片元着色器
			v2f vert(a2v v) {
			}
			
			//片元着色器,逐片元运行,输入v2f是当前片元的信息,输出颜色
			fixed4 frag(v2f i) : SV_Target {
			}

			ENDCG //CG结束


		}
	}
	
}

这个流水线大约是这样子的:

Created with Raphaël 2.2.0 开始 程序给出当前顶点信息(a2v) 顶点着色器 储存当前顶点信息(v2f) 顶点遍历完了? 三角形设置: 连接顶点得到三角形网格 三角形遍历: 检查每个像素,判断是否被一个三角形所覆盖。 如果是的话就由该像素生成一个片元,并使用三 角网格的3个顶点信息插值得到该片元的信息。 划重点:片元其实就是一个像素带着它的信息 (我们在v2f中(也就是顶点着色器的返回中)定 义需要的信息),这些信息由片元所在的三角形 顶点信息插值生成 获取当前片元信息(v2f) 片元着色器 输出当前片元颜色到颜色缓冲区 片元遍历完了? 结束 yes no yes no

定义a2v和v2f结构体

现在我们的a2v还没有任何字段,也就是说我们的顶点着色器(vert)什么输入都没有,让我们使用语义来给它增加一些数据。

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
			
			CGPROGRAM //CG开始
			#pragma vertex vert
			#pragma fragment frag

			struct a2v {
				//POSITION语义告诉Unity,用模型空间中当前顶点的坐标填充vertex变量
				float4 vertex : POSITION; 
				//NORMAL语义告诉Unity,用模型空间中当前顶点法的线方向填充normal变量
				float3 normal : NORMAL;
			};

			struct v2f {
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}

			ENDCG //CG结束
		}
	}
}

接着给我们的v2f定义结构体,以确保我们在稍后的片元着色器中能够有足够的数据进行运算

我们回顾一下兰伯特模型
反射光线的强度表面法线光源方向之间夹角的余弦值成正比

也就是说,只要得到了片元的表面法线光源方向,我们就能得到该片元漫反射光线的强度

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) //漫反射底色
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } //向前渲染
			
			CGPROGRAM //CG开始
			#pragma vertex vert
			#pragma fragment frag

			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL;
			};

			struct v2f {
				//SV_POSITION语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
				//这也是顶点着色器最重要的一个工作:将顶点坐标从模型空间转换到裁剪空间
				float4 pos : SV_POSITION;
				//TEXCOORD0语义表示worldNormal变量占用了TEXCOORD0插值寄存器
				//每个插值寄存器可以存储4个浮点值(float)
				float3 worldNormal : TEXCOORD0; //世界空间下的顶点法线向量
			 	float3 worldLightDir : TEXCOORD1; //世界空间下的光源位置
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}
			
			ENDCG //CG结束
		}
	}
}

包含头文件以及声明属性变量

在正式开始计算之前,为了使用一些Unity内置的变量和函数,我们需要包含进内置文件 "UnityCG.cginc"

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) 
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } 
			
			CGPROGRAM 
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc" //包含内置文件"UnityCG.cginc"

			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldLightDir : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}
			
			ENDCG
		}
	}
}

而为了可以使用我们在Properties 语义块中定义的属性(那个_Color),我们需要在CG中对属性进行申明

Shader "Diffuse-Lambert"{
	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1) 
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" } 
			
			CGPROGRAM 
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc" 

			fixed4 _Color; //这里的变量名需要和属性的名字完全一致
			
			struct a2v {
				float4 vertex : POSITION; 
				float3 normal : NORMAL;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldLightDir : TEXCOORD1;
			};
			
			v2f vert(a2v v) {
			}
			
			fixed4 frag(v2f i) : SV_Target {
			}
			
			ENDCG
		}
	}
}

编写顶点着色器

好了,开始写我们的顶点着色器(vert函数)。首先申明一个v2f类型的变量,对结构体中的字段依次赋值,最后将其返回。

v2f vert(a2v v) {
	//申明返回值v2f
	v2f o;
	
	//这是顶点着色器最重要的一个任务,将顶点坐标从模型空间转换到裁剪空间
	//UnityObjectToClipPos函数接受一个模型空间的坐标,返回该坐标在裁剪空间的坐标
	o.pos = UnityObjectToClipPos(v.vertex);
	
	//UnityObjectToWorldNormal函数接受一个模型空间的法线向量,将其转换到世界空间中并返回
	o.worldNormal = UnityObjectToWorldNormal(v.normal);
	
	//WorldSpaceLightDir函数接受一个模型空间中的顶点位置,并返回世界空间中从该点到光源的光照方向。未被归一化。(由于是平行光,任何点的光照方向都是一样的,参数填fixed(0)都可以)
	o.worldLightDir = WorldSpaceLightDir(v.vertex);
	return o;
}

经过顶点着色器的处理,我们的每个片元中已经包含我们需要的两个信息:法线向量光源方向。现在,让我们在片元着色器中为每个片元计算他们的光线强度

编写片元着色器

将法线向量和光源方向归一化(转为长度为1的单位向量)

由于我们稍后要用点积来求得两向量夹角的cos值,而点积的公式是a·b=|a||b|cosθ(θ为a和b的夹角),很明显,只有当a和b均为单位向量时,点积的结果才会是我们要的两向量的cos值。

fixed4 frag(v2f i) : SV_Target
{
	//使用normalize()函数对向量进行归一化
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
}
计算光线强度并返回颜色
fixed4 frag(v2f i) : SV_Target
{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
	//saturate()函数可以将值截取到[0,1],而dot()函数用于计算两向量间的点积
	//计算出光线强度后,和我们的_Color属性相乘,就能实现一个亮度更改了
	fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
	
	//返回计算完成的颜色
	return fixed4(diffuse, 1.0);
}

在这里插入图片描述

增加对灯光颜色的考虑

刚刚的计算我们实际上忽略了灯光的颜色,现在我们获取灯光颜色并加入计算。

包含头文件

为了获取灯光颜色,我们需要先在CG中包含头文件 "Lighting.cginc"

Shader "Diffuse-Lambert"{

	Properties{
		_Color("Color Tint", Color) = (1, 1, 1, 1)
	}

	SubShader {
		Pass {
			Tags { "LightMode"="ForwardBase" }

			CGPROGRAM
			
			#pragma vertex vert 
			#pragma fragment frag 
			#include "Lighting.cginc"	 //额外包含一个"Lighting.cginc"
			#include "UnityCG.cginc"
			
			fixed4 _Color;

			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldLightDir : TEXCOORD1;
			};

			v2f vert(a2v v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldLightDir = WorldSpaceLightDir(v.vertex);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(i.worldLightDir);
				fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color;
				return fixed4(diffuse, 1.0);
			}
			ENDCG
		}
	}
}
修改片元着色器
fixed4 frag(v2f i) : SV_Target
{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
	//直接乘上灯光颜色 _LightColor0
	fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
	return fixed4(diffuse, 1.0);
}

在这里插入图片描述

另一种实现,半兰伯特模型

在兰伯特模型中,光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现。【candycat】
Valve公司在开发《半条命》时提出了一个新技术,被称为半兰伯特光照模型。在半兰伯特模型中,我们不将片元上法线向量光源方向的cos值截取到[0, 1],而是为这个cos值乘上α倍的缩放然后加上一个β大小的位移,通常,α和β都是0.5。也就是说,半兰伯特模型将法线向量光源方向的cos值从[-1, 1]映射到了[0, 1]。

修改片元着色器

fixed4 frag(v2f i) : SV_Target
{
	fixed3 worldNormal = normalize(i.worldNormal);
	fixed3 worldLightDir = normalize(i.worldLightDir);
	//兰伯特模型
	//fixed3 diffuse = saturate(dot(worldNormal, worldLightDir)) * _Color * _LightColor0;
	//半兰伯特模型
	fixed3 diffuse = (dot(worldNormal, worldLightDir) * 0.5 + 0.5) * _Color* _LightColor0;

	return fixed4(diffuse, 1.0);
}

在这里插入图片描述

发布了23 篇原创文章 · 获赞 63 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_15505341/article/details/83929931