Godot 中同一投影点的采样深度与计算深度为何相差很大?
优先检查两次计算是否使用了同一坐标空间,以及计算出的 UV 是否指向深度图中的同一位置。两段代码使用了相同的深度公式,但传给 flashlight_projection 的坐标含义不同:一端是网格局部坐标,另一端是相对 2D 相机的位置。即使地面高度为零,这种差异也可能导致整片区域出现深度偏差。
现有材料没有给出 flashlight_projection 的构造代码、地形变换和 viewport 设置,因此还不能确定唯一根因。可以按以下顺序排查。
1. 先统一坐标空间
深度 shader 中的这行代码没有得到世界坐标:
v_world_position = VERTEX;
在当前使用的默认 spatial shader 变换方式下,vertex() 中的 VERTEX 是模型局部坐标,变量名不会改变它的坐标空间。要转换到世界坐标,需要使用 MODEL_MATRIX。相关定义见 Godot 4.6 Spatial shaders 文档。
覆盖层传入的坐标则是:
vec2 vector_from_camera = position - camera_position;
代入 get_global_from_UV() 后,得到:
vector_from_camera = (SCREEN_UV - 0.5) * viewport_size / zoom;
这里的 camera_position 被抵消了。除非地形和光源矩阵也使用同一个随 2D 相机移动的坐标原点,否则两端不会表示同一个场景点。
建议明确采用以下变换关系:
网格局部坐标 → MODEL_MATRIX → 世界坐标
世界坐标 → 光源 VIEW_MATRIX → 光源视空间
光源视空间 → 光源 PROJECTION_MATRIX → clip space
也就是:
clip = P_light * V_light * world_position
如果 flashlight_projection 只是投影矩阵 P_light,输入必须已经处于光源视空间;如果它包含 P_light * V_light,输入必须是世界坐标。先检查传入 uniform 的实际内容,避免遗漏或重复执行 view 变换。
2. 让编码矩阵与实际渲染相机一致
第一段 shader 没有写入 POSITION,因此网格实际落在哪个像素,仍由 Godot 的正常顶点变换决定。片元中自行计算的 flashlight_projection 只影响写入颜色的数值,不会改变片元所在的位置。
实际渲染使用的相机与自定义矩阵必须匹配,包括位置、朝向、投影方式、宽高比及裁剪范围。否则,即使颜色中的深度计算正确,覆盖层也可能从另一个位置读取它。
如果统一使用世界坐标,可以将深度 shader 的关键部分改为:
uniform mat4 light_view_projection;
varying vec3 v_world_position;
void vertex() {
VERTEX.y = height_scale * texture(heightmap, UV).r;
v_world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
void fragment() {
vec4 clip_space = light_view_projection
* vec4(v_world_position, 1.0);
float encoded_depth = -(clip_space.z / clip_space.w) * 0.5 + 0.5;
ALBEDO = vec3(encoded_depth);
}
这里的 light_view_projection 必须是与深度 viewport 实际渲染相机对应的 P_light * V_light。示例保留了原来的深度编码,便于先检查坐标变换。
覆盖层也要构造同一世界坐标系中的点。只有在 2D 的 (x, y) 与 3D 的 (x, z) 原点、方向和单位完全一致时,零高度地面才能直接写成:
vec2 position = get_global_from_UV(SCREEN_UV);
vec3 world_position = vec3(position.x, 0.0, position.y);
vec4 clip_space = light_view_projection * vec4(world_position, 1.0);
如果地形有平移、缩放或轴向转换,需要在这里补上对应映射。对于非零高度的接收点,也不能始终把高度写成 0.0。
3. 单独验证 UV,避免把采样位置错误当成深度错误
原代码无条件翻转了纹理 Y 坐标:
flashlight_depth_texture_uv.y = 1.0 - flashlight_depth_texture_uv.y;
是否需要这一步,应根据实际纹理方向确认。可以在地形不同角落放置可辨识的标记,检查投影后的 UV 是否落在对应位置。仅凭深度灰度图,通常不容易发现上下颠倒。
另外,设置 COLOR.a = 0.0 不会终止后续执行。原代码在 clip_space.w 无效时仍会继续除法,在 UV 越界后仍会采样。对于不需要绘制的片元,可以提前 discard:
if (clip_space.w <= 1e-5) {
discard;
}
vec2 depth_uv = clip_space.xy / clip_space.w * 0.5 + 0.5;
// 仅在验证纹理方向后保留这行。
depth_uv.y = 1.0 - depth_uv.y;
if (any(lessThan(depth_uv, vec2(0.0))) ||
any(greaterThan(depth_uv, vec2(1.0)))) {
discard;
}
除了检查 XY 范围,还应按实际投影约定排除 near/far 范围之外的点。
原来的屏幕坐标反推公式也有适用条件:它假设视口中心对应 camera_position,并且没有尚未计入的相机旋转、偏移或额外 canvas 变换。如果这些条件不成立,应根据实际 canvas 变换重建位置。
4. 确认颜色纹理保留了写入的深度值
当前代码通过 ALBEDO 存储深度,因此 flashlight_depth 实际上是承载深度编码的颜色纹理。写入和读取时使用相同公式,并不能证明中间的颜色输出与采样过程没有改变数值。
先把深度 shader 的输出临时改成固定值,例如:
ALBEDO = vec3(0.25);
再分别测试 0.5 和 0.75,检查覆盖层采样得到的 .r。应检查数值,不要只看显示出来的灰度。
如果固定值无法按预期读回,应先检查颜色空间转换、tonemapping、后处理、混合、抗锯齿和纹理格式精度。unshaded 不能证明整条数值传输链路保持了原值。
还需要确认 heightmap 是否按预期解释为数值数据。原声明带有 source_color;高度数据是否应经过颜色空间转换,要根据纹理的实际编码决定,不能仅因为它表现为灰度就认定设置正确。
5. 深度编码要一致,遮挡比较要有方向
原来的编码是:
float encoded_depth = -(clip_space.z / clip_space.w) * 0.5 + 0.5;
不要仅根据 depth_01 这个名称认定它覆盖了完整的 [0, 1] 范围。Godot 的 NDC Z 范围与渲染器有关,Forward+、Mobile 与 Compatibility 的约定存在差异,还需要考虑 reverse Z。具体说明见 Godot 4.6 深度纹理文档。
如果两端确实对同一个点应用同一个矩阵和同一个编码,数值应当一致。仅修改这条公式中的负号或 0.5,不足以解决坐标或采样位置不一致的问题。
这里的数值是投影深度编码,并非到光源的欧氏距离。用于比较时不一定需要线性化,但两端的表示必须一致。
下面的绝对差适合显示误差:
abs(sampled_depth - point_depth)
它不能直接表达遮挡关系。阴影映射要判断目标点是否位于当前光线方向上最近表面的后方。
如果编码值随距离增大而增大:
bool occluded = point_depth > sampled_depth + bias;
如果编码值随距离增大而减小:
bool occluded = point_depth < sampled_depth - bias;
应使用已知远近的两个点确认比较方向。bias 用来容纳采样和精度误差,不能掩盖整片区域的坐标偏移。没有几何体写入的背景像素,也需要使用与编码方向一致的清除值,或通过单独的有效性标记识别。
6. 建议的排查顺序
- 使用零高度平面,明确 2D 坐标与 3D 地形坐标的映射。
- 检查
flashlight_projection的构造,确认两端输入空间以及实际渲染相机是否一致。 - 用位置标记验证投影 UV,确认是否需要翻转 Y。
- 用固定灰度值检查颜色写入和采样是否保持数值。
- 在匹配位置上查看未经
abs()和clamp()处理的深度差,保留误差方向和大小。 - 最后加入非零高度、真实遮挡物、单向比较和适当的
bias。
最值得先检查的是 v_world_position = VERTEX 与 position - camera_position 这两处:它们目前没有保证两端使用同一坐标空间。
备注:内容仅供参考。
