[unity shader] 光照&阴影 - maggie03230/maggie03230.github.io GitHub Wiki

以下整理主要摘自《Unity Shader 入门精要》

渲染路径

主要有: 前向渲染路径延迟渲染路径

一般使用前向渲染路径。 延迟渲染适合光源数目多,前向渲染会造成性能瓶颈的情况。不支持抗锯齿,不能处理半透明,对显卡有要求,具体选择哪种看文档 https://docs.unity3d.com/Manual/RenderingPaths.html

后文主要讨论前向渲染路径。

前向渲染路径

原理

如果一个物体在多个逐像素光源影响区域,则此物体需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果。假设N个物体,受M个光源影响,整个场景共N*M个Pass,由于数目很大,故会限制每个物体的逐像素光照数目。

unity处理

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

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

那在哪里进行光照处理呢?当然是在Pass里。Forward Rendering有两种Pass:Base Pass,Additional Passes。这两种Pass的图例说明如下: image 注意两个Pass的Tags和#pragma的设置为必需。如果需要点光源等也有阴影可以在Additional Pass里替换成:#pragma multi_compile_fwdadd_fullshadows

光源

分为:平行光、点光源、聚光灯、面光源

光源设置
image

光源属性 : 位置、方向、颜色、强度、衰减

  1. 平行光 只有方向且没有衰减
  2. 点光源 一个点发出的,向所有方向延伸的光,可用一个球表示。有衰减有位置。
  3. 聚光灯 一个点发出的,向特定方向延伸的光,可用一个锥形表示。

在前向渲染中处理各种光源

上文说到有两种Pass,首先是Base Pass: 负责计算环境光和平行光。

Pass {
    // Pass for ambient light & first pixel light (directional light)
    Tags { "LightMode"="ForwardBase" }
    CGPROGRAM

    #pragma multi_compile_fwdbase   
    ……
    
    fixed4 frag(v2f i) : SV_Target {
        ……
        
        fixed atten = 1.0;
        return fixed4(ambient + (diffuse + specular) * atten, 1.0);
    }
}

解释: 这个LightMode决定了Pass只处理平行光,所以衰减度为1. Tags和#pragma的设置上面图例里提过。

其次是Additional Pass

Pass {
    Tags { "LightMode"="ForwardAdd" }
    
    Blend One One

    CGPROGRAM
    
    #pragma multi_compile_fwdadd
    ……

上面是必须的配置代码,Blend可以变,见上一篇(透明效果提到)将改变光照。后面是计算。

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

这里分两块解释,首先是_WorldSpaceLightPos0,想得到光照方向,对于平行光来说是_WorldSpaceLightPos0.xyz,对于非平行光则是光源位置减去面片位置。得到光照方向才能继续如之前的相同计算漫反射和高光反射等。

    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
    
    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
    fixed3 halfDir = normalize(worldLightDir + viewDir);
    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
    
    
    #ifdef USING_DIRECTIONAL_LIGHT
        fixed atten = 1.0;
    #else
        #if defined (POINT)
            float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
            fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
        #elif defined (SPOT)
            float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1));
            fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
        #else
            fixed atten = 1.0;
        #endif
    #endif

    return fixed4((diffuse + specular) * atten, 1.0);
}

解释2: 这里atten(衰减)的计算分三种,平行光已解释过。点光源&聚光灯:由于计算衰减公式比较麻烦,Unity内置了一张映射表,计算的时候用_LightMatrix0将点转换到光源空间,用该坐标在映射表里采样,最终得到衰减值。使用dot(lightCoord,lightCoord).rr作为坐标的原因是,这样可以用事先平方省下一次开方。效果如下。

image

阴影

原理

ShadowMap技术:把摄像机与光源重合,看不见的地方即是阴影区,生成一张深度图(后文称为阴影纹理),之后只要是接受阴影的物体,会把顶点变到光源空间下,比对自己的深度和深度图此xy的深度,若是自己的深度更大,则处于阴影中。
Unity获得ShadowMap的方式:调用LightMode为ShadowCaster的Pass。因为此Pass比较通用,所以可以使用Fallback,在Fallback中找到此Pass。
以下是在unity标准shader中Mobile-VertexLit.shader内找到的Pass

Pass 
{
    Name "ShadowCaster"
    Tags { "LightMode" = "ShadowCaster" }
    
    ZWrite On ZTest LEqual Cull Off

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"

    struct v2f { 
        V2F_SHADOW_CASTER;
    };

    v2f vert( appdata_base v )
    {
        v2f o;
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }

    float4 frag( v2f i ) : SV_Target
    {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}   

unity设置

  1. 光源设置 ShadowType决定此光源阴影模式;
  2. 物体的Mesh Renderer组件的CastShadows参数和Receive Shadows参数决定是否发射/接受阴影。
    image

接収阴影 using内置宏

前文提到用Fallback可以用unity内置的Pass实现阴影显示,然而也可以调用unity内置宏来编写阴影显示的shader。

后面会用到三个内置宏所以在之前需要加上引用:

#include "AutoLight.cginc"

顶点输出增加内置宏SHADOW_COORDS,增加用于阴影纹理采样的坐标。

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    SHADOW_COORDS(2)
};

顶点输出钱使用内置宏TRANSFER_SHADOW计算

v2f vert(a2v v) {
    v2f o;
    ……
    TRANSFER_SHADOW(o);
    return o;
}

查看TRANSFER_SHADOW源代码可看到,a2v必须包含vertex且名字必须为v,而且v2f必须包含a.pos,否则会GG。运算结果是存储顶点转换到光源空间后的坐标

#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) );

这里用到内置宏SHADOW_ATTENUATION()来计算阴影值,算出来以后跟其它颜色相乘。计算原理就是前文说过的对阴影纹理采样。

fixed4 frag(v2f i) : SV_Target {
    ……
    fixed shadow = SHADOW_ATTENUATION(i);
    return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}

统一光照和阴影

无论是光照还是阴影,都用到了了从光源出发的纹理以及在光源空间的坐标等,故统一管理与计算。unity又发了福利 UNITY_LIGHT_ATTENUATION
所以在Base Pass 和Additional Pass 里都这么写就好:

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    SHADOW_COORDS(2) //!!
};

v2f vert(a2v v) {
    v2f o;
    ……
    TRANSFER_SHADOW(o);
    return o;
}

fixed4 frag(v2f i) : SV_Target {
    ……
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

如此计算出来的atten就是原来的衰减颜色*阴影颜色了。

透明+多重光照+阴影

……整合好一看代码太长了不想贴,就说下思路……其实就是把光照的两个Pass都加上,分别和之前AlphaTest或者AlphaBend代码合并下,即可。
不行有几行还是得记录下...

ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

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

计算之类的还是跟之前一样啦╮(╯▽╰)╭

两个光源效果图
image

别人的文章:
Shader中的光照
Unity3D光照前置知识——Rendering Paths(渲染路径)及LightMode(光照模式)译解