《Unity3D高级编程之进阶主程》第七章,渲染管线与图形学(五) - Projector投影原理
Projector 投影的原理与应用
Unity3D中的 Projector 组件投影像是一个很神秘的组件,但其实它依然运用的是以着色器为基准的渲染流程,和普通的3D模型渲染从本质上来看并没有实质上的区别,唯一的区别是它从它自己的视体(视锥体或平视体)中检测到的模型,并根据默认或者自定义的材质球与Shader着色器,将这些物体又重新绘制了一遍。
从Unity3D引擎上来讲,可以这样解释 Projector组件:
根据 Projector组件自身的视体的范围,平视体或视锥体,遍历并计算出视体范围内与视体占边的所有物体。接着 Projector组件取得这些物体模型数据,并计算投影矩阵。这个投影矩阵是什么呢,其实就是前面说的 Projector 组件视体空间的投影矩阵。最后将投影矩阵传入Shader中,根据这个投影矩阵,对这些物体再渲染一次。
投影中的Shader着色器是投影绘制的主要手段,Projector组件的工作只是检测了所有范围内的模型,并传递了投影空间的矩阵而已。投影Shader中,通常会结合传入的投影矩阵,将顶点转为 Projector 组件投影空间中,并以此为投影贴图的UV来渲染模型。
例如这个简单的投影着色器:
sampler2D _MainTex;
float4x4 unity_Projector;
struct v2f{
float4 pos:SV_POSITION;
float4 texc:TEXCOORD0;
}
v2f vert(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MAXTRIX_MVP, v.vertex);
o.texc = mul(unity_Projector, v.vertex);
return o;
}
float4 frag(v2f i) : COLOR
{
float4 c = tex2Dproj(_MainTex, i.texc);
return c;
}
投影着色器中 unity_Projector 变量就是由 Projector 组件传入到材质球的投影矩阵。在顶点着色器中 unity_Projector 矩阵被用来构造投影坐标,和普通的空间投影转换矩阵MVP(Model View Project)不同的是,其中的V是 Projector 空间的相关矩阵,即 Projector 组件所属的 Transform 的 worldToLocalMatrix变量,而P则是和Projector远近裁切相关的矩阵。用 unity_Projector 矩阵计算出vertex(顶点)在投影空间中的坐标后,我们就可以以此坐标为uv坐标绘制物体了。
投影着色器中为什么可以将顶点坐标视为uv坐标呢?当坐标变换到投影空间后,其坐标空间就变成了投影平面视角,在这个视角中如果我们将平面视为纹理大小,就相当于一一匹配上顶点的uv坐标。
这个转化后的投影空间中的坐标,也就是我们常说的”投影纹理坐标”。”投影纹理坐标”不能直接当作uv来作为纹理坐标来使用,需要调用 tex2Dproj 方法来获取纹理坐标:
tex2Dproj(texture,uvproj);
这个纹理投影函数,其实就是在使用之前会将该投影纹理坐标除以透视值
可以等价于按如下方法使用普通二维纹理查询函数
float4 uvproj = uvproj/uvproj.w;
tex2D(texture,uvproj);
效果如下图:
缺图
###### 投影的技巧还有很多,我们下面介绍几种投影技巧在游戏项目中的运用。
平面阴影
平面阴影也是投影技巧的一种,它稍微需要运用些图形计算,主要的原理是在着色渲染模型时,另外做一个Pass将模型上顶点转换到平面上再渲染一次,以此作为平面的阴影,无论地上有没地形,都以平面呈现。如下图:
缺图
要计算这个平面阴影需要我们从光源点,顶点,法线,平面这些已知数据出发计算顶点转换到平面上的点。
我们首先已知的是地面法线向量 TerrainNormal,和平面上随便一个初始点点的坐标 TerrainPos,我们假设我们需要计算的点为p点
由平面表达公式得知,平面上的任意向量与该平面的点乘所得值为0,因此地面上的方向向量 TerrainNormal 与 要投影的坐标与初始点所形成的方向向量点乘为零,即:
(p - TerrainPos) 点乘 TerrainNormal = 0
又由于平面映射的P点是由光射到顶点延伸到平面而得到的,所以
p = d*L + L0 其中L0为顶点,的l为光到顶点的射线方向,d为L0射到p点的距离。
因此根据这两个公式,代入得到:
(dL + L0 - TerrainPos) 点乘 TerrainNormal = 0
解析后为
dL 点乘 TerrainNormal + (L0 - TerrainPos) 点乘 TerrainNormal = 0
于是再得到d为:
d = ((TerrainPos - L0) 点乘 TerrainNormal) / (l 点乘 TerrainNormal)
再代入进 p = d * l + L0 这个公式得到p,即如下Shader中的顶点函数所写
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float4 TerrainPos, TerrainNormal;
v2f vert (appdata v)
{
v2f o;
float4 wPos = mul(unity_ObjectToWorld, v.vertex);
// 光的方向
float3 direction = normalize(_WorldSpaceLightPos0.xyz);
// d 值的计算
float dist = dot(TerrainPos.xyz - wPos.xyz, TerrainNormal.xyz) / dot(direction, TerrainNormal.xyz);
// 代入 p = d * l + L0 公式
wPos.xyz = wPos.xyz + dist * direction;
// 空间顶点转换
o.vertex = mul(unity_MatrixVP, wPos);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(0,0,0,1);
}
ENDCG
}
上述Shader中,利用了_WorldSpaceLightPos0来确定光的反向,也可以用一个光的坐标与顶点的差值来得到一个光的方向。计算过程的公式转换可以以这张手绘的图作为参考。
图中清晰的标明了所有已知向量,和可计算向量,以及最终需要结算的点,上述所说的这些公式的转换都是基于这个图来做的。
利用深度信息计算图片投影
图片投影其实就是贴花的动态版,其做法也有很多种,我们在这里简单讲讲其中一种方法为:
绘制一个box,其纹理的展示方式则以深度信息为依据,纹理贴近其绘制的模型上。
这种方式可以随着这个box的移动贴到不同物体上。
大致的方向是,以一个BOX为渲染对象,渲染时在片元着色器中重新判定渲染纹理与渲染坐标。怎么判定呢?
先从顶点着色器上获得顶点坐标的屏幕坐标,在片元着色器中传入的屏幕坐标时成了片元在屏幕上的坐标,用屏幕坐标获取对应的深度值,再让深度值作为z坐标来形成一个三维坐标,这时三维坐标只是屏幕空间上的坐标,我们让其屏幕空间转换到相机空间,再从相机空间转换到世界空间,再转换到投影空间,这样坐标就到了投影空间,再除以透视值,就得到坐标在0-1范围的坐标值,用这个值去提取纹理上的颜色,最后用这个颜色绘制片元。
这个方法比较费的点为片元着色器中重新计算渲染坐标,这个坐标会以深度信息为z轴信息需要经过几个空间的转换,导致计算量比较大,不能放在顶点着色器中的原因是因为只有在片元着色器中才能得到片元深度信息。
贴花(喷图)
贴花的制作,可以以摄像机为节点向外计算一个长方体,所有与长方体有交集的模型上的面上的顶点都被存储起来,另外如果有顶点里外都有的面,则计算与长方体的面相交的新节点并加入进来,最后把所有得到的顶点和三角面制作成一个新的模型,这个模型的顶点转换到立方体空间再除以透视值,则成为了uv点,以此来呈现一个贴花(喷图)的效果。
参考文献:
《Unity3D ShaderLab 开发实战详解》
《维基百科 Line–plane intersection》 https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection
Planar Shadow http://qiankanglai.me/2016/12/23/planar-shadow/
感谢您的耐心阅读
Thanks for your reading
版权申明
本文为博主原创文章,未经允许不得转载:
《Unity3D高级编程之进阶主程》第七章,渲染管线与图形学(五) - Projector投影原理
Copyright attention
Please don't reprint without authorize.
微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解
QQ交流群: 777859752 (高级程序书友会)