UnityShader入门精要-阴影

Posted hippodu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UnityShader入门精要-阴影相关的知识,希望对你有一定的参考价值。

Unity的阴影

        一条光线遇到一个不透明物体就不可以继续照亮其他物体,因此这个物体就会向该方向的物体投射阴影。实时渲染中使用一种ShadowMap技术,首先把相机的位置放置光源的位置,阴影区域就是摄像机看不到的地方。unity会为光源计算其阴影映射纹理,本质上是一张深度图,用一个额外的pass专门更新映射纹理而非在bass/additional中完成,LightMode为”ShadowCaster“,找到一个这样的pass才可以投射阴影。

屏幕空间的阴影映射技术

        延迟渲染中的方法,需要显卡支持MRT。

流程:首先通过调用lightmode为shadowcaster的pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理,然后根据这两个纹理得到屏幕空间的阴影图。如果深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,说明可见但在阴影中,即得到阴影图。阴影图再屏幕空间中,所以把表面坐标变换到屏幕坐标中再使用这个坐标对阴影图采样即可。

一个物体接收来自其他物体的阴影,和 它向其他物体投射阴影是两个过程。

 ①想要接收阴影,就要再shader中对阴影映射纹理采样,将结果与最后的光照结果相乘。

②投射纹理,需要将其添加到光源的阴影映射纹理的计算中,让其他物体对该纹理的采样时可以得到它的信息。

不透明物体的阴影

        先前写的shader赋给材质,即使没有一个pass的lightmode为shadowcaster,但是cube依然向平面投射了阴影,这是因为fallback了specular,specular本身fallback了vertexLit,内置的着色器中对其完成了实现。使物体完成阴影的投射。

让物体接收阴影:

shadow_coords(2)宏的作用是,声明一个用于对阴影纹理采样的坐标。

 transfer_shadow宏用于在顶点着色器中计算上一步声明的阴影纹理坐标。

最后shadow_attenuation用于计算阴影值,并将结果乘进光照结果。

上面三个宏在AutoLight.cginc中可以找到声明。结果效果如下:

        要注意的是,这些宏会使用上下文变量进行相关计算,所以需要使自定义的变量名与这些宏中使用的变量名一致,如a2f中顶点坐标变量必须交vertex,vert输出必须为v2f且命名为v,顶点位置变量需要叫pos。 

统一管理光照衰减和阴影

这个宏接受三个参数,第一个参数atten无需声明,该宏会帮我们声明,v2f i 传入用于计算阴影值,最终会得到atten为光照衰减和阴影的乘积。

UNITY_LIGHT_AATENUATION(atten,i,i.worldPos);
//i --> v2f i ,为frag的输入变量。

透明度物体的阴影 

        前面投射阴影使用的fallback中的vertexlit中提供的shadowcaster的实现往往不能实现透明物体的阴影,透明度测试中需要舍弃某些片元。因此我们在之前透明度测试的shader中,加入关于阴影的计算,计算和上面没什么区别,要注意的是要修改fallback为vertexlit。 

 

 如果出现不应该透过光的部分,是因为默认情况下把物体渲染到深度图和阴影映射纹理中仅考虑正面,由于有一些面完全背对光源,他们的深度信息并没有被加入阴影映射纹理的计算中,可以将正方体meshrenderer中cast shadows 设置为two sided,强制unity计算所有面的信息。

透明度混合 

当仅对透明度混合的shader加入阴影计算的部分的话,半透明物体并不会向下方平面投射阴影,也不会接受右侧平面的阴影,看起来像完全透明一样,unity这样为了优化性能,可以强制为半透明物体生成音乐,通过把他们的fallback设置为vertexlit、diffuse等不透明物体用的,就可以找到阴影投射的pass,并且在mesh renderer组件上cast shadows 和receive shadows 选择是否投射或接收。

一个标准的光照着色器:

Shader "Unity Shaders Book/Common/Bumped Specular" 
	Properties 
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" 
		_BumpMap ("Normal Map", 2D) = "bump" 
		_Specular ("Specular Color", Color) = (1, 1, 1, 1)
		_Gloss ("Gloss", Range(8.0, 256)) = 20
	
	SubShader 
		Tags  "RenderType"="Opaque" "Queue"="Geometry"
		
		Pass  
			Tags  "LightMode"="ForwardBase" 
		
			CGPROGRAM
			
			#pragma multi_compile_fwdbase	
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float4 _BumpMap_ST;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v 
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			;
			
			struct v2f 
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
				float4 TtoW0 : TEXCOORD1;  
                float4 TtoW1 : TEXCOORD2;  
                float4 TtoW2 : TEXCOORD3; 
				SHADOW_COORDS(4)
			;
			
			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;

				TANGENT_SPACE_ROTATION;
				
				float3 worldPos = mul(_Object2World, v.vertex).xyz;  
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.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);  
  				
  				TRANSFER_SHADOW(o);
			 	
			 	return o;
			
			
			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 = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));

				fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
			 	fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
			 	
			 	fixed3 halfDir = normalize(lightDir + viewDir);
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
			
				UNITY_LIGHT_ATTENUATION(atten, i, worldPos);

				return fixed4(ambient + (diffuse + specular) * atten, 1.0);
			
			
			ENDCG
		
		
		Pass  
			Tags  "LightMode"="ForwardAdd" 
			
			Blend One One
		
			CGPROGRAM
			
			#pragma multi_compile_fwdadd
			// Use the line below to add shadows for point and spot lights
//			#pragma multi_compile_fwdadd_fullshadows
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"
			#include "AutoLight.cginc"
			
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _BumpMap;
			float4 _BumpMap_ST;
			float _BumpScale;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v 
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT;
				float4 texcoord : TEXCOORD0;
			;
			
			struct v2f 
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
				float4 TtoW0 : TEXCOORD1;  
                float4 TtoW1 : TEXCOORD2;  
                float4 TtoW2 : TEXCOORD3;
				SHADOW_COORDS(4)
			;
			
			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(worldNormal, worldTangent) * v.tangent.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);  
			 	
			 	TRANSFER_SHADOW(o);
			 	
			 	return o;
			
			
			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 = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
				
				fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
				
			 	fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
			 	
			 	fixed3 halfDir = normalize(lightDir + viewDir);
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
			
				UNITY_LIGHT_ATTENUATION(atten, i, worldPos);

				return fixed4((diffuse + specular) * atten, 1.0);
			
			
			ENDCG
		
	 
	FallBack "Specular"

 

UnityShader入门精要-3.5 UnityShader的形式

UnityShader可以做的事情非常多(例如设置渲染状态等),但是其最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语义块中(表面着色器的做法),也可以写在Pass语义块中(定点/片元着色器和固定函数着色器的做法)。

  在Unity中,我们可以使用下面3中形式来编写UnityShader。而不管使用哪种形式,真正意义上的Shader代码都需要包含在ShaderLab语义块中,如下所示:

Shader "MyShader"{
  Properties{
    //所需的各种属性
  }
  SubShader{
    //真正意义上的Shader代码会出现在这里
    //表面着色器(Surface Shader)或者
    //定点/片元着色器(Vertex/Fragment Shader)或者
    // 固定函数着色器 (Fixed Function Shader)
  }
  SubShader{
     //和上一个SubShader类似
  }
}
  1. 表面着色器

表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型。它需要的代码量很小,Unity在背后做了很多工作,但渲染的代价比较大。它在本质上和下面要讲到的定点/片元着色器是一样的。也就是说,当Unity提供一个表面着色器的时候,它在背后仍旧把它转换成对应的顶点/片元着色器。我们可以理解成,表面着色器是Unity对顶点/片元着色器的更高一层的抽象。它存在的价值在于,Unity为我们处理了很多光照细节,使得我们不需要操心这些“烦人的事情”。

一个非常简单的表面着色器的示例代码如下:

Shader "Custom/Simple Surface Shader"{
    SubShader{
        Tags{ "RenderType" = "Opaque"}
        CGPROGRAM
            #prama surface surf Lambert
            struct Input{
                float4 color : COLOR;
            };
            void surf (Input IN, inout SurfaceOutput o){
                o.Albedo =1;
            }
        ENDCG
    }
    FallBack "Diffuse"
}

从上述程序中可以看出,表面着色器被定义在SubShader语义块(而非Pass语义块)中的CGPROGAM和ENDCG之间。原因是,表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情,我们只要告诉他:“嘿!使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型,其他的事情不要来烦我!”。

CGPRORAM和ENDCG之间的代码使用CG/HLSL编写的,也就是说,我们需要把CG/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的CG/HLSL是Unity经封装后提供的,他的语法和标准的CG/HLSL语法几乎一样,但还是有细微的不同,例如有些原生的函数和用法Unity并没有提供支持。

  2。最聪明的孩子:定点/片元着色器

    在Unity中我们可以使用CG/HLSL语言来编写 顶点/片元着色器(Verter/Fragment Shader)。它们更加复杂,但灵活性也更高。

  一个非常简单的顶点/片元着色器示例代码如下:

Shader "Custom/Simple VertexFragment Shader"{
    SubShader{
        Pass{
            CGPROGRAM
                #prama vertex vert
                #prama fragment frag
                float4 vert (float4 v : POSITION) : SV_POSITION{
                    return mul (UNITY_MATRIX_MVP ,  v);
                }

                float4 frag ( ) : SV_Target{
                    return fixed4 (1.0 , 0.0 , 0.0 , 1.0);
                }
            ENDCG
        }
        
    }
}

  和表面着色器类似,顶点/片元着色器的代码也需要定义在CGPROGRAM和ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的,原因是,我们需要自已定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高,更重要的是,我们可以控制渲染的实现细节,同样这里的CGPROGRAM和ENDCG之间的代码也是使用CG/HLSL编写的。

3. 被抛弃的角落:固定函数着色器

 上面两种Unity Shader 形式都使用了可编程管线。而对于一些较旧的设备(其CPU仅支持DirectX7.0、OpenGL 1.5 或 OpenGL ES 1.1),例如iPhone3,他们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器(Fixed Function Shader)来完成渲染。这些着色器往往可以完成一些非常简单的效果。

一个非常简单的固定函数着色器示例代码如下:

Shader "Tutorial/Basic"{
    Properties{
        _Color("Main Color",Color) = (1, 0.5, 0.5 , 1)
    }
    SubShader{
        Pass{
            Material {
                Diffuse [ _Color]
            }
            Lighting On
        }
    }
}

可以看出,固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置,正如我们之前提到的一样。

 对于固定函数着色器来说,我们需要完全使用ShaderLab的语法(即 使用ShaderLab的渲染设置命令) 来编写,而非使用 CG/HLSL。

 由于现在大多数GPU都支持可编程的渲染管线,这种固定管线的编程方式已经逐渐被抛弃。实际上,在Unity5.2中,所有固定函数着色器都会在背后被Unity编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了。

 

对于选择Shader  的一些建议。

  • 除非你有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行你的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表明着色器或顶点/片元着色器。
  • 如果你想和各种光源打交道,你可能更喜欢使用表明着色器,但需要小心他在移动平台的性能表现。
  • 如果你需要使用的光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
  • 最重要的是如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。

以上是关于UnityShader入门精要-阴影的主要内容,如果未能解决你的问题,请参考以下文章

Unity Shader入门精要学习笔记 - 第3章 Unity Shader 基础

图形学31 Unity 的光源衰减和阴影

Unity Shader入门精要学习笔记 - 第11章 让画面动起来

UnityShader入门精要-3.5 UnityShader的形式

Unity Shader入门精要学习笔记 - 第9章 更复杂的光照

Unity Shader:白平衡