WebGL第四十五课:研究一下shader中的随机数

友情提示

这篇文章是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环境来试验一下:

好家伙:

    1. / 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 乱入搅全场!
若问救星何处有?纹理随机数,救你于水火------
任它浮点地震海啸,我自纹理采样稳如🐕!

相关推荐
大飞pkz1 天前
【Unity】Application类常见路径
unity·游戏引擎·游戏开发·application常用路径·数据路径·数据存储位置
Thomas游戏开发1 天前
Unity Root Motion 开发详解
前端框架·unity3d·游戏开发
烛阴2 天前
ABS - Rhomb
前端·webgl
Mintopia2 天前
网格布尔运算的三重奏:从像素的邂逅到模型的重生
前端·javascript·计算机图形学
烛阴3 天前
Smoothstep
前端·webgl
Mintopia3 天前
3D Quickhull 算法:用可见性与冲突图搭建空间凸壳
前端·javascript·计算机图形学
格调UI成品4 天前
元宇宙工厂前端新形态:Three.js与WebGL实现3D产线交互的轻量化之路
前端·javascript·webgl
谷宇4 天前
【Unity3D实例-功能-移动】角色移动-通过WSAD(Rigidbody方式)
unity3d·游戏开发
XZen4 天前
再战catmull插值算法 —— 在Bender中使用
计算机图形学