HLSL计算着色器中SNorm16解包/打包出错,请求排查
HLSL SNorm16x4 解包/打包数据丢失问题排查
问题描述
在HLSL计算着色器中处理SNorm16x4数据(占8字节)时,由于Load2/Store2仅能读写4字节,因此将其拆分为两个4字节uint处理,但最终结果出现数据丢失,计算错误。
原代码
float2 UnpackFromSNorm16x2(uint v) { uint2 tempU = asuint(uint2(v, v >> 16) & 0xFFFF); int2 tempI = int2(tempU.x - 32767, tempU.y - 32767); return float2( tempI * float(1.0 / 32767.0)); } int FloatToSNorm16(float v) { //According to D3D10 rules, the value "-1.0f" has two representations: // 0x1000 and 0x10001 //This allows everyone to convert by just multiplying by 32767 instead //of multiplying the negative values by 32768 and 32767 for positive. return int(clamp(v >= 0.0f ? (v * 32767.0f + 0.5f) : (v * 32767.0f - 0.5f), -32768.0f, 32767.0f)); } uint PackToSNorm16x2(float2 v) { int intX = int(FloatToSNorm16(v.x)); int intY = int(FloatToSNorm16(v.y)); uint2 uintXY = uint2(clamp(intX + 32767, 0, 65535), clamp(intY + 32767, 0, 65535)); uint x = (uintXY.x << 0) & 0x0000FFFF; uint y = (uintXY.y << 16) & 0xFFFF0000; return x | y; } uint2 inputTangentUInt = asuint(vertices.Load2(baseOffset + tangentOffset)); float4 qTangentUnpacked = float4(UnpackFromSNorm16x2(inputTangentUInt.x), UnpackFromSNorm16x2(inputTangentUInt.y)); //Do some work with qTangentUnpacked uint2 qTangentPacked = uint2(PackTwoSNORM16(qTangentUnpacked.xy), PackTwoSNORM16(qTangentUnpacked.zw)); vertices.Store2(baseOffset + tangentOffset, asuint(qTangentPacked));
错误点分析
解包函数中多余的
asuint调用uint2 tempU = asuint(uint2(v, v >> 16) & 0xFFFF);里的asuint完全错误:uint2类型的变量不需要用asuint转换(该函数用于将float的位模式转为uint),这会把原本正确的uint值当成float二进制解析后再转uint,直接破坏数据。SNorm16 映射逻辑错误
标准SNorm16的范围是无符号065535**对应**归一化-11,正确的转换公式是:- 解包:
float = (int(u) - 32768) / 32767.0f - 打包:
uint = (uint)(clamp(v, -1.0f, 1.0f) * 32767.0f + 32768.0f)
原代码中解包用tempU.x - 32767,打包用intX + 32767,导致映射偏移,最大值/最小值无法正确对应,出现精度丢失。
- 解包:
打包函数调用名称错误
最后打包时调用的PackTwoSNORM16与定义的函数名PackToSNorm16x2不匹配,会导致调用错误函数或编译失败,直接产生错误结果。FloatToSNorm16 取整逻辑错误
原代码中对正值加0.5、负值减0.5的取整方式不符合SNorm16的标准舍入规则,应该使用round函数进行四舍五入,而非手动偏移。
修正后的代码
// 正确解包SNorm16x2:将单个uint的高低16位转为-1~1的float2 float2 UnpackFromSNorm16x2(uint v) { // 拆分高低16位,保留有效位 uint2 tempU = uint2(v & 0xFFFF, v >> 16); // 转换为有符号整数并映射到-1~1范围 int2 tempI = int2(tempU) - 32768; return float2(tempI) * (1.0f / 32767.0f); } // 将单个float转为SNorm16对应的有符号整数(-32768~32767) int FloatToSNorm16(float v) { // 先 clamp 到有效范围,再四舍五入映射 float clamped = clamp(v, -1.0f, 1.0f); // 负值用32768缩放以正确对应-1.0到-32768 float scaled = clamped >= 0.0f ? clamped * 32767.0f : clamped * 32768.0f; return int(round(scaled)); } // 将float2打包为SNorm16x2的uint uint PackToSNorm16x2(float2 v) { int2 intXY = int2(FloatToSNorm16(v.x), FloatToSNorm16(v.y)); // 转换为无符号0~65535范围 uint2 uintXY = uint2(intXY + 32768); // 拼接高低16位 return (uintXY.y << 16) | uintXY.x; } // 读取8字节数据为两个uint uint2 inputTangentUInt = asuint(vertices.Load2(baseOffset + tangentOffset)); // 解包为4个归一化值 float4 qTangentUnpacked = float4(UnpackFromSNorm16x2(inputTangentUInt.x), UnpackFromSNorm16x2(inputTangentUInt.y)); // 处理数据(省略业务逻辑) // 重新打包为两个uint uint2 qTangentPacked = uint2(PackToSNorm16x2(qTangentUnpacked.xy), PackToSNorm16x2(qTangentUnpacked.zw)); // 存储回8字节数据 vertices.Store2(baseOffset + tangentOffset, asuint(qTangentPacked));
额外说明
- 修正后的代码严格遵循D3D标准的SNorm16转换规则,确保解包/打包的一致性。
- 若使用HLSL 5.0及以上版本,可直接使用内置的
unpacksnorm2x16和packsnorm2x16函数,避免手动实现的错误:float2 UnpackFromSNorm16x2(uint v) { return unpacksnorm2x16(v); } uint PackToSNorm16x2(float2 v) { return packsnorm2x16(v); }
内容的提问来源于stack exchange,提问作者user3169894
相关产品推荐
相关产品推荐

