unity shader base pass and additional pass 写在前面 Forward Rendering Path的渲染细节 光照变量和函数 一个基本的Shader Base Pass Additional Passes 写在最后

 

【Unity Shaders】Shader中的光照,shadersshader


自己写过Vertex & Fragment Shader的童鞋,大概都会对Unity的光照痛恨不已。当然,我相信这是因为我们写得少。。。不过这也是由于官方文档对这方面介绍很少的缘故,导致我们无法自如地处理很多常见的光照变量。这篇我们就来讨论下Unity内置的一些光照变量和函数到底怎么用。

以下内容均建立在Forward Rendering Path的基础上。

自己总结的,如果有硬伤一定要告诉我啊!感激不尽~

主要参考:

  • http://en.wikibooks.org/wiki/Cg_Programming/Unity/Multiple_Lights
  • http://docs.unity3d.com/Manual/RenderTech-ForwardRendering.html
  • http://docs.unity3d.com/Manual/SL-BuiltinIncludes.html
  • http://www.cnblogs.com/wonderKK/p/4031754.html

Forward Rendering Path的渲染细节

在开始后面的讨论之前,先要弄懂一个问题就是Unity可以在Forward Rendering Path中可以处理哪些以及处理多少光照。这里只提取官方文档中的一些内容加以说明。

在Forward Rendering中,有三种处理光照(即照亮物体)的方式:逐顶点处理,逐像素处理,球谐函数(Spherical Harmonics,SH)处理。而决定一个灯光是哪种处理模式取决于它的类型和模式:

  • 场景中最亮的平行光总是逐像素处理的。这意味着,如果场景里只有一个平行光,是否设置它的模式都无关紧要。
  • Render Mode被设置成Not Important的光源,会按逐顶点或者球谐函数处理。经试验,第一点中的平行光不受这点的约束。
  • Render Mode被设置成Important的光源,会按逐像素处理。
  • 如根据以上规则得到的像素光源数量小于设置中的像素光源数量(Pixel Light Count),为了减少亮度,会有更多的光源以逐像素的方式进行渲染。
    • 这一点我没有读懂,按我的实验结果是,如果所有的光源设置成Auto,那么逐像素光源的数目不会超过Pixel Light Count。但如果设置了Render Mode为明确的Not Important或者Important,那么设置Pixel Light Count似乎没有任何影响。


那在哪里进行光照处理呢?当然是在Pass里。Forward Rendering有两种Pass:Base Pass,Additional Passes。这两种Pass的图例说明如下:

unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后

注意其中的Per-Vertex Lights/SH Lights前面我标注了可选的,这是说,我们可以选择是否处理这些光源。如果我们没有在Base Pass中写明相关的处理函数,那么这些光源实际上不会对物体产生影响。另一点就是其中橘黄色字表明的代码,其中Tags我就不赘述了,这是基本要求。“#pragma multi_compile_fwdbase”这种在长久的实验中表明最好是写上它们,这会让一些函数和宏可以正确工作,很可惜,现在官方没有给出明确的文档说明,因此我们还是乖乖地每次都加上它们比较好。最后,注意对于Forward Rendering来说,只有Bass Pass中处理的第一个平行光可以有阴影效果

从上面的图中,我们已经知道,由于逐像素的光源是最重要的一种光源,因此Unity会花费一整个Pass来处理它。而对于逐顶点/SH光源来说,它们都将会在Bass Pass中处理(和最重要的平行光一起)。没分量就是这种结果。那么,Base Pass会说,“我这么小就让我做这么多东西,平行光就一个数量少就算了,SH光工作量少也算了,但顶点光也来捣乱我就不干了,不行!我得有条件!”于是Unity规定说,最多只有4个光源会按照逐顶点光源来处理,其他只能按SH光源处理。

这里很容易就弄混弄蒙了。我们先来看官方给的情况,即第一种情况:所有光源都被设置成Auto。这种情况下,Unity会自动为光源选择合适的类型。这时,有一个项目设置很重要就是Pixel Light Count,它决定了逐像素光的最大数目。当Pixel Light Count为4时,就是那张著名的图例情况(来自官方文档):

unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后 unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后

上面的类型选择过程大概是这样的:首先,前Pixel Light Count(这里是4)个光源会按照逐像素进行处理,然后最多4个逐顶点光源,剩下的就是SH光了。其中,注意每种光源之间会有重叠的情况,这主要是为了防止物体移动时光照产生突变。

但是,如果光源没有被设置为Auto,而是被指明是Important和Not Important,又会怎样呢?(不要问我有的被设置成Auto,有的设置成Important会怎样,你这人真讨厌自己分析吧。。。)那么,第二种情况:自定义光源类型。首先,记住一点,这时不再受Pixel Light Count的限制,那么被设置成Important全部会被当成逐像素光源,一个不剩;如果被设置成Not Important,那么最多有4个光源会被当成逐顶点光源,其他就会被当做SH光源进行处理。

上面听起来很复杂,其实就是个“物竞天择”的过程。我们可以想象,所有的光源都在争抢更多的计算资源,都想让自己成为最重要的逐像素光,再不济点就逐顶点光,要是实在混的不好就只能当成SH光了。那么挣到了资源又怎么处理呢?对于逐像素光,它有一整个Pass的资源可以挥霍,而这里会涉及到各种光照变量和函数的使用,后面会讲;对于逐顶点光和SH光来说,很可惜,Unity并没有明确的文档来告诉我们如何访问它们,我们只能通过UnityShaderVariables.cginc中的变量声明和Surface Shader的编译结果来“揣测”用法。这也是后面讲的内容。

吐槽时间:虽然文档上这么写,但实际过程中还是有很多莫名其妙的问题:

  • 奇葩情况一:我在4.6.1版本中,创建一个场景包含了1个平行光+4个点光源,如果使用的Shader没有Additional Passes的定义话,那么4个点光源即便设置成Important,还是会被Unity当成逐顶点光源。
  • 奇葩情况二:如果只定义了Additional Passes,而没有Base Pass的话,就更奇葩了,整个Pass感觉都没有在工作,而得到的结果像是上次缓存之类的东西。总之,请一定要先定义Base Pass再定义Additional Passes。不要任性!
  • 其他更多奇葩等待你发现

光照变量和函数

在UnityShaderVariables.cginc文件中,我们可以找到Unity提供的和处理光照有关的变量:

CBUFFER_START(UnityLighting)

	#ifdef USING_DIRECTIONAL_LIGHT
	uniform fixed4 _WorldSpaceLightPos0;
	#else
	uniform float4 _WorldSpaceLightPos0;
	#endif

	uniform float4 _LightPositionRange; // xyz = pos, w = 1/range

	// Built-in uniforms for "vertex lights"
	float4 unity_4LightPosX0;	// x coordinates of the 4 light sources in world space
	float4 unity_4LightPosY0;	// y coordinates of the 4 light sources in world space
	float4 unity_4LightPosZ0;	// z coordinates of the 4 light sources in world space
	float4 unity_4LightAtten0;	// scale factors for attenuation with squared distance

	float4 unity_LightColor[8];	// array of the colors of the 4 light sources
	float4 unity_LightPosition[8];	// apparently is not always correctly set
	// x = -1
	// y = 1
	// z = quadratic attenuation
	// w = range^2
	float4 unity_LightAtten[8];	// apparently is not always correctly set
	float4 unity_SpotDirection[8];

	// SH lighting environment
	float4 unity_SHAr;
	float4 unity_SHAg;
	float4 unity_SHAb;
	float4 unity_SHBr;
	float4 unity_SHBg;
	float4 unity_SHBb;
	float4 unity_SHC;
CBUFFER_END

在UnityCG.cginc可以找到光照处理辅助函数:

// Computes world space light direction
inline float3 WorldSpaceLightDir( in float4 v );

// Computes object space light direction
inline float3 ObjSpaceLightDir( in float4 v );

// Computes world space view direction
inline float3 WorldSpaceViewDir( in float4 v );

// Computes object space view direction
inline float3 ObjSpaceViewDir( in float4 v );

float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal);

float3 ShadeVertexLights (float4 vertex, float3 normal);

// normal should be normalized, w=1.0
half3 ShadeSH9 (half4 normal);

下面我们来看下如何在两种Pass中使用上面的变量和函数处理不同类型的光照。

一个基本的Shader

下面的讨论主要建立在下面的代码下,可以先扫一遍,这里不用细看。它主要计算了漫反射光照和高光反射光照,还示例了逐顶点光源和SH光源的计算等。

Shader "Light Test" {
    Properties {
        _Color ("Color", color) = (1.0,1.0,1.0,1.0)
    }
    SubShader {
    	Tags { "RenderType"="Opaque"}
    	
        Pass {
            Tags { "LightMode"="ForwardBase"}	// pass for 4 vertex lights, ambient light & first pixel light (directional light)
            
            CGPROGRAM
            // Apparently need to add this declaration 
            #pragma multi_compile_fwdbase	
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
			#include "AutoLight.cginc"
             
            uniform float4 _Color;
             
            struct vertexInput {
            	float4 vertex : POSITION;
            	float3 normal : NORMAL;
         	};
         	struct vertexOutput {
            	float4 pos : SV_POSITION;
            	float4 posWorld : TEXCOORD0;
            	float3 normalDir : TEXCOORD1;
            	float3 lightDir : TEXCOORD2;
            	float3 viewDir : TEXCOORD3;
            	float3 vertexLighting : TEXCOORD4;
            	LIGHTING_COORDS(5, 6)
         	};
			
            vertexOutput vert(vertexInput input) {
                vertexOutput output;
                
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
               	output.posWorld = mul(_Object2World, input.vertex);
                output.normalDir =  normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
				output.lightDir = WorldSpaceLightDir(input.vertex);
				output.viewDir = WorldSpaceViewDir(input.vertex);
				output.vertexLighting = float3(0.0);
				
				 // SH/ambient and vertex lights
  				#ifdef LIGHTMAP_OFF
				float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
				output.vertexLighting = shLight;
				#ifdef VERTEXLIGHT_ON
				float3 vertexLight = Shade4PointLights (
					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				    unity_4LightAtten0, output.posWorld, output.normalDir);
				output.vertexLighting += vertexLight;
				#endif // VERTEXLIGHT_ON
  				#endif // LIGHTMAP_OFF
				
				// pass lighting information to pixel shader
  				TRANSFER_VERTEX_TO_FRAGMENT(output);
  
                return output;
            }
             
            float4 frag(vertexOutput input):COLOR{
                float3 normalDirection = normalize(input.normalDir); 
            	float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
            	float3 lightDirection;
           		float attenuation;
 
            	if (0.0 == _WorldSpaceLightPos0.w) // directional light?
       			{
               		attenuation = 1.0; // no attenuation
               		lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            	} 
            	else // point or spot light
            	{
               		float3 vertexToLightSource =  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               		float distance = length(vertexToLightSource);
               		attenuation = 1.0 / distance; // linear attenuation 
               		lightDirection = normalize(vertexToLightSource);
            	}
                      
                // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
//                attenuation = LIGHT_ATTENUATION(input);
                // Compare to directions computed from vertex
//				viewDirection = normalize(input.viewDir);
//				lightDirection = normalize(input.lightDir);
                
                // Because SH lights contain ambient, we don't need to add it to the final result
                float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.xyz;
                 
                float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;
                
                float3 specularReflection;
                if (dot(normalDirection, lightDirection) < 0.0)  // light source on the wrong side?
	            {
	               	specularReflection = float3(0.0, 0.0, 0.0);  // no specular reflection
	            }
	            else // light source on the right side
	            {
	               	specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);
	            }
                
                return float4(input.vertexLighting +  diffuseReflection + specularReflection, 1.0);  
            }               
            ENDCG
        }
        
        Pass{
            Tags { "LightMode"="ForwardAdd"}		// pass for additional light sources
            ZWrite Off Blend One One Fog { Color (0,0,0,0) }	// additive blending
            
            CGPROGRAM
            // Apparently need to add this declaration
            #pragma multi_compile_fwdadd
            
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
			#include "AutoLight.cginc"
             
            uniform float4 _Color;
             
            struct vertexInput {
            	float4 vertex : POSITION;
            	float3 normal : NORMAL;
         	};
         	struct vertexOutput {
            	float4 pos : SV_POSITION;
            	float4 posWorld : TEXCOORD0;
            	float3 normalDir : TEXCOORD1;
            	float3 lightDir : TEXCOORD2;
            	float3 viewDir : TEXCOORD3;
            	LIGHTING_COORDS(4, 5)
         	};
             
            vertexOutput vert(vertexInput input) {
                vertexOutput output;
                
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
               	output.posWorld = mul(_Object2World, input.vertex);
                output.normalDir =  normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
				output.lightDir = WorldSpaceLightDir(input.vertex);
				output.viewDir = WorldSpaceViewDir(input.vertex);
				
				// pass lighting information to pixel shader
				vertexInput v = input;
  				TRANSFER_VERTEX_TO_FRAGMENT(output);
  
                return output;
            }
             
            float4 frag(vertexOutput input):COLOR{
                float3 normalDirection = normalize(input.normalDir); 
            	float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
            	float3 lightDirection;
           		float attenuation;
 
            	if (0.0 == _WorldSpaceLightPos0.w) // directional light?
       			{
               		attenuation = 1.0; // no attenuation
               		lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            	} 
            	else // point or spot light
            	{
               		float3 vertexToLightSource =  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               		float distance = length(vertexToLightSource);
               		attenuation = 1.0 / distance; // linear attenuation 
               		lightDirection = normalize(vertexToLightSource);
            	}
                      
                // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
//                attenuation = LIGHT_ATTENUATION(input);
                // Compare to directions computed from vertex
//				viewDirection = normalize(input.viewDir);
//				lightDirection = normalize(input.lightDir);
               	
                float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;
                
                float3 specularReflection;
                if (dot(normalDirection, lightDirection) < 0.0)  // light source on the wrong side?
	            {
	               	specularReflection = float3(0.0, 0.0, 0.0);  // no specular reflection
	            }
	            else // light source on the right side
	            {
	               	specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);
	            }
                
                return float4(diffuseReflection + specularReflection, 1.0);  
            }              
            ENDCG
        }
    } 
    FallBack "Diffuse"
}

Base Pass

回想一下,上面我们说过在Bass Pass中,我们可以处理全部三种光照:处理第一个平行光作为逐像素光处理,处理所有的逐顶点光,处理其他所有SH光。还有很重要的一点就是,我们还要处理环境光、阴影等。一句话,由于Additional Passes只能处理逐像素光,如果你想要其他光照效果,都需要在Bass Pass中处理。

环境光

这里的环境光指的是我们在Edit->Render Setting里面的Ambient Light的值。在Shader中获取它很容易,只需要访问全局变量UNITY_LIGHTMODEL_AMBIENT即可。它是全局变量,因此在在哪个Pass里访问都可以,但环境光只需要加一次即可,因此我们只需要在Bass Pass中叠加到其他颜色上即可。

阴影和光照衰减

Base Pass还有一个非常重要的作用就是添加阴影。上面提到过,对于Forward Rendering来说,只有Bass Pass中处理的第一个平行光可以有阴影效果。也就是说,错过了这里就不会得到阴影信息了。程序中模拟阴影主要是依靠一张Shadow Map,里面记录了从光源出发距离它最近的深度信息。Unity很贴心地提供了这样的一张纹理(_ShadowMapTexture),不用我们自己再编程实现了。

与阴影的实现类似,Unity还提供了一张纹理(_LightTexture0),这张纹理包含了光照衰减(attenuation)。

由于阴影和光照衰减都是对纹理进行采样,然后将结果乘以颜色值,因此Unity把这两步合并到一个宏中,让我们通过一个宏调用就可以解决这两个问题。既然是对纹理采样,那么首先就要知道顶点对应的纹理坐标,Unity同样是通过宏来辅助我们完成的,我们只需要在v2f(vertexOutput)中添加关于宏LIGHTING_COORDS即可。然后,为了计算顶点对应的两张纹理上的坐标,需要在vert函数里面调用一个新的宏:TRANSFER_VERTEX_TO_FRAGMENT。

这个过程中使用的宏定义都在AutoLight.cginc文件中。

一个完整的过程如下:

Unity就是使用了这三个宏来完成阴影和衰减的计算的。我们来看一下这三个宏到底是个什么东东。这里仅以不开启cookie的平行光和点光源为例:
#ifdef POINT
#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
uniform sampler2D _LightTexture0;
uniform float4x4 _LightMatrix0;
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a)
#define LIGHT_ATTENUATION(a)	(tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
#endif

#ifdef DIRECTIONAL
	#define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1)
	#define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a)
	#define LIGHT_ATTENUATION(a)	SHADOW_ATTENUATION(a)
#endif

#define SHADOW_COORDS(idx1) float4 _ShadowCoord : TEXCOORD##idx1;
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

可以发现,对于点光源来说,会计算两种纹理,即光照衰减纹理和阴影纹理,并在最后计算attenuation的时候,就是将两种纹理的采样结果相乘。而对于平行光来说更加简单,由于平行光没有衰减,因此只需要计算阴影纹理就可以了。
再次强调以下,Forward Rendering来说,只有Bass Pass中处理的第一个平行光可以有阴影效果。例如,下面左图中的平行光可以投射出阴影,而右图中即便小球在光源和小苹果的中间也不会产生任何阴影:unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后 unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后



逐顶点光照


其实逐顶点光照就是一个名字,Unity把这些所谓的“逐顶点光照”的数据存储在一些变量中,我们完全可以按逐像素的方式来处理它们。当然,处于性能的考虑,我们通常还是会在顶点函数阶段处理它们,因此把它们称为逐顶点光照。
逐顶点光照涉及的变量和函数有两组。这里的组别主要是依靠Unity提供的顶点光照计算函数使用的变量来归类的。
第一组如下:
   uniform float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space
   uniform float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space
   uniform float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space
   uniform float4 unity_4LightAtten0; // scale factors for attenuation with squared distance

对应的函数如下:
float3 Shade4PointLights (
	float4 lightPosX, float4 lightPosY, float4 lightPosZ,
	float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
	float4 lightAttenSq,
	float3 pos, float3 normal)
{
	// to light vectors
	float4 toLightX = lightPosX - pos.x;
	float4 toLightY = lightPosY - pos.y;
	float4 toLightZ = lightPosZ - pos.z;
	// squared lengths
	float4 lengthSq = 0;
	lengthSq += toLightX * toLightX;
	lengthSq += toLightY * toLightY;
	lengthSq += toLightZ * toLightZ;
	// NdotL
	float4 ndotl = 0;
	ndotl += toLightX * normal.x;
	ndotl += toLightY * normal.y;
	ndotl += toLightZ * normal.z;
	// correct NdotL
	float4 corr = rsqrt(lengthSq);
	ndotl = max (float4(0,0,0,0), ndotl * corr);
	// attenuation
	float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
	float4 diff = ndotl * atten;
	// final color
	float3 col = 0;
	col += lightColor0 * diff.x;
	col += lightColor1 * diff.y;
	col += lightColor2 * diff.z;
	col += lightColor3 * diff.w;
	return col;
}

调用的话代码如下:
				float3 vertexLight = Shade4PointLights (
					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				    unity_4LightAtten0, output.posWorld, output.normalDir);

注意其中顶点位置和法线方向都是指在世界坐标系下的。

第二组变量:
	float4 unity_LightPosition[8];	// apparently is not always correctly set
	// x = -1
	// y = 1
	// z = quadratic attenuation
	// w = range^2
	float4 unity_LightAtten[8];	// apparently is not always correctly set
	float4 unity_SpotDirection[8];

函数:
float3 ShadeVertexLights (float4 vertex, float3 normal)
{
	float3 viewpos = mul (UNITY_MATRIX_MV, vertex).xyz;
	float3 viewN = mul ((float3x3)UNITY_MATRIX_IT_MV, normal);
	float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
	for (int i = 0; i < 4; i++) {
		float3 toLight = unity_LightPosition[i].xyz - viewpos.xyz * unity_LightPosition[i].w;
		float lengthSq = dot(toLight, toLight);
		float atten = 1.0 / (1.0 + lengthSq * unity_LightAtten[i].z);
		float diff = max (0, dot (viewN, normalize(toLight)));
		lightColor += unity_LightColor[i].rgb * (diff * atten);
	}
	return lightColor;
}

用法:
vertexLight = ShadeVertexLights(input.vertex, input.normal)

注意其中的顶点坐标和法线方向是在对象坐标系下的。而且,其计算结果包含了环境光。。。
这两组函数看起来做了一样的工作,但其实Forward Rendering我们只可以选择第一组。下面是官方文档中的解释:

Forward rendering helper functions in UnityCG.cginc

These functions are only useful when using forward rendering (ForwardBase or ForwardAdd pass types).

  • float3 Shade4PointLights (...) - computes illumination from four point lights, with light data tightly packed into vectors. Forward rendering uses this to compute per-vertex lighting.

Vertex-lit helper functions in UnityCG.cginc

These functions are only useful when using per-vertex lit shaders (“Vertex” pass type).

  • float3 ShadeVertexLights (float4 vertex, float3 normal) - computes illumination from four per-vertex lights and ambient, given object space position & normal.
文档里说的很清楚,对于Forward Rendering来说,我们应该使用Shade4PointLights来计算最多四个逐顶点光照,而且只能计算Point Lights和Spot Lights,如果一个平行光被设置成逐顶点光源,那么是不会被计算的。换句话说,我们应该使用unity_4LightPosX0、unity_4LightPosY0、unity_4LightPosZ0、unity_4LightAtten0这些数据来访问逐顶点的光源数据。而另一组是在Vertex Pass(e.g. Tags { "LightMode"="Vertex"})中使用的。
还有有一些需要我们了解的地方
  • Unity给出的函数只是为了方便我们提供的一种计算方法,可以看出来Shade4PointLights中,只是按逐顶点的方法(即只需在vert函数中提供顶点位置和法线)计算了漫反射方向的光照,但我们也完全可以自己根据这些光照变量处理逐顶点光源,例如添加高光反射等等。
  • 我们甚至还可以按照逐像素的方式来处理它们,即在frag函数里访问并计算它们。只要你愿意,没有什么可以阻止你这么做。(就是这么任性。)


好啦,说完了理论我们来看下视觉效果是怎样的。我们在场景里放了一个小苹果+一个球,并且放了四个不同颜色的点光源,只输出Shade4PointLights的结果如下(左图为逐顶点光照,右图为逐像素光照):unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后 unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后


可以看出来,逐顶点光源从视觉效果上不如逐像素光源,但性能更好。
那么,还有一个问题,即支持计算的逐顶点光源数目最多为4个,定义的存储逐顶点光源信息的变量数组也只有4维。也就是说,如果场景里被设置(或者排序后得到的数目)成逐顶点光源的数目大于4个,那么Unity会对它们进行排序,把其中最重要的4个光源存储到那些变量中。但这种排序方法Unity没有文档进行说明,而从实验结果来看,这个排序结果和光的颜色、密度、距离都有关。例如,如果我们再加一个蓝色光源,可以发现不会对结果有任何变化:unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后


而如果我们调整它的颜色、密度、或者位置时,由于排序结果发生变化,就会生成光照突变(左图为改变颜色,右图为改变密度): unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后 unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后


SH光照

那些既不是逐像素光又不是逐顶点光的光源,如果想对物体产生影响,就只能按SH光照进行处理。宫斗失败就是这个结果。Unity里和计算SH光有关的变量和函数如下:

	// SH lighting environment
	float4 unity_SHAr;
	float4 unity_SHAg;
	float4 unity_SHAb;
	float4 unity_SHBr;
	float4 unity_SHBg;
	float4 unity_SHBb;
	float4 unity_SHC;
// normal should be normalized, w=1.0
half3 ShadeSH9 (half4 normal)
{
	half3 x1, x2, x3;
	
	// Linear + constant polynomial terms
	x1.r = dot(unity_SHAr,normal);
	x1.g = dot(unity_SHAg,normal);
	x1.b = dot(unity_SHAb,normal);
	
	// 4 of the quadratic polynomials
	half4 vB = normal.xyzz * normal.yzzx;
	x2.r = dot(unity_SHBr,vB);
	x2.g = dot(unity_SHBg,vB);
	x2.b = dot(unity_SHBb,vB);
	
	// Final quadratic polynomial
	float vC = normal.x*normal.x - normal.y*normal.y;
	x3 = unity_SHC.rgb * vC;
    return x1 + x2 + x3;
} 

调用代码如下:
float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

关于SH光照的实现细节我没有研究,有兴趣的可以查资料理解下上面函数的含义。之前有网友留言告诉我一篇文章。但太长了我没看。。。还有论坛中的一个帖子,可以看看里面的代码初步了解一下。

我们以之前的例子为例,看一下只输出SH光照的结果。下面左图中,是只有四个光源的情况,可以看出此时并没有任何SH光,这是因为这四个光源此时被当做是逐顶点光照。这里物体颜色非黑是因为unity_SHAr、unity_SHAg、unity_SHAb包含了环境光数据,而非真正的光照造成的,因此理论上只要包含了计算SH光照的代码就不需要在最后结果上添加上面提到的环境光了。右图则是增加了4个新的Not Important光源后的SH光照结果。

unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后 unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后

我们将逐顶点光照和SH光照结合在一起,代码如下:

				 // SH/ambient and vertex lights
  				#ifdef LIGHTMAP_OFF
				float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
				output.vertexLighting = shLight;
				#ifdef VERTEXLIGHT_ON
				float3 vertexLight = Shade4PointLights (
					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				    unity_4LightAtten0, output.posWorld, output.normalDir);
				output.vertexLighting += vertexLight;
				#endif // VERTEXLIGHT_ON
  				#endif // LIGHTMAP_OFF

其中,需要添加#ifdef这些声明是为了保证,在Unity不提供这些数据时可以不用计算这些光照。

我们把两者相加的结果输出,可以得到以下的结果:

unity shader base pass and additional pass
写在前面
Forward Rendering Path的渲染细节
光照变量和函数
一个基本的Shader
Base Pass
Additional Passes
写在最后

Additional Passes

最后,我们来谈谈Additional Passes中的逐像素光。我们需要知道的是,其实在Base Pass中我们也需要处理逐像素光,但我们可以明确的知道这个逐像素光只能是第一个平行光。而在Additional Passes中,逐像素光可能是平行光、点光源、聚光灯光源(Spot Light)。这里不讨论使用了LightMap或者开启了Cookie的情况。

同样,这里的逐像素光其实也只是一个名字,Unity只是负责把所谓的逐像素光的数据放到一些变量中,但是,没有什么可以阻止我们是在vert中计算还是在frag中计算。

注意:想要Additional Passes是叠加在Bass Pass上的话(一般人的目的都是这个),请确保你给Pass添加了合适的混合模式。例如:

        Pass{
            Tags { "LightMode"="ForwardAdd"}		// pass for additional light sources
            ZWrite Off Blend One One Fog { Color (0,0,0,0) }	// additive blending


对于逐像素光照,我们最长使用的变量和函数如下:

来自UnityShaderVariables.cginc:

uniform float4 _WorldSpaceLightPos0;
uniform float3 _WorldSpaceCameraPos;

来自Lighting.cginc:

fixed4 _LightColor0;
来自UnityCG.cginc(文档说明):
// Computes world space light direction
inline float3 WorldSpaceLightDir( in float4 v );
// Computes object space light direction
inline float3 ObjSpaceLightDir( in float4 v );
// Computes world space view direction
inline float3 WorldSpaceViewDir( in float4 v );
// Computes object space view direction
inline float3 ObjSpaceViewDir( in float4 v );

可以发现,只有函数给出了明确的文档说明,其他都只能靠Unity内部Shader的结构来揣测了。

我们先不管这些变量和函数,先来想想我们到底想利用逐像素光照来计算什么,在哪里计算。最常见的需求就是计算光源方向和视角方向,然后再进行漫反射和高光反射的计算。在Unity里在哪里计算这些方向似乎从视觉上没有太大的区别,理论上在vert中计算比在frag中计算更快一点。但计算位置的选择决定了我们可以如何使用上面的变量和函数。

可以注意到,Unity提供的函数都是在vert函数中的辅助函数,即都是只需要提供顶点位置就可以得到光照方向和视角方向的。也就是说,如果我们想要在vert函数中就计算各个方向的值,可以这么做:

				output.lightDir = WorldSpaceLightDir(input.vertex);
				output.viewDir = WorldSpaceViewDir(input.vertex);

当然,上面是得到世界坐标系下的用法,我们也可以得到对象坐标系下的,看需求即可。这些函数其实也是利用了_WorldSpaceLightPos0和_WorldSpaceCameraPos而已。例如WorldSpaceLightDir的定义如下:
// Computes world space light direction
inline float3 WorldSpaceLightDir( in float4 v )
{
	float3 worldPos = mul(_Object2World, v).xyz;
	#ifndef USING_LIGHT_MULTI_COMPILE
		return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;
	#else
		#ifndef USING_DIRECTIONAL_LIGHT
		return _WorldSpaceLightPos0.xyz - worldPos;
		#else
		return _WorldSpaceLightPos0.xyz;
		#endif
	#endif
}

其中,由于平行光的方向不随顶点位置发生变化,因此直接使用_WorldSpaceLightPos0.xyz即可,此时里面存储的其实就是平行光的方向,而非位置。同时,_WorldSpaceLightPos0.w可以表明该光源的类型,如果为0表示是平行光,为1表示是点光源或者聚光灯光源。因此,我们常常可以看到类似下面的代码:
            	if (0.0 == _WorldSpaceLightPos0.w) // directional light?
       			{
               		attenuation = 1.0; // no attenuation
               		lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            	} 
            	else // point or spot light
            	{
               		float3 vertexToLightSource =  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               		lightDirection = normalize(vertexToLightSource);
            	}

其实是和WorldSpaceLightDir函数的意义是一样的。

_LightColor0就没什么可说的了,就是存储了该逐像素光的颜色。

写在最后

今天就到这里。