ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

UE4投影矩阵原理与实战:反向Z、深度重建与常见坑

UE4投影矩阵原理与实战:反向Z、深度重建与常见坑 搞渲染或者引擎底层相关的东西绕不开“投影矩阵”Projection Matrix这个东西。很多人蓝图里拖个节点就能拿到手但一旦遇到自定义渲染、深度重建、镜像平面、特殊镜头效果就得亲自跟矩阵打交道。这篇就按我实际摸过的路子把UE4里的投影矩阵掰开揉碎讲一遍原理、获取方式、实操案例和踩坑记录都在里面了。1. 投影矩阵到底在做什么从三维场景到屏幕的那一步1.1 一张屏幕截图背后的完整变换链路你先想一个问题场景里一个三维点凭什么最后能变成屏幕上某个像素中间其实经历了四步模型空间到世界空间世界空间到视图空间也就是相机空间视图空间到裁剪空间裁剪空间做透视除法变成NDC归一化设备坐标最后视口变换映射到屏幕像素坐标。投影矩阵干的就是第三步把视图空间里的视锥体Frustum压成一个标准裁剪立方体。透视投影和正交投影的矩阵长不一样本质区别就在“xyz怎么压、w怎么参与”。透视投影里w分量会被设置成视图空间的深度值这就是近大远小的根源正交投影里w恒为1所以没有透视缩放。很多人把“投影”理解成“把3D变成2D”这个说法不严谨。投影矩阵之后还没有真正变成2D它只是把视锥体变成了裁剪空间的立方体真正的降维发生在透视除法除以w之后。1.2 为什么UE4要单独拧出投影矩阵来讲因为UE4在现代图形API的约定上有自己的习惯。首先UE4是左手坐标系Z轴朝上相机朝X方向看这跟很多图形学教材里的右手坐标系不一样。其次UE4在绝大多数平台上默认使用反向ZReversed-Z深度近裁剪面映射到深度1.0远裁剪面映射到0.0这样能大幅提升深度的浮点精度。这两个习惯直接决定了矩阵长得跟教材上的公式不完全一样。你如果直接拿一篇OpenGL教程里的矩阵塞进UE4大概率渲染出来是错的。这也是项目里经常有人“矩阵调不动”的真实原因——不是矩阵本身错了是约定没对齐。2. UE4里投影矩阵的实现细节与获取方式2.1 UE4坐标系约定和矩阵存储方式先把这个搞明白UE4的FMatrix在内存里是一维数组M[0][0]到M[3][3]但它在HLSL Shader里通常配合mul(float4(pos, 1.0), Matrix)这种写法也就是行向量左乘矩阵。很多从OpenGL/OpenGL ES迁过来的人习惯mul(Matrix, vec4(pos, 1.0))拿到UE4就发现矩阵要转置一下。这里有一个我实战里养成的习惯碰到从网上扒来的投影矩阵公式先在CPU端打印一遍UE4自己的投影矩阵跟自己构造的矩阵做对比元素对不上就先转置再对比别盲改Shader。UE4源码里FMatrix的存储虽然看着像行优先但跟渲染管线的整体约定是配套的你要改的是自己那颗“外来矩阵”。2.2 FPerspectiveMatrix与反向ZUE4默认投影矩阵的真相UE4里常用的透视投影矩阵封装有几个FPerspectiveMatrix、FPerspectiveMatrixFov、FReversedZPerspectiveMatrix等。默认情况下场景视图的投影矩阵是带反向Z的也就是把原来的z映射公式反过来用。反向Z的好处在于浮点深度缓冲在近处精度高、远处精度低而一般的深度线性化之后分母里有far值导致远处精度浪费严重。反向Z把1.0留给近处0.0留给远处配合浮点缓冲能把精度分布调得跟视觉敏感度正好匹配。理解了这一点你就知道为什么自己手动搭一个标准透视矩阵时场景里会出现离相机越远越容易抖动花片的诡异现象——因为你的深度范围和平台默认的深度约定反了。2.3 C和蓝图中获取投影矩阵的几种实用姿势蓝图层面上最常见的节点是“Project World Space To Screen Position”和“Get View Size”这两个节点底层就是在用投影矩阵做换算。你不需要手动拼接但如果你需要的是原始矩阵值蓝图里没有现成节点必须走C。在C里我常用的获取位置是在FSceneView的ViewMatrices结构里。具体来说const FViewMatrices ViewMatrices View.ViewMatrices; const FMatrix ViewProjectionMatrix ViewMatrices.GetViewProjectionMatrix(); const FMatrix ProjectionMatrix ViewMatrices.GetProjectionMatrix(); const FMatrix ViewMatrix ViewMatrices.GetViewMatrix();FViewMatrices里还提供了GetInvProjectionMatrix()和GetInvViewProjectionMatrix()做深度重建或者反向投影时直接拿逆矩阵省得自己求逆。这里有个提醒FSceneView的投影矩阵默认包含了反向Z和平台差异比如某些移动平台甚至会用不对称投影矩阵做单眼渲染所以一定不要用自己MakeProjectionMatrix的普通矩阵去替代它。3. 实操案例三个能落地能跑的投影矩阵玩法3.1 从屏幕像素反推世界坐标自己动手写深度重建屏幕空间特效、自定义水波纹、后处理里做体积光经常需要从当前像素的UV和深度反推世界坐标。思路很清楚拿到屏幕UV对应的NDC坐标再把NDC坐标乘上“视图投影矩阵的逆”就能得到世界空间位置。在材质编辑器里我常用的Custom节点写法大致长这样float2 UV screenPosition * ViewSizeAndInvSize.zw; // 或者直接用 ScreenPosition 节点 float DeviceDepth GetSceneTextureUV(Parameters, 14).r; // 这里是场景深度非反向Z需额外处理 float4 ndc; ndc.x UV.x * 2.0 - 1.0; ndc.y (1.0 - UV.y) * 2.0 - 1.0; // 注意Y轴翻转 ndc.z DeviceDepth; ndc.w 1.0; float4 worldPos mul(ndc, ResolvedView.InverseViewProjection); worldPos.xyz / worldPos.w;注意几点设备的深度在非反向Z情况下需要做Depth Depth * 2.0 - 1.0的换算但在反向Z管线里深度值本身就是反的如果直接按标准公式来重建出来的位置会在相机身后乱跑。判断自己项目是不是反向Z最快的办法就是C里打印FReversedZPerspectiveMatrix之类的类型或者看第一个深度值在近裁剪面时是接近1还是接近0。这个案例做出来后你可以顺便加一个“点击屏幕高亮像素对应的世界坐标点”的小工具调试其他渲染问题时特别有用。3.2 用自定义宽高比和FOV做“镜头畸变”风格的投影某些演出效果或者游戏开场动画里需要模拟鱼眼镜头的透视畸变或者瞬间改变FOV营造速度感。直接修改主相机的FOV在蓝图里也能做但颗粒度太粗而且会导致UI、粒子等所有跟随相机的东西都变。更好的方式是单独构造一个投影矩阵用在特定的网格材质上或者做全屏后处理。举个例子假设你要做一个“边缘拉伸”的伪鱼眼效果思路是改变透视除法时使用的w值。标准透视矩阵中w分量来自视图空间的 -z也就是深度。你可以把矩阵最后一行改成“部分乘以一个系数这个系数依赖x或y”让同一个像素在不同屏幕位置拥有不同的“缩放程度”。具体的矩阵改法不唯一我给出一个参数化的伪代码思路// 假设 baseFov 90度scaleFactor 控制畸变强度 float fovRad FMath::DegreesToRadians(BaseFov); float cot 1.0f / FMath::Tan(fovRad * 0.5f); FMatrix DistortionProj FMatrix::Identity; DistortionProj.M[0][0] cot / AspectRatio; DistortionProj.M[1][1] cot; DistortionProj.M[2][2] -(Far Near) / (Far - Near); DistortionProj.M[2][3] -1.0f; DistortionProj.M[3][2] -2.0f * Far * Near / (Far - Near); // 关键让 w 分量随屏幕位置变化 DistortionProj.M[3][0] 0.3f; DistortionProj.M[3][1] 0.3f;上面这个矩阵第4行不再只由z决定而是掺入了x和y的信息透视除法的分母在不同位置就会不同从而出现桶形畸变。不过要提醒的是这种做法会让影子和深度计算变得不可预测只适合后处理或非光照的装饰性网格不适合一上来就替换主相机的投影矩阵。3.3 镜像/传送门效果里的反射视角投影矩阵的“歪用”做镜面反射或传送门效果时核心不只是“画一个平面”而是让相机相对于反射平面翻转拿到一张正确的反射纹理。这里其实分两步视图矩阵要翻转投影矩阵的裁剪面也要跟着调整才能把反射后视锥体以外的内容正确切掉。我在某个模拟项目里实现过一个简版镜面复制一个SceneView然后手工修改视图矩阵的Y轴方向把相对于镜面的观察方向翻转再把近裁剪面设置为镜面到相机距离减去一个小的偏移避免反射内容穿到相机背后导致画面撕裂。投影矩阵在这一步不需要大改但必须同步更新近远裁剪面。如果只翻转视图矩阵不调整投影矩阵反射画面里会出现物体被“砍掉一半”或者被背后遮挡的现象像是有一道看不见的墙。具体代码我不全贴了核心是// 在CustomDepthPass或者SceneExtension里 NewViewMatrix SceneView-ViewMatrices.GetViewMatrix(); // 对镜面平面做反射 NewViewMatrix.M[0][2] -NewViewMatrix.M[0][2]; NewViewMatrix.M[1][2] -NewViewMatrix.M[1][2]; NewViewMatrix.M[2][2] -NewViewMatrix.M[2][2]; NewViewMatrix.M[3][2] -NewViewMatrix.M[3][2]; ProjMatrix SceneView-ViewMatrices.GetProjectionMatrix(); // 保证近裁剪面不会比镜面还近 float DistanceToMirror FVector::DotProduct(...); float NewNear FMath::Max(1.0f, DistanceToMirror - 5.0f); // 重新生成投影矩阵这里面的关键教训是不要试图手动“补”一个翻转矩阵而是把视图矩阵里跟镜面法线相关的分量直接翻转投影矩阵只改近裁剪面。这套路说白了就是“渲染半条命”的老玩法但放到UE4里细节特别多一帧里要处理的东西比想象中多。4. 投影矩阵常见问题与排查实录4.1 画面拉伸变形、深度闪烁、转置迷局我整理了一张实操问题速查表做渲染相关调试时基本都能用到。现象最可能的原因怎么处理场景物体横竖比例不对圆球变椭圆宽高比Aspect算错投影矩阵使用了错误的ViewWidth/ViewHeight核对一下用到的分辨率是“视图实际渲染分辨率”还是“视口UI分辨率”别混用远处物体疯狂闪烁近处正常深度精度不够常见于near/far比例过大或者改了非反向Z矩阵调小Far/Near比值、开启反向Z或者把深度缓冲改成浮点格式自定义Shader里位置对不上世界空间乱飞矩阵乘法顺序错了或者行列向量约定不一致先打印一次引擎原始矩阵和自定义矩阵逐元素对比确认行优先还是列优先渲染结果和蓝图节点算出来差一点点没考虑平台特定的裁剪矩阵比如VR单目渲染的不对称投影获取ProjectionMatrix时用FSceneView平台裁剪的那一套不要自己重建深度闪烁这个我多说一点。假设你的Near是10Far是10000场景又比较大传统的24位深度缓冲下只要相机稍微移动一点巨远处的山体交界线就会疯狂跳动。但如果你把Far改成5000Near保持10又会看不到很远的东西。这种情况下有的团队会改用“对数深度”或者“反向Z”。在UE4里最简单的路径反而是保证自己不要覆盖引擎默认的投影矩阵因为默认矩阵已经做了大部分优化。4.2 用RenderDoc和断点调试投影矩阵排查渲染问题我最推荐的思路是分三路走。第一路是抓RenderDoc。RenderDoc的Mesh Viewer可以看每一帧VS顶点着色器输入的常量和输出。如果发现网格跑飞到十万八千里外直接看VS输出的Clip Position跟CPU端的投影矩阵算出来的结果做比对很快就能锁定是矩阵的问题还是Shader代码的问题。第二路是在C里断点打印。在SceneView的调DisplayDebug或自定义延迟渲染插件代码里拿到ViewProjection矩阵然后打印关键元素比如M[2][2]和M[3][2]。反向Z矩阵和标准矩阵这两个位置的值差异极其明显一眼就能看出来你是在用默认矩阵还是一颗“野生矩阵”。第三路是写一个像素调试器把深度值和屏幕UV编码成颜色输出到屏幕。算出来之后跟“Project World Space To Screen Position”节点结果交叉验证如果偏差超过0.5像素那基本可以断定是矩阵或者UV翻转的问题不用去怀疑数学公式本身。5. 关于改动投影矩阵我想说的几件小事5.1 别乱动SceneView的三条军规每一条都是从实际项目里摔出来的第一条不要在渲染线程中途直接改SceneView-ViewMatrices的投影矩阵。那玩意儿是每帧同步好的读的地方很多你中途改了后处理、阴影、粒子、UI全都跟着乱。真要改就在CameraManager或者SceneViewExtension里做一个副本然后在合适的位置覆盖渲染用的矩阵。第二条凡是涉及深度重建、屏幕空间光线步进、自定义ShadowProjection的永远要用“同一份”视图投影矩阵。有时候你会因为省事分别拿GetProjectionMatrix()和GetViewMatrix()再拼一次但里面可能有一个已经包含平台差异一个没包含最后差之毫厘谬以千里。第三条不要随手复用其他项目的投影矩阵代码。不同引擎的投影矩阵公式看着差不多但Z的范围0到1还是-1到1、Y轴朝向向上还是向下、坐标系手性左手还是右手这三板斧只要有一个不一样渲染出来就是完全不同的画面。复制粘贴前先花十分钟把MakeProjectionMatrix的参数和函数注释看清楚。5.2 一正一反两个实际项目踩坑记录先讲一个反面的。之前做某个跨平台系统项目我在PC上自定义了一个投影矩阵做某种“非均匀缩放镜头”的特效PC上看起来一切正常。打包到某移动设备上一看画面直接半边全绿半边全红后来排查半天发现是移动端的旋转屏幕逻辑会重新构造视口而我的自定义矩阵没有跟着朝向一起旋转导致投影矩阵和视口尺寸交叉错位。从那以后我养成了一个习惯在项目里写一个“矩阵链路自检”的隐藏调试模式每帧打印视口宽高、投影矩阵前两行至少能快速看出分辨率方向变了没。再看一个正面的经验。另一个模拟类项目里需要在指定区域显示一种“透视透明”的预览效果当时不想改动材质和写深度去排除物体索性直接构造了一个带逆向深度偏移的小范围透视矩阵把FOV故意调大让边缘物体轻微拉伸配合深度越小越清晰的逻辑最后效果既省开销又可控。这个案例告诉我投影矩阵不只是一个“把3D变2D”的数学工具它本身就是一种可以灵活调用的表现资源关键在于你要知道自己在改哪一行、改了之后会影响哪些依赖它的功能。要在UE4里玩转投影矩阵核心就是三件事搞懂约定手性、Z范围、行列向量、掌握获取/备份/替换的代码路径、最后习惯用RenderDoc一帧一帧地比对验证。这三点做到了你基本就能驾驭绝大部分跟投影相关的奇怪需求了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表