Unity Shader入门精要扩展

表面着色器

我的理解是对顶点着色器和片元着色器的一层封装

表面着色器的一个例子

Shader "Custom/Chapter17_1_BumpDiffuseShader"
{
    Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 300
		
		CGPROGRAM
		#pragma surface surf Lambert
		#pragma target 3.0

		sampler2D _MainTex;
		sampler2D _BumpMap;
		fixed4 _Color;

		struct Input {
			float2 uv_MainTex;
			float2 uv_BumpMap;
		};

		void surf (Input IN, inout SurfaceOutput o) {
			fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
			o.Albedo = tex.rgb * _Color.rgb;
			o.Alpha = tex.a * _Color.a;
			o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
		}
		
		ENDCG
	} 
	
	FallBack "Legacy Shaders/Diffuse"
}

预处理命令

image-20250320211243959

表面函数surf
image-20250320211408212

里面的参数基于物理渲染而新加的两种结构体

表面函数当中会使用输入结构体Input IN来设置表面的各种属性并且把这些属性存储在输出结构体SurfaceOutput、SurfaceOutputStandard、SurfaceOutputStandardSpecular当中,之后再传递给光照函数计算光照结果。

光照函数

内置的基于物理的光照函数模型Standard和StandardSpecular,以及简单的非基于物理的光照模型Lambert和BlinnPhong。

可以定义自己的光照函数,下面的是前向渲染的光照函数

image-20250320211750654

其他可选参数

​ 自定义的修改函数:顶点修改函数vertex:VertexFunction、最后的颜色修改函数finalcolor:ColorFunction。前者允许我们自定义一些顶点属性,例如把顶点颜色传递给表面数,或者是修改顶点位置,比如实现某些顶点动画等等。最后的颜色修改函数可以在颜色绘制到屏幕前年最后一次修改颜色值、比如自定义雾效等等。

​ 阴影:
addshadow为表面着色器生成一个阴影投射的Pass,通常情况下Unity可以直接在FallBack当中找到通用的光照模式为ShadowCaster的Pass,从而将物体正确地渲染到深度和阴影纹理当中,但是对于一些进行了顶点动画、透明度测试的物体,我们就需要对阴影投射进行特殊处理,来为他们产生正确的阴影。
fullforwardshadows可以在前向渲染路径中支持所有光源类型的阴影,默认情况下unity只支持平行光的阴影效果。
noshader可以禁用阴影

​ 透明度测试和透明度混合
通过alpha、alphatest指令实现混合和测试。
alphatest:VariableName指令会使用变量来剔除不满足条件的片元
可以使用上面的addshadow来生成正确的阴影投射的Pass

​ 光照
noambient:告诉unity不要使用然后环境光和光照探针(light probe)
novertexlights:告诉unity不要用任何顶点光照
noforwardadd去掉前向渲染当中所有额外的Pass

​ 控制代码的生成(控制由表面着色器自动生成的代码)
默认情况unity会为一个表面着色器生成相应的前向渲染路径,延迟渲染路径使用的Pass,这会导致生成的Shader文件较大,可以使用exclude_path:defferred、exclude_path:forward、exclude_path:prepass来告诉unity不要为某些渲染路径生成代码

两个结构体

Input包含许多表面属性的数据来源(如果自定义了顶点修改函数,他可以是顶点修改函数的输出结构体)

Input内置许多变量名

image-20250320213232837

除此之外:uv_MainTex、uv_BumpMap主纹理和法线纹理的变量,这些采样坐标必须用uv为前缀,后接纹理名称

Unity在背后为我们准备好这些数据,我们直接在表面着色器当中使用即可

SurfaceOutput

有了Input结构体来提供所需要的数据之后,我们可以据此计算各种表面属性。因此另一个结构体就是用于存储这些表面属性的结构体,即SurfaceOutput、SurfaceOutputStandard、SurfaceOutputStandardSpecular,它会作为表面函数的输出,随后会作为光照函数的输入来进行各种光照计算。相比与Input结构体的自由性,这个结构体里面的变量是提前就声明好的,不可以增加也不可以减少(如果没有对某些变量赋值,就会使用默认值)。SurfaceOutput的声明可以在Lighting.cginc当中找到

struct SurfaceOutput
{
	fixed3 Albedo;
	fixed3 Normal;
	fixed3 Emission;
	half Specular;
	fixed Gloss;
	fixed Alpha;
}

而SurfaceOutputStandard和SurfaceOutputStandardSpecular的声明可以在UnityPBSLighting.cginc当中找到

struct SurfaceOutputStandard
{
	fixed3 Albedo;
	fixed3 Normal;
	half3 Emission;
	half Metallic;
	half Smoothness;
	fixed Alpha;
};
   
struct SurfaceOutputStandardSpecular
{
    fixed3 Albedo;
    fixed3 Specular;
    fixed3 Normal;
    half3 Smoothness;
    half Occulusion;
    fixed Alpha;
}

在一个表面着色器当中,需要选择上述三者当中的其一,这取决于我们选择使用的光照模型。

Unity内置两种光照模型:
1,简单的Lambert BlinnPhong
2,基于物理的Standard StandardSpecular

使用了简单的非基于物理的,使用SurfaceOutput

使用了基于物理的光照模型Standard和StandardSpecular

image-20250320222634204

Unity做了什么

Unity对Pass的自动生成大概如下

image-20250320224135423

image-20250320224153811

Unity会根据表面着色器生成一个包含了许多Pass的顶点片元着色器

以unity生成的LightMode为Forward的Pass为例:

1 直接将表面着色器中CGPROGRAM ENDCG之间的代码复制过来,这些代码包括了我们对Input结构体、表面函数、光照函数(如果自定了的话)等变量和函数的定义。这些函数和变量会在之后被当成正常的结构体和函数进行调用

2 Unity分析代码,生成顶点着色器的输出v2f_surf结构体,如果我们在表面着色器Input中定义了某些变量,但是后面没有使用,unity会帮助我们把变量优化掉,不在v2f_surf当中生成。v2f_surf还包含了一些其他变量比如阴影纹理坐标、光照纹理坐标、逐顶点光照

3 生成顶点着色器
如果自定义了顶点修改函数,则会先调用顶点修改函数

​ 计算v2f_surf中其他生成的变量值。这主要包括了顶点位置、纹理坐标、法线方向、逐顶点光照、光照纹理的采样坐标等等,可以通过编译指令控制某些变量是否需要计算

​ 将v2f surf传递给片元着色器

4 生成片元着色器

​ 使用v2f_surf中对应变量填充Input结构体,例如纹理坐标、视角方向等等

​ 调用我们自定义的表面函数填充surfaceOutput结构体

​ 调用函数得到初始的颜色值

​ 进行其他的颜色叠加,例如没有使用光照烘焙,还会添加顶点光照的影响

​ 最后如果自定义了最后的颜色修改函数,Unity就会调用它进行最后的颜色修改

表面着色器实例分析(实现对模型的膨胀)

表面着色器的缺点

表面着色器只是Unity在顶点片元着色器上面提供的一种封装

会对性能造成一定的影响,想进行优化,表面着色器难以满足我们的需求

表面着色器无法完成一些自定义的渲染效果,比如透明玻璃的效果

基于物理的渲染PBS

这只是对于PBS的一个简短介绍

PBS的理论和数学

一些理论可以在Games101里面找到

光是什么

​ 光是一种电磁波,由光源发出来,与场景当中的对象相交,一些被光线吸收、而另一些则是被散射,最后光线被一个感应器吸收成像。

​ 材质与光线相交有两种物理现象:散射和吸收(以及发光)

​ 影响光的一个重要特性是材质的折射率。在均匀的介质当中,光是沿着直线传播的。但是如果光在传播时候介质的折射率发生了变化,光的传播方向就会发生变化。特别是,如果折射率是突变的,就会发生光的散射现象。

​ 为了渲染对光照建模我经常只考虑两个介质的边界是无限大并且是光学平滑的。

​ 尽管真实物体的表面并不是无限延伸的,也不是绝对光滑的。但是和光的波长相比,它们的大小可以被近似认为是无限大以及光学平滑的。

​ 再这样的前提下,光在不同介质的边界会被分割成两个方向:反射方向和折射方向。

​ 利用菲涅耳等式可以描述多少百分比的光会被反射

image-20250320232728663

​ 上面的图描述了漫反射和镜面反射的原理

​ 有的光会被微表面折射到内部,一部分被介质吸收,一部分又被散射到外部,金属往往具有很高的反射系数。因此所有被折射的光往往会被立即吸收,被金属内部的自由电子转化为其他形式的能量。

​ 而非金属材质则会同时表现出吸收和散射两种现象。这些被散射出去的光被称为次表面散光。

image-20250321000020945

从渲染的层级大小重新审视光与物体表面的交互行为。

​ 由微表面反射的光可以被认为是该点上一些方向变化不大的反射光

​ 而折射光线需要考虑更多的信息,那些次表面散射光会从不同于入射点的位置从物体内部再次射出,如下方左图。

​ 而这些离入射点的距离值和像素大小之间的关系会产生两种建模结构。

​ 如果像素要大于这些散射距离,那么意味着这些次表面散射产生的距离可以忽略,我们的渲染可以在局部进行。

​ 如果像素要小于这些散射距离,那我们就不能去忽略它们了

​ 实现这种特殊的渲染模型,就是次表面散射渲染技术

image-20250321000854364

双向反射分布函数BRDF

我们可以使用辐射率Radiance来量化光

辐射率是单位面积单位方向上光源的辐射通量。通常用L来表示。

在渲染当中我们通常会基于表面的入射光线的入射辐射率Li来计算出射辐射率L0,这个过程也往往被称为是着色的过程。

我们要得到出射辐射率L0,可以BRDF函数来模拟物体表面与光的交互,定量分析。

大多数情况下,BRDF可以使用f(l,v)来进行表示,其中I为入射方向和v为观察方向(双向的含义)。

这种情况下,绕着表面法线旋转入射方向或者观察方向并不会影响BRDF的结果。这种BRDF被称作为各项同性的BRDF。与之对应的则是各向异性的BRDF

那么BRDF代表含义是什么呢?

第一种理解:当给定入射角度之后,BRDF可以给出所有出射方向上的反射和散射光线的相对分布情况。

第二种理解:当给定观察方向(出射方向)的时候,BRDF可以给出所有入射方向到该出射方向的光线分布。

一个更加直观的理解是,当一束光线沿着入射方向I到达表面某点的时候,f(I , v)表示了有多少部分的能量被反射到了观察方向v上,

image-20250321002516355

书上对这个公式的解释

image-20250321003327254

在游戏渲染当中,我们通常是和一些精确的光源打交道,与现实世界一个点要接收四面八方的光照不同。

我们可以用这个等式来计算它在某个方向上的出射辐射率

image-20250321003530647

大大简化了计算。

如果场景中包含了多个精确光源,我们可以把它们分别带入上面的算式进行计算然后相加得到结果。

下面我们来看BRDF是如何得到的。可以看出BRDF决定了着色过程是否是基于物理的。这可以由BRDF是否满足两个特性来进行判断:它是否满足交换律和能量守恒。

image-20250321003903319

基于这些理论BRDF可以用来描述两种不同的物理现象:表面反射和次表面反射。

在BRDF中有两个部分描述:描述反射的是高光反射项,描述次表面反射的是漫反射项。

漫反射项

lambert模型是最简单也是应用最广泛的BRDF,而准确的Lambertian BRDF被描述为

image-20250321004213720

书上所讲的BRDF中的漫反射项与Lambert的不同之处
image-20250321004406391

总之,这个式子实际上是一个定值,余弦实际上是定值,包括在所谓的反射等式中,不包括在BRDF中,整除pi是因为brdf在半球内的积分值为1,,,

更加复杂的漫反射

image-20250321004956308

总之,迪士尼反射考虑了更多,考虑了在掠射角漫反射项的能力变化,以及表面粗糙度对漫反射的影响。

高光反射项

BDRF的高光反射项大多建立在微面元理论的假设上。

现在只考虑被反射的光线,折射光线在漫反射的时候已经讨论过了、

仍然有一些材质无法使用微面理论来进行描述

image-20250321005345929

image-20250321005352513

总之,只有一部分微面元反射的光线会进入到我们眼睛当中。

这个微元面上的法线也叫作半角度矢量

image-20250321005405916

image-20250321005410280

总之,某些光源方向被遮挡的微元面上的反射不会全部加入计算,即使现实仍然有其他地方的漫反射会计入,但是不在讨论范围之内

image-20250321005423018

image-20250321010642469

总之D(h)是微面元的法线分布函数,用于计算多少比例微面元法线满足m=h,只有这部分才可以发生I到v的反射

G(I , v , h)是阴影——遮掩函数,它用于计算那些满足m = h的微面元中会有多少由于遮挡而不会被人眼看到,它给出了活跃的微面所占的浓度。只有活跃的微面元才会成功地把光线反射到观察方向上。

F(I,h)是活跃微面元的菲涅尔反射系数,表示了反射光线占据入射光线的比例。

分母用于校正微面元的局部空间到整体宏观数量差异的校正因子。

image-20250321011259634

还有更多复杂的模型如GGX[3],Beckmann[4]

Unity5实现PBS

Unity有两种基于物理的工作流程,金属工作流和高光反射工作流

其中金属工作流是默认的工作流程,对应的Shader为Standard Shader。

高光反射工作流则是Standard(Specular Setup)

不同的工作流可以实现相同的效果,只是它们使用的参数不同。

如何实现

源代码在Unity内置的builtin_shaders-5.x/DefaultResourceExtra当中得到,这些shader以来builtin_shaders-5.x/CGIncludes文件夹中定义的一些头文件。

其中定义了许多和PBS相关的各个函数、结构体、宏等

image-20250321100715868

总体来讲Standard.shader和StandardSpecular.shader的代码基本相同

第一个subshader使用的计算更加复杂,主要针对非移动平台。

并且定义了前向渲染、延迟渲染路径使用的Pass,以及用于投射阴影和提取元数据的Pass,第二个SubShader定义了4个Pass,其中两个Pass用于前向渲染路径,一个Pass用于投射投影,另一个Pass用于提取元数据,

第二SubShader定义了4个Pass,其中两个Pass用于前向渲染路径,一个Pass用于投射投影,另一个Pass用于提取元数据,主要面对移动平台

最大的不同:它们在设置BRDF的输入的时候使用了不同的函数来设置各个参数——基于金属工作流的StandardShader使用MetallicSetup函数来设置各个参数,基于高光反射工作流的Standard(Specular Setup)使用SpecularSetup函数来设置

两个Pass的代码大体相同,只是ForwardBase Pass进行了更多的光照计算,例如计算全局光照、自发光等效果。

使用Standard Shader

image-20250321103335512

金属材质

几乎没有漫反射,因为所有被吸收的光都会被自由电子立刻转化为其他形式的能量。

有非常强烈的高光反射

高光反射通常是有颜色的,例如金子的反光颜色为黄色。

非金属材质

大多数角度高光反射的强度比较弱,但是在掠射角时高光反射轻度反而会增强,即菲涅耳现象。

高光反射的颜色比较单一

漫反射的颜色多种多样

——————

Unity的工作流就是更加方便的让我们针对以上特性来调整材质效果

下面是两种校准表格

image-20250321103824220

image-20250321103840643

image-20250321104015896

MainMap部分

在金属工作流中,albedo定义了物体的整体颜色

非金属的亮度范围在50243之间,而金属材质的亮度一般在186255之间

我们可以使用一张纹理作为albedo值

也可以用一张纹理采样得到metallic值

smoothness是上一个属性metallic的附属值,定义了从视觉上来看该表面的光滑程度。

如果我们在设置metallic属性的时候使用的是一张纹理,那么这张纹理的A通道就对应了表面的Smoothness值

高光反射工作流使用的面板几乎相同

只是使用了不同含义的albedo属性,并且使用了Specular代替了上述的Metallic属性。

在高光反射工作流当中,材质的albedo属性定义了表面的漫反射强度。

对于非金属物体,albedo的值被认为是视觉上的物理颜色,对于金属材质,albedo的值通常非常接近黑色(因为黑色会吸收次表面反射,金属几乎不存在次表面反射)

Specular属性定义了表面的高光反射强度,非金属通常使用灰度值0~55的深灰色代表Specular值,表面高光反射较弱

金属材质会使用视觉上认为的该金属颜色来作为specular值。

Smoothness定义了光滑程度,specular纹理的alpha值即为smoothness

渲染模式

Opaque:不透明物体

Transparent:玻璃等不透明物体

Cutout:albedo上的alpha值会成为一个掩码纹理,它的子属性alpha cutoff将会是透明度测试时候使用的阈值

Fade:和transparent类似,但是当材质透明度降低的时候所有渲染效果都会从屏幕上淡出,transparent则可以保留高光

一个更加复杂的例子

使用HDR格式的cubemap作为skybox,可以让场景中物体的反射更加真实。

什么是全局光照

概念:模拟光线是如何在场景中进行传播的,它不仅会考虑那些直接光照的结果,还会计算光线被不同物体表面反射而产生的间接光照。

在使用基于物理的着色技术的时候,当渲染表面上一点时,我们需要计算该点的半球范围内所有会反射到观察方向的入射光线的光照结果,这些入射光线中就包含了直接光照和间接光照。

通常来说这些间接光照的计算非常耗时间,通常不会用在实时渲染当中

Unity采用来Enlighten方案来让全局光照在各种平台上有不错的性能表现

预计算光照包含了我们常见的光照烘焙,也就是指我们把光源对场景中静态物体的光照效果提前烘焙到一张光照纹理当中。然后把这张光照纹理直接贴在这些物体的表面,来获得光照效果,这些光照纹理不仅存储了直接光照的结果,还包含了那些由物体反射得到的间接光照。

由于静态的光照烘焙无法在光照条件改变的时候更新物体的光照效果,因此unity使用了预计算实时全局光照为我们提供了一个解决途径,来动态地为场景实时更新复杂的光照效果。并且这些效果都是实时的。

如何实现:一旦物体和光源的位置被固定了,这些物体对光线的反弹路径以及漫反射光照也是固定的,也就是说和摄像机无关的。因此我们可以使用与计算来把这些物体的关系提前计算出来。在实时运行的时候,只要光源的位置不变,即便改变了光源的颜色和强度、物体材质属性,这些信息就一直有效,不需要实时更新。

在预计算阶段Enlight会在所有静态物体组成的场景上进行简化的光线追踪处理。在这个过程Enlight会自动把场景分割成很多的子系统,它并不是为了得到精确的光照效果,而是为了得到场景中物体之间的关系。

我们至少要把场景中的一个物体勾选上static才能使用

另一个例外的高光反射,这是和摄像机的位置相关的,unity的解决方法是使用光照探针。对于动态移动的物体,我们使用光照探针来模拟它的光照环境。

因此在实时运行的时候unity会利用预计算得到的信息来计算光照信息,并且把它们存储在额外的光照纹理当中。