我有一个WebGL电路模拟器。它存在的问题之一是,由于仿真时使用了大量中间浮点纹理,因此无法在各种移动设备上运行。它们仅支持字节纹理。

我对此问题的预期解决方案是将高精度(即32位)浮点数编码为字节。每个输出浮点数都打包成接近IEEE的格式(我在另一端放了符号位以避免一些移位,我不执行非规范化的值,也不执行无穷大/ NaN)。同样,每个输入在使用前都要先进行打包。

我已经在Internet上找到了与此任务相关的各种博客文章和答案(示例1,示例2,示例3),但我没有找到了在所有有限的非规范化浮点数上都能正常工作的值。

我遇到的问题是精度。我想使浮点数往返而不会引入任何错误,但是我似乎无法制作一个可保留尾数全部23位的着色器。在某些机器上似乎总会有一些舍入丢失了最后一点,尽管我可以扰乱发生舍入的情况,并且在我测试过的各种机器上发生舍入的情况也不同。

这里是我的包装方法:

vec4 packFloatIntoBytes(float val) {
    if (val == 0.0) {
        return vec4(0.0, 0.0, 0.0, 0.0);
    }

    float mag = abs(val);
    float exponent = floor(log2(mag));
    // Correct log2 approximation errors.
    exponent += float(exp2(exponent) <= mag / 2.0);
    exponent -= float(exp2(exponent) > mag);

    float mantissa;
    if (exponent > 100.0) {
        // Not sure why this needs to be done in two steps for the largest float to work.
        // Best guess is the optimizer rewriting '/ exp2(e)' into '* exp2(-e)',
        // but exp2(-128.0) is too small to represent.
        mantissa = mag / 1024.0 / exp2(exponent - 10.0) - 1.0;
    } else {
        mantissa = mag / float(exp2(exponent)) - 1.0;
    }

    float a = exponent + 127.0;
    mantissa *= 256.0;
    float b = floor(mantissa);
    mantissa -= b;
    mantissa *= 256.0;
    float c = floor(mantissa);
    mantissa -= c;
    mantissa *= 128.0;
    float d = floor(mantissa) * 2.0 + float(val < 0.0);
    return vec4(a, b, c, d) / 255.0;
}


这是我的包装方法:

float unpackBytesIntoFloat(vec4 v) {
    float a = floor(v.r * 255.0 + 0.5);
    float b = floor(v.g * 255.0 + 0.5);
    float c = floor(v.b * 255.0 + 0.5);
    float d = floor(v.a * 255.0 + 0.5);

    float exponent = a - 127.0;
    float sign = 1.0 - mod(d, 2.0)*2.0;
    float mantissa = float(a > 0.0)
                   + b / 256.0
                   + c / 65536.0
                   + floor(d / 2.0) / 8388608.0;
    return sign * mantissa * exp2(exponent);
}


这种方法很接近。据我所知,它可以在我的笔记本电脑上工作。但这不适用于我的Nexus平板电脑。例如,浮点数-0.20717763900756836应该被编码为[124, 168, 76, 193]。当我打开包装然后在Nexus平板电脑上重新包装时,输出降低了一个ulp:[124, 168, 76, 191](编码为-0.2071776[5390872955])。接近,但是我想要完美。

大多数情况下,我都想弄清楚这种方法在哪里破坏了精度。更改几乎似乎具有随机效果,例如用x * exp2(n)替换x / exp2(-n)可能会在一个地方修复错误,但在另一地方引入错误。

有没有一种精确的方法可以将浮点数打包为字节而不丢失精度?


这种方法应在以下示例值上起作用:

var testValues = new Float32Array([
    0,
    0.5,
    1,
    2,
    -1,
    1.1,
    42,
    16777215,
    16777216,
    16777218,
    0.9999999403953552, // An ulp below 1.
    1.0000001192092896, // An ulp above 1.
    Math.pow(2.0, -126), // Smallest non-denormalized 32-bit float.
    0.9999999403953552 * Math.pow(2.0, 128), // Largest finite 32-bit float.
    Math.PI,
    Math.E
]);


评论

我投票结束这个题为离题,因为这是代表浮点数。这里没有什么是图形专用的。

@DanHulme在这个SE上似乎有很多问题,就是关于GPU和opengl / dx的“ X如何工作”。我碰巧只是间接地将结果用于绘图,但这并不影响该问题是否对其他人有用。结束感觉就像是在给我分叉,而我结束的路线对新用户而言根本不清楚。如果它们必须足够抽象的计算机科学任务并且没有具体的“ make GPU do X”问题,那么您将花费所有的mod时间关闭问题。

我们仍然是一个新站点,因此在结束问题上还没有很多一致性。特别是对于我来说,这比我更适合SO,因为这是“我如何使用该库来完成这一特定工作”-将WebGL作为一种编程环境而不是作为图形平台。但是我只是一个封闭的投票者,因此我们将了解社区的整体看法。无论哪种方式,谢谢您帮助我们考虑我们的范围。

关于问题本身,您是否仅通过调用两个没有纹理的函数就遇到了同样的问题?您可以确定错误不是由某些纹理过滤引起的吗?

我个人觉得这很有趣。实时计算机图形学的核心通常是“如何高效,准确地将数据传输到GPU”,而向着色器浮动则恰恰是正确的方向。例如,在做sharttoy事情时总是出现这种情况,最好的回答者可能是在那里解决了这些问题的人之一,无论是shadertoy还是在演示类型代码上,这些人都是IMO。