【Unity シェーダー】グラデーションに巨大な値を足すと諧調が落ちる話

# はじめに
巨大な値を使った際にグラデーションの諧調が落ちるという現象に遭遇したので、まとめたいと思います。

本記事は、簡潔さを重視した内容となっているため、情報の正確さに欠ける面があるかもしれません。

# 環境
Unity 2020.2.0f1
Universal RP 10.2.2

# 事の発端
シェーダーグラフにて、巨大な数を使うとグラデーションの諧調が減るという不可解な現象に遭遇しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/928fc3cd-9090-6a26-78de-b35aa7098a20.png)

今回は**float(浮動小数点数)**の視点から、階調が減る現象について検証していきたいと思います。
検証では**ShaderLab**を使用します。


# 検証 その1 : UV.xを画面に出す
ShaderLabでUVのxだけを返すようなフラグメントシェーダーを書いてみます。

```hlsl
fixed4 frag (v2f i) : SV_Target
{
    return i.uv.x;
}
```

## 結果
滑らかな0 ~ 1 のグラデーションが画面に表示されます。
<img width = 240 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/7a0b0e0d-9961-a752-965a-f4408b76f799.png">


# 検証 その2 : UV.xに巨大な整数を足す
次に、```0.001``` という小数を足して、```frac``` で小数部分だけを返すようにしてみます。

```hlsl
fixed4 frag (v2f i) : SV_Target
{
    return frac(i.uv.x + 0.001);
}
```

<img width = 240 src = "image_007_0000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/05641c5e-ccfb-5779-b657-fea9604a1491.png">


## 結果
同じく滑らかなグラデーションが表示されます。
ここまでは、問題ないかと思います。
<img width = 240 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/849dd96b-5136-cd87-fb63-1b398e26ab02.png">

# 検証 その3 : 10^6を足してみる
```10^6``` という巨大な数を足してから、```frac``` で小数部分だけを返すようにしてみます。

```hlsl
fixed4 frag (v2f i) : SV_Target
{
    return frac(i.uv.x + 1e6 + 0.001);
}
```

## 結果
整数を足しているだけだから、**滑らかなグラデーション**が表示されるのでは？と思ってしまいそうですが、
結果は以下のような**カクカクしたグラデーション**になります。

<img width = 240 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/489fc822-786c-3452-771e-b0283540eb1d.png">

この現象は、float(**浮動小数点数**) が関係していると考えられます。


# floatの内部表現
シェーダーのfloat は **32bit float**(単精度浮動小数点数) というもので、
0.3や0.7といった実数を**32ケタの2進数**で表現します。

![float.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/81b68eaa-79c3-2934-d6a6-375562543b8d.png)

## ビット列から実数を計算する
符号部を $sign$ (0か1) 、指数部を $e$ (0 ~ 255の整数)、仮数部のビット列を $b_1, b_2, b_3, ... , b_{23} $  と置きます。
この時、floatが表現する実数 $value$ は以下の計算式で求めることができます。(IEEE754規格)

```math
value = (-1)^{sign} \cdot (1 + \sum_{i=1}^{23}b_{i}2^{-i} ) \cdot 2^{e - 127}
```

## クイズ
以下のようなビット列が表す実数 $value$ はどんな値になるでしょうか？

![-02-float.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4b8c01df-d831-67ba-718b-84c222f24beb.png)


### 答え
符号部 : $sign = 0$
指数部 : 8桁目のビット列が1になっているので、$e = 2^7 = 128$
仮数部のビット列 : $b_1 = 1, b_4 = 1, b_5 = 1$


ビット列が表す値$value$は以下のようになります。

```math
\begin{align}
value &= (-1)^0 \cdot (1 + 2^{-1} + 2^{-4} + 2^{-5}) \cdot 2^{128- 127} \\
\\
&=
(1 + 0.5 + 0.0625 + 0.03125) \cdot 2
\\
\\
&= 3.1875
\end{align}
```


# 本題
さて、ここからが本題です。
以下のようなフラグメントシェーダーを書いた場合、グラデーションがカクカクしてしまいます。

```hlsl
float4 frag (v2f i) : SV_Target
{
    return frac(i.uv.x + 1e6 + 0.001);
}
```

カクカクしてしまうのは、```i.uv.x + 1e6 + 0.001``` という計算に原因があります。
桁数が大きく異なる数を足し合わせると、小さい方の数の情報が落ちてしまうためです。

# 1e6のビット列

10^6 のビット列は以下のようになります。
![06-1e6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/526defc6-57d1-aa06-eb90-fe3f37246a6f.png)

# i.uv.x + 1e6 の計算
次に、```i.uv.x + 1e6```を計算します。

## uv.x = 0.3の場合
```i.uv.x = 0.3``` の時、ビット列は以下のようになります。
![04-value0.3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/2f12cf2e-f2f3-4433-a562-cf9d476321f2.png)


そして、```0.3 + 10^6``` のビット列は以下のようになります。
![05-1e6+0.3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/38d498e5-7e55-1d3d-b8be-e6dad9ccb678.png)

```10^6``` の仮数はそのままですが、```0.3``` の仮数はだいぶ下の方にズレてしまいました。
```0.3``` のビット列の大部分が欠損しています。

このような現象は**情報落ち**と呼ばれます。


## uv.x = 0.9の場合
```i.uv.x = 0.9``` の場合を考えてみます。
```0.9 + 10^6``` のビット列は以下のようになります。
![-06-1e6+0.9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/568bf911-7f62-97be-a8e1-4786fabf5a43.png)
仮数の下4ケタを```0.9```の仮数(だったもの)が埋めています。

## uv.x = 1.0 の場合
```i.uv.x = 1.0```の場合を考えてみます。
```1.0 + 10^6``` のビット列は以下のようになります。
![-06-1e6+1.0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/ac385c42-811b-5f5c-993a-9d317d5ad623.png)

仮数の下5ケタを```1.0``` の仮数(だったもの)が埋めています。

## i.uv.x + 1e6 のUVデータ制度
```i.uv.x + 1e6``` を計算した場合、グラデーションの表現に使える領域は 4 ~ 5ビット程度になりそうです。

カクカクしたグラデーションを見てみると、グラデーションの諧調は16段階になっていますね
<img width = 240 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/489fc822-786c-3452-771e-b0283540eb1d.png">

# おまけ : さらに大きい数を足してみる

これまでは1e6を足していましたが、2倍の2e6を足してみます。

```hlsl
float4 frag (v2f i) : SV_Target
{
    return frac(i.uv.x + 2e6 + 0.001);
}
```

## 結果
諧調数が半分の```8```になります。

<img width = 240 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/b0641071-1b58-5aad-75c5-b59ef7963d9f.png">

# おまけ2 : 0.001を外す
0.001を外して以下のようなフラグメントシェーダーを書くと、グラデーションがなめらかになります。

```hlsl
float4 frag (v2f i) : SV_Target
{
    return frac(i.uv.x + 2e6);
}
```

<img width = 240 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/222b9a0a-c85e-a7b5-6d10-2f2c9b05757d.png">

知識不足で理由がよくわからないのですが、シェーダーコンパイラが良い感じに最適化してくれているのかもしれません。




# 関連
単精度浮動小数点数
https://www.wikiwand.com/ja/%E5%8D%98%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0#

【Unity】【シェーダ】小数点の精度と型の使い分けについて（float / half / fixedの話）
https://light11.hatenadiary.com/entry/2018/06/01/001008

地味にヤバい、シェーダ変数の精度について
https://techblog.kayac.com/unity-shader-parameters-precision