在GLSL/WebGL中,如何将一个32位整数打包成4个8位整数?

23

我正在寻找一种并行化复杂数学运算的方法,WebGL看起来是最完美的方法。

问题在于,你只能从纹理中读取8位整数。

我希望能够从纹理中获得32位数字。

我的想法是使用4个颜色通道每像素获得32位,而不是4次8位。

我的问题在于,GLSL没有“%”运算符或任何按位运算符!

简而言之:

如何通过使用glsl中的运算符将32位数字转换为4个8位数字?

有关该技术(使用按位运算符)的一些额外信息:

如何将64位整数存储在两个32位整数中并再次转换


OpenGL ES没有位运算符,因为图形硬件不实现整数操作。 - Matt Randell
4
这完全不正确。所有现代GPU都有ALU。OpenGL ES没有实现按位运算符,因为旧的着色器模型没有暴露这个功能。从着色器模型4.0(dx10)开始,引入了按位运算符。OpenGL 3.0+中的GLSL具有按位运算符,只有OpenGL ES的简化版GLSL没有。 - Andon M. Coleman
3个回答

33

通过乘以/除以2的幂次方来进行位移。

如评论中所指出,我最初发布的方法是有效但不正确的,这里有一个由Aras Pranckevičius提供的方法,请注意帖子中源代码包含一个错别字且为HLSL,这是一个已更正错别字并转换为GLSL的版本:

const vec4 bitEnc = vec4(1.,255.,65025.,16581375.);
const vec4 bitDec = 1./bitEnc;
vec4 EncodeFloatRGBA (float v) {
    vec4 enc = bitEnc * v;
    enc = fract(enc);
    enc -= enc.yzww * vec2(1./255., 0.).xxxy;
    return enc;
}
float DecodeFloatRGBA (vec4 v) {
    return dot(v, bitDec);
}

1
这非常聪明,我想知道为什么我没有想到只使用乘法来进行位移。 - william malo
1
这个 glsl 代码的 JavaScript 等效代码是什么?我需要以纹理的形式在 JavaScript <-> glsl 之间共享位数据,但无法在 JavaScript 中做到:/ - Buksy
经过一些测试后进行编辑:对我来说,这并不真正起作用,除非我移动了一些东西,return vec4(comp.yzw, floor(depth)/256.0)以恢复所有四个分量。此外,值得注意的是,对我来说,颜色分量输入范围为[0, 256),但输出范围为[0, 1)。可能只是在我的一端搞砸了,但通过这些更改,似乎对我来说工作正常。 - verma
1
我认为由于浮点数的存储方式,第一个组件的精度不会很高。假设使用IEEE754单精度浮点数,其中您可以获得24位精度和8位指数,当您开始处理大于2^24(16,777,216)的数字时,您将开始失去整数精度。因此,随着w的增加,您会在x中失去精度。 如果w> 1/256,则x误差最高可达1/256; 如果w> 1/128,则x误差最高可达1/128; ...... 如果w> 0.25,则最大x误差为0.25; 如果w> 0.5,则最大x误差为0.5! - Wivlaro
5
太好了,谢谢! 然而在iOS平台上,我需要在DecodeFloatRGBA函数的开头添加这段代码: v = floor(v * 255.0 + 0.5) / 255.0; 确保v包含精确的0、1/255、2/255、...、1的值。 这是必要的,因为通常“vec4 v”来自texture2D函数,在某些GPU(例如iOS设备)上,texture2D返回的值可能是“0.000001”而不是精确的0.0。 - Slyv
显示剩余2条评论

4

一般来说,如果您想将浮点数的有效数字打包成字节,则需要连续提取8个位的有效数字包,并将其存储在一个字节中。

将浮点数编码为预定义范围内的值

为了将浮点值打包成4个8位缓冲区,首先必须指定源值的范围。
如果您已经定义了一个值范围[minVal, maxVal],它必须映射到范围[0.0, 1.0]:

float mapVal = clamp((value-minVal)/(maxVal-minVal), 0.0, 1.0);

函数Encode将值在范围[0.0, 1.0]内的浮点数打包到一个vec4中:

vec4 Encode( in float value )
{
    value *= (256.0*256.0*256.0 - 1.0) / (256.0*256.0*256.0);
    vec4 encode = fract( value * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return vec4( encode.xyz - encode.yzw / 256.0, encode.w ) + 1.0/512.0;
}

函数Decodevec4中提取一个范围在[0.0, 1.0]的浮点数值:

float Decode( in vec4 pack )
{
    float value = dot( pack, 1.0 / vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return value * (256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0);
}

以下函数可以将浮点值打包到区间[minVal, maxVal]中,并从该区间中提取浮点值:
vec4 EncodeRange( in float value, flaot minVal, maxVal )
{
    value = clamp( (value-minVal) / (maxVal-minVal), 0.0, 1.0 );
    value *= (256.0*256.0*256.0 - 1.0) / (256.0*256.0*256.0);
    vec4 encode = fract( value * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return vec4( encode.xyz - encode.yzw / 256.0, encode.w ) + 1.0/512.0;
}

float DecodeRange( in vec4 pack, flaot minVal, maxVal )
{
    value = dot( pack, 1.0 / vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    value *= (256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0);
    return mix( minVal, maxVal, value );
}

使用指数将浮点数进行编码

另一种可能性是将有效数字编码为RGB值的3 * 8位,并将指数编码为alpha通道的8位:

vec4 EncodeExp( in float value )
{
    int exponent  = int( log2( abs( value ) ) + 1.0 );
    value        /= exp2( float( exponent ) );
    value         = (value + 1.0) * (256.0*256.0*256.0 - 1.0) / (2.0*256.0*256.0*256.0);
    vec4 encode   = fract( value * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return vec4( encode.xyz - encode.yzw / 256.0 + 1.0/512.0, (float(exponent) + 127.5) / 256.0 );
}

float DecodeExp( in vec4 pack )
{
    int exponent = int( pack.w * 256.0 - 127.0 );
    float value  = dot( pack.xyz, 1.0 / vec3(1.0, 256.0, 256.0*256.0) );
    value        = value * (2.0*256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0) - 1.0;
    return value * exp2( float(exponent) );
}

请注意,由于标准的32位IEEE 754数字仅具有24个有效数字,因此将该数字编码为3个字节完全足够。 另请参见如何在float 和 vec4、vec3、vec2之间进行转换?

1
指数可以直接编码:floor(log2(abs(value))) + 127.0 - Brian Cannard

0

大家在处理WebGL中这类问题时都是完全正确的,但我想分享一个获取值的技巧。

假设您想对两个适合16位的值进行比较:

// Generate a list of random 16bit integers
let data16bit = new Uint16Array(1000000);
for(let i=0; i < data16bit.length; i+=2){
    data16bit[i]   = Math.random()*(2**16);
    data16bit[i+1] = Math.random()*(2**16);
}
// Read them one byte at a time, for writing to 
// WebGL
let texture = new Uint8Array(data16bit.buffer);

现在当您在片段着色器中获取值时,您可以选择数字进行操作:
vec4 here = texture2D(u_image, v_texCoord);
// Read the "red" byte and the "green" byte together (as a single thing) 
// as well as the "blue" byte and the "alpha" byte together as a single
// thing
vec2 a = here.rg;
vec2 b = here.ba;
// now compare the things
if(a == b){
    here.a = 1;
}
else{
    here.a = 0;
}
// return the boolean value
gl_FragColor = here;

这个要点只是提醒您可以将同一块JavaScript内存视为不同的大小:Uint16ArrayUint8Array(而不是尝试进行位移并将其分解)。

更新

针对更多细节的请求,该代码非常接近于直接从此代码和说明中剪切/粘贴。

关于相应的示例,可以在GitLab上找到(同一文件的两个部分)。


你能详细说明一下你对一个运行示例的回答吗? - mix3d
1
@mix3d 这段代码最初是从我正在开发的一个更大的程序中剪切/粘贴/调整而来的。后来,我为课堂使用编写了一个较小的演示,并将其链接到此处。 - Jefferey Cave

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接