LoginSignup
0
1

More than 3 years have passed since last update.

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

Posted at

はじめに

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

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

環境

Unity 2020.2.0f1
Universal RP 10.2.2

事の発端

シェーダーグラフにて、巨大な数を使うとグラデーションの諧調が減るという不可解な現象に遭遇しました。
image.png

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

検証 その1 : UV.xを画面に出す

ShaderLabでUVのxだけを返すようなフラグメントシェーダーを書いてみます。

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

結果

滑らかな0 ~ 1 のグラデーションが画面に表示されます。

検証 その2 : UV.xに巨大な整数を足す

次に、0.001 という小数を足して、frac で小数部分だけを返すようにしてみます。

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

結果

同じく滑らかなグラデーションが表示されます。
ここまでは、問題ないかと思います。

検証 その3 : 10^6を足してみる

10^6 という巨大な数を足してから、frac で小数部分だけを返すようにしてみます。

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

結果

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

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

floatの内部表現

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

float.png

ビット列から実数を計算する

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

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

クイズ

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

-02-float.png

答え

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

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

\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}

本題

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

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

i.uv.x + 1e6 の計算

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

uv.x = 0.3の場合

i.uv.x = 0.3 の時、ビット列は以下のようになります。
04-value0.3.png

そして、0.3 + 10^6 のビット列は以下のようになります。
05-1e6+0.3.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
仮数の下4ケタを0.9の仮数(だったもの)が埋めています。

uv.x = 1.0 の場合

i.uv.x = 1.0の場合を考えてみます。
1.0 + 10^6 のビット列は以下のようになります。
-06-1e6+1.0.png

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

i.uv.x + 1e6 のUVデータ制度

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

カクカクしたグラデーションを見てみると、グラデーションの諧調は16段階になっていますね

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

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

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

結果

諧調数が半分の8になります。

おまけ2 : 0.001を外す

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

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

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

関連

単精度浮動小数点数
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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1