unity3d内建着色器源码解析(三)

UnityCG.cginc文件中的工具函数和宏(下)

4.2.8 法线贴图及其编解码操作的函数

法线贴图(normal map)中存储的信息是对模型顶点法线的扰动方向向量。利用此扰动方向,在光照计算时对顶点原有的法线进行扰动,从而使法线方向排列有序的平滑表面产生法线方向杂乱无序,表面凹凸不平的效果。因为法线贴图有如此特性,所以在实际使用中需使用"低面数模型+法线贴图"的方式实现高面数模型才能达到的精细程度。在Unity3d中导入和使用法线贴图,需要单击该贴图,在Inspector面板中Texture Type选项设置为Normal Map类型。之所以设置这个类型,是因为对不同的平台上,Unity3D可以利用该平台的硬件加速纹理格式去对导入的法线贴图进行压缩。同时也因为法线贴图和普通纹理贴图在采样和解码时的方式也有很多不同。

法线贴图中存储的扰动方向方向不是基于世界坐标系下 ,也不是基于顶点所处的模型坐标系下,而是要使用该法线贴图上某一个纹理像素点对应片元的切线空间中。法线指向就是Z轴,因此法线贴图偏蓝色。片元的法线、切线和副法线可以在定义定点格式时指定,然后经过硬件光栅化插值后生成,因此可以直接考察顶点的发现切线生成方式。一般地,在建模阶段可以明确的指定顶点的法线。而过顶点有无数条与给定法线垂直的切线,通常都会选定和本顶点所使用的纹理映射坐标方向相同的那一条切线。得到法线和切线后,将两者进行叉乘便得到垂直于法线和切线的副切线。

构建好切线空间后,就可以定义法线扰动向量。法线扰动方向向量的每个分量的取值有正有负,落在区间[-1,1]中,而纹理贴图中每个纹素的每个RGB通道值的区间是[0,1],因此需要把扰动方向向量做一个映射,即:

TEXELrgb = 0.5NORMALxyz + 0.5

将法线纹理解码到法线扰动方向向量:

NORMALxyz = 2TEXELrgb - 1

如果不使用DXT5nm格式去压缩法线贴图,上式就是UnpackNormal函数中UNITY_NO_DXT5nm宏启用的那一部分解码算法。如果UNITY_NO_DXT5nm宏启用了,表示引擎使用了DXT5nm压缩格式或者BC5压缩格式的法线贴图纹理,则调用UnpackNormalmapRGorAG函数去解码。

//Line 673~680
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
    return packednormal.xyz * 2 - 1;
#else
    return UnpackNormalmapRGorAG(packednormal);
#endif
}

DXT是一种纹理压缩格式,以前称为S3TC。当前很多图形硬件已经支持这种格式,即在显存中依然保持这种压缩格式,从而减少显存占用量。目前有DXT1~5这5种编码格式,在DirectX10及后续版本,这系列格式成为块状压缩(block compression),所以DXT1称为BC1,DXT2~3称为BC2,DXT4~DXT5称为BC3。

DXT系列压缩格式被很多格式的文件所使用,如DDS文件格式就是用了DXT系列压缩格式。要使用DXT格式压缩图形,要求图像大小至少是4x4纹素,且宽高纹素个数是2的整数次幂(即可以整除4)。DXT5nm和BC5格式类似,当把法线存储进DXT5nm或BC5格式的法线贴图时,该贴图的RGBA纹素各个通道对应存储法线是(1,y,1,x)或(x,y,0,1)。

//Line 653~672
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
    fixed3 normal;
    normal.xy = packednormal.wy * 2 - 1;
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
// Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
{
    // This do the trick
   packednormal.x *= packednormal.w;

    fixed3 normal;
    normal.xy = packednormal.xy * 2 - 1;
    normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
    return normal;
}

4.2.9 线性化深度值的工具函数

从投影矩阵可以看出,片元的深度值往往是非线性的,即从近截面到远截面之间的深度值精度分布不均匀。但有些场合需要用线性化的深度值,如在观察空间中利用深度值计算时,便要把从深度纹理中获取到的深度值重新映射到一个线性区域。Unity3D提供了一系列操作深度纹理的宏,如下:

//Line 684~692
// Z buffer to linear 0..1 depth
// 把从深度纹理中取得的顶点深度值z变换到观察空间中,然后映射到[0,1]
inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

//Line 734~737
// Depth render texture helpers
#define DECODE_EYEDEPTH(i) LinearEyeDepth(i)
#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z
#define COMPUTE_DEPTH_01 -(UnityObjectToViewPos( v.vertex ).z * _ProjectionParams.w)
//把法线从模型空间转到观察空间
#define COMPUTE_VIEW_NORMAL normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal))

4.2.11 用来实现图像效果的工具函数和预定义结构体

Unity3D定义了一系列用来实现图像效果时所用到的工具函数、顶点着色器使用的顶点描述结构体、从顶点着色器返回传递到片元着色器的结构体。

//Line 742~773
struct appdata_img
{
    float4 vertex : POSITION; //顶点齐次化位置坐标
    half2 texcoord : TEXCOORD0; //顶点用到的第一层纹理映射坐标
    UNITY_VERTEX_INPUT_INSTANCE_ID //硬件instance id值
};

struct v2f_img
{
    float4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO //立体渲染时左右眼索引
};

float2 MultiplyUV (float4x4 mat, float2 inUV) {
    float4 temp = float4 (inUV.x, inUV.y, 0, 0);
    temp = mul (mat, temp);
    return temp.xy;
}

v2f_img vert_img( appdata_img v )
{
    v2f_img o;
    UNITY_INITIALIZE_OUTPUT(v2f_img, o);
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

    o.pos = UnityObjectToClipPos (v.vertex);
    o.uv = v.texcoord;
    return o;
}

4.2.12 计算屏幕坐标的工具函数

//Line 776~791
// Projected screen position helpers
#define V2F_SCREEN_TYPE float4

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    //计算xy值,o.xy=0.5*pos.xy + 0.5*pos.ww
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    //zw保值不变直接赋值,这样o.xy取值范围就是[0,w],齐次除法后取值范围[0,1]
    o.zw = pos.zw;
    return o;
}
inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

如何得到传递给ComputeNonStereoScreenPos的参数pos,它是从模型空间变换到裁剪空间后得到的齐次坐标值,一般调用UnityObjectToClipPos(vertexPosInLocalSpace)。顾名思义,不使用立体渲染时调用此函数才有效。上面函数并不会得到真正的屏幕坐标,而是视口坐标,虽然取名带ScreenPos。如果要求得到屏幕坐标下的值,视口坐标应该再乘以屏幕的宽高值,UnityPixelSnap得到真正的屏幕坐标。

// snaps post-transformed position to screen pixels
inline float4 UnityPixelSnap (float4 pos)
{
    float2 hpc = _ScreenParams.xy * 0.5f;
    float2 pixelPos = round ((pos.xy / pos.w) * hpc);
    pos.xy = pixelPos / hpc * pos.w;
    return pos;
}

当把当前屏幕内容截屏并保存在一个目标纹理时,有时要求知道在裁剪空间中的某一点将会对应保存在目标纹理中的哪一点。ComputeGrabScreenPos函数即是实现这个功能的函数。该函数传递进裁剪空间某点的齐次坐标,返回该点在目标纹理中的纹理贴图坐标。

inline float4 ComputeGrabScreenPos (float4 pos) {
    #if UNITY_UV_STARTS_AT_TOP
    float scale = -1.0;
    #else
    float scale = 1.0;
    #endif
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*scale) + o.w;
#ifdef UNITY_SINGLE_PASS_STEREO
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    o.zw = pos.zw;
    return o;
}

两个版本的TransformViewToProjection函数

//Line 817~823
inline float2 TransformViewToProjection (float2 v) {
    return mul((float2x2)UNITY_MATRIX_P, v);
}

inline float3 TransformViewToProjection (float3 v) {
    return mul((float3x3)UNITY_MATRIX_P, v);
}

4.2.13 与阴影处理相关的工具函数

深度值编解码,点光源需要编解码用到RGBA里。

//Line 827~843
float4 UnityEncodeCubeShadowDepth (float z)
{
    #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS
    return EncodeFloatRGBA (min(z, 0.999));
    #else
    return z;
    #endif
}

float UnityDecodeCubeShadowDepth (float4 vals)
{
    #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS
    return DecodeFloatRGBA (vals);
    #else
    return vals.r;
    #endif
}

8.6节会提到阴影渗漏的问题,解决阴影渗漏的方式就是让阴影贴图沿着某个方向做一定的偏移。
找到正确的偏移量是很复杂的,如果偏移量过大,会导致相互摇摄或者光线泄露伪影。如果偏移量太小,则不会完全消除阴影渗漏。
UnityClipSpaceShadowCasterPos函数就是根据法线和光线的夹角的正弦值得到需要的偏移值,然后沿着法线做偏移,如图4-13所示。
UnityApplyLinearShadowBias函数的功能是将调用UnityClipSpaceShadowCasterPos函数得到的裁剪空间坐标的z值再做一定的增加。因为这个增加操作时在裁剪空间这样的齐次坐标系下进行的,所以要对透视投影产生的z值进行补偿,使得阴影偏移值不会随着与摄像机的距离的变化而变化,同时必须保证增加的z值不能超出裁剪空间的远近截面的z值。

//Line 846~891
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal)
{
    float4 wPos = mul(unity_ObjectToWorld, vertex);

    if (unity_LightShadowBias.z != 0.0)
    {
        float3 wNormal = UnityObjectToWorldNormal(normal);
        float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz));

        // apply normal offset bias (inset position along the normal)
        // bias needs to be scaled by sine between normal and light direction
        // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/)
        //
        // unity_LightShadowBias.z contains user-specified normal offset amount
        // scaled by world space texel size.

        float shadowCos = dot(wNormal, wLight);
        float shadowSine = sqrt(1-shadowCos*shadowCos);
        float normalBias = unity_LightShadowBias.z * shadowSine;

        wPos.xyz -= wNormal * normalBias;
    }

    return mul(UNITY_MATRIX_VP, wPos);
}
// Legacy, not used anymore; kept around to not break existing user shaders
float4 UnityClipSpaceShadowCasterPos(float3 vertex, float3 normal)
{
    return UnityClipSpaceShadowCasterPos(float4(vertex, 1), normal);
}


float4 UnityApplyLinearShadowBias(float4 clipPos)
{
#if defined(UNITY_REVERSED_Z)
    // We use max/min instead of clamp to ensure proper handling of the rare case
    // where both numerator and denominator are zero and the fraction becomes NaN.
    clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0));
    float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#else
    clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w);
    float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#endif
    clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y);
    return clipPos;
}

用于SHADOWCASTER产生阴影贴图使用3个宏工具
V2F_SHADOW_CASTER_NOPOS宏 v2f数据结构增加float3 vec(点光源)。
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)宏 vert函数内给上述结构赋值且把opos转到clip空间(点光源);或者偏移顶点位置根据法线,避免阴影渗漏。
SHADOW_CASTER_FRAGMENT宏 frag函数内返回深度编码后的颜色值(点光源);或者0。

//Line 894~925
#ifdef SHADOWS_CUBE
    // Rendering into point light (cubemap) shadows
    // 用来存储世界坐标系下当前顶点到光源位置的连线向量
    #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0;
    #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
    // _LightPositionRange.xyz是光源的位置,w是光源照射范围的倒数
    // TRANSFER_SHADOW_CASTER_NOPOS的功能一是计算在世界坐标系下当前顶点到光源位置的连线向量,二是把顶点位置变换到裁剪空间赋值给opos
    #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
    // 把一个float类型深度值转换成float4类型并返回
    #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
#else
    // 渲染由平行光或聚光灯光源产生的阴影
    #define V2F_SHADOW_CASTER_NOPOS
    // Let embedding code know that V2F_SHADOW_CASTER_NOPOS is empty; so that it can workaround
    // empty structs that could possibly be produced.
    #define V2F_SHADOW_CASTER_NOPOS_IS_EMPTY
    #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) \
        opos = UnityObjectToClipPos(v.vertex.xyz); \
        opos = UnityApplyLinearShadowBias(opos);
    #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \
        opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \
        opos = UnityApplyLinearShadowBias(opos);
    #define SHADOW_CASTER_FRAGMENT(i) return 0;
#endif

// Declare all data needed for shadow caster pass output (any shadow directions/depths/distances as needed),
// plus clip space position.
#define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS UNITY_POSITION(pos)

// Vertex shader part, with support for normal offset shadows. Requires
// position and normal to be present in the vertex input.
#define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)

// Vertex shader part, legacy. No support for normal offset shadows - because
// that would require vertex normals, which might not be present in user-written shaders.
#define TRANSFER_SHADOW_CASTER(o) TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,o.pos)

4.2.14 与雾效相关的工具函数和宏

// Line 934~1019
// ------------------------------------------------------------------
//  Fog helpers
//
//  multi_compile_fog Will compile fog variants.
//  UNITY_FOG_COORDS(texcoordindex) Declares the fog data interpolator.
//  UNITY_TRANSFER_FOG(outputStruct,clipspacePos) Outputs fog data from the vertex shader.
//  UNITY_APPLY_FOG(fogData,col) Applies fog to color "col". Automatically applies black fog when in forward-additive pass.
//  Can also use UNITY_APPLY_FOG_COLOR to supply your own fog color.

// In case someone by accident tries to compile fog code in one of the g-buffer or shadow passes:
// treat it as fog is off.
#if defined(UNITY_PASS_PREPASSBASE) || defined(UNITY_PASS_DEFERRED) || defined(UNITY_PASS_SHADOWCASTER)
#undef FOG_LINEAR
#undef FOG_EXP
#undef FOG_EXP2
#endif

// UNITY_Z_0_FAR_FROM_CLIPSPACE将不同的裁剪空间z的范围情况转换到范围[0,far],用于雾化因子计算。
#if defined(UNITY_REVERSED_Z)
    #if UNITY_REVERSED_Z == 1
        //D3d with reversed Z => z clip range is [near, 0] -> remapping to [0, far]
        //max is required to protect ourselves from near plane not being correct/meaningfull in case of oblique matrices.
        #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0)
    #else
        //GL with reversed z => z clip range is [near, -far] -> should remap in theory but dont do it in practice to save some perf (range is close enough)
        #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(-(coord), 0)
    #endif
#elif UNITY_UV_STARTS_AT_TOP
    //D3d without reversed z => z clip range is [0, far] -> nothing to do
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#else
    //Opengl => z clip range is [-near, far] -> should remap in theory but dont do it in practice to save some perf (range is close enough)
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#endif

#if defined(FOG_LINEAR)
    // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start))
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w
#elif defined(FOG_EXP)
    // factor = exp(-density*z)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
#elif defined(FOG_EXP2)
    // factor = exp(-(density*z)^2)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
#else
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0
#endif

// UNITY_CALC_FOG_FACTOR宏的参数coord是未经透视除法的裁剪空间中的坐标值z分量,参数coord经过宏UNITY_Z_0_FAR_FROM_CLIPSPACE先处理,以解决不同平台z轴倒置的问题,作为参数传入UNITY_CALC_FOG_FACTOR_RAW计算unityFogFactor。
#define UNITY_CALC_FOG_FACTOR(coord) UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord))

//利用顶点格式声明中的纹理坐标语义,借用一个纹理坐标寄存器将雾化因子或将裁剪空间齐次坐标z值传递到frag里再计算。
#define UNITY_FOG_COORDS_PACKED(idx, vectype) vectype fogCoord : TEXCOORD##idx;

#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #define UNITY_FOG_COORDS(idx) UNITY_FOG_COORDS_PACKED(idx, float1)

    #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
        // mobile or SM2.0: calculate fog factor per-vertex
        #define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord.x = unityFogFactor
    #else
        // SM3.0 and PC/console: calculate fog distance per-vertex, and fog factor per-pixel
        #define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord.x = (outpos).z
    #endif
#else
    #define UNITY_FOG_COORDS(idx)
    #define UNITY_TRANSFER_FOG(o,outpos)
#endif

// 利用雾的颜色和当前像素颜色,根据雾化因子进行插值计算,得到最终雾化后的颜色。
#define UNITY_FOG_LERP_COLOR(col,fogCol,fogFac) col.rgb = lerp((fogCol).rgb, (col).rgb, saturate(fogFac))


#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
        // mobile or SM2.0: fog factor was already calculated per-vertex, so just lerp the color
        #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,(coord).x)
    #else
        // SM3.0 and PC/console: calculate fog factor and lerp fog color
        #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_CALC_FOG_FACTOR((coord).x); UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor)
    #endif
#else
    #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol)
#endif

#ifdef UNITY_PASS_FORWARDADD
    #define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0))
#else
    #define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor)
#endif

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值