基于法线的相邻像素深度投影问题:卡通游戏描边异常排查
我正在为一款低分辨率卡通风格游戏开发屏幕空间描边效果。作为HLSL编程和全屏Shader Graph的新手,我通过教程实现了基于深度的描边(即绘制相机近处与远处表面间的线条),但遇到了一个问题:当表面与视角夹角极小时,表面上相邻像素的深度差异会被系统检测为边缘,导致整个表面被涂黑。
当前系统的逻辑是:每个像素检查其四个相邻像素,若任一相邻像素的实际深度比“假设该像素位于当前像素法线所在平面”的预期深度更近,则将当前像素设为描边色。以下是HLSL代码:
//Return 1 if any of the current pixel's neighbors are closer to the camera than would be expected if they were both on a plane //with the current pixel's normal void Depth4Neighbors_float(float2 UV, float pixelSize, out float Out) { float depth = LinearEyeDepth(SHADERGRAPH_SAMPLE_SCENE_DEPTH(UV), _ZBufferParams); [unroll] for(int i = 0; i < 4; ++i) { float expectedNeighborDepth = depth - SHADERGRAPH_SAMPLE_SCENE_NORMAL(UV) * pixelSize * fourNeighborsSamplePoints[i]; float neighborDepth = LinearEyeDepth(SHADERGRAPH_SAMPLE_SCENE_DEPTH(UV + fourNeighborsSamplePoints[i] * pixelSize), _ZBufferParams); if(neighborDepth < expectedNeighborDepth) { Out = 1; return; } } Out = 0; }
我搭配了对应的Shader Graph(包含子图和全屏图),但运行后几乎所有非直面相机的表面都被描边覆盖(描边色设为透明红时的效果如图所示)。
我尝试过添加阈值(仅当预期深度与实际深度差超过一定值时才绘制描边),但无论调整什么阈值,都无法在保留边缘敏感度的同时消除该问题,且我需要实现类似《Pizza Tower》的精确1像素描边(因此未使用Sobel算子等方法)。我已确保渲染通道能访问场景深度和法线信息。
请问是计算expectedNeighborDepth的数学逻辑有误、存在坐标空间错误(我找不到SHADERGRAPH_SAMPLE_SCENE_...函数的文档)、整体思路存在根本性缺陷,还是其他问题?
1. 核心问题:坐标空间不匹配
你的expectedNeighborDepth计算逻辑存在根本性错误——直接用法线(3D向量)和2D像素偏移量做运算,完全忽略了坐标空间的差异:
SHADERGRAPH_SAMPLE_SCENE_NORMAL返回的是世界空间或视图空间的3D法线向量,而fourNeighborsSamplePoints是2D屏幕UV的偏移方向。两者维度不同,直接相乘毫无意义,导致预期深度计算完全错误。- 低角度表面的法线和视角方向夹角大,错误的计算会让预期深度和实际深度的差值被无限放大,最终整个表面都被判定为边缘。
2. 正确的预期深度计算逻辑
要计算同一平面上相邻像素的预期深度,需要把2D的UV偏移转换到视图空间的3D方向,再结合法线计算深度变化,修正后的HLSL逻辑框架如下:
void Depth4Neighbors_float(float2 UV, float pixelSize, out float Out) { // 获取当前像素的线性眼空间深度 float depth = LinearEyeDepth(SHADERGRAPH_SAMPLE_SCENE_DEPTH(UV), _ZBufferParams); // 获取视图空间法线(Shader Graph中需设置法线纹理输出为视图空间) float3 viewNormal = normalize(SHADERGRAPH_SAMPLE_SCENE_NORMAL(UV)); // 反推视图空间位置 float3 viewPos = ComputeViewSpacePosition(UV, depth, _ZBufferParams); [unroll] for(int i = 0; i < 4; ++i) { float2 uvOffset = fourNeighborsSamplePoints[i] * pixelSize; // 计算相邻像素的视图空间偏移 float3 viewOffset = ComputeViewSpacePosition(UV + uvOffset, depth, _ZBufferParams) - viewPos; // 将偏移点投影到当前法线平面上,得到预期位置 float3 expectedViewPos = viewPos + viewOffset - dot(viewOffset, viewNormal) * viewNormal; // 转换为预期深度(视图空间z轴通常指向相机外,深度为负z) float expectedNeighborDepth = -expectedViewPos.z; float neighborDepth = LinearEyeDepth(SHADERGRAPH_SAMPLE_SCENE_DEPTH(UV + uvOffset), _ZBufferParams); // 添加合理阈值,避免平面内的微小误差 float depthDiff = abs(neighborDepth - expectedNeighborDepth); if(depthDiff > 0.01) // 阈值可根据游戏分辨率调整 { Out = 1; return; } } Out = 0; }
3. 针对《Pizza Tower》风格的优化
《Pizza Tower》的描边是基于物体边缘而非深度突变,你可以结合以下两点优化:
- 加入法线差异检测:同时对比当前像素和相邻像素的法线夹角,只有当深度差超过阈值且法线夹角超过15°-30°时,才判定为边缘,避免平面误判。
- 固定像素对齐阈值:低分辨率游戏中,直接用
pixelSize * 0.2作为深度阈值基准,匹配像素级的精度需求,确保描边是精确的1像素宽度。
4. 关于SHADERGRAPH_SAMPLE_SCENE_...函数的说明
这些是Shader Graph内置的场景数据采样函数:
SHADERGRAPH_SAMPLE_SCENE_DEPTH(UV):采样当前相机的深度纹理,返回非线性深度值,必须用LinearEyeDepth转换为线性眼空间深度才能进行正确计算。SHADERGRAPH_SAMPLE_SCENE_NORMAL(UV):采样场景的法线纹理,默认返回世界空间法线,你可以在Shader Graph的「Graph Settings」中把「Normal Space」切换为「View」,直接获取视图空间法线,减少坐标转换的麻烦。
内容的提问来源于stack exchange,提问作者TT87

