投影式纹理映射(Projective texturing) 常用于在低端平台实现实时阴影效果,或实现一些类似幻灯片投射的效果。从其名字就可以看出,这种技术与普通的网格渲染流程是有关系的:

泰课在线

​ 常规的网格渲染流程如左图,首先将网格从物体本地坐标转换到世界坐标,然后通过反转的摄像机世界坐标变换矩阵将其转换到摄像机的本地坐标(Eye Space),接下来将其乘以投影矩阵,进入裁剪空间(Clip Space)。通过将裁剪空间的四维向量除w进入NDC空间(范围在(-1, 1)),最后通过ViewPort和深度的范围进行剔除操作即可。

​ 将投影器看作摄像机,将投影目标(接收投影结果的网格)看作摄像机渲染的屏幕,就能看出,投影式纹理映射的原理与网格渲染的流程很相似。首先我们定义投影器的方向及投影矩阵,接着将本地坐标的网格通过相同的转换,转换到投影器的NDC空间,再进行一次重映射后(将(-1,1)映射到(0,1)),就得到了投影目标网格在投影器的屏幕空间中正确的UV值,使用这个UV对投影器产生的贴图进行采样,就可以得到正确的投影像素。

泰课在线

​ 以上图为例,红色的炸弹是产生投影的网格,地面和地上的汽车是接收投影的网格。首先我们通过设置投影的方向(左手系沿X轴转90度)与投影的矩阵(平行投影),接着将一定范围的网格渲染到一张RenderTexture上。通过给接受投影的网格置上自定义的着色器,对着色器传入用于将世界坐标的投影接收网格转换到投影器屏幕坐标的矩阵:

泰课在线

其中OffsetMatrix是一个用于重映射的矩阵,可将NDC空间的坐标(-1,1),映射到(0,1)的范围:

泰课在线

ProjectionMatrix代表投影器的投影矩阵,InvertWorldMatrix代表投影器的世界到本地坐标的转换矩阵。在着色器中,通过将世界坐标的顶点位置ProjectorMatrix相乘,传入片段着色器,将传入的四维向量除w,就得到了正确的可以用于采样投影器RenderTexture的UV坐标。我们就可以实现幻灯片投影效果,硬阴影,甚至是假的软阴影(即对RenderTexture做一次模糊)。

下面是部分着色器代码,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
float4x4 ProjectorMatrix;
  
v2f vert(float4 vertex : POSITION)
{
    v2f o;
    o.pos = mul(UNITY_MATRIX_MVP, vertex);
    float4 worldPos = mul(_Object2World, vertex);
    o.uv = mul(ProjectorMatrix, worldPos);
    return o;
}
  
sampler2D Maou_ProjectorTexture;
  
fixed4 frag(v2f i) : SV_Target
{
    float2 texCroodProj = i.uv.xy / i.uv.w;
    fixed4 color = tex2D(ProjectorTexture, texCroodProj);
  
    // clip掉(0,1)范围之外的像素
    float2 uvmasks = min(texCroodProj, 1.0 - texCroodProj);
    float mask = min(uvmasks.x, uvmasks.y);
    color = mask < 0.0 ? fixed4(1, 1, 1, 0) : color;
  
    return color;
}