float 精度_Unity编译OpenGL ES Shader的低精度问题

OpenGL ES的Shader中有三种精度的数据类型:highp、mediump、lowp。使用更低精度的数据计算会占用更少的内存、速度更快、功耗更低。只有移动平台的GPU才有低精度的类型,PC等平台都是高精度的。它们的精度和表示的范围如下:

32be3c5dfdafd95ee1c403a00b3de265.png

highp精度的浮点数变量遵循IEEE 754标准。支持NaN和Infs。

在实现中,mediump的范围和精度必须大于等于lowp。highp的范围和精度必须大于等于mediump。

不同精度间的转换

从低精度转到高精度必须是精确的。从高精度转到低精度时,如果该值可以由目标精度的类型表示,转换也必须是精确的(比如从highp转到mediump,如果这个值比mediump能表示的最大值都大,那就是不能表示)。如果不能表示,转换的行为依赖数据的类型:

  • 对于有符号和无符号整数,值是被截断的。目标精度不能表示的位就成了0
  • 对于浮点数,会clamp到正无穷或负无穷,或者能表示的最大值/最小值。

精度限定符

在OpenGL ES的Shader中通过精度限定符来指定变量的最小可支持的范围和精度,有highp、mediump、lowp三个精度限定符:

69336a7a19e6673936979555d6f44dce.png

精度限定符放在数据类型的前面,存储限定符的后面,字面值常量和Bool类型的变量没有精度限定符:

uniform mediump sampler2D _MainTex;
in highp vec2 vs_TEXCOORD0;

Shader运行时使用的精度跟变量声明的精度可能是不一致的,规则是:经过操作的精度以及运算得到的结果的精度应该不低于运算时传入参数的精度,只是在少量的 buildin 的运算中,比如 atan,运算得到的结果的精度低于运算时传入参数的精度。

如果某个参加运算的参数没有精度修饰符,那么就以另外一个参加运算的参数的精度修饰符为准,如果都没有,就看下一个操作中的参数的精度修饰符,以此类推,一直到找到一个精度修饰符为止。 这里的下一个操作包括初始化赋值、 包括作为别的函数的传入参数、包括作为别的函数的返回参数。 如果依然找不到一个精度修饰符,那么就认为当前的精度修饰符为默认值。这种情况发生在FS时,必须为这种类型指定默认精度。

关于精度修饰符和其他GLSL的语法, @王烁 大佬的文章讲的非常好:

OpenGL ES 2.0 知识串讲 (5)--GLSL 语法(III)​geekfaner.com

下面代码段演示了这个规则:

77f56d4b1b3cdfe48bc288102bcbf972.png

默认精度

可以这样指定默认精度:

precision precision-qualifier type;
  • type是int、float或其他Opaque Type。
  • precision-qualified可以使lowp、mediump、highp。

预定义精度

OpenGL ES的Shader预定义了一些精度:

顶点着色器

631c3087cd37b4aa6c781cdfc40da612.png

片元着色器

33c5a6cbfdc56d9828bafd94505ebcb3.png

Unreal

在Unreal中的材质里也有有关精度的选项,如下图:

0b93d290deffb20a99d49e1eb79900fc.png

勾选Full Precision就会在渲染这个材质时使用highp计算,不勾选就会使用mediump计算。跟Unity相比,Unreal这个操作不够自由,要么全部全精度,要么全部半精度。

Unity

Unity的Shader跟这三个精度对应的是float、half、fixed。但是在Unity的Shader中指定了变量的精度,Unity编译出来的GLSL Shader中这个变量的精度并不一定是我们指定的精度。这就跟Unity Shader的跨平台编译有关,Unity会使用HLSL编译器编译成DirextX字节码,然后再通过自研的 HLSLcc 模块将字节码翻译成目标API的Shader源码。HLSLcc的代码还没时间看,猜测在编译成DirectX字节码时有跟上面讲的精度确定的类似的规则,所以编译出来的字节码的精度跟我们声明的不一样,然后转成GLSL代码时也就不一样了。

ShaderLab->GLSL过程中精度确定的规则需要看HLSLcc的代码,下面只列举几个我遇到问题的情况:

Example 1

Unity Shader代码如下:

2e31d85219c8db4a35c2383c4f3bf5f3.png

编译后GLSL的代码:

b023cb08eda90e1b8e960fe7df8fdfc5.png

这个例子中我们给result指定了half精度,但是编译后的精度取了加法操作的两个操作数中的最高精度,就成了highp。虽然第一行的half对这一行的result的精度没有起作用,但是下一行中,我们给col指定了float精度,编译后成了mediump精度,取的是乘法的两个操作数的最高精度。

把_Float改为half精度,result的精度也就成了half:

dfd2dcb8b7110ec9b2fbf764aa9e344e.png

a20c441c3a9e229186eb64829a529c36.png

Example 2

下面这种情况result声明为float,对下一行的计算的精度也没有提高。

Unity Shader代码:

aae7aa9f41af3732bf0df842d5d6d907.png

编译后GLSL代码:

6e7ac2c8567dd1c27c860c006416d215.png

Example 3

在写Unity的Shader时,想要指定纹理采样的结果的精度,修改tex2D返回值的变量是不行的,比如这样的Shader:

c4576f0aa94c110590e16d9e418d41fc.png

编译后发现采样的结果精度还是mediump:

7aeae7c489fd68c080a008f02022c5d2.png


因为tex2D是这一次操作运算,它的结果的精度至少跟这一次操作中的操作数的最高精度一致,要想修改采样结果的精度在Unity中可以这样写:

sampler2D_half _MainTex;
samplerCUBE_half _Cubemap;

sampler2D_float _MainTex;
samplerCUBE_float _Cubemap;

对于包含HDR颜色的纹理可以使用half精度的采样器,对于包含全精度数据的纹理(比如深度纹理),可以使用float精度的采样器。

使用规范

使用更低精度的数据计算会占用更少的内存、速度更快、功耗更低。但是要注意因为精度降低、范围变小,会使有的计算出问题。

1,精度低

我遇到过的一个精度低引起的问题就是计算出一个很小的值,因为使用低精度表示,这个值比低精度能表示的最小值还要小,就近似成了0。接着,这个值又在后续的计算中作为除数,就导致了计算错误。

解决方法就是提高这种计算使用的精度,如果还有问题,可以限制一个最小值。

在normalize一个零向量或者一个很小的向量的时候,也可能产生类似错误。Unity内置的Shader中有个SafeNormalize的函数,可以用用:

74fc419216518fbe267d2c60ab8443d1.png

2,范围小

在使用length()、normalize()和distance()等函数时,需要求向量的长度,求长度时就很有可能计算出很大的值,这种情况要使用高精度。

可以大概定下精度的使用规范:世界坐标、UV以及需要高精度的复杂计算使用highp。比较小的向量、模型空间坐标、HDR颜色使用mediump。普通的颜色和简单的计算使用lowp。

Reference

丛越:游戏引擎随笔 0x02:Shader 跨平台编译之路​zhuanlan.zhihu.com
c227ae6cfa75d3a9d391e100c101f868.png
OpenGL ES 2.0 知识串讲 (5)--GLSL 语法(III)​geekfaner.com

https://www.khronos.org/registry/OpenGL/specs/es/3.1/GLSL_ES_Specification_3.10.withchanges.pdf

https://www.asawicki.info/news_1596_watch_out_for_reduced_precision_normalizelength_in_opengl_es

https://docs.unity3d.com/Manual/SL-DataTypesAndPrecision.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值