在Unity3D中使用Projector的方式生成并优化阴影

projective texture和projector的工作原理

中文网络上,对于projective texture和projector的工作原理,康玉之在他的《GPU编程与CG语言之阳春白雪下里巴人》中解析得最清晰。现在摘抄如下:

对投影纹理映射,很多教程上都是这么解释的:纹理好比一张幻灯片,灯光好比投影机,然后将纹理投影到一个物体上,类似于投影机将幻灯片投影到墙上。这个比喻没有太大的问题,也找不到更加形象的比喻了。问题是:这个解释刚好颠倒了算法的实现流程。

投影纹理映射真正的流程是“根据投影机(视点相机)的位置、投影角度,物体的坐标,求出每个顶点所对应的纹理坐标,然后依据纹理坐标去查询纹理值”,也就是说,不是将纹理投影到墙上,而是把墙投影到纹理上。投影纹理坐标的求得,也与纹理本身没有关系,而是由投影机的位置、角度,以及3D模型的顶点坐标所决定。所以,我一直觉得“投影纹理映射”这个术语具有很强的误导性,总让人觉得是把纹理投射出去。

根据顶点坐标获得纹理坐标的本质是将顶点坐标投影到 NDC 平面上,此时投影点的平面坐标即为纹理坐标。如果你将当前视点作为投影机,那么在顶点着色程序中通过 POSTION 语义词输出的顶点投影坐标,就是当前视点下的投影纹理坐标没有被归一化的表达形式。

所以,使用projector产生阴影的方式类比就是,把“幻灯片”(角色模型),投“”(这个影就是角色模型的阴影),投射到“屏幕”(场景)上。

Unity3D的projector

怎样使用Unity3D的projector组件,这篇文章就说得比较清楚了。

具体实现

首先要根据projector的投影方向,其实就是光线的方向,用“replacement render”的shader,去把场景中的角色模型绘制一遍,并且把角色模型的位置信息绘制到一个render texture中,然后在绘制阴影的过程中读取这张render texture,比较以下,如果读到的R分量是1,则表示光线不可达到,此时应绘制阴影。整个C#代码如下:

  1
  2using UnityEngine;
  3
  4[ExecuteInEditMode]
  5public class ShadowProjector : MonoBehaviour
  6{
  7    public static float ShadowTextureSizeFactor = 1;
  8    public float Intensity = 1f;
  9    public Color LightColor = Color.white;
 10
 11    // 是否启用阴影
 12    public bool m_Shadow = false;
 13    public float m_ShadowAreaSize = 10f;                // 阴影的作用区域
 14    public Color m_ShadowColor = Color.black;           // 阴影的颜色
 15    public static int ms_ShadowCullingMask;				// 指定将会产生阴影的物体所在层
 16    public LayerMask ShadowLayerMask;
 17    public float m_ShadowFarClipPlane = 100f;
 18    public int m_ShadowTextureSize = 512;
 19    public Vector3 m_LightDirection;					// 光照的方向,自然也是projector的投影方向
 20    public Camera ShadowCamera { get; private set; }	// 专门用来产生projector shadow的摄像机
 21    public Material m_ShadowMaterial;
 22    private Projector m_ShadowProjector;				// 用来执行投影纹理的projector对象
 23    private bool m_Initialized = false;
 24    public Shader m_ReplacementShader;					// 用来执行replacement render的那个shader
 25    public string m_ReplacementRederTag = "MyProjectorShadow";			// 用来执行replacement render的那个shader的名字
 26    public Texture2D m_ShadowMaskTexture;
 27    public Shader m_ShadowShader;						// 绘制阴影用的shadow、
 28
 29    public ShadowProjector()
 30    {
 31        ms_ShadowCullingMask = 1556480;
 32    }
 33
 34    public void SetAllValue()
 35    {
 36        if (!m_Shadow)
 37            return;
 38
 39        SetCameraValue();
 40        SetProjectorValue();
 41    }
 42
 43    private void CreateShadow()
 44    {
 45        // 在创建阴影之前,首先查询下摄像机在不在,如果不在的话就创建
 46        ShadowCamera = GetComponent<Camera>();
 47
 48        if (!ShadowCamera)
 49            ShadowCamera = gameObject.AddComponent<Camera>();
 50
 51        SetCameraValue();
 52        m_ShadowProjector = GetComponent<Projector>();
 53
 54        if (!m_ShadowProjector)
 55            m_ShadowProjector = gameObject.AddComponent<Projector>();
 56
 57        SetProjectorValue();
 58    }
 59
 60    private void DestroyShadow()
 61    {
 62        if (!ShadowCamera)
 63            ShadowCamera = GetComponent<Camera>();
 64
 65        if (ShadowCamera)
 66        {
 67            if (ShadowCamera.targetTexture != null)
 68                ShadowCamera.targetTexture.Release();
 69
 70            DestroyImmediate(ShadowCamera);
 71        }
 72
 73        if (!m_ShadowProjector)
 74            m_ShadowProjector = GetComponent<Projector>();
 75
 76        if (m_ShadowProjector)
 77            DestroyImmediate(m_ShadowProjector);
 78    }
 79
 80    private void Init()
 81    {
 82        if (m_Initialized)
 83            return;
 84
 85        m_Initialized = true;
 86
 87        int layer2 = 1 << LayerMask.NameToLayer("Hero");
 88        int layer3 = 1 << LayerMask.NameToLayer("NPC");
 89        int layer1 = 1 << LayerMask.NameToLayer("Monster");
 90
 91        // 指定能使用projector投射阴影的各个层
 92        ms_ShadowCullingMask = layer1 | layer2 | layer3;
 93
 94        if (m_Shadow)
 95            CreateShadow();
 96        else
 97            DestroyShadow();
 98
 99
100       /*方法解释
101		1.Tags可以自定义。
102		2.subshader标签的使用:
103		原本的shader:
104		Tags { “RenderType”=“Opaque” }
105		替换的shader:
106		Tags { “RenderType”=“Opaque” }
107		Tags { “RenderType”=“SomethingElse” }
108
109		标签为"RenderType"=“Opaque"的,会被同样是"RenderType”="Opaque"标签的替换shader所替换。需要替换的shader,RenderType后面的键值要一样。
110		官网文档解释:SetReplacementShader将浏览场景中的所有对象,不是使用其常规着色器,而是使用第一个子着色器,该子着色器具有与指定关键帧匹配的值。 
111		其Shader具有Rendertype =“ Opaque”标记的对象将被替换的shader中的第一个具有Rendertype =“ Opaque”的subshader替换,
112		任何具有RenderType =“ SomethingElse”着色器的对象将使用第二个具有RenderType =“ SomethingElse”的替换子着色器,依此类推。 如果着色器的
113		替换着色器中的指定关键帧的着色器没有匹配的标记值,则将不呈现任何对象。*/
114        if (ShadowCamera)
115            ShadowCamera.SetReplacementShader(m_ReplacementShader, m_ReplacementRederTag);
116    }
117
118    private void LateUpdate()
119    {
120#if UNITY_EDITOR
121        if (Application.isPlaying)
122        {
123            return;
124        }
125        SetAllValue();
126#endif
127    }
128
129    private void OnEnable()
130    {
131        Init();
132    }
133
134    private void SetCameraValue()
135    {
136        // ShadowCamera沿着光线的方向,用正交投影的方式,把场景中会产生
137        // 影子的角色,往ShadowCamera的Target texture上投射。这时候投射
138        // 的影子的外观,就和角色往场景中投下的影子的外观,是一致的。
139        ShadowCamera.allowHDR = false;
140        ShadowCamera.allowMSAA = false;
141        ShadowCamera.orthographic = true;
142        ShadowCamera.orthographicSize = m_ShadowAreaSize;
143        ShadowCamera.depth = -1;
144        ShadowCamera.clearFlags = CameraClearFlags.SolidColor;
145        ShadowCamera.backgroundColor = new Color(0f, 0f, 0f, 0f);
146        ShadowCamera.cullingMask = ms_ShadowCullingMask;
147        SetRenderedTexture();
148        ShadowCamera.farClipPlane = m_ShadowFarClipPlane;
149    }
150
151    private void SetProjectorValue()
152    {
153        m_ShadowProjector.orthographic = true;
154        m_ShadowProjector.material = SetShadowMaterial();
155        m_ShadowProjector.ignoreLayers = ShadowLayerMask;
156        m_ShadowProjector.orthographicSize = m_ShadowAreaSize;
157        m_ShadowProjector.farClipPlane = m_ShadowFarClipPlane;
158    }
159
160    public void SetRenderedTexture()
161    {
162        if (ShadowCamera == null)
163            return;
164
165        if (ShadowCamera.targetTexture != null)
166            RenderTexture.ReleaseTemporary(ShadowCamera.targetTexture);
167
168        RenderTextureFormat rtFormat = RenderTextureFormat.R8;
169        var rt = RenderTexture.GetTemporary((int)(m_ShadowTextureSize * ShadowTextureSizeFactor),
170            (int)(m_ShadowTextureSize * ShadowTextureSizeFactor), 0);
171
172        rt.name = "ShadowRenderTemp";
173        rt.format = rtFormat;
174        rt.antiAliasing = 1;
175        rt.filterMode = FilterMode.Bilinear;
176        rt.wrapMode = TextureWrapMode.Clamp;
177        ShadowCamera.targetTexture = rt;
178
179        if (m_ShadowMaterial != null)
180            m_ShadowMaterial.SetTexture("_ShadowTex", ShadowCamera.targetTexture);
181    }
182
183    private Material SetShadowMaterial()
184    {
185        if (!m_ShadowMaterial)
186        {
187            m_ShadowMaterial = new Material(m_ShadowShader)
188            {
189                name = "ShadowMaterialTemp"
190            };
191        }
192
193        m_ShadowMaterial.SetColor("_Color", m_ShadowColor);
194        m_ShadowMaterial.SetTexture("_ShadowTex", ShadowCamera.targetTexture);
195        m_ShadowMaterial.SetTexture("_FalloffTex", m_ShadowMaskTexture);
196        return m_ShadowMaterial;
197    }
198
199    private void Start()
200    {
201        Init();
202    }
203
204    private void OnDestroy()
205    {
206        if (ShadowCamera != null && ShadowCamera.targetTexture != null)
207        {
208            RenderTexture.ReleaseTemporary(ShadowCamera.targetTexture);
209        }
210    }
211}
212

用于replacement render的shader

这个shader很简单,就是用简单的红色绘制要绘制的模型,一看就懂

 1Shader "KumaCG/ReplacementRender/Red"
 2{
 3    SubShader
 4    {
 5	// 待渲染的物体中,如果它的shader里面也有"MyProjectorShadow" = "True"这种的标签
 6	// 就会被替换成用本shader的这个subshader来渲染了
 7        Tags { "RenderType"="Opaque" "MyProjectorShadow" = "True"}
 8        Pass
 9        {
10            Cull back
11            Lighting Off
12            ZWrite On
13
14            CGPROGRAM
15            #pragma vertex vert
16            #pragma fragment frag
17            #include "UnityCG.cginc"
18
19            struct appdata
20            {
21                float4 vertex : POSITION;
22            };
23
24            struct v2f
25            {
26                float4 vertex : SV_POSITION;
27            };
28
29            fixed4 _Color;
30
31            v2f vert (appdata v)
32            {
33                v2f o;
34                o.vertex = UnityObjectToClipPos(v.vertex);
35                return o;
36            }
37            
38            fixed4 frag (v2f i) : SV_Target
39            {
40                return fixed4(1.0, 0, 0, 1);
41            }
42            ENDCG
43        }
44    }
45}

在物体表面上绘制阴影的shader

  1Shader "KumaCG/Projector/Shadow"
  2{
  3    Properties
  4    {
  5        _Color("Color", Color) = (0.5, 0.5, 0.5 ,1)
  6
  7        /* 阴影纹理,由专门负责产生阴影的摄像机生成,游戏在运行时创建一个render texture。
  8        作为该阴影摄像机的targetTexture。阴影摄像机将其拍摄到的图案绘制到target Texture上。
  9        作为它的targetTexture。这个_ShadowTex就是阴影摄像机的targetTexture。具体细节参见
 10        ShadowProjector类*/
 11        _ShadowTex ("Cookie", 2D) = "gray" {}
 12        _FalloffTex("FallOff", 2D) = "white" {}
 13        _DisplayShadowThreshold("Display Shadow Threshold", Range(0, 4)) = 2
 14
 15        /*当受影物的片元的世界坐标值Y值大于本阈值的时候,不对齐进行clip操作,
 16        让没有受影的片元也能参与对阴影边缘进行柔化操作的计算*/
 17        _NoClipShadowEdgeHeightThreshold("No Clip Shadow Edge Threshold", Range(0, 2)) = 1
 18    }
 19
 20    Subshader
 21    {
 22        Tags {"Queue"="Transparent"}
 23
 24        Pass
 25        {
 26            ZWrite Off
 27            //ColorMask RGB
 28
 29            // 最终颜色计算为:被投阴影物体的颜色 * 阴影颜色 + 0 * 被投阴影物体的颜色
 30            Blend DstColor Zero
 31            Offset -1, -1
 32
 33            CGPROGRAM
 34            #pragma vertex vert
 35            #pragma fragment frag
 36            #pragma multi_compile_fog
 37            #include "UnityCG.cginc"
 38
 39            struct v2f
 40            {
 41                float4 pos : SV_POSITION;
 42                float4 uvShadow : TEXCOORD0;
 43                float4 uvShadowFalloff : TEXCOORD1;
 44                float4 worldPos : TEXCOORD2;
 45                UNITY_FOG_COORDS(3)
 46            };
 47
 48            float4 _Color;
 49            sampler2D _ShadowTex;
 50            sampler2D _FalloffTex;
 51            float _DisplayShadowThreshold;
 52            float _NoClipShadowEdgeHeightThreshold;
 53            float4 _ShadowMapTexelSize; // 由CPU传递进来的,阴影纹理的像素高宽度的倒数
 54
 55            /*unity_Projector是投影矩阵,unity_ProjectorClip是投影裁剪矩阵。这些矩阵由引擎底层填充赋值并发送过来*/
 56            float4x4 unity_Projector;
 57            float4x4 unity_ProjectorClip;
 58
 59            v2f vert (float4 vertex : POSITION)
 60            {
 61                v2f o;
 62                o.pos = UnityObjectToClipPos(vertex);
 63                o.uvShadow = mul (unity_Projector, vertex);
 64                o.uvShadowFalloff = mul (unity_ProjectorClip, vertex);
 65                o.worldPos = mul(unity_ObjectToWorld, vertex); // 计算出受影物顶点的世界坐标
 66                UNITY_TRANSFER_FOG(o,o.pos);
 67                return o;
 68            }
 69            
 70            // shadowReceiverWorldPosY 受影物的世界坐标Y值
 71            float CheckNeighbourShadow(float4 neighbourShadowTexel,float shadowReceiverWorldPosY)
 72            {
 73                if (neighbourShadowTexel.x > 0 ) // 如果这个邻居texel原本受影,即X通道不为0(大于0)。
 74                {
 75                    // 判断其受影物Y和投影物世界坐标Y的距离是否超过阈值,超过的话,原本属于受影的像素,
 76                    // 也认为是不受影neighbourShadowTexel.y就是世界坐标下投影物的Y值
 77                    float heightDist = abs(neighbourShadowTexel.y * 65536 - shadowReceiverWorldPosY);
 78                    
 79                    if (_DisplayShadowThreshold > heightDist ) // 没有超过阈值,就认为这处应该受影的
 80                        return neighbourShadowTexel.x; // 实质上就是return 1
 81                    else // 如果超过了阈值了,表示这处是不应受影的,即不应该提供阴影贡献度给它的邻居阴影
 82                        return 0;
 83                }
 84                else
 85                    return 0; // 返回0,表示不受影,所以无法提供阴影贡献度给它的邻居阴影
 86            }
 87
 88            // neighbourShadowTexel: 某个邻接texel
 89            // shadowReceiverWorldPosY 当前受影物体的片元的世界坐标Y值
 90            // curTexelNoShadow 表示当前片元是否是原本并不受影的,如果是,为1,否,为-1 
 91            float3 CalculateNeighbourShadowColor(float4 neighbourShadowTexel,float shadowReceiverWorldPosY, float curTexelNoShadow)
 92            {
 93                float4 whiteColor = float4(1,1,1,1);
 94                // if分支:如果本片元原本并不受影,那么就要判断其周边的8个像素的受影情况,以敲定这个
 95                // 原本不受影的片元要不要加上阴影。这样子是为了柔化阴影边缘 
 96                if ( curTexelNoShadow > 0 ) // 当前片元原本并不受影
 97                {
 98                    float nbst = CheckNeighbourShadow(neighbourShadowTexel,shadowReceiverWorldPosY);
 99                    return lerp(_Color,whiteColor,(1-nbst.x));
100                }
101                else // 当前片元原本就要受影
102                {
103                    return lerp(_Color,whiteColor,(1-neighbourShadowTexel.x));
104                }
105            }
106
107            // projShadowUVCoord 用来对阴影纹理进行采样的坐标
108            // shadowReceiverWorldPosY 当前受影物体的片元的世界坐标Y值
109            // curTexelNoShadow 表示当前片元是否是原本并不受影的,如果是,为1,否,为-1
110            float4 FetchTexelColorAverage9(float2 projShadowUVCoord,float shadowReceiverWorldPosY, float curTexelNoShadow)
111            {
112                float4 whiteColor = float4(1,1,1,1);
113                float4 finalColor = float4(0,0,0,0);
114                float4 c = float4(0,0,0,0);
115
116                c = tex2D(_ShadowTex, projShadowUVCoord + float2(-1.0,-1.0) * _ShadowMapTexelSize.xy); // 左上
117                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
118                
119                c = tex2D(_ShadowTex, projShadowUVCoord + float2( 0.0,-1.0) * _ShadowMapTexelSize.xy); // 正上
120                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
121                
122                c = tex2D(_ShadowTex, projShadowUVCoord + float2( 0.0,-1.0) * _ShadowMapTexelSize.xy); // 右上
123                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
124
125                c = tex2D(_ShadowTex, projShadowUVCoord + float2(-1.0, 0.0) * _ShadowMapTexelSize.xy); // 正左
126                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
127
128                c = tex2D(_ShadowTex, projShadowUVCoord + float2( 0.0, 0.0) * _ShadowMapTexelSize.xy); // 本身
129                finalColor.rgb += lerp(_Color,whiteColor,(1-c.x));
130
131                c = tex2D(_ShadowTex, projShadowUVCoord + float2( 1.0, 0.0) * _ShadowMapTexelSize.xy); // 正右
132                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
133
134                c = tex2D(_ShadowTex, projShadowUVCoord + float2(-1.0, 1.0) * _ShadowMapTexelSize.xy); // 左下
135                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
136                
137                c = tex2D(_ShadowTex, projShadowUVCoord + float2( 0.0, 1.0) * _ShadowMapTexelSize.xy); // 正下
138                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
139
140                c = tex2D(_ShadowTex, projShadowUVCoord + float2( 1.0, 1.0) * _ShadowMapTexelSize.xy); // 右下
141                finalColor.rgb += CalculateNeighbourShadowColor(c,shadowReceiverWorldPosY,curTexelNoShadow);
142
143                finalColor.rgb /= 9.0;
144                return finalColor;
145            }
146
147            struct ShadowColorValuePos
148            {
149                // x分量存储着平均采样得到的shadow值,1表示完全受影,0表示完全不受影
150                // y分量存储着平均采样得到的某片元对应的投影物的world pos Y值
151                float2 shadowValuePosY;
152                float4 shadowColor;
153            };
154
155            // 返回参数,某片元的平均阴影颜色值
156            // projShadowUVCoord 某片元对应的投影纹理的UV坐标
157            // out shadowValuePosY x分量存储着平均采样得到的shadow值,1表示完全受影,0表示完全不受影,
158            //                     y分量存储着平均采样得到的某片元对应的投影物的world pos Y值
159            ShadowColorValuePos FetchShadowColorValueAndPosY9(float2 projShadowUVCoord)
160            {
161                ShadowColorValuePos scvp;
162                scvp.shadowColor = float4(0 , 0 , 0, 0);
163                scvp.shadowValuePosY = float2(0,0);
164                float4 whiteColor = float4(1 , 1 , 1, 1);
165                float4 c = float4(0,0,0,0);
166
167                for( int i = -1 ; i <= 1 ; ++i )
168                {
169                    for( int j = -1 ; j <= 1 ; ++j)
170                    {
171                        c = tex2D(_ShadowTex, projShadowUVCoord + float2( j*1.0 , i*1.0) * _ShadowMapTexelSize.xy);
172                        scvp.shadowValuePosY += c.xy;
173                        scvp.shadowColor += lerp(_Color,whiteColor,(1-c.x));
174                    }
175                }
176
177                scvp.shadowColor /= 9;
178                scvp.shadowValuePosY /= 9;
179                return scvp;
180            }
181
182            float4 frag (v2f i) : SV_Target
183            {
184                /* tex2D函数和tex2DProj的区别就是在于:tex2Dproj函数coord在把参数传递进去之后,
185				会把参数除以coord的w分量后,再进行后续运算。而tex2D函数则不会。两者的区别用
186				下面的示例代码可以说明这两个语句的效果是等价的
187                float4 textureColor = tex2Dproj(projectiveMap, coord);
188                float4 textureColor = tex2D(projectiveMap, coord.xy/ coord.w);*/
189                float4 projShadowUVCoord = UNITY_PROJ_COORD(i.uvShadow);
190                projShadowUVCoord.xy = projShadowUVCoord.xy / projShadowUVCoord.w;
191                float4 texF = tex2D(_FalloffTex, projShadowUVCoord);
192
193                // 拿到当前片元
194                ShadowColorValuePos scvp = FetchShadowColorValueAndPosY9(projShadowUVCoord);
195
196                // 如果当前处理的片元的高度超过阈值高度,比如就是两米高
197                if ( i.worldPos.y > _NoClipShadowEdgeHeightThreshold)
198                {
199                    /*
200                    经测试,进入到本段if statements时,经9个采样点平均采样后,如果本片元所记录的
201                    投影物高度值为0,即shadowAndPosY.y == 0时,
202                    则该片元对应的阴影值信息,将一定为0,即shadowAndPosY.x == 0
203                    如果投影物高度值不为0,即shadowAndPosY.y > 0时,
204                    则该片元对应的阴影值信息,将不会为0,即shadowAndPosY.x > 0
205                    也就是说阴影纹理就算经过了
206                    */
207                    // return scvp.shadowColor;
208                    return lerp(float4(1, 1, 1, 1), scvp.shadowColor, texF.r);
209                }
210
211                /* i.WorldPos.y是受影物体的垂直高度,基于世界坐标
212                scvp.shadowValuePosY.y是投影物顶点的垂直高度,基于世界坐标
213                算出投影物和受影物垂直高度上的距离值,加上abs,是为了处理影子往墙壁上投影的情况*/
214                float heightDist = abs(scvp.shadowValuePosY.y * 65536 - i.worldPos.y);
215
216                // 当投影物和受影物垂直距离超过阈值的时候,就不投影了
217                clip(_DisplayShadowThreshold - heightDist);
218
219                /*当取到一个片元,其“投影物顶点的垂直高度”为0的时候,即影子没有覆盖到它
220                这时候_DisplayShadowThreshold - heightDist。有可能为正数,
221                例如当上面的计算式中:
222                如果scvp.shadowValuePosY.y为0,但i.worldPos.y也为0,或者为0.1,的时候,
223                hegihtDist就是一个很小的正数,通过了clip的测试。没有发生丢弃。
224
225                当投影物站在地面上的时候,正是因为这个clip测试,没发生丢弃,所以使得原本
226                没有阴影覆盖的片元,通过平均采样,覆盖了部分阴影,使得整体的阴影产生柔化
227                的效果。这个是对的。但是,当投影物站在桥上,产生的影子投在桥下的时候,这个
228                原本就应该的丢弃的片元,如果不能被丢弃掉的话,就会产生淡淡的一圈黑影。所以
229                要针对这个情况,做特殊处理。要进敲定,到底这个片元是受影还是不受影。
230                
231                没有高度值,但是要受影的情况就是发生在真正受影的片元的旁边的片元。在对阴影做
232                柔化操作时,原本不受影的片元,也要受部分阴影*/
233
234                if ( shadowAndPosY.x > 0 )
235                    curTexelNoShadow = -1;
236
237                // fragmentWorldPos 当前受影物体的片元的世界坐标Y值
238                // curTexelNoShadow 表示当前片元是否是原本并不受影的,如果是,为1,否,为-1
239                return lerp(float4(1, 1, 1, 1), FetchTexelColorAverage9(projShadowUVCoord,i.worldPos.y,curTexelNoShadow), texF.r);
240            }
241            ENDCG
242        }
243    }
244}

参考网页

在Unity3D中使用Projector实现动态阴影

替换着色器Replacement Shader的用法

【Shader进阶】SubShader块标签Tags——RenderType

SubShader TAG的说明

准确认识SubShader语义块结构、渲染状态设定、Tags标签