Unity无光照假阴影Shader实现及常见问题总结

效果可点击放大查看

FakerShadow.gif

游戏实现阴影的常见处理方式 (动态人或物,非烘焙)

1.实时光照
实时光照属于真阴影,一般来说效果是最好的,但是开销也是最大的

2.脚底放置阴影面片模拟阴影
一般是无光照小型游戏的常见解决方案,开销较小,表现形式较差,面片是死的,无法根据人物动作变化

3.通过顶点shader变换成面片模拟阴影
如上图Gif所示
优点 : 表现形式上比方案2强,阴影可跟随顶点动画,开销比实时阴影要少
缺点 : 无法在 "非平面" 使用,比如在斜坡上,会穿帮

4.通过 Projector 或者 Decal 来模拟投射阴影
优点 : 表现效果更近一步,也可以在斜面上进行投影了
缺点 : 开销也更近一步

主要讲方案3的实现方式

思路
1.我们通过2个Pass来渲染,第二个Pass正常渲染角色,第一个Pass模拟渲染阴影
2.我们需要将模型的所有 Y 值压到地面高度,这样就形成了一个头顶俯视图的阴影效果
3.我们再对 XZ 方向进行偏移,偏移量根据模型原先 Y 值高度为参考做插值
4.阴影的方向我们规定在 XZ 平面上 (X=0,Z=1) 为初始默认方向,以这个向量为基准进行旋转
5.旋转我们可以通过 二维旋转矩阵 来计算

提示:代码放在最后了

过程中遇到的问题:

无max.gif

1.我在对以Y值高度为参考做XZ方向偏移的插值的时候,由于Y值低于地面取到了负值而这些值有没有被裁剪掉就导致了上图的问题,然后首先想到的是对插值做 max(0,插值结果),然后我们又得到了如下结果


无Clip.gif

2.上面第一个问题好像是解决了,但是实际上人物到地面以下之后人物还是有一个顶部投影的值,即XZ平面未偏移的结果,那么解决方案自然而然的想到,通过 Y 值和地面的值比较,Y值如果小于地面那么就Clip掉当前片段 Clip( Y值_ws - 地面Y高度_ws) , ws表示世界空间,那么实际上第一个问题也就处理掉了,无需多一步max函数了

无Stencil.gif

3.紧接着,我们需要考虑降低阴影的透明度,但是出现上图问题,导致该问题的原因是,由于将人物顶点变换的同一个Y值后,部分片段是有重叠,就相当于如果直接把人压扁后那么就前胸贴后背,我们需要的是前胸或者后背只渲染依次就可以了,那么自然想到了模板测试Stencil,模板缓存的初始值都为0,我们将渲染阴影通道的Pass中的Ref引用值设为1,当我们的值 [大于] 模板缓存的值就替换,那么假设我们前胸进来一看发现缓冲区里的值为0,自己是1,然后进行替换,但是后背再进来发现已经是1了,不满足[大于]的条件了,所以就不写入了...


Alpha为0时阴影不消失.gif

阴影穿透了地面看到地面以下内容.gif

FrameDebug渲染顺序

阴影根据摄像机远近产生变化.gif

4.紧接着,又得到了一些奇怪的问题,在我已经将Blend模式设置为SrcAlpha OneMinusSrcAlpha后发现将影子的颜色Alpha值设置为拖动到0的时候并不会让影子消失,而是颜色更深了,很奇怪,想了很久不知道为什么,但是左边就很正常,左边人物和右边人物的shader唯一差别在于:
左边Shader的两个Pass先渲染人物再渲染阴影
右边出错的Pass先渲染阴影再渲染人物
然后我尝试将左边Shader中的Pass1删除掉,只渲染阴影,我发现它也不正常了...真特娘的奇怪,而且FrameDebug逐Draw渲染的结果也有点奇怪
然后改变摄像机颜色后,发现阴影颜色跟随摄像机颜色改变,于是我发现那可能那不是阴影而是透过去的背面,于是我在地面下方放置了一个小球,然后发现确实是看到地面下方的内容了,再通过FrameDebug调试发现问题所在,将镜头拉近的时候,先渲染的小球,然后紧接着就渲染了右边人物的阴影通道,所以结果是不正确的,因为我们需要阴影渲染在地面上,但是地面还没有渲染,所以我们Blend的时候是和背后的天空盒还有小球混合了导致阴影出错的,但是问题又来了,为什么左边的就没有问题呢,原因是这样的...
地面,左侧人物,右侧人物...由于他们的渲染队列Queue都是Geometry,且值都为2000,所以他们在渲染的时候会随着距离摄像头的远近而可能出现不同的Draw绘制顺序,但是针对多通道的Shader

在Queue相同的情况下,都是先绘制它们的第1个通道,所有集合体第一个通道绘制完了之后再统一去绘制第2个Pass

所以,第一个通道Pass会随着摄像机的远近而变化,但是由于左侧人物的阴影绘制是放在了第2个通道里的,所以绘制的时候,地面是肯定已经绘制完了的,而右侧人物第1个Pass就是阴影,具体是绘制在Plane之前还是之后会随着摄像机远近而不同

该问题解决方案 :
1.将右侧的阴影通道放到第二个Pass中渲染
2.手动修改右侧Shader的渲染排序"Queue" = Gemotry+1" ,让它排在Plane后面渲染
3.将"Queue" = "Transparent",这个实际和2相同,但是不清楚原理的情况下比较容易想到这个方案

代码如下 (具体参考阴影Pass即可,另一个Pass只做了纹理采样)

Shader "loom/fake_shadow_test_pass_order"
{
    Properties
    {
        //材质属性面板
        _MainTex ("主贴图",2D) = "white"{}

        _GroundY ("地面Y高度 (外部传入)",float) = 0
        _Shadow_Color("影子颜色",Color) = (1,1,1,1)
        _Shadow_Length("影子长度",float) = 0
        _Shadow_Rotated("影子旋转角度",range(0,360)) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Geometry+1"  //注意这里很重要,因为影子是要绘制在地面上,所以地面必须应该先绘制,否则blend混合的时候就是和背后的skybox进行混合了
        }

        pass
        {
            Stencil{

                Ref 1
                //Comp取值依次为  0:Disabled  1:Never  2:Less  3:Equal  4:LessEqual  5:Greater  6:NotEqual  7:GreaterEqual  8:Always
                Comp Greater //或者改成NotEqual
                //Pass取值依次为  0:Keep  1:Zero  2:Replace  3:IncrementSaturate  4:DecrementSaturate  5:Invert  6:IncrementWrap  7:DecrementWrap
                Pass Replace
            }

            Blend SrcAlpha oneMinusSrcAlpha   

            //因为和地面重叠所以做个偏移
            //也可以不做偏移,将传入的地面高度抬高一点即可
            Offset -2,-2

            CGPROGRAM

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

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                //这里worldPos一定是float4,因为vert()中实际是手动两次空间变换如果是float3会导致w分量丢失,透视除法会出错
                //如果不参与变换,只是传到frag()中使用的话,比如进行Blinn-Phong光照计算V向量那么float3就够了
                float4 worldPos : TEXCOORD0;
                //做阴影插值和Clip地面以下阴影用
                float cacheWorldY : TEXCOORD1;
            };

            half _GroundY;
            half4 _Shadow_Color;   
            half _Shadow_Length;     
            half _Shadow_Rotated;
            
            v2f vert(appdata v)
            {
                v2f o = (v2f)0;

                //获取世界空间的位置
                o.worldPos = mul(unity_ObjectToWorld,v.vertex);
                //缓存世界空间下的y分量,后续两点作用
                //第一点 : 做插值用做计算xz的偏移量的多少
                //第二点 : 防止在地面以下
                o.cacheWorldY = o.worldPos.y;

                //设置世界空间下y的值全部都设置为传入的地面高度值
                o.worldPos.y = _GroundY;

                //根据世界空间下模型y值减去传入的地面高度值_GroundY
                //以这个值为传入 lerp(0,_Shadow_Length) 进行线性插值
                //最后获取到模型y值由低到高的插值lerpVal
                //这个max()函数 假设腿部在地面以下则裁切掉腿部阴影,后续使用clip后无需Max
                //half lerpVal = lerp(0,_Shadow_Length,max(0,o.cacheWorldY-_GroundY));
                half lerpVal = lerp(0,_Shadow_Length,o.cacheWorldY-_GroundY);

                //常量PI
                //const float PI = 3.14159265;
                //角度转换成弧度
                half radian = _Shadow_Rotated / 180.0 * UNITY_PI;

                //旋转矩阵,对(0,1)向量进行旋转,计算旋转后的向量,该向量就是阴影方向
                //2D旋转矩阵如下
                // [x]        [ cosθ , -sinθ ]
                // [ ]  乘以  
                // [y]        [ sinθ , cosθ  ]
                // x' = xcosθ - ysinθ
                // y' = xsinθ + ycosθ
                half2 ratatedAngle = half2((0*cos(radian)-1*sin(radian)),(0*sin(radian)+1*cos(radian)));
                
                //用以y轴高度为参考计算的插值 lerpVal 去 乘以一个旋转后的方向向量,作为阴影的方向
                //最终得到偏移后的阴影位置
                o.worldPos.xz += lerpVal * ratatedAngle;
                
                //变换到裁剪空间
                o.pos = mul(UNITY_MATRIX_VP,o.worldPos);

                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET
            {
                //剔除低于地面部分的片段
                clip(i.cacheWorldY - _GroundY);
                //用作阴影的Pass直接输出颜色即可
                return _Shadow_Color;
            }

            ENDCG
        }

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

            sampler2D _MainTex;half4 _MainTex_ST;

            struct appdata{
                float4 vertex : POSITION;
                float2 uv0 : TEXCOORD0;
            };

            struct v2f{
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert(appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv0,_MainTex);
                return o;
            }

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

推荐阅读更多精彩内容