Skip to content

Commit cfcea45

Browse files
committed
Translate webgl-precision-issues.md to Chinese
1 parent 8416ebf commit cfcea45

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
Title: WebGL2 精度问题
2+
Description: WebGL2里的各种精度问题
3+
TOC: 精度问题
4+
5+
本文讨论 WebGL2 中的各种精度问题。
6+
7+
## `lowp`, `mediump`, `highp`
8+
9+
[本站的第一篇文章](webgl-fundamentals.html)中,我们创建了顶点着色器和片段着色器。
10+
在创建片段着色器时,顺带提到片段着色器没有默认的精度,所以我们需要通过添加这行代码来设置。
11+
12+
```glsl
13+
precision highp float;
14+
```
15+
16+
这到底是怎么回事?
17+
18+
`lowp``mediump``highp`是精度设置。
19+
这里的精度实际上指的是用多少位(bit)来存储一个值。
20+
JavaScript 中的数字使用 64 位,大多数 WebGL 中的数字只有 32 位。
21+
位数越少意味着速度越快,位数越多意味着精度越高和/或范围越大。
22+
23+
我不确定自己是否能解释清楚。
24+
你可以搜索[double vs float](https://www.google.com/search?q=double+vs+float)
25+
了解更多精度问题的示例,但一种简单的理解方法是将其比作字节和短整型,或者在 JavaScript 中的 Uint8Array 和 Uint16Array 的区别。
26+
27+
* Uint8Array 是一个无符号 8 位整数数组。8 位能表示 2<sup>8</sup>(256)个数,范围是 0 到 255。
28+
* Uint16Array 是一个无符号 16 位整数数组。16 位能表示 2<sup>16</sup>(65536)个数,范围是 0 到 65535。
29+
* Uint32Array 是一个无符号 32 位整数数组。32 位能表示 2<sup>32</sup>(约42亿)个数,范围是 0 到 4294967295。
30+
31+
`lowp``mediump``highp` 也是类似的概念。
32+
33+
* `lowp` 至少是 9 位。对于浮点数,其值范围大致是 -2 到 +2,整数则类似于 `Uint8Array``Int8Array`
34+
* `mediump` 至少是 16 位。对于浮点数,其值范围大致是 -2<sup>14</sup> 到 +2<sup>14</sup>,整数类似于 `Uint16Array``Int16Array`
35+
* `highp` 至少是 32 位。对于浮点数,其值范围大致是 -2<sup>62</sup> 到 +2<sup>62</sup>,整数类似于 `Uint32Array``Int32Array`
36+
37+
需要注意的是,并非范围内的所有数值都能被表示。
38+
最容易理解的是 `lowp`,它只有 9 位,因此只能表示 512 个唯一值。
39+
虽然它的范围是 -2 到 +2,但在这之间有无限多个值,比如 1.9999999 和 1.999998,这两个数值都不能被 `lowp` 精确表示。
40+
例如,如果你用 `lowp` 做颜色计算,可能会出现色带现象。颜色范围是 0 到 1,而 lowp 在 0 到 1 之间大约只有 128 个可表示值。
41+
这意味着如果你想加一个非常小的值(比如 1/512),它可能根本不会改变数值,因为无法被表示,实际上就像加了 0。
42+
43+
理论上,我们可以在任何地方使用 `highp` 完全避免这些问题,但在实际设备上,使用 `lowp``mediump` 通常会比 `highp` 快很多,有时甚至显著更快。
44+
45+
还有一点,和 `Uint8Array``Uint16Array` 不同的是,`lowp``mediump``highp` 允许在内部使用更高的精度(更多位)。
46+
例如,在桌面 GPU 上,如果你在着色器中写了 `mediump`,它很可能仍然使用 32 位精度。
47+
这导致在开发时很难测试 `lowp``mediump` 的真正表现。
48+
要确认你的着色器在低精度设备上能正常工作,必须在实际使用较低精度的设备上测试。
49+
50+
如果你想用 `mediump` 以提高速度,常见问题包括比如点光源的高光计算,它在世界空间或视图空间传递的值可能超出 `mediump` 的范围。
51+
可能在某些设备上你只能舍弃高光计算。下面是将[点光源](webgl-3d-lighting-point.html)示例的片段着色器改为 `mediump` 的代码示例:
52+
53+
```glsl
54+
#version 300 es
55+
56+
-precision highp float;
57+
+precision mediump float;
58+
59+
// Passed in and varied from the vertex shader.
60+
in vec3 v_normal;
61+
in vec3 v_surfaceToLight;
62+
in vec3 v_surfaceToView;
63+
64+
uniform vec4 u_color;
65+
uniform float u_shininess;
66+
67+
// we need to declare an output for the fragment shader
68+
out vec4 outColor;
69+
70+
void main() {
71+
// because v_normal is a varying it's interpolated
72+
// so it will not be a uint vector. Normalizing it
73+
// will make it a unit vector again
74+
vec3 normal = normalize(v_normal);
75+
76+
vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
77+
- vec3 surfaceToViewDirection = normalize(v_surfaceToView);
78+
- vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
79+
80+
// compute the light by taking the dot product
81+
// of the normal to the light's reverse direction
82+
float light = dot(normal, surfaceToLightDirection);
83+
- float specular = 0.0;
84+
- if (light > 0.0) {
85+
- specular = pow(dot(normal, halfVector), u_shininess);
86+
- }
87+
88+
outColor = u_color;
89+
90+
// Lets multiply just the color portion (not the alpha)
91+
// by the light
92+
outColor.rgb *= light;
93+
94+
- // Just add in the specular
95+
- outColor.rgb += specular;
96+
}
97+
```
98+
99+
注意:即便如此还不够。在顶点着色器中我们有以下代码:
100+
101+
```glsl
102+
// compute the vector of the surface to the light
103+
// and pass it to the fragment shader
104+
v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
105+
```
106+
107+
假设光源距离表面有 1000 个单位。
108+
然后我们进入片段着色器,执行这一行代码:
109+
110+
```glsl
111+
vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
112+
```
113+
114+
看起来似乎没问题。除了归一化向量的常规方法是除以其长度,而计算长度的标准方式是:
115+
116+
117+
```
118+
float length = sqrt(v.x * v.x + v.y * v.y * v.z * v.z);
119+
```
120+
121+
如果 x、y 或 z 中的某一个值是 1000,那么 1000×1000 就是 1000000。
122+
而 1000000 超出了 `mediump` 的表示范围。
123+
124+
这里的一个解决方案是在顶点着色器中进行归一化(normalize)。
125+
126+
```
127+
// compute the vector of the surface to the light
128+
// and pass it to the fragment shader
129+
- v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
130+
+ v_surfaceToLight = normalize(u_lightWorldPosition - surfaceWorldPosition);
131+
```
132+
133+
现在赋值给 `v_surfaceToLight` 的数值范围在 -1 到 +1 之间,这正好落在 `mediump` 的有效范围内。
134+
135+
请注意,在顶点着色器中进行归一化实际上不会得到完全相同的结果,但结果可能足够接近,以至于除非并排对比,否则没人会注意到差异。
136+
137+
`normalize``length``distance``dot` 这样的函数都会面临一个问题:如果参与计算的值过大,那么在 `mediump` 精度下就可能超出其表示范围。
138+
139+
不过,你实际上需要在一个 `mediump` 为 16 位的设备上进行测试。在桌面设备上,`mediump` 实际上使用的是与 `highp` 相同的 32 位精度,因此任何相关的问题在桌面上都不会显现出来。
140+
141+
## 检测对16位 `mediump` 的支持
142+
143+
你调用 `gl.getShaderPrecisionFormat`,传入着色器类型(`VERTEX_SHADER``FRAGMENT_SHADER`),以及以下精度类型之一:
144+
145+
- `LOW_FLOAT`
146+
- `MEDIUM_FLOAT`
147+
- `HIGH_FLOAT`
148+
- `LOW_INT`
149+
- `MEDIUM_INT`
150+
- `HIGH_INT`
151+
152+
它会[返回精度信息]
153+
154+
{{{example url="../webgl-precision-lowp-mediump-highp.html"}}}
155+
156+
`gl.getShaderPrecisionFormat` 会返回一个对象,包含三个属性:`precision``rangeMin``rangeMax`
157+
158+
对于 `LOW_FLOAT``MEDIUM_FLOAT`,如果它们实际上就是 `highp`,那么 `precision` 将是 23。否则,它们通常分别是 8 和 15,或者至少会小于 23。对于 `LOW_INT``MEDIUM_INT`,如果它们等同于 `highp`,那么 `rangeMin` 会是 31。如果小于 31,则说明例如 `mediump int``highp int` 更高效。
159+
160+
我的 Pixel 2 XL 对于 `mediump``lowp` 都使用 16 位。我不确定自己是否用过使用 9 位表示 `lowp` 的设备,因此也不清楚在这种情况下通常会遇到哪些问题。
161+
162+
在本文系列中,我们在片段着色器中通常会指定默认精度。我们也可以为每个变量单独指定精度,例如:
163+
164+
165+
```glsl
166+
uniform mediump vec4 color; // a uniform
167+
in lowp vec4 normal; // an attribute or varying input
168+
out lowp vec4 texcoord; // a fragment shader output or varying output
169+
lowp float foo; // a variable
170+
```
171+
172+
## 纹理格式
173+
174+
纹理是规范中另一个指出“实际使用的精度可能高于请求精度”的地方。
175+
176+
例如,你可以请求一个每通道 4 位、总共 16 位的纹理,像这样:
177+
178+
```
179+
gl.texImage2D(
180+
gl.TEXTURE_2D, // target
181+
0, // mip level
182+
gl.RGBA4, // internal format
183+
width, // width
184+
height, // height
185+
0, // border
186+
gl.RGBA, // format
187+
gl.UNSIGNED_SHORT_4_4_4_4, // type
188+
null,
189+
);
190+
```
191+
192+
但实现上实际上可能在内部使用更高分辨率的格式。
193+
我认为大多数桌面端会这样做,而大多数移动端 GPU 不会。
194+
195+
我们可以做个测试。首先我们会像上面那样请求一个每通道 4 位的纹理。
196+
然后我们会通过渲染一个 0 到 1 的渐变来[渲染到它](webgl-render-to-texture.html)
197+
198+
接着我们会将该纹理渲染到画布上。如果纹理内部确实是每通道 4 位,
199+
那么从我们绘制的渐变中只会有 16 个颜色级别。
200+
如果纹理实际上是每通道 8 位,我们将看到 256 个颜色级别。
201+
202+
{{{example url="../webgl-precision-textures.html"}}}
203+
204+
在我的智能手机上运行时,我看到纹理使用的是每通道4位
205+
(至少红色通道是4位,因为我没有测试其他通道)。
206+
207+
<div class="webgl_center"><img src="resources/mobile-4-4-4-4-texture-no-dither.png" style="image-rendering: pixelated; width: 600px;"></div>
208+
209+
而在我的桌面上,我看到纹理实际上使用的是每通道8位,
210+
尽管我只请求了4位。
211+
212+
<div class="webgl_center"><img src="resources/desktop-4-4-4-4-texture-no-dither.png" style="image-rendering: pixelated; width: 600px;"></div>
213+
214+
需要注意的一点是,WebGL 默认会对结果进行抖动处理,
215+
使这种渐变看起来更平滑。你可以通过以下方式关闭抖动:
216+
217+
218+
```js
219+
gl.disable(gl.DITHER);
220+
```
221+
222+
如果我不关闭抖动处理,那么我的智能手机会产生这样的效果。
223+
224+
<div class="webgl_center"><img src="resources/mobile-4-4-4-4-texture-dither.png" style="image-rendering: pixelated; width: 600px;"></div>
225+
226+
就我目前所知,这种情况通常只会在以下特定场景出现:当开发者将某种低比特精度的纹理格式用作渲染目标,却未在实际采用该低分辨率的设备上进行测试时。
227+
若仅通过桌面端设备进行测试,由此引发的问题很可能无法被发现。

0 commit comments

Comments
 (0)