franciscochonge 发表于 2022-4-3 18:42

Unreal Engine4 一个略诡异的bug

最近组上有同学反馈ue4开启Shader的debug模式后(skip optimize),角色的脸上出现黑色闪烁的斑块(z-fighting的症状)。正常的发布模式暂时没有发现问题。本来没有太在意这个问题。版本也比较赶时间。直到年前有一点点的时间空余出来。同时也非常好奇这到底啥问题引起的,总得给大家解个惑吧。然后开启了为期近3个晚上的查bug之旅。
起初怀疑是shadow map的自阴影问题。关闭阴影后。症状仍然存在。
上render doc调试。发现base pass就有问题了。
使用查看像素的历史纪录,发现有pixel touch.但是pixel的深度测试未通过(有开prepass(depth only pass))。遂回头查看是否是材质有使用特殊节点,发现在vertex stage使用了一些顶点偏移的处理。如果去掉这个,就正常了。随后继续深入一下这个偏移是怎么做的。发现是使用了顶点的uv做其偏移的数据。由于是skin mesh.在vs是通过uniform buffer存取uv的。每个顶点通过vertex id来索引自己的uv。如果使用一个固定值做偏移就没有问题了。基本可以判断是这个节点出了问题。1。要么是uniform buffer出了问题。2.要么是vertex id出了问题。然后诡异的事情发生了。uniform buffer的数据ok。 vertex id也没有出现越界的情况。但就是不对。线索还得回到render doc查一下。看看是否有无效浮点等问题导致了该pixel出现了问题。使用render doc调试inf等问题。确实找到了一些无效浮点问题。随后修改了一下。去掉了所有浮点数的问题。并确保不出现无效浮点数后。症状仍然存在。更诡异的是,basepass出来的depth值跟depth buffer的值是一样的。depth func是less equal。难道是depth buffer精度的问题吗?再次确认已经为D32S8了。理论上不会有精度问题。那到底是啥问题呢?是不是inverse-z的问题?然后又把ue4的depth相关的改成了正常的z顺序。症状仍然存在。
回到ue4的代码再看看。里面有一段是这样的:
//basepassvertexshader.usf
        FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
        float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
        float4 WorldPosition = WorldPositionExcludingWPO;
        float4 ClipSpacePosition;

        float3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);       
        FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal);

        // Isolate instructions used for world position offset
        // As these cause the optimizer to generate different position calculating instructions in each pass, resulting in self-z-fighting.
        // This is only necessary for shaders used in passes that have depth testing enabled.
        {
WorldPosition.xyz += GetMaterialWorldPositionOffset(VertexParameters);
        }
//DepthOnlyVertexShader.hlsl
        // Isolate instructions used for world position offset
        // As these cause the optimizer to generate different position calculating instructions in each pass, resulting in self-z-fighting.
        // This is only necessary for shaders used in passes that have depth testing enabled.
        {
index += GetMaterialWorldPositionOffset(VertexParameters);
        }
关键部分就在我标黑色的这行代码和它的注释。大概意思是编译器对即使相同的代码编译后,和相同的入参,仍然不保证有相同的结果。针对prepass 和 base pass。正常情况下如果可被看见的像素,他们的z应该是相等的。但是就是由于存在编译器的优化。即使你在两个pass中计算vertex position的算法完全一致。都有可能导致输出的z不一致。从而出现z-fighting的情况。
这个问题应该怎么改?dx sdk的文档有说明,可能大家比较陌生一点。我整个链接:
Variable Syntax - Win32 apps | Microsoft Docs
仔细看一下变量的声明 有一个标签 precise
如果带有这个标签的变量将不会被优化。然后做了如下操作:
struct FBasePassVSToPS
{
        FVertexFactoryInterpolantsVSToPS FactoryInterpolants;
        FBasePassInterpolantsVSToPS BasePassInterpolants;
        float4 precise Position : SV_POSITION;
};
depth only pass使用到的也做类似的操作。
问题解决了。。。
会不会有性能损失?可能有。但是我的测试数据没有发现明显差异。加上这个关键字后。汇编指令有不同吗?
有。以前是类似mov c a b 或者add c a b会变成mov c a b或者add c a b等等 会加上一个约束。
其它平台是否有类似问题?如何处理?
其它平台比如opengl /opengl es也有类似问题。可以使用修饰符 invariant
shader - Invariant and precise keywords in GLSL - Stack Overflow
metal也有类似问题:
https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
问题就解决的差不多了。
其实之前在玩cuda就有类似的概念了。fast-math。以及精度问题了。但是自己没太往这个方向想。看来还是知识的连贯性不够。加油吧~。
页: [1]
查看完整版本: Unreal Engine4 一个略诡异的bug