友情提示
这篇文章是WebGL课程专栏的第45篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
进入正题
我们由一个最简单的 fragment shader 开始吧:
shader
precision mediump int;
precision mediump float;
void main() {
vec3 color = vec3(1.0, 0.0, 0.0);
gl_FragColor = vec4(color, 1.0);
}
由于目的不在于讲解怎么初始化html webgl环境,以及怎么应用上述的shader生效,我们这里就不赘言上述知识,若有兴趣,可查看我的专栏。
好了,这个 fragment shader 平平无奇,不管你画的是什么东西,应该呈现的都是纯红一片。

整一些幺蛾子
那么如下的代码,你认为会呈现什么样的效果呢:
shader
precision mediump int;
precision mediump float;
void main() {
float testNumber = sin(1./0.);
vec3 color = vec3(1.0, 0.0, 0.0);
if (testNumber < 2.0) {
color.r = 0.0;
color.g = 1.0;
}
gl_FragColor = vec4(color, 1.0);
}
好,先别着急实验,我们一步一步拆解一下:
- 我们计算了一个 testNumber
- 然后如果 这个testNumber < 2.0, 那么就更新颜色为 绿色
- 否则,就保持红色
再看看 testNumber 是怎么计算的呢:
testNumber = sin(xxx);
好了,我们初中还是高中就已经滚瓜烂熟的:
sin 的范围是 【-1, 1】
所以说:
- testNumber < 2.0 ,肯定是true。
所以最后呈现的颜色肯定是:绿色。
但是结果就是:

开始探索
眼尖的小伙伴应该看到一个异常点:
- testNumber = sin(1. / 0.);
小学就学过,不能除以0
。
那么在计算机里,我非要除以0,会造成什么后果呢?
不妨用浏览器自带的js环境来试验一下:

好家伙:
-
- / 0. 结果叫 Infinity
- sin(Infinity) 结果叫 NaN
实际上这俩玩意,可不是js发明的,这俩玩意是在这里定义的:
IEEE 754-2019 IEEE Standard for Floating-Point Arithmetic
我们来看一下具体的:
- Infinity : Represented with all exponent bits set to 1 and mantissa bits set to 0. The sign bit distinguishes between positive and negative infinity.
- NaN (Not a Number) : Used to represent undefined or unrepresentable values (e.g., 0/0). Exponent bits are all 1, and the mantissa is non-zero.
好了,英文部分就不解释了,解释起来就要整个把 float 的编码规则全弄明白才行。
我们其实想研究的是:
- 为什么 NaN < 2.0 为false。
Infinity 和 NaN 的运算规则
看这个表格:

NaN 的比较规则
NaN几乎
与什么东西进行 > < == 的比较都是false。 例如:
- Math.sin(1./0.) > 1.
false
- Math.sin(1./0.) < 1.
false
- Math.sin(1./0.) === 1.
false
- Math.sin(1./0.) < 1./0.
false
NaN 的奇葩但有用的规定
NaN !== NaN true
看下面的js代码:
js
const a = Math.sin(1. / 0.); // a 就是NaN
// 如何检测 a 是不是NaN
const check = a !== a;
// now check 就是 true 了。。。。。。
这玩意,自己不等于自己......
是不是有点跑题
本文标题是研究 shader 中的随机数,搞半天,仅仅搞了一下 NaN。跑题了吗?
正式进入随机数的研究
这里说一个不幸的消息:
- shader 中没有内置的 random 之类的函数,可以生成随机数。
所以,去网上搜,shader 如何搞随机数,或者去AI,那么大都会使用一些数学公式,来生成随机数,比如说我AI的一次结果是这样的:
shader
float precisionRandom(vec2 coord) {
const vec2 largeNumbers = vec2(127.1, 311.7);
const float multiplier = 1e6;
float dotProduct = dot(coord.xy, largeNumbers);
float chaos = sin(dotProduct * multiplier);
return fract(chaos);
}
我们不去研究这个算法的具体数学道理,这不是本篇内容。
我们会传入一个coord,一般就是指当前像素
的坐标,或者归一化之后的坐标,一般叫 uv
;
然后这里面最有可能发生问题的就是这个:
dotProduct * multiplier
这个会乘以一个比较大的数:multiplier
。
如果我们指定了:
precision mediump float;
在一些设备上,那么所有的浮点数,就是 float16,就是说只有16 bit 的存储空间来存储一个浮点数。那么计算结果就非常容易超过 float16 的最大值。一旦超过最大值之后,某些设备上,计算结果就是:
- Infinity
然后 sin(Infinity) -> NaN。
然后就一切朝着 false 的逻辑去运行,啥啥都是false。
想要测试一个代码里到底有没有出现NaN呢,简单:
- NaN != NaN ->
true
写一个证明NaN出现的shader
下面代码就能证明,已经出现NaN了:
shader
precision mediump int;
precision mediump float;
void main() {
float testNumber = sin(1./0.);
vec3 color = vec3(1.0, 0.0, 0.0);
if (testNumber != testNumber) {
color.r = 0.0;
color.g = 1.0;
}
gl_FragColor = vec4(color, 1.0);
}
刷新网页一看:

已经变成绿色,说明走到了:
shader
if (testNumber != testNumber) {
color.r = 0.0;
color.g = 1.0;
}
好了,一切都尘埃落定,要小心 float16 也就是 mediump float 的陷阱哟!!
别的方法引入随机数到shader
下面的方法也是一个常见的方法:
- 其实也不难,提前生成一张
随机数纹理
,然后在 shader 中去使用这个纹理就行了。
就像这样:

然后在 fragment shader 中, 用uv去这个纹理上进行采样,就自然获得了比较好的随机数,也不用考虑什么NaN的问题了,那么自然有一点坏处:
- 就是 gpu 显存又多了一个纹理,😔😔😔
结束语(AI生成的小俏皮话😁)
着色器里浪,精度要提防:
highp
高富帅,稳如老狗不发慌;
mediump
精打细算,半精度省电忙,
谁知 Infinity
大数爆,NaN
乱入搅全场!
若问救星何处有?纹理随机数,救你于水火------
任它浮点地震海啸,我自纹理采样稳如🐕!