返回未定义结果的 GLSL 日志

GLSL log returning an undefined result

我正在尝试绘制 mandelbrot 集。我已经在 CPU 上创建了算法,但现在我想在 GPU 上重现它,但代码的行为有所不同。

在 CPU 程序中,有一次,我取 std::abs(z),其中 z 是一个复数,并将值写入屏幕上的绿色通道。

在 GPU 上,我采用相同的 z 并调用以下函数(Vulkan、GLSL):

double module(dvec2 z) {
    return sqrt(z.x * z.x + z.y * z.y);
}

double color(z) {
    return module(z);
}

当我将 color(z) 写入绿色通道时,我得到了与 CPU 程序完全相同的图片,因此代码的工作方式完全相同,至少到那时为止是这样。

接下来,我将 CPU 代码更改为取 std::log(std::abs(z)) / 20 并将其放入绿色通道。这是我得到的图像(mandelbrot 集中的数字为白色):

您可以看到绿色从未被剪裁,因此每个像素的结果都在 (0, 1) 范围内。

然后我将 GPU 代码更改为:

double module(dvec2 z) {
    return sqrt(z.x * z.x + z.y * z.y);
}

double color(z) {
    return log(module(z));
}

我把color(z) / 20写进了绿色通道。这是生成的图像:

如您所见,color(z) / 20 的值必须 <=0。我尝试将 color 函数更改为:

double color(z) {
    return -log(module(z));
}

查看该值是 0 还是负数。我仍然得到相同的图像,因此该值必须为 0。为了确认这一点,我再次更改代码,现在更改为:

double color(z) {
    return log(module(z)) + 0.5;
}

并将 color(z) 写入绿色通道(将除法减去 20)。我希望结果是中等绿色。

令我惊讶的是,图像没有变化,像素仍然是黑色。

百思不得其解,我又恢复原状:

double color(z) {
    return log(module(z));
}

但是,我将 color(z) + 0.5 写入绿色通道,我得到了这个:

总而言之,log(module(z)) 似乎是 returning 一些未定义的值。如果你否定它或试图向它添加任何东西,它仍然是未定义的。当此值为 return 来自具有 double 作为 return 类型的函数时,值 returned 为 0,现在可以将其添加到。

为什么会这样?函数 module(z) 保证 return 一个正数,因此对数函数应该 return 一个有效结果。 std::logGLSL log的定义都是自变量的自然对数,所以值应该完全一样(忽略精度误差)。

如何让 GLSL log 正常运行?

事实证明,当你要求它计算一个非常大的数字的对数时,GPU 并不真正喜欢它。据我所知,log(实际上是 ln)是作为泰勒级数实现的。这很不幸,因为它包含 n 个成员的 n 次方的多项式。

但是,如果您有一个表示为 x = mantissa * 2^exp 的数字,您可以从以下公式得到 ln(x)

ln(x) = exp * ln(2) + ln(mantissa)

无论 x 是什么,尾数都应该小得多。这是片段着色器的函数:

float ln(float z) {
    int integerValue = floatBitsToInt(z);
    int exp = ((integerValue >> mantissaBits) & (1 << expBits) - 1)
              - ((1 << (expBits - 1)) - 1);

    integerValue |= ((1 << expBits) - 1) << mantissaBits;
    integerValue &= ~(1 << (mantissaBits + expBits - 1));

    return exp * log2 + log(intBitsToFloat(integerValue));
}

请注意,在 GLSL 中,此技巧仅适用于浮点数 - 没有 64 位整数类型,因此没有 doubleBitsToLong,反之亦然。