Unity Shader

  • 第二章 渲染流水线
  • 2.1.2 渲染流水线
  • 2.3 GPU流水线
  • 2.3.1 顶点着色器
  • 2.3.2 曲面细分着色器
  • 2.3.3 几何着色器
  • 2.3.4 裁剪
  • 2.3.5 屏幕映射
  • 2.3.6 逐片元操作
  • 第三章 Unity Shader基础
  • ShaderLab
  • 3.1 Uniyt Shader的结构
  • 3.1.1 取名字
  • 3.1.2 Properties
  • 3.2 Unity Shader的编辑
  • 3.3 Shader属性类型和Cg变量类型的匹配关系
  • 3.4 Unity提供的内置文件和变量的使用
  • 3.5 漫反射光照模型的计算
  • 3.6 高光反射光照模型的计算
  • 第七章 基础纹理
  • 第八章 透明效果
  • 8.1 透明度测试
  • 8.2 透明度混合
  • 8.3 双面渲染的透明效果
  • 8.3.1 透明度混合的双面渲染
  • 第九章 更复杂的光照
  • 9.1 渲染路径
  • 9.2 阴影是如何实现的
  • 9.3 统一计算光照衰减和阴影
  • 9.4 透明度测试的阴影
  • 第十章 高级纹理
  • 10.1 立方体纹理
  • 10.1.1 天空盒子
  • 10.1.2 创建用于环境映射的立方体纹理
  • 第十一章 纹理动画


第二章 渲染流水线

Shader即为着色器。

2.1.2 渲染流水线

分为三个阶段:1、应用阶段。2、几何阶段。3、光栅化阶段。

  • 应用阶段(CPU)
    大致可分为下面三个阶段
  • 把数据加载到显存种。
  • 设置渲染状态。
  • 调用Draw Call1

在这一阶段开发者有三个主要任务。

1、首先,我们需要准备好场景数据。

2、为了提高渲染性能,我们往往需要做一个2工作。

3、设置好每个模型的渲染状态。

这一阶段最重要的输出是渲染所需的几何信息,即***渲染图元***(rendering primitives)。

  • 几何阶段(GPU)
    几何阶段负责和每个渲染图元打交道。它的一个重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。
  • 光栅化阶段(GPU)
  • 这一阶段会使用上一个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。

2.3 GPU流水线

几何阶段和光栅化阶段可以分为若干更小的流水线阶段,这些阶段由GPU来是实现,每个阶段GPU提供了不同的可配置性或可编程性。

几何阶段包含:顶点着色器—>曲面细分着色器—>几何着色器—>裁剪—>屏幕映射

光栅化阶段包含:三角形设置—>三角形遍历—>片元着色器—>逐片元操作

光栅化阶段的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。

下面介绍各个阶段的作用:

2.3.1 顶点着色器

完全可编程的,通常用于实现顶点的空间变化、顶点着色等功能。需要完成的主要工作有:坐标变换和逐顶点光照。

特点:本身不可创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。

一个最基本的顶点着色器必须完成的一个工作是:把顶点坐标从模型空间转换为齐次裁剪空间3

输入数据来自于CPU。

输出后续阶段所需的数据。

2.3.2 曲面细分着色器

可选的着色器,用于细分图元。

2.3.3 几何着色器

可选着色器,被用于执行逐图元的着色操作,或者被用产生更多的图元。

2.3.4 裁剪

可配置的,不可编程的。此为硬件上的固定操作。将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的图片。

2.3.5 屏幕映射

不可配置和编程的,负责把每个图元的坐标转换到屏幕坐标系中。

屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下。

三角形遍历三角形设置都是固定函数阶段。

片元着色器:完全可编程的,用于实现逐片元的着色操作。

2.3.6 逐片元操作

不可编程的,但是具有很高的可配置性。负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等。

只有通过了测试(深度测试、模板测试),才能进行合并。

第三章 Unity Shader基础

unity shader本质上就是一个文本文件。

ShaderLab

Unity提供的一种专门为Unity Shader服务的语言。

3.1 Uniyt Shader的结构

3.1.1 取名字

每个Unity Shader文件的第一行都需要通过Shader语义来指定该Unity Shader的名字。

Shader "name"{}

可以通过添加"/"来控制Unity Shader在材质面板中4出现的位置

Shader "Custom/MyShader"{}
3.1.2 Properties

材质和Unity Shader的桥梁。

Properties语义块中包含了一系列属性(property),这些属性将会出现在材质面板当中。

Properties{
	Name("display name",PropertyType) = DefaultValue
}

Name一般以下划线开头。

**显示名称(display name)**会出现在材质面板上的名称。

PropertyType为每个属性指定类型。

Properties的作用仅仅是为了让这些属性出现在材质面板当中。

3.2 Unity Shader的编辑

下面是一个最简单的顶点/片元着色器。

Shader "Custom/Simple Shader" //取名字
{
	SubShader{
		Pass{
			CGPROGRAM
//告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。               
#pragma vertex vert 
#pragma fragment frag
//函数声明
            //1、顶点着色器代码,逐顶点执行。
            //POSITION和SV_POSITION都是Cg/HLSL的语义,这些语义是不可省略的,它们将告诉系统用户需要            哪些输入值,以及用户输出的是声明。
            //在这里POSITION告诉Unity,把模型的顶点坐标填充到输入参数V中,SV_POSITION告诉Unity,顶点着色器的输出是裁剪空间中的顶点坐标。
			float4 vert(float4 v: POSITION) :SV_POSITION{
				return UnityObjectToClipPos(v);
			}
            //SV_Target的作用:告诉渲染器,把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。
			fixed4 frag() : SV_Target{
				return fixed4(1.0,1.0,1.0,1.0);//表示白色的fixed4类型的变量
			}
			ENDCG
		}
	}
}

片元着色器输出的颜色的每一个分量范围在[0,1],其中(0,0,0)表示黑色,而(1,1,1)表示白色。

POSITION、TANGENT、NORMAL这些语义的数据来源。

在Unity中,它们是由使用该材质的Mesh Render组件提供。在每帧调用Draw Call的时候,Mesh Render组件会把它负责渲染的模型数据发送给Unity Shader。

3.3 Shader属性类型和Cg变量类型的匹配关系

ShaderLab属性类型

Cg变量类型

Color , Vector

float4 , half4 , fixed4

Range , Float

float , half , fixed

2D

sampler2D

Cube

samplerCube

3D

sampler3D

3.4 Unity提供的内置文件和变量的使用

在编写Shader时,我们可以使用#include指令把这些文件包含进来,这样我们就可以使用Unity为我们提供的一些非常有用的变量和帮助函数了。

CGPROGRAM
//···
#include "UnityCG.cginc"//文件后缀是.cginc
//···
ENDCG

3.5 漫反射光照模型的计算

漫反射光照模型也被称为兰伯特光照模型。

兰伯特定律:在平面某点漫反射的光强与该反射点的法向量和入射光角度的余弦值成正比。

可采用逐像素光照或是逐顶点光照,其中逐像素光照显示的图像更加的平滑!

逐像素光照

Shader "Custom/Chapter6-DoffisePixelLevelMat"
{
	Properties{
		_Diffuse("Diffuse",Color) = (1,1,1,1)
	}
		SubShader{
			Pass{

			CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	#include "Lighting.cginc"
			fixed4 _Diffuse;
		struct a2v {
			float3 normal:NORMAL;
			float4 vertex:POSITION;
		};
		struct v2f {
			float4 pos:SV_POSITION;
			float3 worldNormal:TEXCOORD0;
		};
		v2f vert(a2v v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);//将顶点坐标转换为齐次裁剪空间下的坐标
			o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);//将模型坐标下的法线转换为世界空间下的法线
			return o;
		}
		fixed4 frag(v2f i):SV_Target {
			fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
			//用漫反射光照模型进行计算
			//光照方向·法线方向
			fixed3 worldNormal = normalize(i.worldNormal);
			fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

			fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb *saturate(dot(worldNormal, worldLightDir));
			fixed3 color = diffuse + ambient;
			return fixed4(color, 1.0);
		}
		ENDCG
		}
	}
    FallBack "Diffuse"
}

逐顶点光照

将光照模型的计算放在顶点着色器当中。

3.6 高光反射光照模型的计算

逐顶点计算

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'

Shader "Custom/Chapter-SpecularVertexLevel"
{
	Properties{
		_Diffuse("Diffuse",Color) = (1,1,1,1)
		//控制材质的高光反射颜色
		_Specular("Specular",Color) = (1,1,1,1)
		//控制高光区域的大小
		_Gloss("Gloss",Range(8.0,256)) = 20
	}
	SubShader{
		Pass{
		//只有定义了正确的LightMode,我们才能得到一些Unity的内置光照变量,例如:_LightColor()
			Tags{"LightMode" = "ForwardBase"}//Pass标签中的一种
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#include "Lighting.cginc"
				fixed4 _Diffuse;
				fixed4 _Specular;
				float _Gloss;
				struct a2v {
					float4 vertex:POSITION;
					float4 normal:NORMAL;
				};
				struct v2f {
					fixed3 color : COLOR;
					float4 pos:SV_POSITION;
				};
				v2f vert(a2v v) {
					v2f o;
					o.pos = UnityObjectToClipPos(v.vertex);
					//获取环境光
					fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

					fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
					//获取世界坐标系下的光照方向
					fixed3 worldlightDir = normalize(_WorldSpaceLightPos0.xyz);
					//计算基本光照模型下的漫反射
					fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldlightDir));

					fixed3 reflectDir = normalize(reflect(-worldlightDir, worldNormal));
					//视角方向等于相机世界坐标减去顶点世界坐标
					fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
					fixed3 specular = _LightColor0.rgb*_Specular*pow(saturate(mul(viewDir, reflectDir)), _Gloss);

					o.color = ambient + specular + diffuse;
					return o;
				}
				fixed4 frag(v2f i) :SV_Target{
					return fixed4(i.color,1.0);
				}
			ENDCG
		}
	}
	Fallback "Specular"
}

逐顶点的方法得到的高光效果有比较大的问题,高光部分很明显不平滑。

这主要是因为,高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破坏了原计算的非线性关系,就会出现较大的视觉问题,因此,我们就需要使用***逐像素的方法***来计算高光反射。

第七章 基础纹理

在Unity中,我们需要使用纹理名_ST的方式来声明某个纹理的属性。其中ST是缩放(scale)和平移(translation)的缩写。

在切线空间下计算法线纹理

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Chapter7-NormalMapTangentSpace"
{
	Properties{
		_Color("Color Tint",Color) = (1,1,1,1)
		_MainTex("Main Tex",2D) = "white"{}
		_BumpMap("Normal Map",2D) = "bump"{}
		_BumpScale("Bump Scale",Float) = 1.0
		_Specular("Specular",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"
				float _Gloss;
				fixed4 _Specular;
				fixed4 _Color;
				float _BumpScale;
				sampler2D _MainTex;
				sampler2D _BumpMap;
                //得到纹理的属性(平铺和偏移系数)
				float4 _MainTex_ST;
				float4 _BumpMap_ST;

				struct a2v {
					float4 vertex:POSITION;
					float3 normal:NORMAL;
					//纹理法线方向
					float4 tangent:TANGENT;
					//第一组纹理坐标
					float4 texcoord:TEXCOORD0;
				};
				struct v2f {
					float4 pos:SV_POSITION;
					float4 uv:TEXCOORD0;
					//将光照方向和视角方向转换为切线空间下的坐标,所以在这里要进行定义
					float3 lightDir:TEXCOORD1;
					float3 viewDir:TEXCOORD2;
				};
				//顶点着色器
				v2f vert(a2v v) {
					v2f o;
					o.pos = UnityObjectToClipPos(v.vertex);
					//存储第一张纹理的坐标
					o.uv.xy = v.texcoord.xy*_MainTex_ST.xy + _MainTex_ST.zw;
					//存储第二张纹理的坐标
					o.uv.zw = v.texcoord.xy*_BumpMap_ST.xy + _BumpMap_ST.zw;
					//cross()函数:返回两个三元向量的叉积
					//float3 binormal = cross(normalize(v.normal), //normalize(v.tangent.xyz))*v.tangent.w;
					TANGENT_SPACE_ROTATION;

					//由内置函数ObjSpaceLightDir(v.vertex)来获取光照方向
					o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
					//由内置函数ObjSpaceViewDir(v.vertex)来获取视角方向
					//normalize(_WorldSpaceCameraPos.xyz-mul(_Object2World,v.vertex).xyz);
					o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
					return o;
				}
				//片元着色器
				fixed4 frag(v2f i) :SV_Target{
					//在切线空间下计算法线映射
					fixed3 tangentLightDir = normalize(i.lightDir);
					fixed3 tangentViewDir = normalize(i.viewDir);
					fixed3 tangentNormal;
					//纹理采样
					fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
					tangentNormal = UnpackNormal(packedNormal);
					tangentNormal.xy *= _BumpScale;
					tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
					fixed3 albedo = tex2D(_MainTex,i.uv).rgb*_Color.rgb;
					fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
					fixed3 diffuse = _LightColor0.rgb*albedo*max(0, dot(tangentNormal, tangentLightDir));
					//采用Blinn-Phong模型
					fixed halfDir = normalize(tangentLightDir + tangentViewDir);

					fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss);

					return fixed4(diffuse + ambient + specular,1.0);
				}
				ENDCG
			}
		}
			Fallback "Specular"
}

在世界空间下计算

1、修改顶点着色器的输出结构体v2f,使它包含从切线空间到世界空间的变换矩阵

struct v2f{
    float4 pos:SV_POSITION;
    float4 uv:TEXCOORD0;
    float4 TtoW0:TEXCOORD1;
    float4 TtoW1:TEXCOORD2;
    float4 TtoW2:TEXCOORD3;
};

将变换矩阵拆分成多行来进行存储,实际上,对方向矢量的变换只需要3X3的大小矩阵,也就是说,每一行只需要使用float3类型的变量即可,但是为了充分利用插值寄存器的存储空间,我们把世界空间下的顶点位置存储在这些变量的w分量中。

2、修改顶点着色器,计算从切线空间到世界空间的变换矩阵。

v2f vert(a2v v){
    v2f o;
    o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
    
    o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
    o.uv.zw=v.texcoord.xy*_BumpMap_ST.xy+_BumpMap_ST.zw;
    
    float3 worldPos=mul(_Object2World,v.vertex).xyz;
    fixed3 worldNormal=UnityObjectToWorldNormal(v.normal);
    fixed3 worldTangent=UnityObjectToWorldDir(v.tangent.xyz);
    fixed3 worldBinormal=cross(worldTangent.xy,worldTangent.xy)*worldTangent.w;
    
    o.TtoW0=float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
    o.TtoW1=float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
    o.TtoW2=float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
    return o;
}

3、修改片元着色器,在世界空间下进行光照计算

fixed4 frag(v2f i):SV_Target{
    float3 worldPos=float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
    fixed3 lightDir=normalize(UnityWorldSpaceLightDir(worldPos));
    fixed3 viewDir=normalize(UnityWorldSpaceViewDir(worldPos));
    
    fixed3 bump=UnpackNormal(tex2D(_BumpMap,i.uv.zw));
    bump.xy*=_BumpScale;
    bump.z=sqrt(1.0-saturate(dot(bump.xy,bump.xy)));
    bump=normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.Ttow2.xyz,bump)));
}

第八章 透明效果

透明效果的实现有两种方式:①透明度测试。②透明度混合

其中透明度测试无法得到真正的半透明效果。

8.1 透明度测试

只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。也就是说,透明度测试时不需要关闭深度写入的,它和其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元。

我们使用clip函数来进行透明度测试。clip函数是Cg中的一个函数。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/M_AlphaTestMat"
{
    //透明度测试
	Properties{
		_Color("Color Tint",Color) = (1,1,1,1)
		_Cutoff("Alpha Cutoff",Range(0.0,1.0)) = 0.5
		_MainTex("Main Tex",2D) = "white"{}
	}
	SubShader{
		Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
		/*
		透明度测试使用的渲染队列是 AlphaTest
		RenderType标签可以让Unity把这个Shader归入到提前定义的组(这里就是TransparentCutout组),以指明该Shader是一个使用了透明度测试的Shader
		IgnoreProjector设置为True,这意味着这个Shader不会受到投影器的影响。
		*/
		Pass{
		Tags{"LightMode" = "ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			fixed4 _Color;
			float _Cutoff;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			struct a2v {
				float4 vertex:POSITION;
				float3 normal:NORMAL;
				float4 texcoord:TEXCOORD0;
			};
			struct v2f{
				float4 pos:SV_POSITION;
				float3 worldPos:TEXCOORD0;
				float3 worldNormal:TEXCOORD1;
				float2 uv:TEXCOORD2;
			};
			v2f vert(a2v v){
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld,v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
				return o;
			}
			fixed4 frag(v2f i) : SV_Target{
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed4 texColor = tex2D(_MainTex, i.uv);
				clip(texColor.a - _Cutoff);
				fixed3 albedo =  texColor.rgb *  _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				return fixed4(ambient + diffuse,1.0);
			}
			ENDCG
		}
	}
}

8.2 透明度混合

这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合。需要关闭深度写入(ZWrite off),所以此时渲染顺序显得非常重要。

需要关闭深度写入的原因:如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被我们看到的,但是由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面将会被剔除,我们就无法透过半透明表面看到后面的物体了。

Unity提前定义的渲染队列

名称

队列索引号

描述

Background

1000

这个渲染队列会在任何其他队列之前被渲染,我们通常使用该队列来渲染那些需要绘制在背景上的物体。

Geometry

2000

默认的渲染队列,大多数物体都使用这个队列。不透明物体使用这个队列。

AlphaTest

2450

需要透明度测试的物体使用这个队列。在Unity 5中它从Geometry队列中被单独分出来,这是因为所有不透明物体渲染之后再渲染它们会更加有效。

Transparent

3000

这个队列中的物体会在所有Geometry和AlphaTest物体渲染后,再按从后往前的顺序进行渲染。任何使用了透明度混合(例如关闭了深度写入的Shader)的物体都应该使用该队列。

Overlay

4000

该队列用于实现一些叠加效果。任何需要在最后渲染的物体都应该使用该队列。

Unity提供的混合命令–Blend。Blend是Unity提供的设置混合模式的命令。

ShaderLab的Blend命令

语义

描述

Blend off

关闭混合

Blend SrcFactor DstFactor

开启混合,并设置混合因子。源颜色(该片元产生的颜色)会乘以SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以DstFactor,然后把两者相加后再存入颜色缓冲中。

Blend SrcFactor DstFactor SrcFactorA DstFactorA

和上面几乎一样,只是使用不同的因子来混合透明通道。

BlendOp BlendOperation

并非是把源颜色和目标颜色简单相加后混合,而是使用BlendOperation对它们进行其他操作。

Shader "Custom/M_AlphaBlend"
{
	Properties{
		_Color("Color Tint",Color) = (1,1,1,1)
		_MainTex("Main Tex",2D) = "white"{}
		_AlphaScale("Alpha Scale",Range(0,1)) = 1
	}
	SubShader{
		Tags{"Queue" = "Transparent" "IgnoreProject" = "True" "RenderType" = "Transparent"}
		Pass{
			Tags{"LightMode" = "ForwardBase"}
            //关闭深度写入,并开启混合模式
            //由于关闭了深度写入,在模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序出错而产生的错误的透明效果。
			ZWrite Off
			Blend SrcAlpha oneMinusSrcAlpha
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			sampler2D _MainTex;
			float4 _MainTex_ST;
			float _AlphaScale;
			fixed4 _Color;
			struct a2v {
				float4 vertex:POSITION;
				float3 normal:NORMAL;
				float4 texcoord:TEXCOORD0;
			};
			struct v2f {
				float4 pos:SV_POSITION;
				float3 worldPos:TEXCOORD0;
				float3 worldNormal:TEXCOORD1;
				float2 uv:TEXCOORD2;
			};
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}
			fixed4 frag(v2f i) :SV_Target{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed4 texColor = tex2D(_MainTex,i.uv);
				fixed3 albedo = texColor.rgb * _Color.rgb;
				fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				return fixed4(diffuse + ambient,texColor.a * _AlphaScale);
			}
			ENDCG
		}
	}
	Fallback "Transparent/VertexLit"
}

由于关闭了深度写入,在模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序出错而产生的错误的透明效果。

为避免这种情况的发生一种解决办法是使用**两个Pass来渲染模型。**第一个Pass开启深度写入,但是不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲种;第二个Pass进行正常的透明度混合,由于上一个Pass已经得到了逐像素的正确深度信息,该Pass就可以按照像素级别的深度排序结果进行透明渲染。

但这个方法也有缺点:使用多个Pass会对性能造成一定的影响。

在第一个Pass

Pass{
    ZWrite On
    ColorMask 0
        //ColorMask用于设置颜色通道的写掩码。
        //语义 ColorMask RGB | A | 0 | 其他任何R、G、B、A的组合
        //当ColorMask设为0时,意味着该Pass不写入任何颜色通道
}

其他代码部分均和上一个透明度混合中的代码一样。

ShaderLab的混合命令

混合是一个逐片元的操作,而且它是不可编程的,但确实高度配置的。也就是说我们可以设置混合时使用的运算操作、混合因子等来影响混合。

ShaderLab中设置混合因子的命令

命令

描述

Blend SrcFactor DstFactor

开启混合,并设置混合因子。源颜色(该片元产生的颜色)会乘以SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以DstFactor,然后把两者相加后再存入颜色缓冲中。

Blend SrcFactor DstFactor SrcFactorA DstFactorA

和上面几乎一样,只是使用不同的因子来混合透明通道。

8.3 双面渲染的透明效果

如果想要得到双面渲染的结果,可以使用Cull指令来控制需要剔除哪个面的渲染图元。

Cull指令的语法如下:

Cull Back | Front | Off

透明度测试的双面渲染只需要在Pass中添加Cull Off即可。

8.3.1 透明度混合的双面渲染

由于我们没有关闭深度写入,因此可以利用深度缓冲按逐像素的粒度进行深度排序,从而保证渲染的正确性。然而一旦关闭了深度写入,我们就需要小心地控制渲染顺序来得到正确的深度关系,而保证渲染的正确性。因此我们需要把渲染工作分为两个Pass,第一个Pass只渲染背面,第二个Pass只渲染正面,由于Unity会顺序执行SubShader中的各个Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。

第九章 更复杂的光照

9.1 渲染路径

如果需要和光源打交道,需要为每一个Pass指定它使用的渲染路径,只有这样才能让Unity知道该怎么处理光照。

Unity主要的渲染路径有

  1. 前向渲染路径
    有三种处理光照的方式:逐顶点处理、逐像素处理、球谐函数处理。
  2. 延迟渲染路径
    对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。但是,延迟渲染路径也有一些缺点
  • 不能支持真正的抗锯齿功能。
  • 不能处理半透明物体。
  • 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

当使用延迟渲染时,Unity要求我们提供两个Pass

(1)第一个Pass用于渲染G缓冲。在这个Pass中,我们会把物体的漫反色颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中。对于每个物体来说,这个Pass仅会执行一次。

指定渲染路径的目的是和Unity的底层渲染引擎沟通(重要)。如果没有指定任何的渲染路径,那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的。

大多数情况下,一个项目只是用一个渲染路径,如果希望使用多个渲染路径,需要为每一个摄像机设置不同的渲染路径。

LightMode标签支持的渲染路径设置选项

标签名

描述

Always

不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照。

ForwardBase

用于前向渲染。该Pass会计算环境光、最重要的平行光,逐顶点/SH 光源和Lightmaps。

ForwardAdd

用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源。

Deferred

用于延迟渲染。该Pass会渲染G缓冲。

ShadowCaster

把物体的深度信息渲染到阴影映射纹理或一张深度纹理中。

PrepassBase

用于遗留的延迟渲染。该Pass会渲染法线和高光反射的指数部分。

PrepassFinal

用于遗留的延迟渲染。该Pass通过合并纹理、光照和自发光来渲染得到最后的颜色。

Vertex、VertexLMRGBM和VertexLM

用于遗留的顶点照明渲染。

9.2 阴影是如何实现的

在实时渲染路径中,我们最常使用的是一种名为Shadow Map的技术,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。而Unity就是使用的这种技术。

当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback指定的Unity Shader中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到一个LightMode为ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。

屏幕空间的阴影映射技术,原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有的平台Unity都会使用这种技术,因为屏幕空间的阴影映射技术需要显卡支持MRT,而有些移动平台不支持这种特性。

当使用了屏幕空间的阴影映射技术后,Unity首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。

如果需要一个物体接收来自其他物体的阴影,只需要在Shader中对阴阳图进行采样。而由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。

让物体接收阴影

1、在Base Pass中包含一个新的内置文件,#include “AutoLight.cginc”

2、在v2f中添加了一个内置宏SHADOW_COORDS;

struct v2f{
	float4 pos:SV_POSITION;
	float3 worldNormal:TEXCOORD0;
	float3 worldPos:TEXCOORD1;
	SHADOW_COORDS(2) //这个宏的作用:声明一个用于对阴影纹理采样的坐标。需要注意的是,这个宏的参数需要时下一个可用的插值寄存器的索引值。
}

3、然后,我们在顶点着色器返回之前添加另一个内置宏TRANSFER_SHADOW;

v2f vert(a2v v){
	v2f o;
	TRANSFER_SHADOW(O);
	return o;
}

4、接着,在片元着色器中计算阴影值,同样使用一个内置宏 SHADOW_ATTENUATION;

fixed shadow = SHADOW_ATTENUATION(i);

在用以上宏进行计算时,需要注意以下问题

  • TRANSFER_SHADOW会使用v.vertex或a.pos来计算坐标
  • 我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。需要保证a2v结构体的顶点坐标变量名必须时vertex,顶点着色器的输入结构体a2v必须命名为v,且v2f中的顶点位置变量必须命名为pos。

5、最后,我们只需要把阴影值shadow和漫反射以及高光反射颜色相乘即可。

9.3 统一计算光照衰减和阴影

主要是通过内置的UNITY_LIGHT_ATTENUATION宏来实现。

1、首先包含进头文件

#include "Lighting.cginc"
#include "AutoLight.cginc"

2、在v2f结构体中使用内置宏SHADOW_COORDS声明阴影坐标。

struct v2f{
	float4 pos:SV_POSITION;
	float3 worldNormal:TEXCOORD0;
	float3 worldPos:TEXCOORD1;
	SHADOW_COORDS(2)
};

3、在顶点着色器中使用内置宏 TRANSFER_SHADOW计算并向片元着色器传递阴影坐标。

v2f vert(a2v v){
	v2f o;
	TRANSFER_SHADOW(O);
	return o;
}

4、在片元着色器中使用UNITY_LIGHT_ATTENUATION来计算光照衰减和阴影;

fixed4 frag(v2f i):SV_Target{
..
	UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
	..
}

9.4 透明度测试的阴影

在fallback中我们使用VertexLit。但在这个ShadowCaster Pass中不包含透明度测试的计算,所以在阴影显示的效果上回是错误的。

那么我们可以自行定义一个ShadowCaster Pass来计算阴影,或者是通过Unity内置的Shader来减少代码量。

将Fallback设置为 Transparent/Cutout/VertexLit即可。

使用此Fallback需要注意以下 问题

由于此Fallback中计算了透明度测试,使用了名为_Cutoff的属性来进行透明测试,因此这要求我们的Shader中必须提供名为此的属性,否则无法得到正确的阴影效果。

经过以上的设置仍然有可能会出现问题,例如出现一些不应该透过光的部分。这是因为背对光源的面的深度信息没有加入到阴影映射纹理的计算中,我们只需要把Cast Shadows设置为Two Sided即可。

在Unity中,所有内置的半透明Shader是不会产生任何阴影效果的。。

第十章 高级纹理

本章节学习如何使用立方体纹理实现环境映射渲染纹理以及程序纹理

10.1 立方体纹理

立方体纹理是环境映射的一种实现方式。

环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样反射出周围的环境。

10.1.1 天空盒子

第一步:新建一个材质选择Unity自带的Shader(Skybox/6 Sided)

第二部:为该材质选择六张纹理,并将纹理的Wrap Mode设置为Clamp,以防止在接缝处出现不匹配的现象。

10.1.2 创建用于环境映射的立方体纹理

立方体纹理最常见的用处是用于环境映射。而创建用于环境映射的立方体纹理的方法有三种

  • 第一种方法是直接由一些特殊布局的纹理创建。
  • 第二种方法是手动创建一个Cubemap资源,再把六张图赋给它。
  • 第三种是使用脚本生成。

通过脚本来创建立方体纹理是最为灵活方便的(个人认为)

using UnityEngine;
using UnityEditor;
using System.Collections;

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 () {
		// create temporary camera for rendering
		GameObject go = new GameObject( "CubemapCamera");
		go.AddComponent<Camera>();
		// place it on the object
		go.transform.position = renderFromPosition.position;
		// render into cubemap		
		go.GetComponent<Camera>().RenderToCubemap(cubemap);
		
		// destroy temporary camera
		DestroyImmediate( go );
	}
	[MenuItem("GameObject/Render into Cubemap")]
	static void RenderCubemap () {
		ScriptableWizard.DisplayWizard<RenderCubemapWizard>(
			"Render cubemap", "Render!");
	}
}

在使用时,切记将Cubemap立体纹理的Readable勾选上

第十一章 纹理动画

11.1 滚动的背景

Shader "Custom/ScollingBackground"
{
	Properties{
		_MainTex("Base Layer(RGB)",2D) = "white"{}
		_DetailTex("2nd Layer(RGB)",2D) = "white"{}
		_ScrollX("Base layer Scroll Speed",Float) = 1.0//控制主纹理的滚动速度
		_Scroll2X("2nd layer Scroll Speed",Float) = 1.0//控制副纹理的滚动速度
		_Multiplier("Layer Multiplier",Float) = 1//控制纹理的整体亮度
	}
	SubShader{
		Pass{
			Tags{"LightMode" = "ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "Lighting.cginc"
			float _ScrollX;
			float _Scroll2X;
			float _Multiplier;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _DetailTex;
			float4 _DetailTex_ST;
			struct a2v {
				float4 vertex:POSITION;
				float4 texcoord:TEXCOORD0;
			};
			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv:TEXCOORD0;
			};
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0)*_Time.y);
				o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0)*_Time.y);
				return o;
			}
			fixed4 frag(v2f i) :SV_Target{
				fixed4 firstLayer = tex2D(_MainTex,i.uv.xy);
				fixed4 secondLayer = tex2D(_DetailTex,i.uv.zw);
				fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
				c.rgb *= _Multiplier;
				return c;
			}
			ENDCG
		}
	}
}

本文章内容均借鉴于《Unity Shader入门精要》冯乐乐著


  1. 发起方CPU,接收方GPU。这个命令仅仅会指向一个需要被渲染的图元列表,而不会包含任何材质信息。 ↩︎
  2. 将不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理。 ↩︎
  3. 描述齐次裁剪空间。 ↩︎