找回密码
 立即注册
查看: 581|回复: 2

Unity编译OpenGL ES Shader的低精度问题

[复制链接]
发表于 2021-2-4 22:27 | 显示全部楼层 |阅读模式
OpenGL ES的Shader中有三种精度的数据类型:highp、mediump、lowp。使用更低精度的数据计算会占用更少的内存、速度更快、功耗更低。只有移动平台的GPU才有低精度的类型,PC等平台都是高精度的。它们的精度和表示的范围如下:
highp精度的浮点数变量遵循IEEE 754标准。支持NaN和Infs。
在实现中,mediump的范围和精度必须大于等于lowp。highp的范围和精度必须大于等于mediump。
不同精度间的转换

从低精度转到高精度必须是精确的。从高精度转到低精度时,如果该值可以由目标精度的类型表示,转换也必须是精确的(比如从highp转到mediump,如果这个值比mediump能表示的最大值都大,那就是不能表示)。如果不能表示,转换的行为依赖数据的类型:
    对于有符号和无符号整数,值是被截断的。目标精度不能表示的位就成了0对于浮点数,会clamp到正无穷或负无穷,或者能表示的最大值/最小值。
精度限定符

在OpenGL ES的Shader中通过精度限定符来指定变量的最小可支持的范围和精度,有highp、mediump、lowp三个精度限定符:
精度限定符放在数据类型的前面,存储限定符的后面,字面值常量和Bool类型的变量没有精度限定符:
uniform mediump sampler2D _MainTex;
in highp vec2 vs_TEXCOORD0;Shader运行时使用的精度跟变量声明的精度可能是不一致的,规则是:经过操作的精度以及运算得到的结果的精度应该不低于运算时传入参数的精度,只是在少量的 buildin 的运算中,比如 atan,运算得到的结果的精度低于运算时传入参数的精度。
如果某个参加运算的参数没有精度修饰符,那么就以另外一个参加运算的参数的精度修饰符为准,如果都没有,就看下一个操作中的参数的精度修饰符,以此类推,一直到找到一个精度修饰符为止。 这里的下一个操作包括初始化赋值、 包括作为别的函数的传入参数、包括作为别的函数的返回参数。 如果依然找不到一个精度修饰符,那么就认为当前的精度修饰符为默认值。这种情况发生在FS时,必须为这种类型指定默认精度。
关于精度修饰符和其他GLSL的语法, @王烁 大佬的文章讲的非常好:
下面代码段演示了这个规则:
默认精度
可以这样指定默认精度:
precision precision-qualifier type;
    type是int、float或其他Opaque Type。precision-qualified可以使lowp、mediump、highp。
预定义精度
OpenGL ES的Shader预定义了一些精度:
顶点着色器
片元着色器
Unreal

在Unreal中的材质里也有有关精度的选项,如下图:
勾选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代码如下:
编译后GLSL的代码:
这个例子中我们给result指定了half精度,但是编译后的精度取了加法操作的两个操作数中的最高精度,就成了highp。虽然第一行的half对这一行的result的精度没有起作用,但是下一行中,我们给col指定了float精度,编译后成了mediump精度,取的是乘法的两个操作数的最高精度。
把_Float改为half精度,result的精度也就成了half:
Example 2
下面这种情况result声明为float,对下一行的计算的精度也没有提高。
Unity Shader代码:
编译后GLSL代码:
Example 3
在写Unity的Shader时,想要指定纹理采样的结果的精度,修改tex2D返回值的变量是不行的,比如这样的Shader:
编译后发现采样的结果精度还是mediump:

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

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

使用更低精度的数据计算会占用更少的内存、速度更快、功耗更低。但是要注意因为精度降低、范围变小,会使有的计算出问题。
1,精度低
我遇到过的一个精度低引起的问题就是计算出一个很小的值,因为使用低精度表示,这个值比低精度能表示的最小值还要小,就近似成了0。接着,这个值又在后续的计算中作为除数,就导致了计算错误。
解决方法就是提高这种计算使用的精度,如果还有问题,可以限制一个最小值。
在normalize一个零向量或者一个很小的向量的时候,也可能产生类似错误。Unity内置的Shader中有个SafeNormalize的函数,可以用用:
2,范围小
在使用length()、normalize()和distance()等函数时,需要求向量的长度,求长度时就很有可能计算出很大的值,这种情况要使用高精度。
可以大概定下精度的使用规范:世界坐标、UV以及需要高精度的复杂计算使用highp。比较小的向量、模型空间坐标、HDR颜色使用mediump。普通的颜色和简单的计算使用lowp。
Reference

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-2-4 22:33 | 显示全部楼层
ue里你可以在custom节点显式声明为float类型,最终编译出来的该部分变量就会是高精度了。
发表于 2021-2-4 22:33 | 显示全部楼层
普通的vector节点不可以吧
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 14:37 , Processed in 0.091350 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表