【Shader入门精要】第九章——更复杂的光照

Shader入门精要项目链接:

https://github.com/candycat1992/Unity_Shaders_Book

一、Unity的渲染路径

前向渲染路径(Forward Rendering Path)

延迟渲染路径(Deferred Rendering Path)

顶点照明渲染路径(Vertex Lit Rendering Path)

在Unity中,渲染路径决定了光照是如何应用到Unity Shader中的。目前Unity5.0版本以后,已经抛弃了顶点照明渲染路径。在Unity2017中,Edit->Project Settings->Graphics打开Graphics Settings面板在Tier Settings中有三个等级分别是Low, Medium, High,其中里面有一个叫Rendering Path的参数就是所谓的“渲染路径”,默认值为Forward

上图配置的渲染路径是针对整个项目的,如果我们想针对局部的话,可以去改摄像机组件身上的渲染路径

Use Graphics Settings(使用Graphics Settings面板上配置的渲染路径)、Forward(前向渲染路径)、Deferred(延迟渲染路径),后面2个可以自行百度,书籍并未详细讲解。

在每一种渲染路径,Unity都会传递一些必要的光源和光照信息给Shader着色器进行处理,至于怎么处理是我们的事情。

1.1 前向渲染路径(Forward Rendering Path)

在第九章之前的所有写过的Shader代码,全都是在这个前向渲染路径下进行的!在前向渲染路径下,我们需要在Shader中写如下代码,Unity才会给我们传递相关的光源和光照信息。

Pass{    
    //使用前向渲染路径的ForwardBase路径(告诉Unity用ForwardBase路径的光源和光照信息给我)
    Tags{"LightMode"="ForwardBase"}
    CGPROGRAM
    #include "Lighting.cginc" //光源和光照相关的内置变量、函数
    ENDCG
}

如果你没有写上ForwardBase那行代码,Unity就不知道你想要的是前向渲染路径中的具体哪个路径(前向渲染路径下还有一个叫"ForwardAdd"的家伙),Unity就会傻傻地传一个和顶点照明渲染路径等同的数据给你,那么你需要的一些光照变量就很可能不会被正确赋值。

前向渲染路径是我们最常用的一种路径,下面是它的原理:

Pass{
    //遍历模型三角面
    for(each primitive in this model)
    {
        //遍历三角面片元
        for(each fragment covered by this primitive){
            //深度测试失败
            if(failed in depth test){
                //抛弃片元,不会进行渲染它
                discard;
            }else{
                //深度测试成功,进行渲染它(Shading可理解是进入了我们的着色器代码,输出的是片元着色器的颜色值)
                float4 color = Shading(materialInfo, pos, normal ,lightDir, viewDir);
                //将这个颜色,更新到帧缓冲的该片元位置上
                writeFrameBuffer(fragment, color);
            }
        }
    }
}

简单来说,一个Pass处理了模型的所有片元的渲染,其中“深度测试”是比较深度缓冲区和当前片元的深度值,如果当前片元深度值小于等于深度缓冲区的颜色值,那么就算通过测试,当前片元的信息、光源、光照等信息都会进入我们的Shader着色器代码,经过着色器处理后得到一个最终片元颜色值,写入到颜色缓冲区中,等待被绘制到屏幕上,最终我们看到屏幕的亮点。

在前向渲染路径中,我们有三种照亮物体的方式:

①在顶点着色器进行处理光照(漫反射、高光反射),被称为“逐顶点处理”

②在片元着色器进行处理光照,被称为“逐像素处理”

③球谐函数(Spherical Harmonics,SH)处理【目前还未清楚】

场景最亮的平行光会按逐像素处理

光源的Light组件的渲染模式(Render Mode)设置为Not Important的光源会按逐顶点处理或SH处理

渲染模式被设置成Important的光源按逐像素处理

若经过上面三大规则后得到的逐像素光源数目小于Quality Setting面板中的逐像素光源上限(Pixel Light Count)会补充一些逐像素光源,即将逐顶点或SH处理的光源强制转成逐像素处理。

Base Pass:前向渲染路径下的指明Tags标签LightMode为ForwardBase的一个Pass,它需要搭配

#pragma multi_compile_fwdbase编译指令才能够正确地为我们提供光照参数,它会为我们去判断是否使用光照贴图、当前处理哪种光照类型、是否开启了阴影,并且把相关的内置变量传递到Shader中(简单来说,就是为我们做了很多复杂的操作)。只有使用它,我们才能知道,光照衰减值等信息。

Additional Pass:指明LightMode为ForwardAdd的一个Pass,它也需要搭配编译指令:

#pragma multi_compile_fwdadd

区别:

Base Pass能接收到一个逐像素的平行光和所有逐顶点和SH光源,Addtional Pass能接收到逐像素的光源,且每一个光源执行一次Pass。注意:逐像素的平行光是只有一个的!所以Base Pass只会执行一次逐像素处理!其他光源进入Base Pass只会在顶点着色器进行处理。而Addtional Pass是可以接收所有逐像素光源的,也就是场上如果有N个光源作用于一个物体,物体就会为每一个光源都执行一遍Additional Pass,每一个光源都会进入片元着色器进行处理输出颜色值,为此我们需要为Addtional Pass开启混合,一般是Blend One One,最终一个物体就会在一帧执行N次Pass,可见这种Additional Pass在场景有多个逐像素光源时是要牺牲一定性能的。

前向渲染可以使用的内置光照变量
名称类型描述
_LightColor0float4 该Pass处理的逐像素光源的颜色
_WorldSpaceLightPos0float4该Pass处理的逐像素光源的位置,若是平行光,w是0,否则为1
_LightMatrix0float4x4从世界空间转光源空间的变换矩阵,可用于采样cookie和光强衰减(attenuation)纹理
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0float4仅用于Base Pass,前4个非重要的点光源在世界空间中的位置
unity_4LightAtten0float4 仅用于Base Pass, 存储了前4个非重要的点光源的衰减因子
unity_LightColorhalf4[4]仅用于Base Pass,存储了前4个非重要的点光源的颜色

1.2 延迟渲染路径(Deferred Rendering Path)

延迟渲染路径,正如其名就是延迟了渲染,延迟到第二个Pass进行,第一个Pass会计算第二个Pass所需的数据(如表面法线、位置、用于光照计算的材质属性等)缓存到一个叫G缓冲(G-buffer)的地方,而前向渲染是只使用了颜色缓冲区和深度缓冲区。

优点:每一个影响物体的光源都会用逐像素处理,且只会在一次Pass中完成!会将这些计算结果缓存到G缓冲区。

缺点:不能处理半透明物体!不支持真正的抗锯齿功能,对显卡有一定要求,显卡必须支持MRT、Shader Model 3.0及以上、深度渲染纹理以及双面的模板缓冲。

当使用延迟渲染时,我们必须要提供两个Pass,第一个用于渲染G缓冲,计算物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中,可理解G缓冲区就是保存了不同的纹理,纹理上的数值都是屏幕空间下的,对每一个物体来说,这个Pass只会执行一次!

第二个用于计算光照模型,会根据G缓冲区中的数据来进行计算。

G缓冲区默认有如下几个渲染纹理(Render Texture, RT)

RT0:(ARGB32格式) 漫反射颜色纹理(A通道没被使用)

RT1:(ARGB32格式)高光反射颜色纹理,A通道存储高光反射的指数

RT2:(ARGB2101010格式)法线纹理(A通道没被使用)

RT3:(ARGB32非HDR)或(ARGBHalf HDR),自发光+lightmap+反射探针

深度缓冲和模板缓存

可访问的内置变量和函数:在UnityDeferredLibrary.cginc可找到:

_LightColor float4 光源颜色、 _LightMatrix0 float4x4 从世界到光源空间的变换矩阵,可采样cookie和光强衰减纹理。

光源类型:平行光、点光源、聚光灯、面光源(面光源仅在烘焙时才发生作用)

1.3 前向渲染中处理不同的光源类型

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

Shader "MilkShader/Nine/ForwardRendering"
{
	Properties
	{
		_Specular("Specular", Color) = (1,1,1,1)
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(8.0, 256)) = 20				
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
		//第一个Pass进行处理环境光、自发光参与的光照模型
		Pass
		{
			Tags{ "LightMode" = "ForwardBase" }			
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdbase
						
			#include "UnityCG.cginc"
			#include "Lighting.cginc"

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

			struct v2f
			{		
				float4 vertex : SV_POSITION;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
			};

			fixed4 _Specular;
			fixed4 _Diffuse;
			float _Gloss;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;		
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{						
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + worldViewDir);
				fixed3 atten = 1.0;//衰减因子为1.0 因为平行光是没有衰减概念的
				//下面有_Diffuse和_Specular自发光(材质颜色)和ambient环境光参与
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				return fixed4(ambient + (specular + diffuse) * atten, 1.0);//衰减因子影响的是两个反射的颜色值
			}
			ENDCG
		}
		//第二个Pass处理的是平行光和其他光源,不考虑环境光和自发光,但要考虑衰减
		Pass{

			Tags{ "LightMode" = "ForwardAdd" }

			Blend One One

			CGPROGRAM

			#pragma multi_compile_fwdadd

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

			#pragma vertex vert
			#pragma fragment frag

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

			struct v2f
			{	
				float4 vertex : SV_POSITION;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
			};
	
			float _Gloss;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;		
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{						
				fixed3 worldNormal = normalize(i.worldNormal);
				
				//如果定义了USING_DIRECTIONAL_LIGHT,则表示当前片元着色器是在处理的是平行光,否则是其他光源
				#ifdef USING_DIRECTIONAL_LIGHT	
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);//平行光没有位置概念
				#else
					//其他光源时,是要用当前顶点(世界)->光源点的向量作为光源向量
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
				#endif			
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + worldViewDir);
				#ifdef USING_DIRECTIONAL_LIGHT
					fixed atten = 1.0;//平行光没有位置概念,因此没有衰减
				#else			
					//将世界空间顶点转换到光源空间下
					float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
					//再通过(光源到该光源空间下的顶点坐标的距离的平方值)作为纹理坐标x,y分量值再用这个纹理坐标进行从_LightTexture0采样衰减值
					fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
				#endif
				fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal,worldLightDir));
				fixed3 specular = _LightColor0.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);				
				return fixed4((specular + diffuse) * atten, 1.0);
			}
			ENDCG
		}
	}
	//使用内置Shader"Specular" 它会帮我们进行计算阴影部分
	FallBack "Specular"
}

上面代码处理了漫反射,第一个Pass和正常处理一样,关键部分在于第二个Pass处理了其他光源(非平行光)的计算光照。简单来说就是,使用了一个宏:USING_DIRECTIONAL_LIGHT来判断是否为平行光,如果不是进入else逻辑才是处理其他光源,此时光照向量已经不同了,因为平行光是没有位置概念,因此光照向量就是等于_WorldSpaceLightPos0.xyz,但是在其他光源例如点光源,需要通过_WorldSpaceLightPos0.xyz - worldPos.xyz得出光照向量。而光照衰减因子在平行光下是等于1的,因为平行光是没有衰减概念的,而在非平行光下会从Unity计算好的光照衰减纹理_LightTexture0中采样,采样的纹理坐标为

float2(光源到光源空间的顶点距离的平方,光源到光源空间的顶点距离的平方)

这个纹理实际上正常来说,最靠近光源的顶点,衰减值越接近1,越远则越接近0,光源衰减纹理上每一个点取值在[0,1],左下角(0,0)为1,呈白色,右上角(1,1)为0,呈黑色。当然,我这个只是概念化的,实际上光照衰减纹理是嵌入了其他纹理的,你可以发现,经过tex2D采样后,返回的是一个fixed4,实际上衰减值是取fixed4其中一个分量,即UNITY_ATTEN_CHANNEL

fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;

然后再用衰减值与漫反射颜色值和高光反射颜色值相乘就完事了。(如果还有其他反射颜色,则同理操作)

关于光照衰减更多知识点,不在本文介绍。

二、3D物体的阴影(Unity的阴影)

Shader从阴影图根据阴影纹理坐标采样阴影值,再使用该阴影值对输出颜色进行相乘,最终表现出阴影效果。

阴影图是一张二维纹理,存储着场景上物体的阴影部分。

阴影图有如下两种方法生成:

①传统方法:Unity首先把摄像机放到光源位置上,调用LightMode为ShadowCaster的Pass来进行渲染出光源空间下的阴影映射纹理(实际上是一个表面深度纹理),之后在正常渲染的Pass中将顶点位置变换到光源空间下,将这个光源空间下的顶点坐标xy分量作为纹理坐标xy分量从光源空间下的阴影映射纹理采样出深度值,再与该Pass处理的顶点或片元深度值进行比较,如果取出来的深度值比自身深度值更小,说明当前处理的顶点或片元处于阴影中,至于阴影值是多少,书上没解释明白,我也没弄明白,具体还要看源码看它是怎么的算法。(问题1)

②屏幕空间的阴影映射技术:Unity5及之后,需显卡支持MRT。也是在LightMode为ShadowCaster的Pass中进行渲染出屏幕空间下的阴影投射纹理,同时Unity会计算出摄像机的表面深度纹理,同一纹理坐标上,若阴影映射纹理的深度值比摄像机的表面深度纹理的深度值更小,说明该Pass处理的顶点或片元处于阴影中,将该纹理坐标上的阴影映射纹理的深度值赋值到阴影图相应坐标上。所以摄像机的表面深度纹理实际上是从阴影映射纹理中选出那些真正的阴影部分,成为所谓的“阴影图”。最终,我们在正常的Pass中,通过将顶点坐标转屏幕空间下,然后根据这个坐标来从阴影图采样阴影值,之后你都懂了。

在总结到这里,不妨猜想一下,问题1中的阴影值是多少?应该是阴影映射纹理的深度值,因为实际上无论①和②真正的阴影都是从这个阴影映射纹理而来的,②只不过是多了一个过滤,①的情况有点复杂,因为我们看的视角是屏幕空间的,而阴影映射纹理是光源空间的,假设光源和摄像机是在同一个位置那还好理解,如果是不同位置,就有点难以理解。

值得庆幸的是Unity为我们提供了接口,即阴影“三剑客”

Shader "MilkShader/Nine/Shadow"
{
	Properties
	{
		_Specular("Specular", Color) = (1,1,1,1)
		_Diffuse("Diffuse", Color) = (1,1,1,1)
		_Gloss("Gloss", Range(8.0, 256)) = 20				
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			Tags{ "LightMode" = "ForwardBase" }			
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdbase
						
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
                        //为了使用三剑客
			#include "AutoLight.cginc"

			struct appdata
			{
                                //POSITION语义修饰的变量名必须是"vertex"(三剑客要求)
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			struct v2f
			{		
                        //SV_POSITION语义:裁剪空间下的顶点坐标 变量名必须为"pos"(三剑客要求)
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
                        //三剑客之一:定义阴影纹理坐标 传入2代表:TEXCOORD2语义修饰,前面0和1已被使用,就要顺着写2,如果前面有用了0,1,2,那你就要传3
				SHADOW_COORDS(2)
			};

			fixed4 _Specular;
			fixed4 _Diffuse;
			float _Gloss;
			
			v2f vert (appdata v)//顶点着色器的输入结构变量名必须是"v"(三剑客要求)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;		
                        //三剑客之一:计算阴影纹理坐标并存入上方定义的阴影纹理坐标,需传入v2f对象
				TRANSFER_SHADOW(o);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{						
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + worldViewDir);
				fixed3 atten = 1.0;
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                        //三剑客之一:根据阴影纹理坐标从阴影图采样阴影值
				fixed shadow = SHADOW_ATTENUATION(i);
                        //最后作用于specular和diffuse上(与衰减值一样的用法)
				return fixed4(ambient + (specular + diffuse) * atten * shadow, 1.0);
			}
			ENDCG
		}
		Pass{

			Tags{ "LightMode" = "ForwardAdd" }

			Blend One One

			CGPROGRAM

			#pragma multi_compile_fwdadd

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

			#pragma vertex vert
			#pragma fragment frag

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

			struct v2f
			{	
				float4 vertex : SV_POSITION;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
			};
	
			float _Gloss;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;		
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{						
				fixed3 worldNormal = normalize(i.worldNormal);
				#ifdef USING_DIRECTIONAL_LIGHT	
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				#else
					fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
				#endif			
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + worldViewDir);
				#ifdef USING_DIRECTIONAL_LIGHT
					fixed atten = 1.0;
				#else			
					float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
					fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
				#endif			
				fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal,worldLightDir));
				fixed3 specular = _LightColor0.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);				
				return fixed4((specular + diffuse) * atten, 1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

上方Shader是实现了漫反射+高光反射+衰减+阴影效果,其中Base Pass部分用到了阴影,你也可以在Additional Pass使用阴影三剑客处理阴影。

1. 需要一个内置文件才能使用阴影三剑客

#include "AutoLight.cginc"

2.三剑客分别为:

第一个剑客:用于在顶点着色器输出结构体/片元着色器输入结构体v2f中,定义阴影纹理坐标

struct v2f{
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    //定义阴影纹理坐标
    SHADOW_COORDS(2)   
};

//解释部分
SHADOW_COORDS是一个宏,如下所示:
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
所以,SHADOW_COORD(2) 等同于 unityShadowCoord4 _ShadowCoord : TEXCOORD2;

第二个剑客:用于在顶点着色器中根据视情况而定使用相应空间下的顶点坐标计算出阴影纹理坐标并存入v2f中的阴影纹理坐标寄存器。

v2f vert(appdata v){
    v2f o;
    //...
    //计算出阴影纹理坐标并存入v2f的阴影纹理坐标中
    TRANSFER_SHADOW(o)
    return o;
}

//宏详细部分解释:
//若显卡不支持,屏幕空间的阴影映射技术
#if define(UNITY_NO_SCREENSPACE_SHADOWS)    
    //阴影纹理坐标使用光源空间下的顶点坐标
    //其中, v.vertex 是 appdata的v,也就是模型空间下的顶点坐标
    //利用mul(_Object2World, v.vertex)将模型空间下顶点坐标转换到世界空间下
    //再用unity_World2Shadow[0]矩阵将世界空间下的坐标转到光源空间下
    #define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex) );
    //...
#else
    //如果显卡支持屏幕空间的阴影映射技术
    //那么可通过ComputeScreenPos(裁剪空间下的顶点坐标)来获得屏幕空间下的顶点坐标作为阴影纹理坐标
    #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
#endif
从上方宏定义来看,为了满足TRANSFER_SHADOW宏,顶点着色器vert的输入结构体变量名必须是"v"
即v2f vert(appdata v){ ... }
而v2f结构体自身内部的SV_POSITION语义修饰的裁剪空间下的顶点坐标变量名必须是"pos"

可能有人不理解,宏到底是什么,上方TRANSFER_SHADOW就是一个宏,在代码里可直接用宏右方的代码块替换宏,所以
v2f vert(appdata v){
    v2f o;
    //...
    //计算出阴影纹理坐标并存入v2f的阴影纹理坐标中
    TRANSFER_SHADOW(o)
    return o;
}

可以理解为如下代码

v2f vert(appdata v){
    v2f o;
    //...
    //计算出阴影纹理坐标并存入v2f的阴影纹理坐标中
    //支持屏幕空间的阴影映射技术时,TRANSFER_SHADOW(o)变为如下代码
    o._ShadowCoord = ComputeScreenPos(o.pos);
    
    //不支持时,变为如下
    //o._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex) );
    return o;
}

第三个剑客:根据上面计算出的阴影纹理坐标从阴影图采样阴影值。

fixed4 frag(v2f i) : SV_Target {
    //...
    fixed shadow = SHADOW_ATTENUATION(i)
    //...
}

SHADOW_ATTENUATION宏定义如下:
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

在支持屏幕空间的阴影映射技术时
inline fixed unitySampleShadow(unityShadowCoord4 shadowCoord)
{
    fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord)).r;
    return shadow;
}

不支持时书籍上并未给出,后期补充(也可自行去看AutoLight.cginc内置文件)
上方所有宏的定义都在AutoLight.cginc文件!可能版本不同和我所说的也不同了

三、统一管理光照衰减和阴影

在上方使用了三剑客的Shader代码中,我们也处理了光照衰减,但是我们的光照衰减处理有点繁琐,即我们要在Addtional Pass中根据
#ifdef USING_DIRECTIONAL_LIGHT
    //平行光计算光照衰减值,直接就是1
#else 
    //其他光源计算光照衰减值,就要计算出光源与点的距离的平方,根据这个作为纹理坐标从光照衰减纹理取值
#endif

现在可以直接使用

fixed4 frag(v2f i) : SV_Target {
    //...
    //UNITY_LIGHT_ATTENUATION宏(参数1:衰减和阴影的乘积变量, 参数2:i(v2f), 参数3: i.worldPos世界空间下的顶点)
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    //*注意:atten是在宏内部计算好会返回出来的,而且不用你定义atten,因为它的定义也是在宏里面进行的。
    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
*注意:UNITY_LIGHT_ATTENUATION只是做了三剑客的SHADOW_ATTENUATION的工作,而且也做了计算衰减值。
三剑客的前两个 SHADOW_COORDS 和 TRANSFER_SHADOW 还是需要进行的!

个人建议:v2f结构体的世界空间下的顶点坐标变量起名为"worldPos",并且参数1:也是起名为"atten" ,因为我还未看具体宏,后期补充。

三剑客:TRANSFER_SHADOW是最令人烦恼的,因为它约束太多了,我们可以自己改写一下这个宏,来减少那些死要求,后续补充。

四、相关代码

4.1 透明度测试 + 漫反射 + 衰减和阴影 (开启双面渲染 即Mesh Renderer组件的Cast Shadow为Two Sided)

Shader "MilkShader/Nine/AlphaTestShadow"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5
		_Color ("Color", Color) = (1,1,1,1)
	}
	SubShader
	{
		Tags { "RenderType"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
		LOD 100

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

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				float3 normal : NORMAL;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;				
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
				SHADOW_COORDS(3)
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Color;
			fixed _Cutoff;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldNormal = UnityObjectToWorldNormal(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);				
				TRANSFER_SHADOW(o);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{								
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed4 texColor = tex2D(_MainTex, i.uv);
				clip(texColor.a - _Cutoff);
				fixed3 albeto = texColor.rgb * _Color.rgb;
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albeto;
				fixed3 diffuse = _LightColor0.rgb * albeto * saturate(dot(worldNormal, lightDir));			
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);//计算阴影*衰减存入atten
				return fixed4(ambient + diffuse * atten, 1.0);
			}
			ENDCG
		}
	}
    //FallBack指定的内置Shader中有LightMode=ShadowCaster的Pass
//帮我们处理了阴影映射纹理(没有它就没有阴影!),并且由于透明度测试要利用clip函数剔除,
//只有在这个内置Shader才会在ShadowCaster的Pass进行处理了透明度测试clip,
//如果你改为VertexLit那么效果就不对了
	FallBack "Transparent/Cutout/VertexLit"
}

注意:ShadowCaster Pass中的透明度测试,也是用_Cutoff属性进行的clip操作,所以你定义的变量必须起名为"_Cutoff"作为裁剪度,只有这样才能保证效果正确。

假如我将FallBack改成VertexLit,虽然它也有ShadowCaster的Pass,但是该ShadowCaster Pass它并不会处理clip。

Unity不支持采用透明度混合的Shader生成阴影,所有内置的透明度混合的Unity Shader,如Transparent/VertexLit等,它就没有包含阴影投射的Pass(即ShadowCaster的Pass),所以不会有阴影,这个ShadowCaster的Pass就是帮我们生成阴影图的,没有它,你即使使用了三剑客也是没效果的,废话不多说实测见真章!

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

Shader "MilkShader/Nine/AlphaBlendShadow"
{
	Properties
	{
		_MainTex ("MainTexture", 2D) = "while"{}
		_Diffuse ("Diffuse", Color) = (1,1,1,1)		
		_Alpha ("Alpha", Range(0,1))  = 1
	}
	SubShader
	{
		Tags { "RenderType"="Transparent" "IgnoreProjector" = "True" "Queue"="Transparent" }
		LOD 100
		
		Pass
		{
			Tags
			{
				"LightMode" = "ForwardBase"
			}
			Blend SrcAlpha OneMinusSrcAlpha
			ZWrite Off
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"

			struct appdata
			{
				float4 vertex : POSITION;			
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{			
				float2 uv : TEXCOORD0;
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;			
				SHADOW_COORDS(3)
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Diffuse;
			fixed _Alpha;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.pos = UnityObjectToClipPos(v.vertex);				
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				TRANSFER_SHADOW(o)
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{				
				fixed4 col = tex2D(_MainTex, i.uv);
				fixed3 albeto = col.rgb * _Diffuse.rgb;
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 diffuse = _LightColor0.rgb * albeto * saturate(dot(worldNormal,worldLightDir));
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albeto;
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				return fixed4(ambient + diffuse * atten, col.a * _Alpha);
			}
			ENDCG
		}
	}
	//正常使用的是如下,不会生成阴影!
	//FallBack "Transparent/VertexLit"

	//开启阴影投射(因为VertexLit中有"LightMode" = "ShadowCaster"的Pass生成阴影图
	FallBack "VertexLit"
}

(注意物体身上是没有接收到阴影的!)

将最后的FallBack改为如下

在《Shader入门精要》208页中看到的效果是在非Transparent渲染队列中的,即物体不仅可以投射阴影,还可以接收阴影。

我们需要将代码中的

	Tags { "RenderType"="Transparent" "IgnoreProjector" = "True" "Queue"="Transparent" }

改为

	Tags { "RenderType"="Transparent" "IgnoreProjector" = "True" "Queue"="AlphaTest" }

主要是Queue渲染队列,要改成非Transparent的!

 

或者 去修改(这里选中Geometry或AlphaTest都可以)

(注意:物体接收到阴影了,而且整体比上面的变暗了)

至此第九章,更高级的光照篇结束,东西还是比较多的,建议慢慢理解,反复去写代码。

要理解shader入门精要,首先需要了解shader是指令代码,需要关联材质才能赋予游戏对象以特定的效果。材质按照关联的shader的规则,处理贴图等输入信息,达到特定的画面效果。 在Unity中,有几种类型的shader可供选择。一种是表面着色器(Surface Shader),它是对顶点/片断着色器的封装,符合人类的思维模式,可以以极少的代码来实现不同的光照模型和不同平台下的需求。在表面着色器的开发中,我们直接在Subshader层次上编写代码,系统会将代码编译成适当的Pass。而顶点/片断着色器是基础的shader类型,能够实现多的效果,但表面着色器不一定能实现这些效果。还有一种被淘汰的固定管线着色器(Fixed Function Shaders),它在硬件上执行基本的命令,速度很快,但功能有限,不再推荐使用。 不同图形API都有各自的shader语言。在DirectX中,顶点shader叫做Vertex Shader,像素shader叫做Pixel Shader。而在OpenGL中,顶点shader也叫做Vertex Shader,但像素shader叫做Fragment Shader或者片断Shader。这些shader语言有不同的语法和特性,需要根据使用的图形API选择适当的语言来编写shader。 总结起来,要入门shader,首先需要了解shader是指令代码,需要关联材质才能实现效果。在Unity中,可以选择使用表面着色器、顶点/片断着色器或固定管线着色器来实现不同的效果。此外,不同图形API有不同的shader语言,需要根据使用的API选择合适的语言来编写shader
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值