Unity Shader:复杂光照

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

Unity渲染路径

在Unity中,渲染路径决定了光照是如何应用到UnityShader中的。

Unity中支持4种渲染路径,其中两种是旧版本使用的,常规使用的两种是前向渲染路径和延迟渲染路径,默认情况下使用的是前向渲染路径。

我们可以在Pass的标签中设置渲染路径,"LightMode"标签支持的渲染路径设置如下:

前向渲染路径

前向渲染路径的原理

每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息,一个是颜色缓冲区,一个是深度缓冲区。

对于每个逐像素光源,我们都进行一次完整的前向渲染。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设场景中有N个物体,每个物体受到N个光源的影响,那么渲染整个场景需要N*M个Pass。渲染引擎通常会限制每个物体的逐像素光照的数目。

Unity中的前向渲染

在Unity中,前向渲染有3种处理光照的方式:逐顶点处理、逐像素处理、球谐函数处理(SH)。决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的。

在前向渲染种,当我们渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、光源强度等)对这些光源进行一个重要度排序。其中,一定数目的光源会按照逐像素的方式处理,最多有4个光源按逐顶点的方式处理,剩下的光源可以按SH方式处理。Unity判断规则如下:

  • 场景中最亮的平行光总是按逐像素处理的。
  • 渲染模式被设置为Not Important的光源,会按照逐顶点或SH处理。
  • 渲染模式被设置为Important的光源,会按照逐像素处理。
  • 如果根据以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量,会有更多的光源以逐像素的方式进行渲染(默认是4)。

前向渲染有两种Pass:Base Pass和Additional Pass,常规设置如下:


  • 首先,在渲染设置中,除了设置标签外,还使用了#pragma multi_compile_fwdbase的编译指令。这些编译指令会保证Unity可以为相应类型的Pass生成所有需要的Shader变种,这些变种会处理不同条件下的渲染逻辑,同时也会在背后声明相关的内置变量并传递到Shader中。通常情况下,只有分别为Base Pass和Additional Pass使用这两个编译指令,我们才可以在相关的Pass中得到一些正确的光照变量。
  • Base Pass中渲染的平行光默认是支持阴影的,而Additional Pass中渲染的光源在默认情况下是没有阴影的。我们可以在Addditional Pass中使用#pragma multi_compile_fwdadd_fullshadows指令来为点光源和聚光灯开启阴影效果。
  • 环境光和自发光在Base Pass中计算。因为我们希望这两种效果只计算一次。
  • 在Additional Pass的渲染设置中,我们还开启和设置了混合模式。我们希望每个Additional Pass可以与上一次的光照结果在帧缓冲中进行叠加,从而得到最终有多个光照的渲染效果。如果没有开启和设置混合模式,那么Additional Pass的渲染结果会覆盖掉之前的渲染结果。通常情况下我们选择的混合模式是Blend One One
  • 对与前向渲染,一个Unity Shader通常会定义一个Base Pass(也可以定义多次,比如双面渲染)以及一个Additional Pass。一个Base Pass仅会执行一次,其它每个逐像素光源会执行一次Additional Pass。

延迟渲染路径

前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。

延迟渲染是一种更古老的渲染方法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区被称为G缓冲。G缓冲区存储了我们所关心的表面信息,例如该表面的法线、位置、用于光照计算的材质属性等。

延迟渲染的原理

延迟渲染主要包含两个Pass,在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,利用深度缓冲。当一个片元可见时,便将相关信息存储在G缓冲中。第二个Pass中,我们利用G缓冲中的片元信息来进行光照计算。

延迟渲染使用的Pass数量就是通常是2个,与场景中的光源数无关。

Unity中的延迟渲染

延迟渲染路径中的每个光源都可以按逐像素的方式处理,但是,延迟渲染也有一些缺点:

  • 不支持MSAA。
  • 不能处理半透明物体。
  • 对显卡有要求,显卡必须支持MRT等。

使用延迟渲染时,Unity要求提供两个Pass。
(1)第一个Pass用于渲染G缓冲。在这个Pass中,我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲中。对于每个物体,这个Pass仅会执行一次。
(2)第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

Unity的光照衰减

我们在Unity中使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减。

用于光照衰减的纹理

Unity在内部使用一张名为_LightTexture0的纹理来计算光照衰减。我们通常只关注对角线上的纹理颜色值,这些值表明了子啊光源空间中不同位置的点的衰减值。

为了对_LightTexture0纹理采样得到给定点到光源的衰减值,我们首先需要得到该点在光源空间中的位置,这里是通过_LightMatrix0变换矩阵得到的。_LightMatrix0可以将顶点从世界空间变换到光源空间。因此我们像这样获取光源空间中顶点的位置:

float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;

然后使用坐标模的平方对衰减纹理进行采样,得到衰减值:

fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

使用坐标模的平方进行采样是为了避免进行开方操作。然后使用UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。

使用数学公式计算衰减

查看https://www.jianshu.com/p/b88a22022e7d

Unity的阴影

阴影实现原理

在实时渲染中,尝试用阴影贴图技术(Shadew Map)来实现阴影。简单来说就是先把摄像机的位置放到与光源重合的位置,摄像机看不到的地方就是阴影区域。

在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影贴图。阴影贴图本质上时深度图,记录了从光源位置观察的能看到的最近的表面的深度信息。

Unity使用一个额外的Pass来专门更新光源的阴影贴图,这个Pass就是LightMode标签被设置为ShadowCaster的Pass,这个Pass的渲染目标时阴影贴图。Unit首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间的位置,并据此来输出深度信息到阴影贴图中。当开启了光源的阴影效果后,底层渲染引擎会在当前渲染物体的Unity Shader中找到LightModeShadwoCaster的Pass,如果没有,就在Fallback中寻找,若还是没有就不产生阴影。

在传统的阴影贴图的实现中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后我们使用xy分量对阴影贴图进行采样,得到阴影贴图中该位置的深度信息。如果该深度值小于该顶点的深度值,说明该顶点在阴影中。但对于支持MRT的显卡,Unity使用屏幕空间的阴影映射技术。

当使用屏幕空间的阴影映射技术时,Unity会通过调用LightModeShadwoCaster的Pass来得到可投射阴影的光源的阴影贴图以及摄像机的深度纹理。然后根据光源的阴影贴图和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影贴图中的深度值,就说明该表面位于阴影中。由于阴影图在屏幕空间下,因此,我们首先需要将表面坐标从模型空间变换到屏幕空间,然后使用这个坐标对阴影图进行采样。

实现阴影和光照衰减的Blinn-Phong光照模型:

Shader代码:

Shader "Unlit/Shadow"
{
    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 = UnityObjectToClipPos(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(unity_ObjectToWorld, 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 = UnityObjectToClipPos(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(unity_ObjectToWorld, 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"
}

上述代码中我们使用"AutoLight.cginc"文件中的衰减和阴影相关代码。SHADOW_COORDS声明一个名为_ShadowCoord的阴影纹理坐标变量。TRANSFER_SHADOW根据是使用传统阴影贴图技术还是屏幕阴影映射技术来判断是将顶点坐标转换到光源空间还是屏幕空间。SHADOW_ATTENUATION用来衰减阴影。注意,要使用这些宏定义,输入顶点着色器的坐标名称需为vertex,输入片元着色器的坐标名称需为pos

我们使用UNITY_LIGHT_ATTENUATION来同时计算阴影和光照衰减。
实现效果:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350