赞
踩
上一篇结尾,给出了一个最简单的Diffuse的surface shader翻译成vertex/fragment shader之后的代码。乍看上去可能一头雾水,下面将会一一分解。
整体来看,相较于surface shader,unity自动生成了两个pass(ForwardBase,ForwardAdd),这两个pass的作用,在上一篇中也已经说明。接下去,对照着代码,我们来分解下unity具体做了些什么。
Tags { "LightMode" = "ForwardBase" }
告诉渲染管线,这个pass作为ForwardBase处理,缺少它将画不出任何东西。
#pragma multi_compile_fwdbase
unity官方文档,只有multi_compile的说明,对于multi_compile_fwdbase可谓只字未提,网上唯一能搜到的一篇也是一语带过(虽然不甚详细,但是非常感谢作者分享)。后来打开unity shader面板的Variants里看到:
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_OFF
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_OFF
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_SCREEN
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_OFF VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN VERTEXLIGHT_ON
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_SCREEN SHADOWS_NATIVE
DIRECTIONAL LIGHTMAP_OFF DIRLIGHTMAP_OFF SHADOWS_SCREEN SHADOWS_NATIVE VERTEXLIGHT_ON
很多看过unity生成的vertex/fragment代码的同学,会感到很困惑,代码里充斥着各种条件编译代码,然而unity也没有给出文档,到底有哪些keyword?各自的作用是什么?
看到这个就非常清楚了,unity为forwardbase pass定义的keyword都在这里,而multi_compile_fwdbase就是unity专门为forwardbase预定义的multi_compile。
如果再打开unity最终编译成的glsl代码的话,可以看到,unity为每一组keywards都生成了单独的代码。
如果缺少这行代码的话,那么unity默认会编译
DIRECTIONAL LIGHTMAP_ON DIRLIGHTMAP_ON SHADOWS_OFF
这一组条件,碰到其他的情况,那么渲染就会出错。
我们看到,上述每一组条件,都是DIRECTIONAL,那么如果场景中没有平行光呢?点光源或者聚光灯还会起作用么?
答案是不会。forwardbase pass只能以逐像素的方式处理平行光,点光源和聚光灯都会被忽略掉,对应的_LightColor0都将是黑色。注意上面黑体的逐像素,因为可能有人会说,我场景里只有一个点光源,可以把场景照亮。是的,但它是以逐顶点的方式照亮的,后面将会细说。
// vertex-to-fragment interpolation data
#ifdef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
fixed3 normal : TEXCOORD1;
fixed3 vlight : TEXCOORD2;
LIGHTING_COORDS(3,4)
};
#endif
#ifndef LIGHTMAP_OFF
struct v2f_surf {
float4 pos : SV_POSITION;
float2 pack0 : TEXCOORD0;
float2 lmap : TEXCOORD1;
LIGHTING_COORDS(2,3)
};
unity定义了v2f结构,有无lightmap的两个版本。
LIGHTING_COORDS(3,4)
这个定义在AutoLight.cginc中,它定义了光照和阴影坐标。因为forwardbase只支持平行光,因为平行光的特性,受到的光照强度是一样的,所以并不需要光照坐标,如果投射阴影的话,只需要阴影坐标来采样shadow map。(稍后再forwardadd章节中,会详细讨论不同光源的光照计算)
现在只要知道,这个宏的定义类似如下所示。参数3,4就是下个可用的TEXCOORD序号。
#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
接下去就是填充v2f结构,计算齐次空间坐标,uv坐标,normal或者lightmap的uv坐标。就如我们平时写vertex/fragment shader那样,就不累述了。
float3 shlight = ShadeSH9 (float4(worldN,1.0));
这里是计算球谐光照,没有作为像素光照和顶点光照处理的光源,都在这里计算,包括light probe。
o.vlight += 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, worldPos, worldN );
这里计算顶点光照,unity只能处理4个顶点光源。unity在上层处理掉了很多东西。如果光源影响到该物体,那么光源的属性(位置,原色,衰减)都会被设置,反之则会直接被忽略。具体计算过程在UnityCG.cginc中,如下所示:
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;
}
可以看到,顶点光照就是用简单的Lambert光照方程。
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
需要注意的是,因为法向量是归一化的,而光源方向未归一化,所以要乘以顶点离光源距离的平方根的倒数,得到光源和法线夹角的余弦。
lightAttenSq这个值也是由unity设定的,平行光恒为1,点光源和聚光灯随着距离增大而递减。
TRANSFER_VERTEX_TO_FRAGMENT(o);
这个的定义也是在AutoLight.cginc中,它计算之前定义的光照和阴影坐标的值。
点光源宏的定义如下:
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a)
实际就是把顶点坐标转换到光源空间中。
fixed atten = LIGHT_ATTENUATION(IN);
这个也是定义在AutoLight.cginc中,它就是把之前得到的光照和阴影坐标,采样光照图和shadow map,来得出光源的最终衰减值。
点光源的宏定义如下:
#define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
那么这里的_LightTexture0又是什么?在哪里设置的?别急,在forwardadd章节会讲到,这里对这个宏定义的作用有个认识就可以了。
c = LightingLambert (o, _WorldSpaceLightPos0.xyz, atten);
unity定义的光照方程都在Lighting.cginc这个文件中,需要注意的是,因为forwarbase只支持平行光,而如果光源类型是平行光,那么_WorldSpaceLightPos0这个变量保存的直接是光源方向。所以这里直接作为第二个参数,传入光照方程。当然也可以定义自己的光照方程,具体戳这里。
Tags { "LightMode" = "ForwardAdd" }
同样告诉渲染管线,这个pass作为forwardadd处理。这里处理额外的像素光源。
#pragma multi_compile_fwdadd
这个也是unity为forwardadd pass定制的multi_compile,打开Viriants看到如下:
POINT
DIRECTIONAL
SPOT
POINT_COOKIE
DIRECTIONAL_COOKIE
unity会把forwardadd pass分别编译成适用于上述5种不同光源类型的版本。如果缺少这一行代码,那么unity只会编译DIRECTIONAL这一种情况。
ZWrite Off Blend One One Fog { Color (0,0,0,0) }
注意一下add pass有别于base pass的地方,因为之前base写过深度了,所以add就不用再次写深度了,以叠加的方式渲染到缓存,并且不受雾效影响。
整个add pass相较于base pass显得精简很多,它只考虑实时的光照计算,没有顶点光照,SH,lightmap,阴影这一系列东西。但是,它支持点光源,聚光灯以及cookie。接下去分别讲下,不同光源的光照处理(这里不讨论阴影部分的处理)。
平行光可以说是最简单的,因为它没有衰减。在AutoLight.cginc中找到unity中对平行光光照的定义:
#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
可以看到,只有对阴影部分处理的代码,光照部分是空的。因此,不考虑阴影的话,平行光的衰减值永远是1。
在AutoLight.cginc中找到unity中对点光源光照的定义:
#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
_LightMatrix0是由unity设置的变换矩阵,把世界空间的顶点,变换到光源空间中。而_LightTexture0是一张由unity生成的渐变图,8位的alpha图,如下图所示:
[图-1 点光源的_LightTexture0贴图]
UNITY_ATTEN_CHANNEL 也就是alpha通道。最终用光照坐标来采样_LightTexture0得到光照的衰减值。
同样在AutoLight.cginc中找到对聚光灯的光照定义:
#ifdef SPOT
#define LIGHTING_COORDS(idx1,idx2) float4 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
uniform sampler2D _LightTexture0;
uniform float4x4 _LightMatrix0;
uniform sampler2D _LightTextureB0;
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)); TRANSFER_SHADOW(a)
inline fixed UnitySpotCookie(float4 LightCoord)
{
return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
}
inline fixed UnitySpotAttenuate(float3 LightCoord)
{
return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).UNITY_ATTEN_CHANNEL;
}
#define LIGHT_ATTENUATION(a) ( (a._LightCoord.z > 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
#endif
可以看到,聚光灯相较于点光源,新增了一张光照贴图,用来表示聚光灯的光照范围,如下图所示:
[图-2 聚光灯的_LightTexture0贴图]
[图-3 聚光灯的_LightTextureB0贴图]
这也符合聚光灯的特性,除了距离的衰减之外,投射到平面的光照范围再通过采样[图-2]来确定,哪里应该被照亮。
以上对于由unity把一个最简单的surface shader转换为vertex/fragment shader之后的代码,做了具体的分析。但也并非面面俱到。限于作者的水平,对于SH,为何用dot(LightCoord, LightCoord).xx来作为uv坐标采样[图-1]光照图,用LightCoord.xy / LightCoord.w + 0.5来采样[图-2],也不理解,如果有大牛知道,还望告知,不胜感激。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。