IEEE 754 浮動小数点入門

レイトレ合宿7 アドベントカレンダー 7週目の記事です。

IEEE 754 浮動小数

IEEE 754 浮動小数点は、本当は完全に扱えるはずもない実数をコンピューター上であたかもそれが実数を扱えるかのような錯覚を感じさせるのに成功している浮動小数点の標準規格です。過去にはたくさんの表現方法があったようですし、新しい表現方法が研究されていたりもするようですが、本記事では浮動小数点はすべて IEEE Standard for Floating-Point Arithmetic のことを指します。

レイトレーシングに限らずコンピューターで浮動小数点を使った数値計算は極めて広い応用がありますが、それにもかかわらず浮動小数点の負の側面、誤差やそれに対する傾向と対策についてはそれほどスポットが当たっていないように思います。 そこで今回はゼロから浮動小数点の扱いについて考えてみようと思います。

内部表現

浮動小数点を理解するには、やはりまずその表現について把握することから始めるのが良いでしょう。 浮動小数点の内部表現といっても、何ビットで表現するか?という問題があります。最も一般的なのは、32bitのbinary32, 64bitのbinary64でしょうか。C++ならそれに対応するのがfloat, doubleというのは極めて身近なデータ型でしょう。しかしC++IEEE 754 浮動小数点が使われているかは処理系定義であることは注意が必要ですが、実際はそのように仮定しても問題ない環境は多いのではないでしょうか。ここではわかりやすさのため、binary32を仮定して話を進めます。テストコードについてはC++で行います。

binary32では浮動小数点は以下のように定義されます。

 
\displaystyle {
{ \left( -1 \right)  }^{ S }\ast 1.M\ast { 2 }^{ \left( E-127 \right)  }
}

ここで  S は符号部で 0 か 1 の1bit 、  M は二進数の仮数部で、Mが23bitつまり23桁あります。 Eは指数部で0から255、つまり8bitです。

wikipediaの図にするとビットの格納方法が一目でわかって便利です。

f:id:ushiostarfish:20190810214723p:plainhttps://ja.wikipedia.org/wiki/IEEE_754 より

...これで浮動小数点を完全に理解できるならばどんなに幸せでしょうか。しかしあきらめずに少しずつ紐解いていくことにします。

仮数

先立って仮数部については簡単に補足します。仮数部は知っての通り、

f:id:ushiostarfish:20190810212226p:plain

のように10進数と対応します。ただしこの図では整数の部分が11の例ですが、浮動小数点では一番左は 1. となる決まりであり、0.とか11.とかはありえません。1つの理由としては、例えば 0.000001 を表現するのに左側のゼロ6個に6ビットつかうのはやはり無駄だからです。せっかく指数部があるのですから、小数点の位置を右に6回動かしてしまえば一番左は 1. になり、もしさらに右に桁があった場合には無駄なく使うことができます。

 
\displaystyle {
0.000001=1.0\ast 2^{ -6 }
}

さらに1. が固定ならば、そこを除いて格納すれば節約になります。というわけで 1.M という表現になっています。ここで 1.M はどんな数かを少し考えてみます。最初に仮にMが4桁で[M=1010]だったとすると、

 
\displaystyle {
1.M=1.1010=1+\frac { 0.1010\ast { 2 }^{ 4 } }{ { 2 }^{ 4 } } \\ =1+\frac { 1010 }{ { 2 }^{ 4 } } 
}

ということは、binary32ではMは23桁であるので、

 
\displaystyle {
1.M=1+\frac { M }{ { 2 }^{ 23 } } 
}

という数だということがわかります。 ということは、仮数部の項というのは、どうやらMをいじくると1以上2未満の間を行ったり来たりできることがわかります。

 
\displaystyle {
1\le 1.M<2
}

指数部

指数部は二進小数点の点の位置を移動させていると先に述べました。しかしこれだけではあまり脳内にイメージできないかもしれません。 そこで数値が与えられたときどのような指数を持つのかを簡単に実験してみます。 実験に先立ってfloatとuint32を相互変換できると便利ですので、以下の関数を準備します。

    inline uint32_t as_uint32(float f) {
        return *reinterpret_cast<uint32_t *>(&f);
    }
    inline float as_float(uint32_t u) {
        return *reinterpret_cast<float *>(&u);
    }

※ unionを使った再解釈は、C++では未定義動作とのことです

そして以下の関数で指数部を取り出してみます。

    inline uint8_t get_exponent(float f) {
        uint32_t e = as_uint32(f) & 0x7F800000;
        return e >> 23;
    }

-10から10くらいの間で浮動小数点を生成して、値をx、E-127 すなわち 指数部が2の何乗であるか? を y にしてプロットしたものが以下の図になります。格子の大きさは1です。

f:id:ushiostarfish:20190810221230p:plain

xが 0から1のエリアを見てみると、指数は0になっています。つまり、

 
\displaystyle {
1\le 1.M\ast 2^{ 0 }<2\\ 1\le 1.M<2
}

右隣はどうでしょうか?2から4のエリアです。

 
\displaystyle {
1\ast 2^{ 1 }\le 1.M\ast 2^{ 1 }<2\ast 2^{ 1 }\\ 2\le 1.M\ast 2^{ 1 }<4
}

なるほど、確かに式の通りです。左隣の0.5から1のエリアはどうでしょうか?

 
\displaystyle {
1\ast 2^{ -1 }\le 1.M\ast 2^{ -1 }<2\ast 2^{ -1 }\\ \frac { 1 }{ 2 } \le 1.M\ast 2^{ -1 }<1

}

元の範囲が1から2であるため、2倍もしくは1/2倍にしていけば確かに領域を重なることなく埋め尽くすことができます。 なるほどよく考えられているものです。

f:id:ushiostarfish:20190810222808p:plain

このことから、指数部というのは担当領域、ある種窓のようなものだと捉えることができるわけです。

再び仮数部へ

指数部によって担当領域が決まること、そしてその幅が可変であることを確認しました。一方で仮数部の項は範囲が固定されているのも確認しました。

 
\displaystyle {
1\le 1.M<2
}

ここで表現できる数の精度限界について考えてみます。指数部と符号はあくまでも範囲を決めているわけなので、精度について重要なのはやはり仮数部になります。浮動小数点の定義から考えると、指数部で決まった範囲を仮数部が均等に刻んでいる、と捉えることができます。

例えばMが2ビットだった場合は、以下のように範囲が刻まれるでしょう。

f:id:ushiostarfish:20190810225114p:plain

実際にはbinary32では23ビットであるため、区間2^{23} 個に分割されることになります。 ということは、例えば1から2の区間の長さは1であるため、最小幅は、


\displaystyle {
{ 2 }^{ -23 }
}

となることがわかります。この数は特別に、”1より大きい最小の数”と1との差、として計算機イプシロン、もしくはマシンイプシロンと呼ばれています。ここでは https://github.com/catchorg/Catch2 を使って簡単なテストとして書いてみると、

TEST_CASE("machine_epsilon") {
    REQUIRE(std::ldexp(1.0f, -23) == std::numeric_limits<float>::epsilon());
}

と、このように実験で確認することができます。

しかし実際には指数部によって定義される区間の幅は可変長であるため、この最小幅も可変であることに気づきます。そこである数を絶対値が大きくなる方向へ1歩だけ動かす最小幅はどんな数であるか考えると、ある数のMを1インクリメントしたときの差分を考えればよさそうです。

 
\displaystyle {
1.M_{ \left( +1 \right)  }\ast { 2 }^{ \left( E-127 \right)  }-1.M\ast { 2 }^{ \left( E-127 \right)  }\\ =\left( 1.M_{ \left( +1 \right)  }-1.M \right) \ast { 2 }^{ \left( E-127 \right)  }
}

ここで、1.Mが、

 
\displaystyle {
1.M=1+\frac { M }{ { 2 }^{ 23 } } 
}

であったのを思い出して、

 
\displaystyle {
\left( 1.M_{ \left( +1 \right)  }-1.M \right) \ast { 2 }^{ \left( E-127 \right)  }=\left( \frac { M_{ \left( +1 \right)  } }{ { 2 }^{ 23 } } -\frac { M }{ { 2 }^{ 23 } }  \right) \ast { 2 }^{ \left( E-127 \right)  }\\ =\left( \frac { 1 }{ { 2 }^{ 23 } }  \right) \ast { 2 }^{ \left( E-127 \right)  }\\ ulp={ 2 }^{ \left( E-127-23 \right)  }\\ ulp={ 2 }^{ \left( E-23-127 \right)  }\\ E_{ ulp }=E-23\\ ulp={ 2 }^{ \left( E_{ ulp }-127 \right)  }
}

とてもシンプルになりました。そしてこの数をulp(unit in the last place) と呼びます。仮数部の最小の桁の単位という気持ちですね。 しかしこの式は浮動小数点の内部表現のEに強く依存しており、若干泥臭いです。そこで少し違うアプローチで表現することを考えます。

ulpと似た概念でufp(unit in the first place)というのがあります。それは、

 
\displaystyle {
ufp\left( x \right) =2^{ \left\lfloor \log _{ 2 } \left| x \right|  \right\rfloor  }
}

ここで、\left\lfloor x \right\rfloorは床関数になります。 これは何かというと、xの値の、指数部が定義する範囲の幅を表しています。

f:id:ushiostarfish:20190810232354p:plain

ということはulpは、その範囲の幅が仮数部によって分解された1つであることから、以下の式で定義できます。

 
\displaystyle {
ulp\left( E \right) ={ 2 }^{ \left( -23 \right)  }ufp\left( x \right) 
}

※ aは0でないとします。

このようにEの代わりに数値そのものと結びつけた表現として扱うこともできます。ついでにいうと左の項は計算機イプシロンになっていますね。

ここである実数 a\in\mathbb{R} を最も近い浮動小数点に丸める関数を

 
\displaystyle {
RN\left( a \right) 
}

とすると、丸めたときの誤差は、

 
\displaystyle {
\left| RN\left( a \right) -a \right| \le \frac { 1 }{ 2 } ulp\left( RN\left( a \right)  \right) 
}

が満たされることになります。ulpを使わない表現としては、

 
\displaystyle {
\left| RN\left( a \right) -a \right| \le \frac { 1 }{ 2 } { 2 }^{ \left( -23 \right)  }ufp\left( a \right) 
}

があり、このとき、

 
\displaystyle {
\frac { 1 }{ 2 } { 2 }^{ \left( -23 \right)  }={ 2 }^{ \left( -24 \right)  }=u
}

のuを単位相対丸め、という名前で使われることがあります。24は仮数部の桁数でもあるので、これをpとして、

 
\displaystyle {
{ 2 }^{ \left( -p \right)  }=u 
}

と表現されることもあります。これを使うと先の式は、

 
\displaystyle {
\left| RN\left( a \right) -a \right| \le u\cdot ufp\left( a \right) 
}

となります。ただし、計算機イプシロンとは違う値になるので、注意が必要です。例えば、GDC Vault - Math for Game Developers: Understanding and Tracing Numerical Errors in C++ ではその点に間違いがあったりします。※もちろんこれはこれで良い資料です。

以下は範囲とulpを表にしたものです。特筆すべき点としては、16777216以上ではもうulpが2以上になるため、もはや整数であっても表すことができない数字がここから出始めています。16777216は32bit整数型ではまだ全然余裕があることを考えると、これはかなりのデメリットです。

また、この表では指数関数的にulpが拡大するようにも見えますが、範囲も指数関数的に広がっているので、実際には表す数字とulpは比例関係(もちろん飛び飛びですが)になっていることには注意しましょう。

ゼロにならない?

浮動小数点の定義より、0に近い数字を表すためにはEにできるだけ小さな値を設定すれば良さそうです。そんなわけで最小値は、

 
\displaystyle {
1.0\ast { 2 }^{ \left( 0-127 \right)  }={ 2 }^{ \left( -127 \right)  }
}

この値は大体、

 
\displaystyle {
0.0000000000000000000000000000000000000058774717541114375398436826861112283890933277838604376075437585313920862972736358642578125
}

なので、かなり0に近いは近いのですが、0ではありません。さらに問題なのは0までの距離です。E=0付近を図にすると、

f:id:ushiostarfish:20190811135706p:plain

 2^{-127} 2^{-126} の間は仮数部により細かく刻まれていることを思い出すと、この0までの距離は { 2 }^{ 23 }=8388608 倍もの大きさの隙間であり、マイナスも考えると、その2倍の隙間が0の場所で存在することになります。そう考えると、これを無視するわけにはいきません。

非正規化数

そんなわけで、浮動小数点では、E=0のときだけ定義を変えてしまおう、ということになっています。それは以下の定義です。

 
\displaystyle {
{ \left( -1 \right)  }^{ S }\ast 0.M\ast { 2 }^{ \left( -126 \right)  }
}

違いはわずかですが、最初の定義では最小の区間は、

 
\displaystyle {
1\le 1.M<2\\ 1*{ 2 }^{ \left( -127 \right)  }\le 1.M*{ 2 }^{ \left( -127 \right)  }<2*{ 2 }^{ \left( -127 \right)  }\\ { 2 }^{ \left( -127 \right)  }\le 1.M*{ 2 }^{ \left( -127 \right)  }<{ 2 }^{ \left( -126 \right)  }
}

という区間でしたが、新しい定義では、

 
\displaystyle {
0\le 0.M<1\\ 0*{ 2 }^{ \left( -126 \right)  }\le 0.M*{ 2 }^{ \left( -126 \right)  }<1*{ 2 }^{ \left( -126 \right)  }\\ 0\le 0.M*{ 2 }^{ \left( -126 \right)  }<{ 2 }^{ \left( -126 \right)  }
}

そう、なんてことはない、0にもっとも近い部分の範囲だけ特別に0まで引き伸ばしているというわけです。

f:id:ushiostarfish:20190811140801p:plain

この特殊化された状態にある浮動小数点を、非正規化数と呼び、最初の定義のほうを正規化数と呼びます。 これなら、0は、E=0, M=0 で表すことができます。

 
\displaystyle {
0.M*{ 2 }^{ \left( E-126 \right)  }\\ =0.0*{ 2 }^{ \left( -126 \right)  }
}

0付近でいきなり"穴"ができるのも防ぐこともできています。しかしいいことばかりではありません。代償は特殊化による演算パフォーマンスの低下であり、もしも非正規化数の取り扱いを辞めてもそれほど最終結果に影響が出ないと判断できる場合は、非正規化数になったとき0にフラッシュするようなモードを使ってこのパフォーマンス低下を回避することも考えられます。例えばembreeではそれをstrongly recommendedとしています。

https://www.embree.org/api.html#performance-recommendations

FLT_MIN, std::numeric_limits::min()

ところで FLT_MIN, std::numeric_limits::min() は負の数ではありません。これは正規化数で表現できる最小の正の数です。これは今までの議論によれば、

 
\displaystyle {
1.0*{ 2 }^{ \left( -126 \right)  }
}

ですので、

TEST_CASE("FLT_MIN") {
    REQUIRE(FLT_MIN == encode(PLUS_SIGN_BIT, 1, 0));
    REQUIRE(std::numeric_limits<float>::min() == encode(PLUS_SIGN_BIT, 1, 0));
    REQUIRE(std::numeric_limits<float>::min() == std::ldexp(1.0f, -126));
}

といったコードで確かめることができます。これは名前から勘違いしやすいところです。

-0 と +0

0近傍の話のついでに、0の符号についてついでに確認します。先の話では、0は、E=0, M=0 で表すことができると述べました。この時実は符号は 0のときと1のときが存在します。これが-0 と + 0です。例えばcopysignといった関数ではちゃんと符号が反映されます。

TEST_CASE("plus_zero, minus_zero") {
    REQUIRE(-1.0f == std::copysign(1.0f, -0.0f));
    REQUIRE(+1.0f == std::copysign(1.0f, +0.0f));
}

注意しなければならない点としては、数としてはやはり0であることから、-0と+0はバイナリとして一致しないにもかかわらず、==で比較するとtureになるということです。

TEST_CASE("special_cases") {
    REQUIRE(-0.0f == +0.0f);
}

そしてこのことは大なり小なりでもそのように動作します。


TEST_CASE("special_cases") {
    REQUIRE(+0.0f >= -0.0f);
    REQUIRE(+0.0f <= -0.0f);
    REQUIRE((+0.0f < -0.0f) == false);
    REQUIRE((+0.0f > -0.0f) == false);
}

これは例えば、


    float x = -0.0f;
    if (0.0f <= x) {
        printf("%f is positive number?\n", x);
    }
    if (x <= -0.0f) {
        printf("%f is negative number?\n", x);
    }

というコードを試してみると、以下のような出力になってしまいます。

-0.000000 is positive number?
-0.000000 is negative number?

上のif文は排他的ではなかったのです。 これはややエッジケースすぎる例かもしれませんが、意外と気づかれていないことかもしれません。

memset(&x, 0, sizeof(x));

ところでfloatやdoubleをmemsetなどでbitを全部0で埋めたらどうなるでしょうか? その場合は当然S=0, E=0, M=0ですから、結局値は+0です。

    float x;
    memset(&x, 0, sizeof(x));
    printf("%f\n", x); // 0.000000

このあたりも良く考えられていて、驚かされます。

例外的な数

 E=0 のときの特殊化の話をしましたが、 E=255 のときも特殊化があり、それは-INF, +INF, NANです。

S E M
0 255 0 +INF
1 255 0 -INF
0~1 255 non 0 NAN

ただ、これについては今回はこれ以上踏み込みません

ulpを手に入れる

0付近の話が整理できたので、一度ulpの話に戻ることにします。floatのある数が与えられたとき、そのulpが欲しいときにはどうすればよいでしょうか。といってもさっきの議論によって、

 
\displaystyle {
E_{ ulp }=E-23
}

であることがわかっているので、これを実装すると、

 const uint32_t MAX_FRACTION = 0x7FFFFF;
    const bool PLUS_SIGN_BIT  = false;
    const bool MINUS_SIGN_BIT = true;

    inline float encode(bool signbit, uint8_t expornent, uint32_t significand) {
        FT_ASSERT(significand < 0x800000);
        return as_float((signbit ? 0x80000000 : 0) | (static_cast<uint32_t>(expornent) << 23) | significand);
    }
    inline float ulp_only_normal_number(float f) {
        int32_t e = get_exponent(f);
        return encode(PLUS_SIGN_BIT, std::max(e - 23, 1), 0);
    }

ここで問題なのは、正規化数の範囲では、1が最小であり、

 
\displaystyle {
1\le E_{ ulp }\\ 1\le E-23
}

を満たさなければなりません。これは結局、

 
\displaystyle {
24\le E
}

の数のulpだけが正規化数で表現できることを意味します。しかし非正規化数が使えるなら話は別です。非正規化数での表現を見つけるには、

 
\displaystyle {
0.M\ast { 2 }^{ \left( -126 \right)  }={ 2 }^{ \left( E-23-127 \right)  }
}

を解いてMを求めれば良さそうです。

 
\displaystyle {
0.M\ast { 2 }^{ \left( -126 \right)  }={ 2 }^{ \left( E-23-127 \right)  }\\ 0.M\ast { 2 }^{ \left( -126 \right)  }=2^{ \left( E-24 \right)  }\ast { 2 }^{ \left( -126 \right)  }\\ 0.M={ 2 }^{ \left( E-24 \right)  }\\ 0.M={ 2 }^{ \left( E-1 \right)  }{ 2 }^{ \left( E-23 \right)  }\\ \frac { M }{ { 2 }^{ 23 } } ={ 2 }^{ \left( E-1 \right)  }\frac { 1 }{ { 2 }^{ 23 } } \\ M={ 2 }^{ \left( E-1 \right)  }

}

求まりました。 というわけで、実装は以下のようになります。

    inline float ulp(float f) {
        int32_t e = get_exponent(f);
        if (24 <= e) {
            return encode(PLUS_SIGN_BIT, e - 23, 0);
        }
        return encode(PLUS_SIGN_BIT, 0, 1 << std::max(e - 1, 0));
    }

e-1にmaxが必要なのは、非正規化数のulpを考えるときです。しかし非正規化数のulpはE=1のときのulpと同じであることは、

f:id:ushiostarfish:20190811155338p:plain

図から明らかです。したがって、maxでガードすれば良いということになります。"floatのulpはfloatで表現することができる"、というわけですね。

floatの隣の数

実数には隣の数はありませんが、floatには隣の数があります。今までの議論から、あるfloatの数の隣の数が欲しいとき、仮数部の数値を増やしたり減らしたりすれば手にはいりそうですが、仮数部がオーバーフローやアンダーフローすることがあります。ところがここでfloatの内部表現を再確認すると、

f:id:ushiostarfish:20190810214723p:plainhttps://ja.wikipedia.org/wiki/IEEE_754 より

指数部が仮数部の上位ビットとして存在しています。ということは全体を整数として解釈するならば、仮数部がオーバーフローやアンダーフローしても、自動的に指数部がインクリメント・デクリメントされるだけであることに気づきます。つまり隣の数が欲しければ整数として解釈してインクリメント・デクリメントしても良いということです。

ただ残念なことに、正のfloatは良いのですが、負のfloatの場合はインクリメントすると絶対値が大きくなる方向に進みますが、一方intをインクリメントすると、絶対値は小さくなります。この対応が逆であることは注意しなければなりません。

2の補数 - Wikipedia

※ただ、正の値のみを考えれば良い問題も多いので、状況によってこのことは無視できます。

この点に注意しながら、floatを順序づけされたintと相互変換する関数を考えてみます。

    inline int32_t to_ordered(float f) {
        uint32_t b = as_uint32(f);

        uint32_t s = b & 0x80000000; // sign bit
        int32_t  x = b & 0x7FFFFFFF; // expornent and significand

        return s ? -x : x;
    }

    inline float from_ordered(int32_t ordered) {
        if (ordered < 0) {
            uint32_t x = -ordered;
            return as_float(x | 0x80000000);
        }
        return as_float(ordered);
    }

これを使うと、floatを任意の歩数だけ移動させる関数は、

    inline float next_float(float x, int move) {
        return from_ordered(to_ordered(x) + move);
    }

と簡単に実装できます。 ただこの実装では-0.0と+0.0は両方とも 0にマッピングされてしまうところは注意が必要です。ただこれはメリットでもあります。なぜなら-0の次の数は+0ではなく、S=0, E=0, M=1 の数であるべきだからです。これは標準の関数である std::nextafter の挙動もそうなっています。もし-0の次が+0だと、数として全く進んでいないことになります。

TEST_CASE("nextafter") {
    REQUIRE(std::nextafter(-0.0f, +std::numeric_limits<float>::max()) != +0.0f);
    REQUIRE(std::nextafter(-0.0f, +std::numeric_limits<float>::max()) == (-0.0f + ulp(-0.0f)));
}

floatの順序性の応用

それって本当に便利なの?という人もいるかもしれません。なので一つここで応用例を挙げます。例えばOpenCLではfloatに対してatomic な max関数がありません。しかし一方で、uintやintに対してはatomic maxが用意されています。

atomic_max

したがって、先のto_ordered関数を使ったり、もし扱う数がすべて正の数に限定できればもっと単純にuintとしてatomic_maxを使っても結果的に正しく処理できるはずです。私独自のラッパーを使っており、少し読みにくいかもしれませんが、例えば以下のようになります。

__kernel void find_max(__global float *values, __global uint *max_value) {
    __local uint local_max;
    if(get_local_id(0) == 0) { 
        local_max = 0;
    }

    barrier(CLK_LOCAL_MEM_FENCE);

    float value = values[get_global_id(0)];
    atomic_max(&local_max, as_uint(value));
    
    barrier(CLK_LOCAL_MEM_FENCE);

    if(get_local_id(0) == 0) { 
        atomic_max(max_value, local_max);
    }
}
TEST_CASE("max_value") {
    using namespace rt;
    auto &env = OpenCLProgramEnvioronment::instance();
    env.setSourceDirectory("../kernels");

    rt::Xoshiro128StarStar random;

    OpenCLContext context;

    int deviceCount = context.deviceCount();
    for (int device_index = 0; device_index < deviceCount; ++device_index) {
        // INFO("device name : " << context.device_info(device_index).name);

        auto lane = context.lane(device_index);

        OpenCLProgram program("find_max.cl", lane.context, lane.device_id);
        OpenCLKernel kernel("find_max", program.program());

        for (int j = 0; j < 100; ++j) {
            std::vector<float> values;
            for (int i = 0; i < 1000; ++i) {
                values.push_back(random.uniform(0.0f, 1000.0f));
            }
            float min_value_truth = 0.0f;
            for (auto f : values) {
                min_value_truth = std::max(min_value_truth, f);
            }
            OpenCLBuffer<float> values_gpu(lane.context, values.data(), values.size(), rt::OpenCLKernelBufferMode::ReadWrite);

            float min_value = 0.0f;
            OpenCLBuffer<uint32_t> min_value_gpu(lane.context, (uint32_t *)(&min_value), 1, rt::OpenCLKernelBufferMode::ReadWrite);

            kernel.setArguments(values_gpu.memory(), min_value_gpu.memory());
            kernel.launch(lane.queue, 0, values.size());

            min_value_gpu.read_immediately((uint32_t *)(&min_value), lane.queue);

            REQUIRE(min_value == min_value_truth);
        }
    }
}

※0以上を仮定しています

このようなハックが可能なのは内部表現の規則のおかげです。

整数

浮動小数点は誤差がある、誤差に気をつけなさい、と耳にタコができるくらいに聞きすぎると、整数の演算にも丸め誤差などが入り込むような錯覚を覚えることがあるかもしれません。 しかし仮数部はbinary32の場合二進数で24桁あり、この範囲では丸め誤差なく完全に表現することができます。

仮数部は、 1.M となっているためビット数よりも桁が一つ多いです

1100110......10.
[       24桁      ] 

せいぜい23歩程度の小数点の移動であり、指数部のビットが足りなくなることもありませんね。24桁ですべて1なら、16777215になります。しかし、16777216=2^{24} であるため、-16777216から16777216までが完全に表現できる最大ということになります。16777217は残念ながら表現できません。ulpが2だからです。この範囲であれば例えば、

    int x = -16777216; /* 2^24 */
    for (float i = x; i <= 16777215.0f /* 2^24 - 1 */; i += 1.0f) {
        REQUIRE(i == x);
        ++x;
    }
    REQUIRE(x == 16777216);

は意図通りに動作します。このように整数をちゃんと表せることが、JavaScriptの数値は64ビットの浮動小数点数だけ、というのを正当化する一つの理由となっています。

誤差

誤差については今まで真の値を丸めたときの誤差しか触れていませんでした。浮動小数点演算では誤差が発生する、と多くの人は認識しており、floatの比較には気を付けましょう、といったことや、答えが0に近づく引き算をしない、などのTipを聞くことはよくあります。有名な既存手法としては、以下のものがあります。

区間演算

floatやdoubleや、より高価な演算を使っても誤差が出るものは出る。ならば正確な答えを出すことよりも、正確な答えのある範囲を確実に求めよう、というのが区間演算のアイディアです。IEEE 754 浮動小数点では四則演算とsqrtは、無限の精度で計算され、それが丸められて答えになるように定められている事実を利用して、精度の保証を行います。

区間演算と交差判定

精度保証付き数値計算の必要性

エラーフリー変換

 
\displaystyle {
fl\left( a\circ b \right) 
}

を、なにかの演算を無限の精度で行ってそれを浮動小数点に丸める計算だとします。多くのシチュエーションでは、

 
\displaystyle {
fl\left( a+b \right) \neq a+b
}

のように丸め誤差のために浮動小数点で真の値を得ることができません。しかし、

 
\displaystyle {
fl\left( a+b \right) =x+y
}

ここでxはなるべく絶対値が大きな数、yは絶対値がなるべく小さい数です。このような変換ができることが知られています。これを利用してあたかも倍の精度で演算が行えるような計算手法が提案されています。

Stef Graillat and Valérie Ménissier-Morain, "Error-free transformations in real and complex floating point arithmetic"

あたかも倍の精度の演算が行えることから double-double演算とも呼ばれるそうです。カハンの加算アルゴリズムもこの手法にアイディアは似ています。

しかし残念ながらこれらの手法はどのように計算をどのように改善すれば良いか?に直接答えてくれるわけではありません。なのでここでは紹介にとどめます。

Practically Accurate Floating-Point Math

そこで、既存の計算アルゴリズムをどのように改善したらよいのか?という視点では、

Neil Toronto and Jay McCarthy | Brigham Young University, "Practically Accurate Floating-Point Math"

が大変よくまとまっており、有益だと感じました。そこで今回はこちらを紹介したいと思います。

エラーの定義

まずはエラーとはどういうものかを定義します。

 
\displaystyle {
error\left( x,r \right) =\frac { \left| x-r \right|  }{ ulp\left( fl\left( r \right)  \right)  } 
}

ここでxは誤差があるかもしれない浮動小数点数、rは真の値です。真の値から何ulpずれているのか?というのがエラーである、というわけです。ulpは相対的な値ですから、相対誤差にも近い概念です。

例えば以下のような関数でいつでも評価できるようにすると便利です。

    template <class T>
    float flulp_error(float x, T r) {
        T e = (T(x) - r) / T(ulp(float(r)));
        return std::fabs(float(e));
    }

Racketではリファレンスとなる値の計算のためにMPFRを使っているそうです。今回私はwindows, C++の環境のため、vcpkg から、

vcpkg install mpfr:x64-windows
vcpkg install mpir:x64-windows

を入れたあと、boost::multiprecision を使います。

    using namespace boost::multiprecision;
    using Real = mpfr_float_1000;

    printf("%f\n", flulp_error(next_float(1.0f, 4), Real(1.0f))); // 4.000000

このように使うことができます。 ほかにも例えばfloatのexpについて調べたい場合、

    using namespace std;
    using namespace boost::multiprecision;

    printf("%f\n", flulp_error<mpfr_float_1000>(exp(10.0f), exp(mpfr_float_1000(10.0f)))); // 

この例では実行すると0.486941が得られます。これは0.5を超えないため、なかなかいい感じではないでしょうか。無限の精度で演算した後に丸められたとしても、0.5ulpはずれるのは回避できないからです。

floatを調べるのにdoubleはリファレンスとして使えるか?

floatよりもdouble, doubleよりもlong doubleで計算したものをリファレンスとするのは、おそらく多くのケースでうまくいくでしょう。しかし反例もあります。

以下の関数があり、x=77617,y=33096 の時の値を求めます。

 
\displaystyle {
f\left( x,y \right) =333.75{ y }^{ 6 }-x^{ 4 }{ y }^{ 6 }+x^{ 2 }\left( 11x^{ 2 }{ y }^{ 2 }-121{ y }^{ 4 }-2 \right) +5.5{ y }^{ 8 }+\frac { x }{ 2y } 
}

これをC++で組むと、

template <class T>
T f(T x, T y) {
    auto x2 = x * x;
    auto y2 = y * y;
    auto y4 = y2 * y2;
    auto y6 = y4 * y2;
    auto y8 = y4 * y4;
    return (T(333.75) * y6 - x2 * y6) + x2 * (T(11.0) * x2 * y2 - T(121.0) * y4 - T(2.0)) + T(5.5) * y8 + x / (T(2.0) * y);
}

そして、型を変えながら実行してみます。

    float a = 77617.0f;
    float b = 33096.0f;
    printf("%.30f, float  (8)\n", (float)f<float>(a, b));
    printf("%.30f, double (15)\n", (float)f<double>(a, b));
    printf("%.30f, mpfr   (30)\n", (float)f<number<mpfr_float_backend<30> >>(a, b));
    printf("%.30f, mpfr   (50)\n", (float)f<mpfr_float_50>(a, b));
    printf("%.30f, mpfr   (100)\n", (float)f<mpfr_float_100>(a, b));
    printf("%.30f, mpfr   (1000)\n", (float)f<mpfr_float_1000>(a, b));

すると、以下の出力が得られました。

1.172603964805603027343750000000, float  (8)
1.172603964805603027343750000000, double (15)
1.172603964805603027343750000000, mpfr   (30)
-0.827396035194396972656250000000, mpfr   (50)
-0.827396035194396972656250000000, mpfr   (100)
-0.827396035194396972656250000000, mpfr   (1000)

※ 括弧の中は10進数での桁数

驚くべきことに、long doubleまで上げても、変化がないので真実かと思いきや、さらに精度を上げていくと突然符号が変化します。ちょっと作為的な例ですが、浮動小数点の信用ならないふるまいを垣間見ることができます。これはRumpの例題というらしいです。"測定"を何度も行って真実を見極めるのは科学として自然なことですが、数値計算でも条件を変えて色々な角度から観察することは有効ではないでしょうか。

エラーの伝播

何か新しい関数を実装するには、多くの場合、浮動小数点の既存の演算を組み合わせて行われます。ある関数が、もし無限の精度で中間の演算が行われ、最後だけ丸められるとしたらその関数は最大でも0.5ulpの誤差しか生まない極めて理想的な関数であると言えます。しかしながら現実的にはパフォーマンス上の問題もありすべての関数をそのように組み立てるのは困難です。一方で演算→丸め→演算→丸め→演算→丸め→...を繰り返しているとどんどん誤差は蓄積していき、最終的には演算の意味消失まであり得ます。青天井です。ではどこで誤差は拡大するのでしょうか?

これを観察するために、もし入力値に1ulpの誤差があったとき、結果にどの程度影響するのか?を以下のようにテストしてみます。

printf("%f\n", flulp_error<mpfr_float_1000>(log(next_float(1.1f, 1)), log(mpfr_float_1000(1.1f))));

// 14.302301

もうちょっと0に近づいてみます。

printf("%f\n", flulp_error<mpfr_float_1000>(log(next_float(1.01f, 1)), log(mpfr_float_1000(1.01f))));

// 126.738930

さらに

printf("%f\n", flulp_error<mpfr_float_1000>(log(next_float(1.0001f, 1)), log(mpfr_float_1000(1.0001f))));

// 16382.376953

入力値が1に近づけば近づくほど、爆発的にエラーが増加しているのがわかります。これはちょうどx軸との交点、結果が0に近づく場所ですね。

f:id:ushiostarfish:20190812133803p:plain

これは結局、入力値に1ulpの誤差が含まれただけで、結果の下位ビットの多くの桁が無意味化するということです。これは計算を組み合わせるときに大いに問題になります。ただしこれはあくまでも入力値に誤差が含まれた時の話です。もし入力値が真実に十分近ければ問題にならないこともあります。例えば以下のように

    printf("%f\n", flulp_error<mpfr_float_1000>(log(1.1f), log(mpfr_float_1000(1.1f))));
    printf("%f\n", flulp_error<mpfr_float_1000>(log(1.01f), log(mpfr_float_1000(1.01f))));
    printf("%f\n", flulp_error<mpfr_float_1000>(log(1.0001f), log(mpfr_float_1000(1.0001f))));

        // 0.302301
        // 0.261070
        // 0.377019

全然問題ありません。伝播のしやすさともともとの関数の精度はまったく関係がないのです。 また、時々桁落ちの誤差の説明で、以下のような説明を目にします。

1.3334-1.3333 を計算したいが、答えは 0.0001 になってしまい、有効数字が5桁から1桁に減少する。

なんだか狐につままれたような感じがします。はっきりしているのは、もしもこの二つの入力値が真実なら、答えの0.0001はやはり真実だということです。実際、問題は入力値が丸め誤差などによって信頼できない場合です。

例えば真実の下位の桁が [ ] のような数だったとすると、

1.3334 [ 439475 ] - 1.3332 [ 635621 ] = 0.0001803854

最後に丸めても、

0.00018039

このように最終結果に本来反映されるべきは [ ] の中だったわけで、真実にかなり近い数を保持できるポテンシャルがありながら、丸められた入力値で計算したことで 0.0001のさらに下の桁が失われてしまったわけです。

伝播は誰の仕業か?

ところでこの伝播はどこで拡大するのでしょうか?いったんエラーの定義を確認します。

 
\displaystyle {
error\left( x,r \right) =\frac { \left| x-r \right|  }{ ulp\left( fr\left( r \right)  \right)  } 
}

ここでulpは以下のように近似できると仮定します。

 
\displaystyle {
ulp\left( x \right) \simeq ulp\left( 1 \right) \left| x \right| 
}

このとき、入力値の 1 ulpのずれを観察します。

 
\displaystyle {
x=f\left( x+ulp\left( x \right)  \right) \\ r=f\left( x \right) \\ error\left( x,r \right) =\frac { \left| f\left( x+ulp\left( x \right)  \right) -f\left( x \right)  \right|  }{ ulp\left( fr\left( f\left( x \right)  \right)  \right)  } \\ =\frac { \left| f\left( x+ulp\left( x \right)  \right) -f\left( x \right)  \right|  }{ ulp\left( 1 \right) \left| fr\left( f\left( x \right)  \right)  \right|  } \\ =\frac { \left| x \right| \cdot \left| f\left( x+ulp\left( x \right)  \right) -f\left( x \right)  \right|  }{ \left| x \right| \cdot ulp\left( 1 \right) \left| fr\left( f\left( x \right)  \right)  \right|  } \\ =\frac { \left| x \right| \cdot \left| f\left( x+ulp\left( x \right)  \right) -f\left( x \right)  \right|  }{ ulp\left( x \right) \left| fr\left( f\left( x \right)  \right)  \right|  } 
}

ここで、

 
\displaystyle {
\Delta x=ulp\left( x \right) 

}

と捉えると、

 
\displaystyle {
=\frac { \left| x \right| \cdot \left| f\left( x+\Delta x \right) -f\left( x \right)  \right|  }{ \left| fr\left( f\left( x \right)  \right)  \right| \Delta x } 
}

これを微分として解釈してしまうと、

 
\displaystyle {
error\left( x,r \right) =\frac { \left| x \right| \cdot \left| f'\left( x \right)  \right|  }{ \left| fr\left( f\left( x \right)  \right)  \right|  } \\ =\left| \frac { x\cdot f'\left( x \right)  }{ fr\left( f\left( x \right)  \right)  }  \right| 
}

最後に浮動小数点丸めを見なかったことにしてもさほど影響がないので無視すると、

 
\displaystyle {
error\left( x,r \right) =\left| \frac { x\cdot f'\left( x \right)  }{ f\left( x \right)  }  \right| 
}

ここからわかることは、このエラーの伝播の性質というのは浮動小数点の問題ではなく、本来その関数が持つ性質であったということです。逆に考えると、エラーの伝播のしやすさは、計算アルゴリズムを工夫しても改善できるものではないということです。我々ができるのは、エラーの伝播&拡大しやすいところになるべく立ち入らず、立ち入る場合は誤差をなるべく少なくした状態で入るようにする、といったことなのです。

バッドランド

エラーの伝播&拡大しやすいところを把握することは重要です。Neil Toronto and Jay McCarthy | Brigham Young University, "Practically Accurate Floating-Point Math" では、

 
\displaystyle {
4\le \left| \frac { x\cdot f'\left( x \right)  }{ f\left( x \right)  }  \right| 
}

の領域を、エラーの伝播&拡大のしやすいところとし、バッドランド(badlands)と呼んでいます。荒地、などと呼んでもいいかもしれませんが、逆にわかりにくい気がするのでカタカナで呼びます。

例えば先のlogのバッドランドを見てみます。gnuplotは割と使いやすいです。

class GNUPlot2D {
public:
    GNUPlot2D() {
        _fp = std::shared_ptr<FILE>(_popen("gnuplot", "w"), _pclose);
        gnu_printf("$data << EOD\n");
    }
    void add(double x, double y) {
        gnu_printf("%f %f\n", x, y);
    }
    void show(const char *label, double zMin, double zMax) {
        gnu_printf("EOD\n");
    
        gnu_printf("set grid\n");
        gnu_printf("set yrange[%e:%e]\n", zMin, zMax);
        gnu_printf("plot '$data' using 1:2 with lines title '%s'\n", label);

        flush();
    }
private:
    void gnu_printf(const char *format, ...) {
        va_list ap;

        va_start(ap, format);
        vfprintf(_fp.get(), format, ap);
        va_end(ap);
    }
    void flush() {
        fflush(_fp.get());
    }
    std::shared_ptr<FILE> _fp;
};

    GNUPlot2D p;
    int N = 1000;
    for (int i = 0; i < N; ++i) {
        float x = glm::mix(0.0f, 2.0f, float(i) / N);
        float e = flulp_error<mpfr_float_1000>(log(next_float(x, 1)), log(mpfr_float_1000(x)));
        p.add(x, e);
    }
    p.show("log", 0.0, 100.0);

f:id:ushiostarfish:20190812172400p:plain

x が1に近い所でバッドランドが観測できます。また、desmosのようなツールを使ってバッドランドを確認するのも良いでしょう。

f:id:ushiostarfish:20190812184639p:plain

ちなみにlogの場合は、バッドランドの定義より、解析的に範囲を求めることができます。

 
\displaystyle {
\left| \frac { x\cdot f'\left( x \right)  }{ f\left( x \right)  }  \right| \le 4\\ \left| \frac { x\cdot \frac { 1 }{ x }  }{ \log { \left( x \right)  }  }  \right| \le 4\\ \left| \frac { 1 }{ \log { \left( x \right)  }  }  \right| \le 4\\ \left| \log { \left( x \right)  }  \right| \le \frac { 1 }{ 4 } \\ -\log { \left( x \right)  } \le \frac { 1 }{ 4 } ,\log { \left( x \right)  } \le \frac { 1 }{ 4 } \\ x\ge exp\left( -\frac { 1 }{ 4 }  \right) ,x\le exp\left( \frac { 1 }{ 4 }  \right) \\ exp\left( -\frac { 1 }{ 4 }  \right) \le x\le exp\left( \frac { 1 }{ 4 }  \right) 
}

続いて、桁落ちの原因とされる x - y のバッドランドも見てみましょう。


class GNUPlot3D {
public:
    GNUPlot3D() {
        _fp = std::shared_ptr<FILE>(_popen("gnuplot", "w"), _pclose);
        gnu_printf("$data << EOD\n");
    }
    void add(double x, double y, double z) {
        gnu_printf("%e %e %e\n", x, y, z);
    }
    void next_row() {
        gnu_printf("\n");
    }
    void show(const char *label, double zMin, double zMax) {
        gnu_printf("EOD\n");
        
        gnu_printf("set pm3d\n");
        gnu_printf("set cbrange[0:8]\n");
        gnu_printf("set palette defined(-1 \"blue\", 0 \"white\", 1 \"red\")\n");
        
        gnu_printf("set ticslevel 0\n");
        gnu_printf("set zrange[%e:%e]\n", zMin, zMax);
        gnu_printf("set xlabel 'x'\n");
        gnu_printf("set ylabel 'y'\n");
        gnu_printf("splot '$data' using 1:2:3 w pm3d title '%s'\n", label);
        
        flush();
    }
private:
    void gnu_printf(const char *format, ...) {
        va_list ap;

        va_start(ap, format);
        vfprintf(_fp.get(), format, ap);
        va_end(ap);
    }
    void flush() {
        fflush(_fp.get());
    }
private:
    std::shared_ptr<FILE> _fp;
};
    GNUPlot3D p;
    int N = 64;
    for (int i = 0; i < N; ++i) {
        float y = glm::mix(-1.0f, 1.0f, float(i) / N);

        for (int j = 0; j < N; ++j) {
            float x = glm::mix(-1.0f, 1.0f, float(j) / N);

            float value = next_float(x, 1) - next_float(y, -1);
            mpfr_float_1000 truth = mpfr_float_1000(x) - mpfr_float_1000(y);
            float e = flulp_error<mpfr_float_1000>(value, truth);
            p.add(x, y, e);
        }
        p.next_row();
    }
    p.show("x - y", 0.0, 16.0);

f:id:ushiostarfish:20190812172540p:plain

0付近は値が大きいので飛んでしまっていますが、xとyが近い値のとき、バッドランドになっていることがわかります。 他にも Neil Toronto and Jay McCarthy | Brigham Young University, "Practically Accurate Floating-Point Math" で既にプロットされたものがあるので、覗いてみるのも良いでしょう。

また、便利なバッドランドリストもあります。

f:id:ushiostarfish:20190812183148p:plain ※ Figure 6 Neil Toronto and Jay McCarthy | Brigham Young University, "Practically Accurate Floating-Point Math"

関数を改善する

今までの知識を用いて、以下の関数を考えます。ただし簡単のため、x, yは正の数とします。

 
\displaystyle {
f\left( x,y \right) =\log { \left( \frac { x }{ y }  \right)  } 
}

まずはこれがどれだけの精度があるのかを確認してみます。

float f(float x, float y) {
    return log(x / y);
}

mpfr_float_1000 f_truth(mpfr_float_1000 x, mpfr_float_1000 y) {
    return log(x / y);
}
    GNUPlot3D p;
    int N = 64;
    for (int i = 0; i < N; ++i) {
        float y = float(i) / N;

        for (int j = 0; j < N; ++j) {
            float x = float(j) / N;

            float value = f(x, y);
            mpfr_float_1000 truth = f_truth(x, y);
            float e = flulp_error<mpfr_float_1000>(value, truth);
            p.add(x, y, e);
        }
        p.next_row();
    }
    p.show("log(x/y)", 0.0, 16.0);

f:id:ushiostarfish:20190812183514p:plain

 
\displaystyle {
f\left( x,y \right) =\log { \left( \frac { x }{ y }  \right)  } 
}

なかなか誤差が大きいエリアがあることが見て取れます。どうやら   \frac { x }{ y } が1に近いエリアでどうやら数値が暴れています。これは先に述べたlogのバッドランドそのものです。割り算の結果が丸められ、その誤差がバッドランドで伝播・拡大したことが原因と考えられます。バッドランドリストによれば、logのバッドランドは、

 
\displaystyle {
\exp\left(-\frac{1}{4}\right)\le x\le\exp\left(\frac{1}{4}\right)
}

のエリアがバッドランドであり、このエリアだけ回避するのが良いと思われます。そこで、ここではlogを直接使うのではなく、以下の関数を活用することを考えます。

 
\displaystyle {
\log _{ 1p }{ \left( z \right)  } =\log { \left( z+1 \right)  } 
}

どうやらいろんな環境でも提供されている関数のようです。

std::log1p, std::log1pf, std::log1pl - cppreference.com

これのバッドランドは、オリジナルとは少しずれています。

f:id:ushiostarfish:20190812190553p:plain

ここで、これを使うように式を変形します。

 
\displaystyle {
f\left( x,y \right) =\log { \left( \frac { x }{ y } +1-1 \right)  } \\ =\log _{ 1p }{ \left( \frac { x }{ y } -1 \right)  } \\ =\log _{ 1p }{ \left( \frac { x-y }{ y }  \right)  } 
}

このとき、\frac { x-y }{ y } は、 exp\left( -\frac { 1 }{ 4 }  \right) \le \frac { x }{ y } \le exp\left( \frac { 1 }{ 4 }  \right) であるとき、

 
\displaystyle {
exp\left( -\frac { 1 }{ 4 }  \right) \le \frac { x }{ y } \le exp\left( \frac { 1 }{ 4 }  \right) \\ exp\left( -\frac { 1 }{ 4 }  \right) -1\le \frac { x }{ y } -1\le exp\left( \frac { 1 }{ 4 }  \right) -1\\ exp\left( -\frac { 1 }{ 4 }  \right) -1\le \frac { x-y }{ y } \le exp\left( \frac { 1 }{ 4 }  \right) -1
}

となりますので、これが \log _{ 1p } のバッドランドを踏んでいなければ良さそうです。確認してみます。

f:id:ushiostarfish:20190812191527p:plain

エクセレント!大丈夫のようです。これを試してみましょう。

float f_enhanced(float x, float y) {
    static const float lower = std::exp(-0.25f);
    static const float upper = std::exp(+0.25f);

    float x_div_y = x / y;
    if (lower <= x_div_y && x_div_y < upper) {
        return log1p((x - y) / y);
    }
    return log(x_div_y);
}

f:id:ushiostarfish:20190812192151p:plain

2ulpを超える所がすべて排除されています!

ところでじゃあ最初から、

 
\displaystyle {
f\left( x,y \right) =\log _{ 1p }{ \left( \frac { x-y }{ y }  \right)  } 
}

を使うのはダメなんだろうか?というのが頭をよぎります。試してみます。

f:id:ushiostarfish:20190812192509p:plain

確かにもともと存在していた山は改善しています。しかし、端の  x=0付近では誤差が大きいことがわかります。

 
\displaystyle {
\frac { x-y }{ y } 
}

は x が0なら、 \frac { -y }{ y } =-1 になるわけで、おっと、この場所はlogp1のバッドランドではありませんか! というわけで結局今回のアプローチでは片方だけではうまくいかないということになります。

ちなみにこの話にはまだ続きがあり、Neil Toronto and Jay McCarthy | Brigham Young University, "Practically Accurate Floating-Point Math" にはさらにロバストにするには、という話もありますし、他の例もありますので、興味のある方は覗いてみると良いと思います。

まとめ

IEEE 754 浮動小数点の内部表現から、誤差の定式化、そしてどのような回避方法があるのかを調べ、そして簡単な改善アプローチを紹介しました。しかし実際に活用する時には、対処したい問題のドメインの知識もさらに加わりますし、6次元、7次元の関数を組み立てるときの対処方法についてもまだあまり良いアプローチを確立できていません。誤差の定式化も、これが最善かというと、確信は持てません。非正規化数のパフォーマンスの低下、フラッシュによる誤差の問題もあります。このように結局銀の弾丸はまだありませんが、浮動小数点がブラックボックスである状況に比べたら余程対策の立てようがあるのではないでしょうか。また、誤差を改善しました、という系の論文を読むときにもこの知識は大いに役に立つのではないかと思います。

参考文献

FLOATING POINT VISUALLY EXPLAINED

数値計算の常識

精度保証付き数値計算の基礎

区間演算と交差判定

精度保証付き数値計算の必要性

Stef Graillat and Valérie Ménissier-Morain, "Error-free transformations in real and complex floating point arithmetic"

Neil Toronto and Jay McCarthy | Brigham Young University, "Practically Accurate Floating-Point Math"

GDC Vault - Math for Game Developers: Understanding and Tracing Numerical Errors in C++

Managing Rounding Error

Boost Interval Arithmetic Library - 1.70.0

Floating Point | Random ASCII – tech blog of Bruce Dawson

Pavel Panchekha, Alex Sachez-Stern, James R. Wilcox, Zachary Tatlock, "Automatically Improving Accuracy for Floating Point Expressions"

f:id:ushiostarfish:20190812205643p:plain

A Practical Microcylinder Appearance Model for Cloth Rendering to implementation (2)

The previous article is here.

ushiostarfish.hatenablog.com

日本語でも良い方はこちらを ( If you like Japanese, please check this )

ushiostarfish.hatenablog.com

Shading Model

Like the following figure, this paper makes shading model from warp and weft that is orthogonal.

f:id:ushiostarfish:20181229174211p:plain

※Fig. 2 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

It shows that each thread has different areas. So the following radiance formulation uses this fact.

 
\displaystyle {
L_{ r }\left( \omega _{ r } \right) =a_{ 1 }L_{ r,1 }\left( \omega _{ r } \right) +a_{ 2 }L_{ r,2 }\left( \omega _{ r } \right) 
}

where  a_1, a_2 is simply parameters. If this is an interpolation, two a is unnecessary. But fig. 2 left picture shows there are cases that cloth contains some gap. In this case  a_1 + a_2 \lt 1. So we need two a. In contrast,  a_1 + a_2=1 if cloth contains no gap.

Each  L_{ r,j }\left( \omega _{ r } \right) is defined by

 
\displaystyle {
L_{ r,j }\left( \omega _{ r } \right) =\frac { 1 }{ N_{ j } } \sum _{ t }^{  }{ \int _{ \Omega  }^{  }{ L_{ i } } f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) \cos { \theta _{ i }d\omega _{ i } }  } 
}

※ This is BRDF, so  \Omega is a hemisphere.

Where  N_{ j } is sample count of tangents because a thread is weaving at least one. We must evaluate each tangent contribution and the average is the final value.

f:id:ushiostarfish:20181229180255p:plain

※ Fig. 12, Polyester Satin Charmeuse fabric IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

 f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) is previous thread model.

There was no reference for how to concretely sample in the paper. In this time I decided to use the control point at Table II in the paper as the sample point. Although this may not be good, I got an image that has a good match with the rendered image of the paper. I think it is a possible stochastic unbiased evaluation, but I think it is not clear whether the final result will lead to better results.

Although the whole flow can be covered above, in this paper, two more adjustments are presented to make it closer to the measurement result. This is "Shadowing and Masking" and "Reweighting".

Shadowing and Masking

Shadowing, masking function must be chosen suitable this shading model that is based on orthogonal warp and weft. So it is defined by

 
\displaystyle {
M\left( t,\omega _{ r } \right) =max\left( \cos { \phi _{ r } } ,0 \right) \\ M\left( t,\omega _{ i } \right) =max\left( \cos { \phi _{ i } } ,0 \right) 
}

Where M depend on only  \phi _{ i }, \phi _{ r }. The reason is clear at Fig 13 in this paper.

f:id:ushiostarfish:20181229221524p:plain

※ Fig. 13, Center, Right Figure, Polyester Satin Charmeuse fabric IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

In left-hand side, the thread isn't shadowed by other thread because the tilt direction is same to thread direction. But right-hand side, the thread can be shadowed by other thread. So, Shadowing, Masking use only  \phi angle.

Next, why we use  \cos { \phi _{ r } } in Shadowing, Masking ? This is because of a cylinder shadow. Please check following two cylinders in cylinder array. A white one cast a shadow on some of the gray one.

f:id:ushiostarfish:20190102161556p:plain

I'll show visibility in expression. These cylinder radius is 0.5.

f:id:ushiostarfish:20181229225827p:plain

So, visibility is  \cos { \phi }.

Correlations of Shadowing, Masking

If Shadowing, Masking are assumed that has no correlations, Shadowing & Masking is defined by the following formulation using a simple product.

 
\displaystyle {
M_{ no-correlation }\left( t,\omega _{ i },\omega _{ r } \right) =M\left( t,\omega _{ i } \right) M\left( t,\omega _{ r } \right) 
}

But this assumption has well-known problem at e.g.  \omega _{ i } = \omega _{ o } case. The visible area is exactly the same in this case, Masking both express surface visibility. Despite only on one side is enough to express the visibility, the product of the above formulation is overkill. This is a typical case that no-correlation assumption does not work well. Against this, a shadowing & masking combination method proposed at Michael Ashikhmin, Simon Premoze, Peter Shirley, "A Microfacet-based BRDF Generator" is chosen in this paper.

This idea is simple, we define the following expression as Shadowing & Masking function with strong correlation contrast to no correlation formulation.

 
\displaystyle {
M_{ correlation }\left( t,\omega _{ i },\omega _{ r } \right) =max\left( M\left( t,\omega _{ i } \right) ,M\left( t,\omega _{ r } \right)  \right) 
}

I think "Shadowing area contains Masking area" or "Masking area contains Shadowing area".

Finally, we interpolate linearly  M_{ no-correlation } to  M_{ correlation } by "similarity between incident direction and outgoing direction ".

 
\displaystyle {
M\left( t,\omega _{ i },\omega _{ r } \right) =\left( 1-u\left( \phi _{ d } \right)  \right) M\left( t,\omega _{ i } \right) M\left( t,\omega _{ r } \right) +u\left( \phi _{ d } \right) max\left( M\left( t,\omega _{ i } \right) ,M\left( t,\omega _{ r } \right)  \right) 
}

Where "similarity between incident direction and outgoing direction" is a unit-height gaussian function that taking  \phi _{ d } as its argument

 
\displaystyle {
u\left( \phi _{ d } \right) =\left( -\frac { \phi _{ d }^{ 2 } }{ 2\sigma ^{ 2 } }  \right) 
}

Where  \sigma is between 15 deg to 25 deg, I chose 20 deg for my implementation. However, it will not change so much when it is 15 degrees or 25 degrees.

And we can rewrite each shading model by

 
\displaystyle {
L_{ r,j }\left( \omega _{ r } \right) =\frac { 1 }{ N_{ j } } \sum _{ t }^{  }{ \int _{ \Omega  }^{  }{ L_{ i } } f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) M\left( t,\omega _{ i },\omega _{ r } \right) \cos { \theta _{ i }d\omega _{ i } }  } 
}

Reweighting

Let's start with fig 13 in this paper for thinking of Reweighting.

f:id:ushiostarfish:20181230124255p:plain

※ Fig 13 Left IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

It looks the blue thread at the center is strong and its edge (=grazing angle) is weak blue. Because an apparent length( projected length to view direction ) of a thread is shorter by tilting it. Conversely, the orange thread apparent length looks uniform in any position.

So apparent length depends on only  \psi.

※ I have another idea that the blue thread weakness variation is caused by the orange thread apparent density. But the orange thread apparent density depends on the projected length of the blue thread to view direction. So, it is equivalent.

We can imagine this phenomenon like this.

f:id:ushiostarfish:20181230134818p:plain

It looks like "distribution of visible normal" in microfacet theory.

 \psi is not exist in my "Coordinate System" section. It is shown at Fig 14 in this paper.

f:id:ushiostarfish:20181230125716p:plain

※ Fig 14 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

It is a clear definition. The angle is in plane of thread direction and cloth normal.

A apparent length is expressed by  \cos \psi . The new weight function is defined by

 
\displaystyle {
P\left( t,\omega _{ r } \right) =max\left( \cos { \psi _{ r } } ,0 \right) \\ P\left( t,\omega _{ i } \right) =max\left( \cos { \psi _{ i } } ,0 \right) 
}

Coincidentally it is the same formula as Shadowing, Masking. Why considering not only outgoing direction but also incident direction is that there is the same phenomenon when light receives at a thread. The final contribution increases as apparent length are longer because it can receive more light. However, LTE contains  \cos \theta_i that is considering of radiance density variation. So I think it is a little bit overkill. But it has a difference that  P term depends on only  \psi, so it may have different significance.

In the paper, because the idea is similar to Shadowing, Masking, the same definitions for incoming and outgoing is chosen.

 
\displaystyle {
P\left( t,\omega _{ i },\omega _{ r } \right) =\left( 1-u\left( \psi _{ d } \right)  \right) P\left( t,\omega _{ i } \right) P\left( t,\omega _{ r } \right) +u\left( \psi _{ d } \right) max\left( P\left( t,\omega _{ i } \right) ,P\left( t,\omega _{ r } \right)  \right) 
}

Where  \psi_d is  \psi_r - \psi_i . This is same idea to  \phi _d .

We can rewrite the shading model with adding new weighting function.

 
\displaystyle {
L_{ r,j }\left( \omega _{ r } \right) =\frac { 1 }{ Q } \frac { 1 }{ N_{ j } } \sum _{ t }^{  }{ \int _{ \Omega  }^{  }{ L_{ i } } f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) M\left( t,\omega _{ i },\omega _{ r } \right) P\left( t,\omega _{ i },\omega _{ r } \right) \cos { \theta _{ i }d\omega _{ i } }  } 
}

There is normalize term  Q. It is because that  P cause energy scaling as A sum of  P weight is not 1. It different from Shadowing, Masking.  Q is

 
\displaystyle {
Q=\frac { a_{ 1 } }{ N_{ 1 } } \sum _{ t }^{  }{ P\left( t \right)  } +\frac { a_{ 2 } }{ N_{ 2 } } \sum _{ t }^{  }{ P\left( t \right)  } +\left( 1-a_{ 1 }-a_{ 2 } \right) a_{ 2 }\cdot n
}

It is a bit confusing. So I'll check each sample weight before adding Reweighting with the following figure.

f:id:ushiostarfish:20181230142635p:plain

The outside box has a unit area (= 1) in this figure. And it separated by parameters  a_1, a_2. Each thread has samples at least one, it is assigned a uniform weight in its thread. Where each weight is area assigned to sample.

We can regard that  P added into shading model scale the each area. So we must normalize by division by a sum of scaled area. The right term  \left( 1-a_{ 1 }-a_{ 2 } \right) a_{ 2 }\cdot n support  a_{ 1 } + a_{ 1 } \lt 1 case, it handle no-thread part using simply normal.

Implementation

My implantation is straightforward. I'll show this without embed by link because it is too long.

microcylindercloth · GitHub

material · GitHub

3d Model

I have prepared the shading model but It needs a 3d model for cloth. And I need to reproduce this paper rendering image, so I want to 3d model very close to the paper image. I heard Houdini 17 got new simulation module Vellum, so I'll try it.

※ Houdini17 have crashed sometime in My MSI laptop pc because of Nahimic. Houdini 17 crashing constantly - General Houdini Questions - od|forum And it has an initialization problem for OpenCL [Solved] OpenCL setup for 17? | Forums | SideFX. But this is maybe environment-dependent, so stop to talk about this problem.

Vellum is constraint-based solver highly inspired by Matthias Müller, Bruno Heidelberger, Marcus Hennix, John Ratcliff, "Position Based Dynamics". It is easy for example it can be completed only on "sop". So I'll introduce it.

Vellum

First, I make simple ground with Tube and Grid.

f:id:ushiostarfish:20181230150113p:plain

Next, I prepare cloth with appropriate density above the ground. it must be added uv by "uvflatten" for tangent vector calculation.

f:id:ushiostarfish:20181230150315p:plain

Next, I prepare constraint. There is cloth preset in houdini for a shortcut.

f:id:ushiostarfish:20181230150557p:plain

The stiffness at Bend can control difficulty in cloth bending. Since there are not so many parameters, if you don't like it, it is a good idea to modify the parameters. However, I heard from my friend who familiar with the simulation, "if it is possible, you should try experimenting with the cloth in hand" . There is a case you don't notice that approach is wrong with the only simulation.

We can check the constraint by connecting to null.

f:id:ushiostarfish:20181230151232p:plain

connecting to Vellum Solver for simulation.

f:id:ushiostarfish:20181230151406p:plain

We can tune friction with about "Forces/Friction/Static Threshold" parameters. If the constraints don't converge, I suggest increasing "Solver/Substeps".

Finally, Let's start the timeline.

f:id:ushiostarfish:20181230151738g:plain

it's easy!

I export obj at the appropriate frame by "File Cache" after "Smooth", "Subdivide" for adjustment.

f:id:ushiostarfish:20181230152009p:plain

The tangent vector can be prepared any method any timing but I precalculate it by "polyframe" in houdini.

f:id:ushiostarfish:20181230152645p:plain

※ This is vertex-based calculation. so I adjusted it the vector orthogonal in primitive.

That's all for preparation of 3d model.

Results

I used the following scene. (Actually, it arranged so as to approach the rendered image of the paper. )

f:id:ushiostarfish:20181230153241p:plain

These image by Table Ⅱ parameter in this paper.

Linen Plain

f:id:ushiostarfish:20181230154032p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.46 Both (0.2, 0.8, 1.0) * 0.3 0.3 12 24 0.33 -25, 25

Silk Crepe de Chine

f:id:ushiostarfish:20181230154233p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.345 Flat (1.0, 0.95, 0.05) * 0.12 0.2 5 10 0.75 -35, -35, 35, 35
1.345 Twist (1.0, 0.95, 0.05) * 0.16 0.3 18 32 0.25 0, 0

Polyester Satin Charmeuse

f:id:ushiostarfish:20181230154800p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.539 Flat (1.0, 0.37, 0.3) * 0.035 0.1 2.5 5 0.9 -32, -32, -18, 0, 0 , 18, 32, 32
1.539 Twist (1.0, 0.37, 0.3) * 0.02 0.7 30 60 0.1 0, 0

Polyester Satin Charmeuse (Back-side)

f:id:ushiostarfish:20181230155451p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.539 Flat (1.0, 0.37, 0.3) * 0.035 0.1 2.5 5 0.67 -30, -30, 30, 30, -5, -5, 5, 5
1.539 Twist (1.0, 0.37, 0.3) * 0.02 0.7 30 60 0.33 0, 0

Silk Shot

f:id:ushiostarfish:20181230155850p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.345 Dir 1 (0.1, 1.0, 0.4) * 0.2 0.1 4 8 0.86 -25, -25, 25, 25
1.345 Dir2 (1.0, 0.0, 0.1) * 0.6 0.1 5 10 0.14 0, 0

Velvet

For some reason my Velvet did not match the result with the pictures of the papers. Perhaps it may be adjusting lighting and normals map to show the Siggraph logo.

f:id:ushiostarfish:20181230160224p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.46 Dir 1 (0.05, 0.02, 0.0) * 0.3 0.1 6 12 0.5 -90, -50
1.46 Dir2 (0.05, 0.02, 0.0) * 0.3 0.1 6 12 0.5 -90, -55, 55, 90

Fig. 24 in the paper

f:id:ushiostarfish:20181230160515p:plain

I could reproduce these render image except Velvet.

Impressions and Conclusion

I implemented velvet shading at ray tracing camp 6 . but I would try because I was short of the survey. The implementation of "A Practical Microcylinder Appearance Model for Cloth Rendering" looks not so heavy for me. However, the source code example could not be obtained, so it took quite some time.

This method might not be a sophisticated method, or it might be difficult to say Mathematically well defined. But this method is using minimum formulation for matching to measurement data, although it is only Far Field, I feel a pretty real taste. I think the point of view of this paper is so great.

She says...

"approximate" is very important in Physics ♡. An integral, equation that can't be solved (Proofed) exist but if these take "approximate" that ignoring some part excluding the extreme conditions or essentials component, it could be solved, and it's often no problem. What it is treated as important depends on the circumstance, so this is a good time to show their skills. ><

※ にゃ=meow, (her favorite way of talking)

On the other hand, energy conservation problems and the cost of shaders still have many tasks, and in particular, I think that it is difficult to intuitively prepare the parameters of Tangent Offsets. In addition, we easy to see details of the cloth, so far-field rendering is important.

Cloth shading technology has many compatibilities to hair and fur shading so we can see a wide range of knowledge on the way. If you are interested in this field, it would be interesting to look it up.

Acknowledgment

In implementing, Mr. Henrik W. Jensen has answered some questions by e-mail and helped me to understand well. Thank you so much.

References

IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

※ You can see their presentation video if you purchase at ACM Digital Library

Iman Sadeghi, "Controlling the Appearance of Specular Microstructures"

信州大学繊維学部, はじめて学ぶ繊維

Michael Ashikhmin, Simon Premoze, Peter Shirley, "A microfacet-based brdf generator"

Piti Irawan, "Appearance of Woven Cloth"

Wenzel Jakob, Adam Arbree, Jonathan T. Moon, Kavita Bala, Steve Marschner, "radiative transfer framework for rendering, materials with anisotropic structure"

Stephen R. Marschner, Henrik Wann Jensen, Mike Cammarano, "Light Scattering from Human Hair Fibers"

Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model"

Bruce Walter, Stephen R. Marschner, Hongsong Li, Kenneth E. Torrance, "Microfacet Models for Refraction through Rough Surfaces"

Subrahmanyan Chandrasekhar, "Radiative Transfer"

Michael Ashikhmin, Simon Premoze, Peter Shirley, "A Microfacet-based BRDF Generator"

Matthias Müller, Bruno Heidelberger, Marcus Hennix, John Ratcliff, "Position Based Dynamics"

A Practical Microcylinder Appearance Model for Cloth Rendering to Implementation (1)

This article is a day 22 part of raytracing advent calendar 2018

Sorry for late!

日本語でも良い方はこちらを ( If you like Japanese, please check this )

ushiostarfish.hatenablog.com

Cloth and Fiber

There are several categories fibers used in cloth. For example, animal, plant and synthetic. Let's check.

animal

Name Based on
Wool Fur of sheep
Cashmere Fur of cashmere goat
Silk Silkworm cocoon
Mohair Angora goat

plant

Name Based on
Cotton Cotton plants of the genus Gossypium in the mallow family Malvaceae
Linen Linum
Ramie Boehmeria nivea

synthetic

Name Based on
Polyester Petroleum etc.
Nylon Coal or petroleum

Each of these fibers is a one-dimensional structure. Clothes need to make two-dimensional plane by some structure. For example, Felt is a sheet of entangled wool. We classify such cloth as nonwoven fabric (Nonwoven fabric) .

f:id:ushiostarfish:20181223175746j:plain

※ Nonwoven fabric(zoom)不織布 - Wikipedia より

In contrast, a cloth by wave orthogonal thread, it is named "woven cloth".

f:id:ushiostarfish:20181223180828p:plain

※ Silk Crepe de Chine (zoom) IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

Other than these, "knitting" is that created by make looping structure and connect vertically and horizontally with curved thread.

f:id:ushiostarfish:20190102160429p:plain

信州大学繊維学部, はじめて学ぶ繊維 P.140

We use knitting cloth for T-shirt, socks and Sportswear because of elasticity from this structure.

f:id:ushiostarfish:20190102160652p:plain

信州大学繊維学部, はじめて学ぶ繊維 P.140

Cloth shading model

Many people have researched cloth shading, for a long time. The old one is Michael Ashikhmin, Simon Premoze, Peter Shirley, "A microfacet-based brdf generator" , mention to microfacet based Satin, Velvet shading model. Irawan, Marschner proposed massive woven cloth BRDF(Piti Irawan, "Appearance of Woven Cloth"). This support large-scale and small-scale analytic shading model. It is implemented to Mitsuba render But...it is too difficult for me...

These models are a surface-based approach. But there are some volumetric approaches. Wenzel Jakob, Adam Arbree, Jonathan T. Moon, Kavita Bala, Steve Marschner, "radiative transfer framework for rendering, materials with anisotropic structure" , Its scarf image is amazing.

f:id:ushiostarfish:20181223192902p:plain

※ Figure 1 (a) Isotropic scattering. Wenzel Jakob, Adam Arbree, Jonathan T. Moon, Kavita Bala, Steve Marschner, "radiative transfer framework for rendering, materials with anisotropic structure"

There are many interesting papers, but I have chosen woven cloth shading model described at IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" . This model support only large-scale appearance, the appearance of individual threads is ignored. But it can reproduce cloth anisotropic behavior and It is a good match to measured cloth BRDF. Additionary, the rendered image is very impressive, in particular, Silk Shot Fabric.

f:id:ushiostarfish:20181224143716p:plain

※ Figure 24 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

This characteristic gradation is caused by weft and warp threads that have each different color.

A Practical Microcylinder Appearance Model for Cloth Rendering

f:id:ushiostarfish:20181224143513p:plain

※ Figure 24 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

This approach in this paper has the following flow.

  1. BSDF measurement for individual threads.
  2. Create a shading model for individual threads.
  3. Create a BRDF model by two orthogonal threads.
  4. Tuning the visibility and shading from a woven pattern and tangent distribution.
  5. Apply Shadowing and Masking, and tuning shading weight from its tangent ( in this paper, called "Reweighting").

Coordinate System

Let's check the coordinate system at Fig. 9 in the paper.

f:id:ushiostarfish:20181224144148p:plain

※ Figure 9 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

  •  \theta is an angle from the normal plane of thread t.
  •  \phi is an angle from cloth normal in thread normal plane.

But  \theta_i, \theta_r, \phi_i, \phi_r is not used for indivisual thread bsdf model. Instead of this,

 
\displaystyle {
\phi_{ d }=\phi_{ i }-\phi_{ r }\\ \theta _{ h }=\frac { \theta _{ i }+\theta _{ r } }{ 2 } \\ \theta _{ d }=\frac { \theta _{ i }-\theta _{ r } }{ 2 } 
}

An amount such as above is used for thread bsdf. Let's check the significance of each angle.



 
\displaystyle {
\phi_{ d }=\phi_{ i }-\phi_{ r }
}

 \phi_d is a difference angle between incident angle  \phi_i and outgoing angle  \phi_r .



 
\displaystyle {
\theta _{ h }=\frac { \theta _{ i }+\theta _{ r } }{ 2 } 
}

 \theta _h is a middle angle between incident angle  \theta _i and outgoing angle  \theta _r . As this is closer to 0, the specular reflection component becomes stronger.



 
\displaystyle {
\theta _{ d }=\frac { \theta _{ i }-\theta _{ r } }{ 2 } 
}

 \theta _d is half angle from difference angle between incident angle  \theta_i and outgoing angle  \theta_r . In other words, it is angle between  \theta _h and  \theta_i .

  \phi_d is half angle but  \theta _d is not half. This cause little bit confusing. But I use the same definition according to this paper. Incidentally, Iman Sadeghi's Ph.D. Thesis "Controlling the Appearance of Specular Microstructures" use no half angle at "5.2 Background Theory".

Thread model

We will describe a thread bsdf model by using above the angle definition. A thread is assumed that it is a rough dielectric cylinder, it is defined by the following two functions.

 
\displaystyle {
f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) =\frac { f_{ r,s }\left( t,\omega _{ i },\omega _{ r } \right) +f_{ r,v }\left( t,\omega _{ i },\omega _{ r } \right)  }{ \cos ^{ 2 } \theta _{ d } } 
}

Where  f_{ r,s } is specular component on cylinder surface,  f_{ r,s } is volumetric scattering component in cylinder. What is the denominator  \cos ^{ 2 } \theta _{ d } ? So I will think about this.

denominator  \cos ^{ 2 } \theta _{ d }

If cylinder surface is perfectly smooth, incident light is reflected  \theta_r = -\theta_i angle. The fact is already mentioned at もふもふレンダリング(3) . But I visualize this behavior following.

f:id:ushiostarfish:20181224160335g:plain

So, we can define a scattering function by using δ function.

 
\displaystyle {
S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) =\frac { N\left( \phi _{ i } \right) \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } 
}

Where  N \left( \phi_i \right) is density function,  \theta = \theta_i = \theta_r

Additionally, I'll check the energy conservation.

 
\displaystyle {
\int _{ \Omega  }^{  }{ S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) \cos { \theta _{ i } } d\omega  } \\ =\int _{ -\pi  }^{ \pi  } \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \: S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) \cos ^{ 2 }{ \theta _{ i } } d\theta d\phi \\ =\int _{ -\pi  }^{ \pi  } \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \frac { N\left( \phi _{ i } \right) \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } \cos ^{ 2 }{ \theta _{ i } } d\theta d\phi \\ =\int _{ -\pi \:  }^{ \pi \: \:  } N\left( \phi  \right) d\phi \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \frac { \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } \cos ^{ 2 }{ \theta _{ i } } d\theta \\ =\int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \frac { \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } \cos ^{ 2 }{ \theta _{ i } } d\theta \\ =\frac { \cos ^{ 2 }{ \theta _{ i } }  }{ \cos ^{ 2 }{ \theta  }  } \\ =1
}

The term  \frac {1}{ \cos ^{ 2 } \theta} is works well. It is essential term for discribe something. The angle θ can be anything angle, but is this scattering invariant? I'll check two extreme cases.

First,  \theta = 0 case.

f:id:ushiostarfish:20181224164108p:plain

Second,  \theta \approx 20° case.

f:id:ushiostarfish:20181224164410p:plain

First case rays is higher density than second case rays. As  \theta is closer to  \frac {\pi}{2},  \frac {1}{ \cos ^{ 2 } \theta} becomes bigger and bigger, so  \frac {1}{ \cos ^{ 2 } \theta} term expresses this change of density.

Extending from δ function

Stephen R. Marschner, Henrik Wann Jensen, Mike Cammarano, "Light Scattering from Human Hair Fibers" extend the above scattering models to a rough surface.

 
\displaystyle {
S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) =\frac { N\left( \phi _{ i } \right) M\left( \theta _{ i } \right)  }{ \cos ^{ 2 }{ \theta _{ d } }  } 
}

 \theta_i = \theta_r is no longer true. It use  \theta_d instead of this, but it is approximate. So thread bsdf model in the paper contains  \theta_d term because of this.

Specular component  f_{ r,s }

Specular component is defined following equation.

 
\displaystyle {
f_{ r,s }\left( t,\omega _{ i },\omega _{ { r } } \right) =F_{ r }\left( \eta ,\vec { w } _{ i } \right) \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) g\left( \gamma _{ s },\theta _{ h } \right) 
}

Where  F_{ r } is fresnel term and  g is normalized gaussian function (average is zero). I'll discuss the fresnel term later,  g is

 
\displaystyle {
g\left( \sigma ,x \right) =\frac { 1 }{ \sqrt { 2\pi { \sigma  }^{ 2 } }  } exp\left( -\frac { { x }^{ 2 } }{ 2{ \sigma  }^{ 2 } }  \right) 
}

 \theta _h is a middle angle between incident angle  \theta _i and outgoing angle  \theta _r . So as  \theta _h close to 0, the specular component is more stronger. Also we can modify the lobe shape by changing the variance.

f:id:ushiostarfish:20181224185718g:plain

※I ignore fresnel term in this plot.

What is  \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) ?

 \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) has a characteristic shape. This is

f:id:ushiostarfish:20181225233337p:plain

※ desmos : cos(theta/2)

This is based on a considering of distribution rays reflection from a smooth cylinder. I'll show this visualization by raytracing.

f:id:ushiostarfish:20181224190158p:plain

It shows that the reflected rays have a high density as incident angle close to an outgoing angle. Conversely, as an incident angle far from an outgoing angle, the reflected rays have low density. I will think about this property.

f:id:ushiostarfish:20190102160840p:plain

In this picture, we can see the relationship between  \phi and the point P.

 
\displaystyle {
x=\frac { 1 }{ 2 } \sin \left( \phi  \right) 
}

Where a incident ray is reflected to  2\phi as a outgoing ray.

f:id:ushiostarfish:20190102160932p:plain

So,

 
\displaystyle {
2\phi =\phi _{ r }\\ \phi =\frac { 1 }{ 2 } \phi _{ r }
}

Using this fact, we get the following relationship  x and  \phi_{ r }.

 
\displaystyle {
x=\frac { 1 }{ 2 } \sin \left( \frac { 1 }{ 2 } \phi _{ r } \right) 
}

The derivative is

 
\displaystyle {
x=\frac { 1 }{ 2 } \sin { \left( \frac { 1 }{ 2 } \phi _{ r } \right)  } \\ \frac { dx }{ d\phi _{ r } } =\frac { 1 }{ 2 } \frac { 1 }{ 2 } \cos  \left( \frac { 1 }{ 2 } \phi _{ r } \right) \\ \frac { dx }{ d\phi _{ r } } =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi _{ r } \right) 
}

Where  \frac { dx }{ d\phi_{ r } } is smallness x per smallness reflected angle. It shows how distrebutions the smallness x belonging to unit length (=1) has. So, in the above section "Extending from δ function" , N can be

 
\displaystyle {
N\left( \phi _{ i },\phi _{ r } \right) =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi _{ d } \right) 
}

And, the integration in a around must be 1 because of N is density function.

 
\displaystyle {
\int _{ -\pi  }^{ \pi  }{ N\left( \phi _{ i },\phi _{ r } \right) d\phi _{ i } } =\int _{ -\pi  }^{ \pi  }{ \frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi  \right) d\phi  } 
}

We can use the following derivative

 
\displaystyle {
\frac { d }{ d\phi  } \left( \frac { 1 }{ 2 } \sin { \left( \frac { 1 }{ 2 } \phi  \right)  }  \right) =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi  \right) 
}

So,

 
\displaystyle {
\int _{ -\pi  }^{ \pi  }{ \frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi  \right) d\phi  } \\ =\frac { 1 }{ 2 } \sin { \left( \frac { 1 }{ 2 } \pi  \right)  } -\frac { 1 }{ 2 } \sin { \left( -\frac { 1 }{ 2 } \pi  \right)  } \\ =\frac { 1 }{ 2 } +\frac { 1 }{ 2 } \\ =1
}

We have verified. So specular component equation

 
\displaystyle {
f_{ r,s }\left( t,\omega _{ i },\omega _{ { r } } \right) =F_{ r }\left( \eta ,\vec { w } _{ i } \right) \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) g\left( \gamma _{ s },\theta _{ h } \right) 
}

 \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) come from here.

Missing  \frac { 1 }{ 4 }

But, there is no  \frac { 1 }{ 4 } in this equation! Actually, In Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model"

When,

 
\displaystyle {
N\left( \phi _{ i },\phi _{ r } \right) =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi _{ d } \right) \\ M\left( \theta _{ i },\theta _{ r } \right) =g\left( \beta ,\theta _{ h } \right) \\ S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) =\frac { N\left( \phi _{ d } \right) M\left( \theta _{ h } \right)  }{ \cos ^{ 2 }{ \theta _{ d } }  } 
}

Then,

 
\displaystyle {
\int _{ -\pi  }^{ \pi  } \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \: S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) \cos ^{ 2 }{ \theta _{ i } } d\theta d\phi 
}

Figure 3 shows how amount this expression value, as several  \beta, as changing  \theta_i .

f:id:ushiostarfish:20181229114321p:plain

※ Figure 3 Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model"

I have plotted graph to verify by using this implementation.

It shows

f:id:ushiostarfish:20181224231933p:plain

 \beta unit is degrees, the horizontal unit is an outgoing angle (unit: radians), the vertical unit is outgoing radiance.

Total reflectance from cylinders - Google スプレッドシート

This is exactly same to Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model" Figure 3. So the energy amplifies in the most part of this. This fact causes a motivation to introduce energy conservation Mp function from spherical gaussian.

From this, specular component  f_{ r,s } has potential amplify the energy 4 times from above expression. But, it is not a mistake. IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" has no energy conservation description. It is not the focal point in this paper. So I'll finish this study, go next.

Fresnel Term

Fresnel term is defined by

 
\displaystyle {
F_{ r }\left( \eta ,\vec { w } _{ i } \right) =F_{ r }\left( \eta ,\cos ^{ -1 }{ \left( \cos { \left( \theta _{ d } \right)  } \cos { \left( \frac { \phi _{ d } }{ 2 }  \right)  }  \right)  }  \right) 
}

Sorry...I can't find where is come from. But  \theta_{ d } and   \frac {\phi _{ d } }{ 2 } is both angle from half angle. And a product of these terms means that the angle is treated as grazing angle if either one of the cosine of them approaches 0. In this paper says "We considered using the full microfacet specular formulation, but found that it did not improve the matching to our measured results".

I choose the following fresnel expression as fresnel term from Bruce Walter, Stephen R. Marschner, Hongsong Li, Kenneth E. Torrance, "Microfacet Models for Refraction through Rough Surfaces". It describes as "A convenient exact formulation for dielectrics with unpolarized light".

 
\displaystyle {
{ F\left( \eta _{ t },\eta _{ i },\theta  \right) =\frac { 1 }{ 2 } \frac { \left( g-c \right) ^{ { 2 } } }{ \left( g+c \right) ^{ { 2 } } } \left( 1+\frac { \left( c\left( g+c \right) -1 \right) ^{ { 2 } } }{ \left( c\left( g+c \right) +1 \right) ^{ { 2 } } }  \right)  }\\ g=\sqrt { \frac { { \eta _{ t } }^{ 2 } }{ { \eta _{ i } }^{ 2 } } -1+{ c }^{ 2 } } \\ c=\cos { \theta  } 
}

Volume scattering component  f_{ r,s }

 f_{ r,s } is defined by

 
\displaystyle {
f_{ r,v }\left( t,\omega _{ i },\omega _{ r } \right) =F\frac { \left( 1-k_{ d } \right) g\left( \gamma _{ v },\theta _{ h } \right) +k_{ d } }{ \cos { \theta _{ i } } +\cos { \theta _{ r } }  } A
}

Where A is a spectrum dependence absorption coefficient. The coefficient that the specular component doesn't contain makes outgoing light colored.

F Term

F is defined by following expression in this paper.

 
\displaystyle {
F=F_{ t }\left( \eta ,\vec { \omega _{ i } }  \right) F_{ t }\left( \eta ',\vec { \omega '_{ r } }  \right) 
}

But I don't find more details in the paper, but maybe

 
\displaystyle {
F_{ t }\left( \eta ,\vec { \omega _{ i } }  \right) =1-F_{ r }\left( \eta ,\vec { w } _{ i } \right) 
}

and,

 
\displaystyle {
F_{ t }\left( \eta ',\vec { \omega '_{ r } }  \right) 
}

is undefined in the paper. However, If cylinder volume multiple scattering is assumed that is described by the  f_{ r,s } term,

 
\displaystyle {
F=F_{ t }\left( \eta ,\vec { \omega _{ i } }  \right) 
}

is not so bad, I think. but I choose the following expression for my implementation.

 
\displaystyle {
F={ { F_{ t } }^{ 2 }\left( \eta ,\vec { \omega _{ i } }  \right)  }
}

 g\left( \gamma _{ v },\theta _{ h } \right) Term

 g\left( \gamma _{ v },\theta _{ h } \right) effect is exactly same to specular component  g\left( \gamma _{ s },\theta _{ h } \right) without the variance parameter. So it is modeled that refracted rays has same behavior with reflected rays on cylinder surface (= these has same  \theta). Is it ture ? Let's check this by tracking refracted rays into a smooth dielectric cylinder. ( Houdini vex: tracking refraction · GitHub )

f:id:ushiostarfish:20181229123015g:plain

Where the incident rays has  \theta = 45° . And plotting its  \sin \theta shows

f:id:ushiostarfish:20190102162337p:plain

Surprisingly, all of the outgoing vectors is exactly the same angle to its incident angle. It has the same behavior even if use other incident angles. This fact is already showed at Stephen R. Marschner, Henrik Wann Jensen, Mike Cammarano, "Light Scattering from Human Hair Fibers" , section "A Loci of reflections". This is the figure 16. In the explanation of "A Loci of reflections", this is explained by treating the normal of the dielectric cylinder as the normal of the equator of the sphere.

f:id:ushiostarfish:20181229152111p:plain

※ The part with n, n 'is the cylinder normal.

where  v_i on sphere is seen incident vector. And this incident vector  v \ _ i can intersect with various normals on the equator (only front-side,  0 \lt v \ _ i \cdot n ). First, I'll talk about typical normal  n directly under  v \ _ i.

The  v_i refract to  v_t with a relative refractive index with surface normal  n. With snell's law,

 
\displaystyle {
\eta _{ i }\sin  \theta _{ i }=\eta _{ t }\sin  \theta _{ t }
}

where  \eta =\frac { \eta _{ t } }{ \eta _{ i } } , we get

 
\displaystyle {
\sin  \theta _{ i }=\eta \sin  \theta _{ t }
}

What is  \sin\theta_i,\sin\theta_t ? Geometrically, we can treat that it is  h \ _ i, h \ _ t in the figure respectively. So we get

 
\displaystyle {
\left| h_{ i } \right| =\eta \left| h_{ t } \right| 
}

But normal is not only  n on the figure but also it can be any vector on the equator of the sphere. Let's check the following figure for other cases  n'.

f:id:ushiostarfish:20181229173945p:plain

※ I add  h'_{ i }, h'_{ t } that  \sin\theta of  n'.

where a amount of  \left| h'_{ t } \right| projected to Y axis is

 
\displaystyle {
H=\left| h'_{ t } \right| \left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

I consider what value it will be. First, with Snell's law,

 
\displaystyle {
\left| h'_{ i } \right| =\eta \left| h'_{ t } \right| 
}

So,

 
\displaystyle {
H=\frac { \left| h'_{ i } \right|  }{ \eta  } \left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

Next,

 
\displaystyle {
\left| h'_{ i } \right| \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right| =\left| h_{ i } \right| \\ \left| h'_{ i } \right| ={ \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right|  }^{ -1 }\left| h_{ i } \right| 
}

Because of the above equation, we get

 
\displaystyle {
H=\frac { 1 }{ \eta  } { \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right|  }^{ -1 }\left| h_{ i } \right| \left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| \\ H=\frac { \left| h_{ i } \right|  }{ \eta  } { \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right|  }^{ -1 }\left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

Then, vector  h'_i direction is exactly inverse of  h'_t direction . So,

 
\displaystyle {
\left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right| =\left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

Using this, we get

 
\displaystyle {
H=\frac { \left| h_{ i } \right|  }{ \eta  } 
}

Oh, I remember  \left| h_{ i } \right| =\eta \left| h_{ t } \right| .

 
\displaystyle {
H=\frac { \left| h_{ i } \right|  }{ \eta  } =\frac { \eta \left| h_{ t } \right|  }{ \eta  } =\left| h_{ t } \right| 
}

It says that a vertical distance of refracted vector from equator plane depend on only refractive index for any  n' . In addition, considering that it goes out after entering from the dielectric cylinder, the refractive indices cancel each other out, and when exiting it exits at the same θ as incident θ (although it will reverse about positive and negative).

Because of this,  g\left( \gamma _{ v },\theta _{ h } \right) term in volume scattering component is plausible. Although it is modeled with a rough dielectric cylinder, it seems that the light that enters inside will repeat scattering inside it. So, g in the volume scattering component has a different variance parameter from the specular component. And all of  \gamma_v is greater than  \gamma_s on Table Ⅱin this paper.

Denominator  \cos { \theta _{ i } } +\cos { \theta _{ r } }

In this paper says "The division by the sum of projected cosines comes from Chandrasekhar [1960], in his derivation for diffuse reflectance due to multiple scattering in a semi-infinite medium." I'm afraid I can't understand these details because of my limited knowledge. But the paper says that this term has an effect to match more to brdf measurements.

※ Chandrasekhar [1960] = Subrahmanyan Chandrasekhar, "Radiative Transfer"

 k_d term

 k_d term simply express isotropic scattering for cotton, linen, and so on. It interpolate  g\left( \gamma _{ v },\theta _{ h } \right) to  1.

I'll show specular component lobe. It's intuitive I think.

f:id:ushiostarfish:20181229172951g:plain

I have completed introducing modeling of an individual thread. Next I will describe the shading model by using this.

ushiostarfish.hatenablog.com

A Practical Microcylinder Appearance Model for Cloth Rendering の実装(2)

前回からの続きです。

ushiostarfish.hatenablog.com

If you like english version, Please check this

ushiostarfish.hatenablog.com

シェーディングモデル

本論文では、以下の図のように2本のたて糸とよこ糸が図のように直行している様子をモデル化します。

f:id:ushiostarfish:20181229174211p:plain

※Fig. 2 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

図を見て分かる通り、明らかに たて(経)糸とよこ(緯)糸で見えている面積が異なります。これをシンプルに反映し、以下の式で出射放射輝度を定義します。

 
\displaystyle {
L_{ r }\left( \omega _{ r } \right) =a_{ 1 }L_{ r,1 }\left( \omega _{ r } \right) +a_{ 2 }L_{ r,2 }\left( \omega _{ r } \right) 
}

ここで、 a_1, a_2 は単純にパラメーターです。線形補間なら、aは一つでいいのではないか?とも思われるのですが、Fig. 2の一番左のように隙間が存在しているケースでは、 a_1 + a_2 は必ずしも 1になりません。そのためこのパラメーターは2つ存在しています。逆に密に糸が織られているケースでは、 a_1 + a_2=1 になります。

では個別の  L_{ r,j }\left( \omega _{ r } \right) ですが、こちらは以下のように定義します。

 
\displaystyle {
L_{ r,j }\left( \omega _{ r } \right) =\frac { 1 }{ N_{ j } } \sum _{ t }^{  }{ \int _{ \Omega  }^{  }{ L_{ i } } f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) \cos { \theta _{ i }d\omega _{ i } }  } 
}

※ もうBRDFを考えており、ここで  \Omega は半球です。

ここで  N_{ j }は何かというと、接線のサンプル数になります。というのも、織布の糸を考えたとき、少なくともどちらか1本は波打っていると考えられるため、その糸の傾き具合をそれぞれ別に評価して平均する、ということになります。

f:id:ushiostarfish:20181229180255p:plain

※ Fig. 12, Polyester Satin Charmeuse fabric IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

 f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) については、前回の糸モデルそのままです。 サンプルを具体的にどうするのかは、論文には言及がありませんでした。今回は横着して、論文にある Table II の制御点をそのままサンプルポイントとすることにしました。これでも論文の画像と良好な一致を確認しました。しかし厳密には確率的に解いたりすると良いのかもしれませんが、最終結果により良い結果につながるかは疑問です。

以上で全体の流れは網羅できましたが、本論文ではより測定結果に近づけるため、さらに2つの調整を提示しています。それは "Shadowing and Masking"と"Reweighting" です。これについて見ていきます。

Shadowing and Masking

たて糸とよこ糸が直行している今回のようなシェーディングにマッチするShadowing, Maskingとして、 本論文では、それぞれ以下のように定義されています。

 
\displaystyle {
M\left( t,\omega _{ r } \right) =max\left( \cos { \phi _{ r } } ,0 \right) \\ M\left( t,\omega _{ i } \right) =max\left( \cos { \phi _{ i } } ,0 \right) 
}

これを見てすぐに分かることとして、それぞれ \phi _{ i }, \phi _{ r } にのみ依存する点があります。これの理由は、論文のFig 13がわかりやすいです。

f:id:ushiostarfish:20181229221524p:plain

※ Fig. 13, 中, 右の図 Polyester Satin Charmeuse fabric IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

左の図を見ると、グレージング角でも糸が糸に隠れているところは存在しません。これは糸の方向と同一方向に傾いているからですね。 ところが右の図では糸の方向に対して横方向に傾いています。この場合は隣の糸に邪魔されて糸が隠れています。すなわちこの方向にのみShadowing, Maskingを考慮すれば良いだろう、ということになります。

では次になぜShadowing, Maskingは  \cos { \phi _{ r } } なのでしょうか。 これはシリンダーが横に並んだときの遮蔽を考えています。以下のようにシリンダーが横に並ぶと、手前の白のシリンダーが奥のグレーのシリンダーの一部を遮蔽します。

f:id:ushiostarfish:20181229223416p:plain

ではその見えている量はどのくらいかを、図に書き込んでみます。円筒の半径は0.5だとします。

f:id:ushiostarfish:20181229225827p:plain

そう、見えている量はまさに  \cos { \phi } であったのです。

Shadowing, Masking の相関

もし入射と出射のShadowing, Maskingに全く相関が無い(=無相関)と仮定すると、Shadowing & Masking関数は以下のような個別の関数の単純な積の定義が用いられます。

 
\displaystyle {
M_{ no-correlation }\left( t,\omega _{ i },\omega _{ r } \right) =M\left( t,\omega _{ i } \right) M\left( t,\omega _{ r } \right) 
}

しかしこの仮定にはよく知られた問題があって、例えば  \omega _{ i } = \omega _{ o } のようなケースを考えてみます。Shadowing, Maskingはどちらも表面の遮蔽具合を表現するものであるため、入射と出射が等しいのであれば、その見えている領域は完全に同一であるはずです。つまり、遮蔽に関して言えば実際には片方だけ作用させれば良いはずなのにもかかわらず、上記の定義ではそれの積、つまり二乗分の遮蔽が計上されてしまうのです。これは相関の仮定がうまく動作しないわかりやすい例です。 本論文ではこれの対策として、Michael Ashikhmin, Simon Premoze, Peter Shirley, "A Microfacet-based BRDF Generator" で提案されている方法を使っています。

このアイディアはシンプルで、入射と出射が等しいような強い相関のあるShadowing & Masking関数を、無相関とは対照的に、

 
\displaystyle {
M_{ correlation }\left( t,\omega _{ i },\omega _{ r } \right) =max\left( M\left( t,\omega _{ i } \right) ,M\left( t,\omega _{ r } \right)  \right) 
}

と定義します。これは直観的にはShadowingの領域がMaskingの領域を包含している、もしくはMaskingの領域がShadowingの領域を包含していたので、遮蔽は大きい方に合わせれば良い、というイメージだと思います。

最後に無相関の関数と、強い相関のある関数を、"入射と出射の類似度" で線形補間します。

 
\displaystyle {
M\left( t,\omega _{ i },\omega _{ r } \right) =\left( 1-u\left( \phi _{ d } \right)  \right) M\left( t,\omega _{ i } \right) M\left( t,\omega _{ r } \right) +u\left( \phi _{ d } \right) max\left( M\left( t,\omega _{ i } \right) ,M\left( t,\omega _{ r } \right)  \right) 
}

ここで、"入射と出射の類似度" は、 \phi _{ i } \phi _{ r } の差である、 \phi _{ d } を引数に取るガウス関数を用いて、

 
\displaystyle {
u\left( \phi _{ d } \right) =\left( -\frac { \phi _{ d }^{ 2 } }{ 2\sigma ^{ 2 } }  \right) 
}

を使います。 \sigma については、15度から 25度となっていたため、ここではひとまず 20度で実装しました。ただ、15度や25度になったところでそれほど大きくは変わらないでしょう。

これにより、個別のシェーディングモデルは以下のように書き直されます。

 
\displaystyle {
L_{ r,j }\left( \omega _{ r } \right) =\frac { 1 }{ N_{ j } } \sum _{ t }^{  }{ \int _{ \Omega  }^{  }{ L_{ i } } f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) M\left( t,\omega _{ i },\omega _{ r } \right) \cos { \theta _{ i }d\omega _{ i } }  } 
}

Reweighting

Reweightingは、まず論文 Fig 13 を見てみましょう。

f:id:ushiostarfish:20181230124255p:plain

※ Fig 13 左 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

まず青色の糸に着目してみると、どうにも真ん中は青色が強く、端に行く(=グレージングアングルに近づく)ほどに相対的に青が弱くなっているのがわかります。 これは何故かと考えてみると、糸が傾くことで見かけの長さ(=視点方向へ投影した長さ)が小さくなっているからではないでしょうか。(=視点方向へ投影した長さ)が小さくなっているからではないでしょうか。 さらにオレンジの糸に着目してみると、全範囲で変化はなく均一です。これは端でもオレンジの糸の見かけ上の長さが変わっていないからでしょう。 このことから見かけ上の長さの変化は、 \psi にのみ依存するということもわかります。

※ところで別な見方をすると、青色が弱く見えるのはオレンジの糸の密度が上がったからではないか?という考えも浮かびます。しかしオレンジの糸の密度が上がったのは見かけ上の青の糸が短くなったことに起因するわけで、結局同じことを違う視点で見ただけのようです。

イメージとしては以下のようなかんじでしょうか。

f:id:ushiostarfish:20181230134818p:plain

さながら、マイクロファセット理論でよく出てくる可視法線分布のようです。

 \psi は最初の座標系の説明には登場しませんでした。これも合わせて確認します。論文のFig. 14を確認しましょう。

f:id:ushiostarfish:20181230125716p:plain

※ Fig 14 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

わかりにくいところはなく、単に糸と同一方向と、布法線が作り出す面上の角度です。

では見かけ上の長さといえば、当然  \cos \psi です。論文ではこれを使って、以下の重みを定義しています。

 
\displaystyle {
P\left( t,\omega _{ r } \right) =max\left( \cos { \psi _{ r } } ,0 \right) \\ P\left( t,\omega _{ i } \right) =max\left( \cos { \psi _{ i } } ,0 \right) 
}

偶然にも Shadowing, Masking と同一の式になっています。ちなみに入射と出射両方向考えているのは、見る方向だけでなく、光を受ける方も同様の現象がおこり、見かけ上の長さ長いほうが受光量が大きくなり、結果寄与がおおきくなるだろう、という考えがあるからです。しかしながら、入射のほうについては、LTE放射輝度の密度変化を考慮した  \cos \theta_i 項があるわけなので、両方向考えるのはいささか過剰な気もしますが、 \psi にのみ依存する P 項とは少し立ち位置が違うのかもしれません。

論文ではShadowing, Maskingとアイディアも似通っているからか、入射と出射の組み合わせについても、同様の以下の定義を用いています。

 
\displaystyle {
P\left( t,\omega _{ i },\omega _{ r } \right) =\left( 1-u\left( \psi _{ d } \right)  \right) P\left( t,\omega _{ i } \right) P\left( t,\omega _{ r } \right) +u\left( \psi _{ d } \right) max\left( P\left( t,\omega _{ i } \right) ,P\left( t,\omega _{ r } \right)  \right) 
}

ここで、 \psi_d は、 \psi_r - \psi_i となります。  \phi _d と同様の考え方ですね。

以上を重みを式に付け加えて以下のようにシェーディングモデルを書き直します。

 
\displaystyle {
L_{ r,j }\left( \omega _{ r } \right) =\frac { 1 }{ Q } \frac { 1 }{ N_{ j } } \sum _{ t }^{  }{ \int _{ \Omega  }^{  }{ L_{ i } } f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) M\left( t,\omega _{ i },\omega _{ r } \right) P\left( t,\omega _{ i },\omega _{ r } \right) \cos { \theta _{ i }d\omega _{ i } }  } 
}

正規化項として  Q が増えています。今回追加した P 項は重みを調整するための恣意的なスケールを行うわけですので、それ単体では重みの和が1にならず、エネルギー全体にスケールを掛けてしまう恐れがあるためです。ここはShadowing, Maskingと違うところですね。  Q は以下のように求めます。

 
\displaystyle {
Q=\frac { a_{ 1 } }{ N_{ 1 } } \sum _{ t }^{  }{ P\left( t \right)  } +\frac { a_{ 2 } }{ N_{ 2 } } \sum _{ t }^{  }{ P\left( t \right)  } +\left( 1-a_{ 1 }-a_{ 2 } \right) a_{ 2 }\cdot n
}

ちょっとややこしいことになっています。 ここでReweightingを加える前では、全体から見ると個別のサンプルはどういう重みになっていたか?を図にしてみます。

f:id:ushiostarfish:20181230142635p:plain

この図では外側の箱がちょうど単位面積になっており、まずパラメーター  a_1, a_2 によって二分されます。その後、それぞれの糸ごとに一つ以上のサンプルが存在し、それぞれ均一な重みが割り振られます。こう考えると個別のサンプルの重みは割り当てられた面積ということになります。

ということは結局のところ今回追加する  P は、この個別の面積をそれぞれスケールした と考えられます。スケール後の面積の和は1になりませんので、結局スケール後の面積の和で除算して正規化すれば良いことがわかります。 ちなみに最後の項  \left( 1-a_{ 1 }-a_{ 2 } \right) a_{ 2 }\cdot n a_{ 1 } + a_{ 1 } \lt 1 のケースを考慮しており、糸が存在していない部分はとりあえず法線に合わせた投影領域に重みを割り振ったと考え、残りを埋めています。

実装

実装は素直にストレートフォアードにやったつもりです。結構長いので埋め込まずリンクにしています。

microcylindercloth · GitHub

material · GitHub

モデル

シェーディングモデルは用意できましたが、絵を出すにはやはりモデルが必要で、また論文の絵をなるべく再現したかったので、それに近い3Dモデルがほしいです。ちょうどHoudini 17には新しいシミュレーションモジュール、Vellum が入ったそうなので、それを試してみることにしました。

※自分のMSI ノートでは、NahimicのせいでHoudini17がクラッシュしたり、Houdini 17 crashing constantly - General Houdini Questions - od|forumOpenCL の初期化でエラーでて設定を調整したりなどちょっと問題もありましたが・・・[Solved] OpenCL setup for 17? | Forums | SideFX 環境依存っぽかったのでこれについてはあまりここでは深入りしないことにします。

VellumはMatthias Müller, Bruno Heidelberger, Marcus Hennix, John Ratcliff, "Position Based Dynamics" をベースとしたソルバーのようで、拘束ベースで設定を行います。またSopで完結できるようになっていたりとかなり楽に使えてとても良かったので、ついでに紹介してみます。

Vellum

まずは地面を作ります。TubeとGridをただ並べるだけにします。

f:id:ushiostarfish:20181230150113p:plain

次に上に重ねる布を適当な分解能で準備します。ただ後に接ベクトルを計算のため、uvflattenを使ってpointにuvを追加しておきます。

f:id:ushiostarfish:20181230150315p:plain

次にConstraintを設定します。Clothはプリセットがあるのでそれを選ぶと楽です。

f:id:ushiostarfish:20181230150557p:plain

BendのStiffnessを調整すると、布の曲がりにくさを調整可能です。パラメーターはそれほど多くはないので狙った見た目にならない場合はちょっといじってみると良いでしょう。ただ、シミュレーションに詳しい人に聞いてみると、可能な場合は、実際手元で布をいじってみてどういう形になるのか?を試してみたほうが良い というのを聞きました。シミュレーションだけしかやっていないと、シミュレーションは正しいが、アプローチが間違っている場合に気づかないこともあるそうです。

真ん中をnullにつなぐと、拘束を確認することもできます。

f:id:ushiostarfish:20181230151232p:plain

心臓部、Vellum Solverにつなぎます。

f:id:ushiostarfish:20181230151406p:plain

Forces/Friction/Static Threshold などを調整して摩擦の具合を調整できます。 また、拘束が伸びすぎて収束していないようなケースでは、Solver/Substeps などを上げると良いでしょう。

あとはタイムラインをスタートすれば、

f:id:ushiostarfish:20181230151738g:plain

ね?簡単でしょう?

あとは適当なフレームで、Smoothと、Subdivideをかけてちょっと綺麗にしつつ File Cacheを使ってobj書き出しを行います。

f:id:ushiostarfish:20181230152009p:plain

接ベクトルは実行時に計算しても良いのかもしれませんが、今回はHoudiniのpolyframeパッチを使って事前計算しました。

f:id:ushiostarfish:20181230152645p:plain

※ただ頂点単位の計算になるので、実際には最後にプリミティブで接ベクトルがそれぞれ直交するように調整しています。

以上でモデルは準備完了です。

結果

シーンは以下のようにしました。(いや、正確には論文の絵に近づくように配置してみたところこうなりました、というのが実情です)

f:id:ushiostarfish:20181230153241p:plain

ここでは論文のTable Ⅱにあるパラメーターを使って再現してみます。

Linen Plain

f:id:ushiostarfish:20181230154032p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.46 Both (0.2, 0.8, 1.0) * 0.3 0.3 12 24 0.33 -25, 25

Silk Crepe de Chine

f:id:ushiostarfish:20181230154233p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.345 Flat (1.0, 0.95, 0.05) * 0.12 0.2 5 10 0.75 -35, -35, 35, 35
1.345 Twist (1.0, 0.95, 0.05) * 0.16 0.3 18 32 0.25 0, 0

Polyester Satin Charmeuse

f:id:ushiostarfish:20181230154800p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.539 Flat (1.0, 0.37, 0.3) * 0.035 0.1 2.5 5 0.9 -32, -32, -18, 0, 0 , 18, 32, 32
1.539 Twist (1.0, 0.37, 0.3) * 0.02 0.7 30 60 0.1 0, 0

Polyester Satin Charmeuse (Back-side)

f:id:ushiostarfish:20181230155451p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.539 Flat (1.0, 0.37, 0.3) * 0.035 0.1 2.5 5 0.67 -30, -30, 30, 30, -5, -5, 5, 5
1.539 Twist (1.0, 0.37, 0.3) * 0.02 0.7 30 60 0.33 0, 0

Silk Shot

f:id:ushiostarfish:20181230155850p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.345 Dir 1 (0.1, 1.0, 0.4) * 0.2 0.1 4 8 0.86 -25, -25, 25, 25
1.345 Dir2 (1.0, 0.0, 0.1) * 0.6 0.1 5 10 0.14 0, 0

Velvet

なぜかVelvetに関しては結果が論文の写真とあまり合いませんでした もしかすると論文のはSiggraph ロゴを出すためにライティングや法線マップを調整しているのかもしれません。

f:id:ushiostarfish:20181230160224p:plain

 \eta Thread A  k_d  \gamma_s (deg)  \gamma_v (deg)  a Tangent Offsets (deg)
1.46 Dir 1 (0.05, 0.02, 0.0) * 0.3 0.1 6 12 0.5 -90, -50
1.46 Dir2 (0.05, 0.02, 0.0) * 0.3 0.1 6 12 0.5 -90, -55, 55, 90

論文 Fig. 24

f:id:ushiostarfish:20181230160515p:plain

Velvet以外については、無事再現することができました。

まとめ

レイトレ合宿6 ではベルベットのシェーディングをまがいなりにも導入していましたが、布のシェーディングについては正直ほとんど調べられておらず、やや不完全燃焼な印象があったので、一度ちゃんと調べてみたかったのもあり、ざっと調べた範囲で手の届きそうなところの A Practical Microcylinder Appearance Model for Cloth Renderingにチャレンジしてみました。ところがソースコードが入手できなかったのもあり、なかなか時間がかかってしまいました。

本手法は、決して洗練された方法でもないかもしれませんし、Mathematically well defined とも言いにくいかもしれません。しかし測定データに対して最小限の定式化を使い、Far Fieldだけとはいえ、かなり真実の味を感じる絵を実現しています。 何を重要とみなすか?というフォーカスの設定がここで肝だったのではないでしょうか。

一方で、エネルギー保存の問題や、シェーダの重さはまだ課題が多いですし、特にTangent Offsetsのパラメーターは直観的に設定するのは難しいと言わざるを得ません。また、布ではカメラが布にある程度近づくだけで詳細が見えてしまう実情もあり、さらなるクオリティアップのためには、Near Fieldの見た目をうまいことハンドリングするのは避けて通れない気もします。

布特性上、かなり毛髪などのレンダリングと理論が共通しているため、わりと幅広く知識がつながっている感触も得られました。ぜひこの分野に興味が湧いた方は調べてみると面白いのではないでしょうか。

謝辞

実装にあたって、Henrik W. Jensen 氏には、メールでいくつか質問に答えていただき、大きく理解を助けていただきました。本当にありがとうございました。

In implementing, Mr. Henrik W. Jensen have answered some questions by e-mail and helped me to understand well. Thank you so much.

参考文献

IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering"

※ちなみに ACM Digital Library で課金すると、彼らのプレゼン映像が見れます。

Iman Sadeghi, "Controlling the Appearance of Specular Microstructures"

信州大学繊維学部, はじめて学ぶ繊維

Michael Ashikhmin, Simon Premoze, Peter Shirley, "A microfacet-based brdf generator"

Piti Irawan, "Appearance of Woven Cloth"

Wenzel Jakob, Adam Arbree, Jonathan T. Moon, Kavita Bala, Steve Marschner, "radiative transfer framework for rendering, materials with anisotropic structure"

Stephen R. Marschner, Henrik Wann Jensen, Mike Cammarano, "Light Scattering from Human Hair Fibers"

Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model"

Bruce Walter, Stephen R. Marschner, Hongsong Li, Kenneth E. Torrance, "Microfacet Models for Refraction through Rough Surfaces"

Subrahmanyan Chandrasekhar, "Radiative Transfer"

Michael Ashikhmin, Simon Premoze, Peter Shirley, "A Microfacet-based BRDF Generator"

Matthias Müller, Bruno Heidelberger, Marcus Hennix, John Ratcliff, "Position Based Dynamics"

A Practical Microcylinder Appearance Model for Cloth Rendering の実装(1)

この記事は レイトレアドベントカレンダー2018 22日目の記事です。

大遅刻しましてすみません!!!

If you like english version, Please check this

ushiostarfish.hatenablog.com

布と繊維

布に加工される繊維は多岐にわたりますが、大きな分類としてな、動物繊維・植物繊維・化学繊維といったものが存在し、 例えば以下のようなものがあります。

動物繊維

名前 原料
羊毛(Wool) 羊の毛
カシミヤ(Cashmere) カシミヤ山羊の毛
絹(Silk) 蚕の繭
モヘア(Mohair) アンゴラ山羊の毛

植物繊維

名前 原料
綿(Cotton) ワタ(アオイ科ワタ属)の木綿(もめん)
リネン(Linen) 亜麻
ラミー(Ramie) 苧麻

化学繊維

名前 原料
ポリエステル(Polyester) 石油等
ナイロン(Nylon) 石炭または石油

これら繊維はどれも1次元的な糸の状態です。布は、これらをなんらかの構造で平面の結合状態を作る必要があります。 例えばフェルトは、羊毛などを絡めてシート状にしたものです。このような布を 不織布(ふしょくふ, Nonwoven fabric) と分類します。

f:id:ushiostarfish:20181223175746j:plain

※ 不織布(拡大図)不織布 - Wikipedia より

これとは対照的に、直交する糸を上下させて作る布を 織布(woven cloth) と呼びます。

f:id:ushiostarfish:20181223180828p:plain

※ Silk Crepe de Chine の拡大写真 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" より

これら以外では、 編物 または ニット(knit) とも呼ばれるものは、糸を屈曲させてループを作って前後左右に繋いだものを言います。

f:id:ushiostarfish:20181223182744p:plain

信州大学繊維学部, はじめて学ぶ繊維 P.140

この構造から、伸縮性が得られるため、Tシャツや靴下やスポーツウェアにも使われます。

f:id:ushiostarfish:20181223183017p:plain

信州大学繊維学部, はじめて学ぶ繊維 P.140

布のシェーディングモデル

布シェーディングについては古くより研究が多くなされています。古いものとしては、Michael Ashikhmin, Simon Premoze, Peter Shirley, "A microfacet-based brdf generator" ではマイクロファセットベースのサテン・ベルベットのモデル化にも触れられています。 大きなものとしては、Irawan, Marschner らが開発したwoven cloth BRDFのモデル(Piti Irawan, "Appearance of Woven Cloth")があり、こちらはラージスケールとスモールスケールをサポートする解析モデルで、Mitsuba にも実装されているそうです。しかしながら大規模すぎてちょっと自分には攻略できませんでした。

これらはサーフェイスベースのアプローチですが、ボリューメトリックなアプローチもあり、Wenzel Jakob, Adam Arbree, Jonathan T. Moon, Kavita Bala, Steve Marschner, "radiative transfer framework for rendering, materials with anisotropic structure" でのscarfの絵はとても美しく心惹かれるものがあります。

f:id:ushiostarfish:20181223192902p:plain

※ Figure 1 (a) Isotropic scattering. Wenzel Jakob, Adam Arbree, Jonathan T. Moon, Kavita Bala, Steve Marschner, "radiative transfer framework for rendering, materials with anisotropic structure" より

いろいろ目移りしますが、自分はボリュームレンダリングには不慣れなので、今回は織布のレンダリングにフォーカスした論文、IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" に着目します。これは織布にフォーカスしたもので、スモールスケールの糸の構造など無視されているものの、布の異方性反射をよく表現でき、計測したBRDFとよく適合するとあります。絵もなかなか綺麗で、Silk Shot Fabricは印象的です。

f:id:ushiostarfish:20181224143716p:plain

※ Figure 24 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" より

これはたて(経)糸とよこ(緯)糸に違う色が使われているからで、このモデルの特徴がよく表われていると言えます。

A Practical Microcylinder Appearance Model for Cloth Rendering

f:id:ushiostarfish:20181224143513p:plain

※ Figure 24 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" より

この論文のアプローチは、以下のような流れになっています。

  1. 布を構成する1本の糸についてのBSDF測定を行う
  2. 個別の糸をうまく表現できるモデルを用意して、パラメーターを調整
  3. 糸を垂直に2本並べたBRDFモデルを作る
  4. 織布の織りパターンに応じて可視性や、糸の角度分布ごとにシェーディングを行う
  5. Shadowing and Masking、および角度分布によるエネルギー再分布(論文ではReweighting)を考慮して重みを調整する

だいたいこんな感じです。

座標系

モデルに先立ってまずは座標系を確認します。論文中 Fig. 9 がわかりやすいです。

f:id:ushiostarfish:20181224144148p:plain

※ Figure 9 IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" より

  •  \theta はスレッド t の 法線面からの角度
  •  \phi は 布文脈での法線からの、法線面における角度でありますが、糸単体では法線は面を形成することには注意が必要です

ただ図にある  \theta_i, \theta_r, \phi_i, \phi_r は、個別の糸BSDFモデルでは直接は使われません。代わりに、

 
\displaystyle {
\phi_{ d }=\phi_{ i }-\phi_{ r }\\ \theta _{ h }=\frac { \theta _{ i }+\theta _{ r } }{ 2 } \\ \theta _{ d }=\frac { \theta _{ i }-\theta _{ r } }{ 2 } 
}

といった量が使われます。 それぞれ意味を確認しておきます。



 
\displaystyle {
\phi_{ d }=\phi_{ i }-\phi_{ r }
}

 \phi_d はそれぞれの入射 \phi_i と出射 \phi_r の差です。



 
\displaystyle {
\theta _{ h }=\frac { \theta _{ i }+\theta _{ r } }{ 2 } 
}

 \theta _h は、入射 \theta _i と出射 \theta _r の中間の角度です。これが0に近いほど、鏡面反射成分が強くなることが想像できます。



 
\displaystyle {
\theta _{ d }=\frac { \theta _{ i }-\theta _{ r } }{ 2 } 
}

 \theta _d はそれぞれの入射 \theta_i と出射 \theta_r の差の半分、言い換えると \theta _h  \theta_i の角度とも言えます。

  \phi_d は半分にされていないのに  \theta _d が半分にされているのは、少し混乱しますね。でも論文の表記がそうなのでそれに揃えます。ちなみに Iman Sadeghi氏のPh.D. Thesis、"Controlling the Appearance of Specular Microstructures" では、5.2 Background Theoryでは両方とも半分になっておらず、統一されています。

糸のモデル

これらを使って、糸のモデルを記述します。糸は、誘導体のラフな円筒であると仮定されており、 糸のモデルは以下のように2つの関数で記述します。

 
\displaystyle {
f_{ s }\left( t,\omega _{ i },\omega _{ r } \right) =\frac { f_{ r,s }\left( t,\omega _{ i },\omega _{ r } \right) +f_{ r,v }\left( t,\omega _{ i },\omega _{ r } \right)  }{ \cos ^{ 2 } \theta _{ d } } 
}

ここで  f_{ r,s } は円筒表面で反射する鏡面反射成分、  f_{ r,s } は円筒内部に入って出てくるボリューム拡散成分になります。では分母にある  \cos ^{ 2 } \theta _{ d } は何か?これを少し考えてみます。

分母の  \cos ^{ 2 } \theta _{ d }

仮に円筒表面が理想的になめらかであり、完全な鏡面であったとします。すると鏡面反射する光は、 \theta_i の角度に対して \theta_r = -\theta_i の角度に鏡面反射します。これは もふもふレンダリング(3)のMpの説明の一部 でも軽く紹介していました。改めてビジュアライズすると、

f:id:ushiostarfish:20181224160335g:plain

このことを踏まえて、拡散関数を以下のようにδ関数を使ってモデリングできます。

 
\displaystyle {
S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) =\frac { N\left( \phi _{ i } \right) \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } 
}

ここで、 N \left( \phi_i \right)  \phi の密度関数、 \theta = \theta_i = \theta_r とします。

エネルギー保存について考えてみます。

 
\displaystyle {
\int _{ \Omega  }^{  }{ S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) \cos { \theta _{ i } } d\omega  } \\ =\int _{ -\pi  }^{ \pi  } \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \: S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) \cos ^{ 2 }{ \theta _{ i } } d\theta d\phi \\ =\int _{ -\pi  }^{ \pi  } \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \frac { N\left( \phi _{ i } \right) \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } \cos ^{ 2 }{ \theta _{ i } } d\theta d\phi \\ =\int _{ -\pi \:  }^{ \pi \: \:  } N\left( \phi  \right) d\phi \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \frac { \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } \cos ^{ 2 }{ \theta _{ i } } d\theta \\ =\int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \frac { \delta \left( \theta _{ i }+\theta _{ r } \right)  }{ \cos ^{ 2 }{ \theta  }  } \cos ^{ 2 }{ \theta _{ i } } d\theta \\ =\frac { \cos ^{ 2 }{ \theta _{ i } }  }{ \cos ^{ 2 }{ \theta  }  } \\ =1
}

ここでの係数  \frac {1}{ \cos ^{ 2 } \theta} はうまく機能しています。この項は必要な係数であるのは明らかで、確かに何かを説明しているようです。 ところで角度θは様々な角度をとりますが、拡散の具合はそれぞれで果たして同じだろうか?というのを考えてみますと、例えば極端な2つのケースを考えて見ます。

まずは  \theta = 0 のケース

f:id:ushiostarfish:20181224164108p:plain

次に  \theta \approx 20° のケース

f:id:ushiostarfish:20181224164410p:plain

明らかに前者は後者に比べて、明らかに光線の密度が高いです。 \theta \frac {\pi}{2} に近づくと、  \frac {1}{ \cos ^{ 2 } \theta} はどんどん大きくなっていきますので、つまりこの  \frac {1}{ \cos ^{ 2 } \theta} 項はこのことを説明していたのではないでしょうか。

δ関数からの拡張

Stephen R. Marschner, Henrik Wann Jensen, Mike Cammarano, "Light Scattering from Human Hair Fibers" では、以上のアイディアをもとにδ関数をラフな表面に拡張しています。それは、

 
\displaystyle {
S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) =\frac { N\left( \phi _{ i } \right) M\left( \theta _{ i } \right)  }{ \cos ^{ 2 }{ \theta _{ d } }  } 
}

というものです。 \theta_i = \theta_r がもはや機能しないため、代わりに  \theta_d が使われているのは、近似的ですが仕方のない側面があります。 そんなわけで、糸のモデルが  \theta_d を含むのもこれを参考にしているからということになります。

鏡面反射成分  f_{ r,s }

鏡面反射成分は、以下のような式で表されます。

 
\displaystyle {
f_{ r,s }\left( t,\omega _{ i },\omega _{ { r } } \right) =F_{ r }\left( \eta ,\vec { w } _{ i } \right) \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) g\left( \gamma _{ s },\theta _{ h } \right) 
}

ここで  F_{ r } はフレネル項、 g は正規化ガウス関数で、平均は0です。フレネルは後ほど議論するとして、正規化ガウス関数は、

 
\displaystyle {
g\left( \sigma ,x \right) =\frac { 1 }{ \sqrt { 2\pi { \sigma  }^{ 2 } }  } exp\left( -\frac { { x }^{ 2 } }{ 2{ \sigma  }^{ 2 } }  \right) 
}

であり、 \theta _h は、入射 \theta _i と出射 \theta _r の中間の角度であり、これが0に近づくと鏡面反射成分が強くなることを表現しています。また、分散を調整することでローブの形を調整できるようになっています。

f:id:ushiostarfish:20181224185718g:plain

※プロットではフレネル項は除いています

 \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) はどこから来るのか?

次に、 \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) の項は特徴的です。プロットしてみると、以下のようになります。

f:id:ushiostarfish:20181225233337p:plain

※ desmos : cos(theta/2)

これは、完全になめらかな円筒に入射した光線の反射が周囲にどのように分布するのかを考えたものになります。これについて実際に光線を飛ばして可視化してみますと、次のようになります。

f:id:ushiostarfish:20181224190158p:plain

入射と出射が近いところの密度は高く、離れるほど密度が小さくなっています。 ではこの密度の関係性は実際にはどうなっているか考えてみます。

f:id:ushiostarfish:20181224194405p:plain

まず図のように \phi と点Pのxとの関係を考えて、これは当然、

 
\displaystyle {
x=\frac { 1 }{ 2 } \sin \left( \phi  \right) 
}

となります。ここで、 \phi の箇所に入射してきた光は、 2\phi のところに出射します。

f:id:ushiostarfish:20181224194500p:plain

したがって、

 
\displaystyle {
2\phi =\phi _{ r }\\ \phi =\frac { 1 }{ 2 } \phi _{ r }
}

つまり、 x \phi_{ r } には以下のような関係性があります。

 
\displaystyle {
x=\frac { 1 }{ 2 } \sin \left( \frac { 1 }{ 2 } \phi _{ r } \right) 
}

これを微分して、

 
\displaystyle {
x=\frac { 1 }{ 2 } \sin { \left( \frac { 1 }{ 2 } \phi _{ r } \right)  } \\ \frac { dx }{ d\phi _{ r } } =\frac { 1 }{ 2 } \frac { 1 }{ 2 } \cos  \left( \frac { 1 }{ 2 } \phi _{ r } \right) \\ \frac { dx }{ d\phi _{ r } } =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi _{ r } \right) 
}

ここで  \frac { dx }{ d\phi_{ r } } とは何かと言えば、微小な反射角度あたりの微小なx、元は単位長さ(=1)の部分が反射角にどのように分配されるかを示しています。 つまり先の節 "δ関数からの拡張" のところのNの一つの形としては、

 
\displaystyle {
N\left( \phi _{ i },\phi _{ r } \right) =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi _{ d } \right) 
}

を使うことができることも意味します。 実際、密度関数なら、1回転分の積分が1になるはずで、

 
\displaystyle {
\int _{ -\pi  }^{ \pi  }{ N\left( \phi _{ i },\phi _{ r } \right) d\phi _{ i } } =\int _{ -\pi  }^{ \pi  }{ \frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi  \right) d\phi  } 
}

は、

 
\displaystyle {
\frac { d }{ d\phi  } \left( \frac { 1 }{ 2 } \sin { \left( \frac { 1 }{ 2 } \phi  \right)  }  \right) =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi  \right) 
}

から、

 
\displaystyle {
\int _{ -\pi  }^{ \pi  }{ \frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi  \right) d\phi  } \\ =\frac { 1 }{ 2 } \sin { \left( \frac { 1 }{ 2 } \pi  \right)  } -\frac { 1 }{ 2 } \sin { \left( -\frac { 1 }{ 2 } \pi  \right)  } \\ =\frac { 1 }{ 2 } +\frac { 1 }{ 2 } \\ =1
}

と検算で確かめることもできました。 というわけで、鏡面反射成分の式、

 
\displaystyle {
f_{ r,s }\left( t,\omega _{ i },\omega _{ { r } } \right) =F_{ r }\left( \eta ,\vec { w } _{ i } \right) \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) g\left( \gamma _{ s },\theta _{ h } \right) 
}

 \cos  \left( \frac { \phi _{ d } }{ 2 }  \right) はここから来ている、ということがわかりました。

消えた  \frac { 1 }{ 4 }

ところが、この式には  \frac { 1 }{ 4 } がどこにもありません! 実際、Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model" には、

 
\displaystyle {
N\left( \phi _{ i },\phi _{ r } \right) =\frac { 1 }{ 4 } \cos  \left( \frac { 1 }{ 2 } \phi _{ d } \right) \\ M\left( \theta _{ i },\theta _{ r } \right) =g\left( \beta ,\theta _{ h } \right) \\ S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) =\frac { N\left( \phi _{ d } \right) M\left( \theta _{ h } \right)  }{ \cos ^{ 2 }{ \theta _{ d } }  } 
}

のとき、

 
\displaystyle {
\int _{ -\pi  }^{ \pi  } \int _{ -\frac { \pi \:  }{ 2 }  }^{ \frac { \pi  }{ 2 }  } \: S\left( \phi _{ i },\phi _{ r },\theta _{ i },\theta _{ r } \right) \cos ^{ 2 }{ \theta _{ i } } d\theta d\phi 
}

がいくつかの \beta で、 \theta_i を変えながら観察したとき、どうなるのか?という検証がFigure 3にのっています。

f:id:ushiostarfish:20181229114321p:plain

※ Figure 3 Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model"

今回は、自分でも検証のために以下の実装でプロットしてみました。

これをグラフにすると、

f:id:ushiostarfish:20181224231933p:plain

 \beta の単位は度で、横軸は出射角度(ラジアン)、縦軸は出射放射輝度

Total reflectance from cylinders - Google スプレッドシート

これは確かに Eugene d’Eon, Guillaume Francois, Martin Hill, Joe Letteri, Jean-Marie Aubry, "An Energy-Conserving Hair Reflectance Model" の図と一致しており、かなり部分でエネルギーが増幅しているのがわかります。 そしてこれが球面ガウス関数を加工して作る "4.2. An Energy-Conserving Longitudinal Scattering Function Mp" の導入の大きな動機になっています。

つまりここから考えると、本論文の鏡面反射成分  f_{ r,s } はこれの結果のさらに4倍ものエネルギーを増やすポテンシャルがあることになります。 しかしながらこの部分が間違った記述というわけではどうやらなく、こういうもののようです。IMAN SADEGHI, OLEG BISKER, JOACHIM DE DEKEN, HENRIK WANN JENSEN, UC San Diego, "Practical Microcylinder Appearance Model for Cloth Rendering" にはそもそもエネルギー保存に関する記述は無く、この論文の興味の対象では無いのでしょう。ここでも考察はこのくらいにして、次に進みます。

フレネル項

フレネル項は、以下のように定義されています。

 
\displaystyle {
F_{ r }\left( \eta ,\vec { w } _{ i } \right) =F_{ r }\left( \eta ,\cos ^{ -1 }{ \left( \cos { \left( \theta _{ d } \right)  } \cos { \left( \frac { \phi _{ d } }{ 2 }  \right)  }  \right)  }  \right) 
}

この項がどこから来たのか、ちょっとわかりませんでした。しかし、 \theta_{ d }  \frac {\phi _{ d } }{ 2 } も、ハーフアングルまでの角度であり、そのコサインの積ということは、どちらか一方でも0に近づけば、グレージング角で入射したことにする、というような意図は感じます。論文にはマイクロファセットを考慮したフレネル項の導入を検討したが、測定したデータとのマッチングに改善が見られなかった、とありました。 論文には記載がありませんでしたが、今回は誘導体フレネル項の計算には、Bruce Walter, Stephen R. Marschner, Hongsong Li, Kenneth E. Torrance, "Microfacet Models for Refraction through Rough Surfaces" の中で、"A convenient exact formulation for dielectrics with unpolarized light" として書かれている以下のものを使うことにしました。

 
\displaystyle {
{ F\left( \eta _{ t },\eta _{ i },\theta  \right) =\frac { 1 }{ 2 } \frac { \left( g-c \right) ^{ { 2 } } }{ \left( g+c \right) ^{ { 2 } } } \left( 1+\frac { \left( c\left( g+c \right) -1 \right) ^{ { 2 } } }{ \left( c\left( g+c \right) +1 \right) ^{ { 2 } } }  \right)  }\\ g=\sqrt { \frac { { \eta _{ t } }^{ 2 } }{ { \eta _{ i } }^{ 2 } } -1+{ c }^{ 2 } } \\ c=\cos { \theta  } 
}

ボリューム拡散成分  f_{ r,s }

続いて f_{ r,s } です。こちらは以下のように定義されています。

 
\displaystyle {
f_{ r,v }\left( t,\omega _{ i },\omega _{ r } \right) =F\frac { \left( 1-k_{ d } \right) g\left( \gamma _{ v },\theta _{ h } \right) +k_{ d } }{ \cos { \theta _{ i } } +\cos { \theta _{ r } }  } A
}

ここでAは波長依存の吸収係数、ここで色がつきます。この項は鏡面反射成分にはありませんでした。

F 項

Fはフレネル項で論文中では、

 
\displaystyle {
F=F_{ t }\left( \eta ,\vec { \omega _{ i } }  \right) F_{ t }\left( \eta ',\vec { \omega '_{ r } }  \right) 
}

となっています。しかしこれ以上の言及はなかったのですが、おそらく、

 
\displaystyle {
F_{ t }\left( \eta ,\vec { \omega _{ i } }  \right) =1-F_{ r }\left( \eta ,\vec { w } _{ i } \right) 
}

であろうと思いつつ、

 
\displaystyle {
F_{ t }\left( \eta ',\vec { \omega '_{ r } }  \right) 
}

の項は意図がわかりません。とはいえ、シリンダー内部の多重拡散もこの  f_{ r,s } で説明できると仮定すると、

 
\displaystyle {
F=F_{ t }\left( \eta ,\vec { \omega _{ i } }  \right) 
}

としてしまうのもそれほど間違っているとは思えませんが、今回は、

 
\displaystyle {
F={ { F_{ t } }^{ 2 }\left( \eta ,\vec { \omega _{ i } }  \right)  }
}

で実装してみることにしました。

 g\left( \gamma _{ v },\theta _{ h } \right)

次に、 g\left( \gamma _{ v },\theta _{ h } \right) 、この項は分散のパラメータが違うだけで、鏡面反射成分の中にある  g\left( \gamma _{ s },\theta _{ h } \right) と全く同じ働きをします。つまり反射成分だけでなく、一度屈折した光線も出るときにも  \theta に関しては同じふるまいだとモデル化されています。これは真なのでしょうか? これを観察するために、なめらかな誘導体シリンダーへ入射する光線の屈折を追跡してみます。(Houdini vex: tracking refraction · GitHub

f:id:ushiostarfish:20181229123015g:plain

ここで入射光線は \theta = 45° であり、光線の  \sin \theta をプロットしてみると、

f:id:ushiostarfish:20181229123735p:plain

なんと、すべての出射ベクトルは、入射と全く同じ角度で出射しています。ちなみにこれは入射角度を変えても同様のふるまいをします。 実はこのことはすでに、Stephen R. Marschner, Henrik Wann Jensen, Mike Cammarano, "Light Scattering from Human Hair Fibers" の "A Loci of reflections" で触れられています。 "A Loci of reflections" の説明では、誘導体シリンダーの法線を、球体の赤道部分の法線と捉えることでこれを説明しています。

f:id:ushiostarfish:20181229152111p:plain

※ n, n'がある部分がシリンダー法線です。

ここで、球面上の点  v_i を入射ベクトルと考えます。とするとこの入射ベクトル v_i は、赤道上のいろいろな法線と衝突する可能性があります(裏面、つまり v_i n が反対向きでないケースに限りますが)。ここではまず代表的な法線、  v_i の真下にある法線を  n と表記します。

この v_i n の 組み合わせで屈折の振る舞いを考えて、ある相対屈折率では、屈折後は図の  v_t のベクトルになりました。スネルの法則によれば、

 
\displaystyle {
\eta _{ i }\sin  \theta _{ i }=\eta _{ t }\sin  \theta _{ t }
}

の関係性があり、ここで、 \eta =\frac { \eta _{ t } }{ \eta _{ i } } とすると、

 
\displaystyle {
\sin  \theta _{ i }=\eta \sin  \theta _{ t }
}

ここで  \sin\theta_i, \sin\theta_t は何か?と考えてみると、幾何学的にはそれぞれ図の  h_i, h_t だと捉えることができます。 つまり、

 
\displaystyle {
\left| h_{ i } \right| =\eta \left| h_{ t } \right| 
}

という関係性を考える事ができます。 しかし法線は実際には  n だけでなく、赤道の上のどんなベクトルにもなりえます。そこで真下の法線ではない法線  n' について考えてみます。下の図をご覧ください。ここでは、 n' のケースの  \sin\theta にあたる部分、 h'_{ i }, h'_{ t } を足しています。

f:id:ushiostarfish:20181229173945p:plain

このとき、 \left| h'_{ t } \right| を Y 軸に投射した量、

 
\displaystyle {
H=\left| h'_{ t } \right| \left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

はどんな値になるのか考えてみます。 まず、やはりスネルの法則によれば、

 
\displaystyle {
\left| h'_{ i } \right| =\eta \left| h'_{ t } \right| 
}

であるので、

 
\displaystyle {
H=\frac { \left| h'_{ i } \right|  }{ \eta  } \left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

次に、

 
\displaystyle {
\left| h'_{ i } \right| \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right| =\left| h_{ i } \right| \\ \left| h'_{ i } \right| ={ \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right|  }^{ -1 }\left| h_{ i } \right| 
}

であることから、

 
\displaystyle {
H=\frac { 1 }{ \eta  } { \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right|  }^{ -1 }\left| h_{ i } \right| \left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| \\ H=\frac { \left| h_{ i } \right|  }{ \eta  } { \left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right|  }^{ -1 }\left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

このとき、ベクトル  h'_i と ベクトル  h'_t は、向きがちょうど正反対になります。したがって、

 
\displaystyle {
\left| \frac { h'_{ i } }{ \left| h'_{ i } \right|  } \cdot Y \right| =\left| \frac { h'_{ t } }{ \left| h'_{ t } \right|  } \cdot Y \right| 
}

でなければばならないでしょう。これを使うと、

 
\displaystyle {
H=\frac { \left| h_{ i } \right|  }{ \eta  } 
}

おっと、 \left| h_{ i } \right| =\eta \left| h_{ t } \right| であったことを思い出すと、

 
\displaystyle {
H=\frac { \left| h_{ i } \right|  }{ \eta  } =\frac { \eta \left| h_{ t } \right|  }{ \eta  } =\left| h_{ t } \right| 
}

これの意味するところは、どのような  n' であっても、屈折後のベクトルの赤道面からの垂直距離は屈折率にのみ支配されるということになります。加えて、誘導体シリンダーから入った後に出ることを考えると、屈折率は打ち消し合い、出るときには入るときの  \theta と同じ  \theta で出射する ということもまた言えることになります(正負については反転しますが)。

したがって、ボリューム拡散成分の g\left( \gamma _{ v },\theta _{ h } \right) 項はこの意味で妥当だと考えることができるでしょう。ただし、ラフな誘導体円筒でモデル化しているとはいえ、内部に入り込んだ光は中で拡散を繰り返すことが考えられ、今回のモデルでは、ボリューム拡散成分のgの分散は、鏡面反射とは違う分散を与えられるようになっており、元論文Table Ⅱに記載されているパラメーターも、すべて \gamma_v \gamma_s よりも大きな分散を与えるようになっています。

分母の  \cos { \theta _{ i } } +\cos { \theta _{ r } }

論文では、半無限媒質の多重散乱による拡散反射を考えたもの(Subrahmanyan Chandrasekhar, "Radiative Transfer")だとありますが、すみませんこの項についてはよくわかりませんでした。ただ、この項により測定値とよりマッチするようになったとのことです。

 k_d

 k_d 項 はかなりシンプルで、綿やリネンなどの等方的な拡散を表すために、 g\left( \gamma _{ v },\theta _{ h } \right)  1 k_d で線形補間しています。

以上をプロットしてみると以下のようになります。以上の理解からすると、おおむね直観的な動きだと言えます。

f:id:ushiostarfish:20181229172951g:plain

これで個別の糸のモデル化が完了しました。次回はこれを使ってシェーディングモデルを構築します。

ushiostarfish.hatenablog.com

Stratified Sampling of Spherical Trianglesを読む

Stratified Sampling of Spherical Triangles

元論文は、James Arvo, "Stratified Sampling of Spherical Triangles" で、前回のこちらの関連になります。

ushiostarfish.hatenablog.com

James Arvo, "Stratified Sampling of Spherical Triangles" は、Spherical Rectanglesよりもだいぶ前の論文で、SIGGRAPH '95 の論文とのことです。Carlos Ureña, Marcos Fajardo, Alan King , “An Area-Preserving Parametrization for Spherical Rectangles” からも当然引用があります。

こちらもやはりモチベーションや達成したい目的は同じで、単位面積  \left[0\, 1 \right]^2 から、任意の 三角形 の領域を単位球面に射影してできた領域への写像を、面積を保存する形で提示します。

球面三角法

やはりこの論文でも、球面三角形の性質をうまく使っていきます。 まずは前回も登場した、単位球面三角形の面積と頂点の角度の関係式、

f:id:ushiostarfish:20181021145939p:plain

 
\displaystyle {
Area =\alpha+\beta+\gamma-\pi 
}

そして、もう一つ単位球面三角形の角度の余弦定理と呼ばれる式、

f:id:ushiostarfish:20181021145820p:plain

※a, b, cは球面上の線の弧の長さです

 
\displaystyle {
\cos { \beta  } =-\cos { \gamma  } \cos { \alpha  } +\sin { \gamma  } \sin { \alpha  } \cos { b } \\ \cos { \gamma  } =-\cos { \beta  } \cos { \alpha  } +\sin { \beta  } \sin { \alpha  } \cos { c } 
}

が論文中の(1) ~ (3)にあります。

(1) については、前回触れていたので省略します。

また、導出については、

risalc.info

の説明が大変わかりやすく、オススメです。

しかし、単位球面三角形の角度の余弦定理についてはなんぞこれ?という感じがします。 イマイチイメージしずらいのですが、ちょっと特殊なケースについて試してみましょう。 例えば  \alpha=\frac {\pi} {2} かつ  \gamma=\frac {\pi} {2} のケースです。この時は以下のようなシチュエーションになるでしょう。

f:id:ushiostarfish:20181021161736p:plain

赤い線を赤道だとすると、 \beta の頂点はちょうど北極点(南極点)に位置します。 これは、先に  \beta が決まっていたとして、 \alpha=\frac {\pi} {2} かつ  \gamma=\frac {\pi} {2} となるように線を引くとしたら以下のようにやはり真ん中を選ぶしか無いから、と考えると直観的です。

f:id:ushiostarfish:20181021162627p:plain

では式はどうなるかというと、

 
\displaystyle {
\cos { \beta  } =-\cos { \gamma  } \cos { \alpha  } +\sin { \gamma  } \sin { \alpha  } \cos { b } \\ \cos { \beta  } =-\cos { \frac { \pi  }{ 2 }  } \cos { \frac { \pi  }{ 2 }  } +\sin { \frac { \pi  }{ 2 }  } \sin { \frac { \pi  }{ 2 }  } \cos { b } \\ \cos { \beta  } =\cos { b } 
}

 b \beta が直接結びつく形になりました。 これはある意味当たり前で、赤道の長さが  2\pi なので、 b は、

 
\displaystyle {
b=2\pi \frac { \beta  }{ 2\pi  } \\ b=\beta 
}

となるからです。ちょっとだけ親近感が湧いてきました。このように単位球面三角形の角度の余弦定理は、ある1辺の長さと三角形の角度を繋げてくれるものだと言えます。 では一緒に導出のほうも見てみることにします。

これについては、張海潮 氏の、"球面三角形的AAA 定理" にわかりやすい解説があったのでこちらを見てみることにします。中国語ですが、google 翻訳があれば結構大丈夫です。

圖二(図2)の下を見てみます。

f:id:ushiostarfish:20181021164450p:plain

※張海潮, "球面三角形的AAA 定理" 圖二 より

記号については同じものを使っているので、混乱しなくて助かります。 l, m, n はそれぞれ球の切り口の法線になります。 ここで2つのベクトル  n\times l n\times m を考えます。これはそれぞれ、球心からAに向かうべクトル、球心からCに向かうべクトルと向きが一致します。図にすると、

f:id:ushiostarfish:20181021165929p:plain

※長さは適当です

この時、さらにこの2つのベクトルの内積を考えます。

 
\displaystyle {
\left( n\times l \right) \cdot \left( n\times m \right) 
}

これは幸運にも スカラー4重積 の形をしており、

 
\displaystyle {
\left( n\times l \right) \cdot \left( n\times m \right) =det\begin{pmatrix} n\cdot n & n\cdot m \\ l\cdot n & l\cdot m \end{pmatrix}=det\begin{pmatrix} 1 & \cos { \gamma  }  \\ -\cos { \alpha  }  & \cos { \beta  }  \end{pmatrix}=\cos { \beta  } +\cos { \gamma  } \cos { \alpha  } 
}

うまいこと球面三角形の角度だけで表すことができます。 他方、内積の定義、 \overrightarrow { a } \cdot \overrightarrow { b } =|\overrightarrow { a } ||\overrightarrow { b } |\cos  \theta から考えると、

 
\displaystyle {
\left( n\times l \right) \cdot \left( n\times m \right) =\left| n\times l \right| \left| n\times m \right| \cos  \theta 
}

ここで\thetaは∠AOCの角度であり、ということはつまり bの長さでもあります。 そして単位ベクトルの外積のベクトルの長さは\sin を使っても表せるので、これらを利用すると、

 
\displaystyle {
\left| n\times l \right| \left| n\times m \right| \cos  \theta =\sin { \alpha  } \sin { \gamma  } \cos  b
}

以上2つの式をつなげると、

 
\displaystyle {
\cos { \beta  } +\cos { \gamma  } \cos { \alpha  } =\sin { \gamma  } \sin { \alpha  } \cos  b
}

となり、これが元論文の式 (2) に対応します。 論文には2つしか書かれていませんが、当然3つ式を考えることができます。

 
\displaystyle {
\cos { \alpha  } +\cos { \beta  } \cos { \gamma  } =\sin { \beta  } \sin { \gamma  } \cos  a\\ \cos { \beta  } +\cos { \gamma  } \cos { \alpha  } =\sin { \gamma  } \sin { \alpha  } \cos  b\\ \cos { \gamma  } +\cos { \alpha  } \cos { \beta  } =\sin { \alpha  } \sin { \beta  } \cos  c
}

なかなか綺麗なものです。

The Sampling Algorithm

上記の式を使って、実際にサンプリングについて考えていきます。 本手法でも、前回と同じく2段階のアルゴリズムになっています。論文 Figure1を見てみましょう。

f:id:ushiostarfish:20181021175259p:plain

※James Arvo, "Stratified Sampling of Spherical Triangles" Figure 1 より

1段階目で AC 上の  \hat { C } を選択、2段階目で  B\hat { C } 上の P を選択する、といった具合です。 まずは1段階目から参ります。 ここでは球面三角形  AB\hat { C } に着目して、先程の球面三角形の角度の余弦定理のうち、  \hat { C } を決めるのに重要な量、 \hat{b}, \beta の両方が組み込まれている、

 
\displaystyle {
\cos { \hat { \beta  }  } =-\cos { \hat { \gamma  }  } \cos { \alpha  } +\sin { \hat { \gamma  }  } \sin { \alpha  } \cos { \hat { b }  } 
}

この式を出発点として考えてみます。 ここで、球面三角形  ABC の面積を  I 、球面三角形  AB\hat { C } の面積を  \hat{I} とおくと、

 
\displaystyle {
\hat { I } =\alpha +\hat { \beta  } +\hat { \gamma  } -\pi \\ \hat { \gamma  } =\hat { I } -\alpha -\hat { \beta  } +\pi 
}

※面積の記号として  I については、論文と違います。論文の記号がAとかぶってややこしいからです。

これを使うと、 \hat{ \gamma } をうまく置き換えることができ、

 
\displaystyle {
\cos { \hat { \beta  }  } =-\cos { \left( \hat { I } -\alpha -\hat { \beta  } +\pi  \right)  } \cos { \alpha  } +\sin { \left( \hat { I } -\alpha -\hat { \beta  } +\pi  \right)  } \sin { \alpha  } \cos { \hat { b }  } \\ \cos { \hat { \beta  }  } =\cos { \left( \hat { I } -\alpha -\hat { \beta  }  \right)  } \cos { \alpha  } -\sin { \left( \hat { I } -\alpha -\hat { \beta  }  \right)  } \sin { \alpha  } \cos { \hat { b }  } 
}

ここで一旦、 \phi =\hat { I } -\alpha とおくと、

 
\displaystyle {

\cos { \hat { \beta  }  } =\cos { \left( \phi -\hat { \beta  }  \right)  } \cos { \alpha  } -\sin { \left( \phi -\hat { \beta  }  \right)  } \sin { \alpha  } \cos { \hat { b }  } \\ \cos { \hat { \beta  }  } -\cos { \left( \phi -\hat { \beta  }  \right)  } \cos { \alpha  } =-\sin { \left( \phi -\hat { \beta  }  \right)  } \sin { \alpha  } \cos { \hat { b }  } \\ \frac { \cos { \left( \phi -\hat { \beta  }  \right)  } \cos { \alpha  } -\cos { \hat { \beta  }  }  }{ \sin { \left( \phi -\hat { \beta  }  \right)  } \sin { \alpha  }  } =\cos { \hat { b }  } 
}

これが論文の式 (4)になります。

写像元の単位面積  \left[0\, 1 \right]^2 の1つ目の変数を \xi _{ u } とすると、

 
\displaystyle {
I\xi _{ u }=\hat { I } 
}

であることもついでに確認しておきます。

ここでポイントなのは、 \xi _{ u } が定まると、 \hat{I} が定まり、 \phi が決まり、自動的に  \cos \hat {b} が定まる、そうすれば  \hat{C} が定まりそうです。ただ論文の式 (4)にはまだ問題があって、 \hat{\beta} が残ってしまっています。本当はこの項は自動的に決まるはずであり、なんとか消したいと考えます。

ここで球面三角形  AB\hat { C } で球面三角形の角度の余弦定理をもう1パターン持ってきます。

 
\displaystyle {
\cos { \hat { \gamma  }  } =-\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c } 
}

これは論文の式(3)に対応します。この式に対して同様の操作をしてみます。

 
\displaystyle {
\begin{eqnarray} \cos { \hat { \gamma  }  }  & = & -\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c }  \\ \cos { \left( \hat { I } -\alpha -\hat { \beta  } +\pi  \right)  }  & = & -\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c }  \\ -\cos { \left( \hat { I } -\alpha -\hat { \beta  }  \right)  }  & = & -\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c }  \\ -\cos { \left( \phi -\hat { \beta  }  \right)  }  & = & -\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c }  \\ \cos { \left( \phi -\hat { \beta  }  \right)  }  & = & \cos { \hat { \beta  }  } \cos { \alpha  } -\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c }  \end{eqnarray}
}

ここで加法定理でさらに続けて、

 
\displaystyle {

\begin{eqnarray}
\cos { \phi  } \cos { \hat { \beta  }  } +\sin { \phi  } \sin { \hat { \beta  }  } &=&\cos { \hat { \beta  }  } \cos { \alpha  } -\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c } \\ \cos { \phi  } \cos { \hat { \beta  }  } +\sin { \phi  } \sin { \hat { \beta  }  } -\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c } &=&0\\ \cos { \phi  } \cos { \hat { \beta  }  } -\cos { \hat { \beta  }  } \cos { \alpha  } +\sin { \phi  } \sin { \hat { \beta  }  } +\sin { \hat { \beta  }  } \sin { \alpha  } \cos { c } &=&0\\ \left( \cos { \phi  } -\cos { \alpha  }  \right) \cos { \hat { \beta  }  } +\left( \sin { \phi  } +\sin { \alpha  } \cos { c }  \right) \sin { \hat { \beta  }  } &=&0
\end{eqnarray}
}

ここで一旦  \beta に集中するため、以下のように、

 
\displaystyle {
u=\cos { \left( \phi  \right)  } -\cos { \left( \alpha  \right)  } \\ v=\sin { \left( \phi  \right)  } -\sin { \left( \alpha  \right)  } \cos { \left( c \right)  } 
}

とおくと、

 
\displaystyle {
u\cos { \left( \hat { \beta  }  \right)  } +v\sin { \left( \hat { \beta  }  \right)  } =0
}

とシンプルにまとめることができました。さらにここで、

 
\displaystyle {
\sin { x } =\sqrt { 1-\cos ^{ 2 }{ x }  } 
}

の関係を使って、

 
\displaystyle {
u\cos { \left( \hat { \beta  }  \right)  } \pm v\sqrt { 1-\cos ^{ 2 }{ \hat { \beta  }  }  } =0
}

ただ、符号については  \beta によって変えなければなりません。

f:id:ushiostarfish:20181021185020p:plain

そのことを一旦  \pm と記述することにしますが、次に、

 
\displaystyle {
\left( u\cos { \left( \hat { \beta  }  \right)  } \pm v\sqrt { 1-\cos ^{ 2 }{ \hat { \beta  }  }  }  \right) \left( u\cos { \left( \hat { \beta  }  \right)  } \mp v\sqrt { 1-\cos ^{ 2 }{ \hat { \beta  }  }  }  \right) =0
}

のように符号と逆になった項をかけます。すると、

 
\displaystyle {
u\cos { \left( \hat { \beta  }  \right)  } \pm v\sqrt { 1-\cos ^{ 2 }{ \hat { \beta  }  }  } =0\\ \left( u\cos { \left( \hat { \beta  }  \right)  } \pm v\sqrt { 1-\cos ^{ 2 }{ \hat { \beta  }  }  }  \right) \left( u\cos { \left( \hat { \beta  }  \right)  } \mp v\sqrt { 1-\cos ^{ 2 }{ \hat { \beta  }  }  }  \right) =0\\ { u }^{ 2 }\cos ^{ 2 }{ \hat { \beta  }  } -{ v }^{ 2 }\left( 1-\cos ^{ 2 }{ \hat { \beta  }  }  \right) =0\\ { u }^{ 2 }\cos ^{ 2 }{ \hat { \beta  }  } -{ v }^{ 2 }+{ v }^{ 2 }\cos ^{ 2 }{ \hat { \beta  }  } =0\\ { u }^{ 2 }\cos ^{ 2 }{ \hat { \beta  }  } +{ v }^{ 2 }\cos ^{ 2 }{ \hat { \beta  }  } ={ v }^{ 2 }\\ \left( { u }^{ 2 }+{ v }^{ 2 } \right) \cos ^{ 2 }{ \hat { \beta  }  } ={ v }^{ 2 }\\ \cos ^{ 2 }{ \hat { \beta  }  } =\frac { { v }^{ 2 } }{ { u }^{ 2 }+{ v }^{ 2 } } \\ \cos { \hat { \beta  }  } =\frac { \pm v }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  }  
}

結果的に最初の部分の符号については気にする必要はない状態で  \cos \hat {\beta} を得る事ができました。結局最後は符号が行方不明になってしまいましたが...。

 \sin についても同様の操作を行うと、

 
\displaystyle {
v\sin { \left( \hat { \beta  }  \right)  } \pm u\sqrt { 1-\sin ^{ 2 }{ \hat { \beta  }  }  } =0\\ \left( v\sin { \left( \hat { \beta  }  \right)  } \pm u\sqrt { 1-\sin ^{ 2 }{ \hat { \beta  }  }  }  \right) \left( v\sin { \left( \hat { \beta  }  \right)  } \mp u\sqrt { 1-\sin ^{ 2 }{ \hat { \beta  }  }  }  \right) =0\\ { v }^{ 2 }\sin ^{ 2 }{ \hat { \beta  }  } -{ u }^{ 2 }\left( 1-\sin ^{ 2 }{ \hat { \beta  }  }  \right) =0\\ { v }^{ 2 }\sin ^{ 2 }{ \hat { \beta  }  } -{ u }^{ 2 }+{ u }^{ 2 }\sin ^{ 2 }{ \hat { \beta  }  } =0\\ { v }^{ 2 }\sin ^{ 2 }{ \hat { \beta  }  } +{ u }^{ 2 }\sin ^{ 2 }{ \hat { \beta  }  } ={ u }^{ 2 }\\ \left( { v }^{ 2 }+{ u }^{ 2 } \right) \sin ^{ 2 }{ \hat { \beta  }  } ={ u }^{ 2 }\\ \sin ^{ 2 }{ \hat { \beta  }  } =\frac { { u }^{ 2 } }{ { u }^{ 2 }+{ v }^{ 2 } } \\ \sin { \hat { \beta  }  } =\frac { \pm u }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } 
}

と、 \sin \hat {\beta} についても、符号はともかく得ることができました。

さて、

 
\displaystyle {
u\cos { \left( \hat { \beta  }  \right)  } +v\sin { \left( \hat { \beta  }  \right)  } =0
}

をちょっとだけ変形してみると、

 
\displaystyle {
u\cos { \hat { \beta  }  } =-v\sin { \hat { \beta  }  } \\ \frac { \cos { \hat { \beta  }  }  }{ v } =-\frac { \sin { \hat { \beta  }  }  }{ u } 
}

ここからわかることは、 \frac { \cos { \hat { \beta  }  }  }{ v }  \frac { \sin { \hat { \beta  }  }  }{ u } は常に符号が逆にならなければならない、ということです。 それを踏まえて、  \cos \hat {\beta} \sin \hat {\beta} についても少しだけ変形してみると、

 
\displaystyle {
\pm \frac { \sin { \hat { \beta  }  }  }{ u } =\frac { 1 }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } \\ \pm \frac { \cos { \hat { \beta  }  }  }{ v } =\frac { 1 }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } 
}

のようになります。 ここで、  \frac { \cos { \hat { \beta  }  }  }{ v } が負のケースを考えてみると、両方とも右辺は常に正であることが求められますので、

 
\displaystyle {
+\frac { \sin { \hat { \beta  }  }  }{ u } =\frac { 1 }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } \\ -\frac { \cos { \hat { \beta  }  }  }{ v } =\frac { 1 }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } 
}

逆に  \frac { \sin { \hat { \beta  }  }  }{ u } が負のケースは、

 
\displaystyle {
-\frac { \sin { \hat { \beta  }  }  }{ u } =\frac { 1 }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } \\ +\frac { \cos { \hat { \beta  }  }  }{ v } =\frac { 1 }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } 
}

つまり結局のところ、

 
\displaystyle {
\sin { \hat { \beta  }  } =\frac { \mp u }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } \\ \cos { \hat { \beta  }  } =\frac { \pm v }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } 
}

のように  \cos \hat {\beta} \sin \hat {\beta} の符号はお互いに逆になるということになります。 ※実際には  \beta によって符号がフリップしますが、後の議論によってさほど本質的は問題ないことがわかります。

これを使って、少し戻ってしまいますが、論文の式(4)

 
\displaystyle {
\cos { \hat { b }  } =\frac { \cos { \left( \phi -\hat { \beta  }  \right)  } \cos { \alpha  } -\cos { \hat { \beta  }  }  }{ \sin { \left( \phi -\hat { \beta  }  \right)  } \sin { \alpha  }  } 
}

にこれを代入していきます。

 
\displaystyle {
\cos { \hat { b }  } =\frac { \cos { \left( \phi -\hat { \beta  }  \right)  } \cos { \alpha  } -\cos { \hat { \beta  }  }  }{ \sin { \left( \phi -\hat { \beta  }  \right)  } \sin { \alpha  }  } \\ \cos { \hat { b }  } =\frac { \left( \cos { \phi  } \cos { \hat { \beta  }  } +\sin { \phi  } \sin { \hat { \beta  }  }  \right) \cos { \alpha  } -\cos { \hat { \beta  }  }  }{ \left( \sin { \phi  } \cos { \hat { \beta  }  } -\cos { \phi  } \sin { \hat { \beta  }  }  \right) \sin { \alpha  }  } \\ \cos { \hat { b }  } =\frac { \left( \cos { \phi  } \frac { \pm v }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } +\sin { \phi  } \frac { \mp u }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  }  \right) \cos { \alpha  } -\frac { \pm v }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  }  }{ \left( \sin { \phi  } \frac { \pm v }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  } -\cos { \phi  } \frac { \mp u }{ \sqrt { { u }^{ 2 }+{ v }^{ 2 } }  }  \right) \sin { \alpha  }  } \\ \cos { \hat { b }  } =\frac { \left( \pm v\cos { \phi  } \mp u\sin { \phi  }  \right) \cos { \alpha  } \mp v }{ \left( \pm v\sin { \phi  } \pm u\cos { \phi  }  \right) \sin { \alpha  }  } \\ \cos { \hat { b }  } =\frac { \pm \left( v\cos { \phi  } -u\sin { \phi  }  \right) \cos { \alpha  } \mp v }{ \pm \left( v\sin { \phi  } +u\cos { \phi  }  \right) \sin { \alpha  }  } \\ \cos { \hat { b }  } =\frac { \pm \left\{ \left( v\cos { \phi  } -u\sin { \phi  }  \right) \cos { \alpha  } -v \right\}  }{ \pm \left( v\sin { \phi  } +u\cos { \phi  }  \right) \sin { \alpha  }  } \\ \cos { \hat { b }  } =\frac { \left( v\cos { \phi  } -u\sin { \phi  }  \right) \cos { \alpha  } -v }{ \left( v\sin { \phi  } +u\cos { \phi  }  \right) \sin { \alpha  }  } 
}

なんと、都合の良いことに、正負が打ち消し合って綺麗に消えてくれました。これが論文の式 (5) ということになります。最後の代入のおかげで、 \betaは完全に姿を消し、写像元の単位面積  \left[0\, 1 \right]^2 の1つ目の変数  \xi _{ u } からダイレクトに  \cos \hat{b} へと繋げる事ができました。

 \hat { C } を求める

 \cos \hat{b} が求まりましたので、次に実際にそこから  \hat { C } の値を求めていきます。 論文では記述の簡略化のために、以下の表記を使っています。

 
\displaystyle {
\left[ x|y \right] =normalize \left( x-\left( x\cdot y \right) y \right) 
}

※ ここで yは単位ベクトルとします

これは、どういう演算かというと、

f:id:ushiostarfish:20181024214549p:plain

そう、x方向の、yと直交するベクトルを得る演算ということになります。 これを使えば、AからCに向かっての回転を簡単に考えることができます。

f:id:ushiostarfish:20181024223531p:plain

※もちろん、実装上クォータニオンなどを使って回転させるのも可能だと思いますが、こちらのやり方のほうがシンプルなように思いました。

また、今手元には  \cos \hat{b} しかありませんが、 0 \le \hat{b} \le \pi の範囲(投影された球面三角形の場合はこの範囲で十分)なら、

 
\displaystyle {
\hat { C } =A\cos { \hat{b} } +\left[ C|A \right] \sqrt { 1-\cos ^{ 2 }{ \hat{b}  }  } 
}

とすることができます。これが論文の擬似コード "Compute the third vertex of the sub-triangle." のところに対応します。

何が達成されたか?

上記の手法によって、単位面積の1軸が  \hat{b} と結びつきました。これはイメージとしては、

f:id:ushiostarfish:20181027131229p:plain

このように面積を保存する形での対応が確立されたということになります。 ということは緯度については結局、経度について小さくスライスした領域について均一なマッピングを考えれば良いことになります。

f:id:ushiostarfish:20181027133151p:plain

となると、やはりこの図でいうところの y軸に対して均一にマッピングをすることになります。 ここについては、

ushiostarfish.hatenablog.com

の話と同じです。

 P を求める

これを今までの球面三角形の文脈で考えてみます。  B をちょうど 先の図で言うところのY軸であると捉えると、

f:id:ushiostarfish:20181027135945p:plain

 \xi_v を、上の図のちょうど赤の部分に均一にマッピングを考え、そこからPを生成すれば良さそうです。 ここで B に射影した  \hat {C} の位置は  \cos { \theta _{ \hat { C }  } } ですので、

f:id:ushiostarfish:20181027142339p:plain

 B 軸上での数値としては、1 から   \cos { \theta _{ \hat { C }  } }  \xi_v マッピングになるので、結局、

 
\displaystyle {
z=1+\xi _{ v }\left( 1- \cos { \theta _{ \hat { C }  } }  \right) 
}

ここで   \cos { \theta _{ \hat { C }  } }  \hat { C } \cdot B であるので、

 
\displaystyle {
z=1+\xi _{ v }\left( 1-\hat { C } \cdot B \right) 
}

とも書くことができ、これが論文の擬似コード、"Use the other random variable to select cos θ." の部分になります。 zは一方で  \cos \theta と捉えられるので、以下の図のように、

f:id:ushiostarfish:20181027142700p:plain

とPを構成できるため、結果、

 
\displaystyle {
P=\cos { \theta  } B+\sin { \theta  } \left[ \hat { C } |B \right] \\ P=\cos { \theta  } B+\sqrt { 1-\cos ^{ 2 }{ \theta  }  } \left[ \hat { C } |B \right] \\ P=zB+\sqrt { 1-{ z }^{ 2 } } \left[ \hat { C } |B \right] 
}

これが論文の擬似コード、"Construct the corresponding point on the sphere." の部分になります。 以上で理論的な部分はすべてクリアになりました。

実装

以上をふまえて実装します。 三角形上の点が欲しかったので、この実装では平面の方程式と、光線の方程式の交点を追加で求めています。

比較

実際にパストレーシングに組み込んでみます。 ここでは、前回実装した、Spherical Rectangleと比較してみようと思います。MISを入れると差がわかりにくいため、MISなしのNext Event Estimation での比較になります。

ここでは2枚のポリゴンは等確率で選ばれています。

左がSpherical Triangle 、右がSpherical Rectangle になります。

わずかにSpherical Rectangleのほうが優れていることが確認できます。ただ、2つのポリゴンの選ばれ方を立体角に比例するように選ぶようにすれば、普通の一様乱数のケースでは少なくとも分散は等価ですし、例えばもう少し弱く、選択の重要度を、

 
\displaystyle {
w=L\frac { Area }{ d^{ 2 }\left( o,TriangleCenter \right)  } 
}

 L を完全拡散面の放射輝度、Areaは三角形の面積、 d\left( o,TriangleCenter \right) を三角形の中心からサンプル元の距離とします。

といった形に定義して、この値に比例するようにサンプリングするようにしてみたりすると、

左がSpherical Triangle 、右がSpherical Rectangle になります。

これだけでも2つの差はかなり少なくなったように思います。もちろんメニーライトやら、不均一な放射輝度を持つポリゴンの場合やらを考えると、このような方法でカバーできる範疇を簡単にはみ出してしまいますが・・・。

まとめ

先にSpherical Trianglesではなく、Spherical Rectanglesをやったのは、単純な話 James Arvo, "Stratified Sampling of Spherical Triangles" の内容がよくわからなかったです(汗)。俯瞰してみると、球面三角形の式をとてもうまく使っていて、最終的には結構シンプルな形に帰着させており、よく思いつくものだとため息がでます。しかし天才的なひらめきが必要かというとそうでもない気もしました。自分でも思いつけるくらいになれたらと夢をみつつ・・・。導出には結構様々な小さなテクニックが使われていたので、追いかけるだけでも得られるものはあるのではないでしょうか。三角形ポリゴンということで、自前のレンダラーに組み込むにも便利ではないかと思います。

参考文献

James Arvo, "Stratified Sampling of Spherical Triangles"

Carlos Ureña, Marcos Fajardo, Alan King , “An Area-Preserving Parametrization for Spherical Rectangles”

張海潮, "球面三角形的AAA 定理"

An Area-Preserving Parametrization for Spherical Rectangles を読む

An Area-Preserving Parametrization for Spherical Rectangles

Carlos Ureña, Marcos Fajardo, Alan King , “An Area-Preserving Parametrization for Spherical Rectangles” は、 レイトレ合宿6 に提出しました MoodyRender で組み込んだものの、あまりしっかりとしたまとめを書いていませんでしたので、こちらにまとめておこうとおもいます。 こちらの論文は、SolidAngle社が EGSR 2013で発表した論文となっております。この論文では、単位面積  \left[0\, 1 \right]^2 から、任意の長方形の領域を単位球面に射影してできた領域への写像を、面積を保存する形で提示します。文章で書くとややこしい印象になってしまいますが、イメージは以下のような感じです。

f:id:ushiostarfish:20180924165141p:plain

これは一度定義してしまえば、射影立体角内で、立体角一様なサンプルを生成したり、準乱数によるサンプルを生成したりも簡単にできます。このテクニックは特に直接照明の計算、いわゆるNext Event Estimationに便利です。

立体角でサンプリングしたくなる理由

Next Event Estimationを行う際、最も簡単なのは面積に一様なサンプルを生成することでしょう。しかしなぜわざわざややこしいことをして、立体角でサンプリングを生成するのでしょうか? 例えば以下のようなシチュエーションを考えてみましょう

f:id:ushiostarfish:20180919233606p:plain

これは結構横長な光源があり、そして手前側の半球の中心が現在光線追跡中の点で、ここの直接照明を考えるとしましょう。 面積に一様なサンプリングをした場合には赤い点のように生成されるでしょう。なんだか良さげですが、これを立体角に投射してみましょう。

f:id:ushiostarfish:20180919234011p:plain

...かなり偏っています。これが運良く、重点サンプリングのように機能するなら良いでしょうが、多くのシチュエーションではそんなことはないでしょう。これでは分散が増加してしまうのが目に見えています。なのでせめて立体角には一様なサンプリングをしたくなります。例えば以下のように。

f:id:ushiostarfish:20180919234504p:plain

そんなわけで、これをどのように達成しているのかを見ていきましょう。

写像

まず最初に面積を保存する写像とは何かを定義します。論文 3. Properties of map M では、以下のような式があります。

 
\displaystyle {
A\left( T \right) =\frac { 1 }{ \sigma \left( Q \right)  } \sigma \left( M\left( T \right)  \right) 
}

この式の意味を考えます。 ここではまず、単位面積の中の任意の領域 T を考えて、その面積を  A\left( T \right) とします。

f:id:ushiostarfish:20180919235500p:plain

そして、写像する立体角の全領域をQ として、その立体角を  \sigma \left( Q \right) とします。 f:id:ushiostarfish:20180919235750p:plain

そして T の写像 M \left( T \right) とすると、 f:id:ushiostarfish:20180919235950p:plain

全立体角あたりの、写像してきた立体角は、やはりTと一致する、これが面積を保存する写像というわけです。 f:id:ushiostarfish:20180920000128p:plain

座標系

次に座標系についてです。この論文では、もろもろ後の問題を単純にするために、長方形領域 P を基準にローカル座標系を定義します。 これは論文のFigure 1にあたります。

f:id:ushiostarfish:20180920232954p:plain ※ 元論文 Figure 1

原点oを基準に、長方形 P 上にサンプルを生成するのをイメージします。 この座標系の重要なところは 、

  •  e_x の向きが x軸
  •  e_y の向きが y軸
  • 長方形 P の原点o側の法線がz軸

となるような座標系であるというところです。 それぞれの軸の単位ベクトルは、長方形Pと、原点oが決まれば自動的に決まってくることになります。 ここでローカル座標系での  x_0, x_1, y_0, y_1 そして  z_0もやはり一意に決まりますので、ここで整理します。

sのローカル座標は、

 
\displaystyle {
s=\left( x_{ 0 },y_{ 0 },z_{ 0 } \right) 
}

であるので、座標軸に単に投射すれば良いため、  x_0, y_0, z_0 は、原点o から 長方形Pへのベクトルを d 、 x,y, z を基底ベクトルとすると、

 
\displaystyle {
x_{ 0 }=d\cdot x\\ y_{ 0 }=d\cdot y\\ z_{ 0 }=d\cdot z
}

そしてそこから長方形のサイズ分ずらせば良いので  x_1, y_1 は、

 
\displaystyle {
x_{ 1 }=x_{ 0 }+\left\| e_{ x } \right\| \\ y_{ 1 }=y_{ 0 }+\left\| e_{ y } \right\| 
}

となります。 長方形の4点については重要な頂点になりますので、名前を  v_{00}, v_{10}, v_{01}, v_{11} と名前をつけておくことにします。

f:id:ushiostarfish:20180921000625p:plain

球面四角形の面積

ところで長方形の領域が射影された領域の立体角すなわち面積はどのように求めることができるでしょうか? 一般に単位球面三角形について、3つの角の角度から求めることができることが知られています。(証明は今回すっ飛ばします)

f:id:ushiostarfish:20180922115249p:plain

 
\displaystyle {
A\left( R \right) =\gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 }-\pi 
}

平面上の通常の三角形の内角の和はもちろん180° ( =\pi) でありますが、球面三角形の内角の和は膨らみがある分 \pi 以上になることは想像に難くないでしょう。 一方でこの式は、球面三角形の内角の和が  \pi のときどうなるかというと、

 
\displaystyle {
A\left( R \right) =\pi -\pi =0
}

面積が0になってしまいました。どういうことかといえば、球面三角形の内角の和が  \pi に近づいていくということは、それだけ平面上の三角形に近づいていくことであり、結局それは以下の図のようにどんどん小さくなっていってしまうわけです。

f:id:ushiostarfish:20180922115959p:plain

では逆に球面三角形の最大値はどんな値になるでしょうか?これを考えるために、上の図と"反対側"の球面三角形を考えてみます。

f:id:ushiostarfish:20180922121025p:plain

この場合、それぞれの外側の角度は、 2\pi - \gamma となるわけなので、面積は、

 
\displaystyle {
A\left( R' \right) =\left( 2\pi -\gamma _{ 0 } \right) +\left( 2\pi -\gamma _{ 1 } \right) +\left( 2\pi -\gamma _{ 2 } \right) -\pi \\ =6\pi -\left( \gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 } \right) -\pi \\ =5\pi -\left( \gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 } \right) 
}

となり、さらに  \gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 } = \pi の場合は、

f:id:ushiostarfish:20180922121633p:plain

のように小さくなっていき、式としても、

 
\displaystyle {
A\left( R' \right) =5\pi -\pi =4\pi 
}

なるほど、単位球の表面積と一致しています。どうやらこれが最大値のようです。だいぶイメージも掴めてきました。 ところで、球面四角形の場合はどうかといえば、二つの球面三角形に分割して考えることができます。

f:id:ushiostarfish:20180922122550p:plain

したがって、

 
\displaystyle {
A\left( R_{ 1 } \right) +A\left( R_{ 2 } \right) \\ =\gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 }-\pi +\gamma '_{ 0 }+\gamma '_{ 1 }+\gamma '_{ 2 }-\pi \\ =\gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 }+\gamma '_{ 0 }+\gamma '_{ 1 }+\gamma '_{ 2 }-2\pi 
}

この時2つの角については、一つにまとめることができるので、

 
\displaystyle {
=\gamma _{ 0 }+\gamma '_{ 0 }+\left( \gamma _{ 1 }+\gamma '_{ 1 } \right) +\left( \gamma _{ 2 }+\gamma '_{ 2 } \right) -2\pi 
}

と4つの角の角度で考えることができます。 この角度で \gamma を再定義すると、

f:id:ushiostarfish:20180922112659p:plain

 
\displaystyle {
A\left( Q \right) =\gamma _{ 0 }+\gamma _{ 1 }+\gamma _{ 2 }+\gamma _{ 3 }-2\pi 
}

なんとシンプルでしょうか。これで立体角について簡単に考えていくことができそうです。 余談なのですが、ここまでの話は、自然に任意の球面N角形に拡張することができ、それはもちろん、

 
\displaystyle {
A_{ n }=\sum _{ i=0 }^{ N-1 }{ \gamma _{ i } } -\left( N-2 \right) \pi 
}

のようになります。 この拡張、以下のようにどこか適当な1点は実は球面N角形の1頂点だったと考えてみても、ちゃんと成り立ちます。

f:id:ushiostarfish:20180922125021p:plain

式の左の項を見ると、 N が1増えると面積は  \pi 増えそうですが、それは右側の項でちゃんと相殺されますね。 なかなか面白いものです。

 \gamma_i を求める

 \gamma_i と立体角には密接な関係性があることがわかりました。そこで  \gamma_i をそれぞれ求めたいわけです。座標系のセクションですでに定義した、原点o と、 v_{00}, v_{10}, v_{01}, v_{11} の4つの頂点で作られる平面の法線から考えます。

f:id:ushiostarfish:20180922132732p:plain

f:id:ushiostarfish:20180922133157p:plain

ベクトルの外積からそれぞれ、

 
\displaystyle {
n_{ 0 }=\frac { v_{ 00 }\times v_{ 10 } }{ \left\| v_{ 00 }\times v_{ 10 } \right\|  } \\ n_{ 1 }=\frac { v_{ 10 }\times v_{ 11 } }{ \left\| v_{ 10 }\times v_{ 11 } \right\|  } \\ n_{ 2 }=\frac { v_{ 11 }\times v_{ 01 } }{ \left\| v_{ 11 }\times v_{ 01 } \right\|  } \\ n_{ 3 }=\frac { v_{ 01 }\times v_{ 00 } }{ \left\| v_{ 01 }\times v_{ 00 } \right\|  } 
}

のように求めることができると、図からわかります。 法線がわかってしまえば、 \gamma_i の値は、以下の図をみながら、

f:id:ushiostarfish:20180922173231p:plain

 
\displaystyle {
\gamma _{ 0 }=\arccos { \left( -{ n }_{ { 0 } }\cdot { n }_{ { 1 } } \right)  } \\ \gamma _{ 1 }=\arccos { \left( -{ n }_{ { 1 } }\cdot { n }_{ { 2 } } \right)  } \\ \gamma _{ 2 }=\arccos { \left( -{ n }_{ { 2 } }\cdot { n }_{ { 3 } } \right)  } \\ \gamma _{ 3 }=\arccos { \left( -{ n }_{ { 3 } }\cdot { n }_{ { 0 } } \right)  } 
}

となります。

ここでついでにひとつ重要な話題があって、今回はローカル座標系としてうまいこと軸が長方形に整列した座標系を定義しました。

f:id:ushiostarfish:20180922134212p:plain

これを踏まえて面について後ろから観察すると、

f:id:ushiostarfish:20180922135806p:plain

と、このようになっています。このことから、

  •  n_1, n_3 は Y軸にいつも直交しており、ローカル座標系でのy 成分は常に0
  •  n_0, n_2 は X軸にいつも直交しており、ローカル座標系でのx 成分は常に0

であることがわかります。これは実装の最適化にも寄与します。

このことから、それぞれの法線はそれぞれ1つの角度の値で表現できることもわかります。

f:id:ushiostarfish:20180922143446p:plain

 
\displaystyle {
{ n }_{ { 3 } }=\cos  \varphi _{ { 0 } }{ z }+\sin  \varphi _{ { 0 } }{ x }\\ { n }_{ { 1 } }=\cos  \varphi _{ { 1 } }{ z }+\sin  \varphi _{ { 1 } }{ x }
}

f:id:ushiostarfish:20180922142752p:plain

 
\displaystyle {
{ n }_{ { 0 } }=\cos  \theta _{ { 0 } }{ z }+\sin  \theta _{ { 0 } }{ y }\\ { n }_{ { 2 } }{ =\cos  \theta _{ { 1 } }{ z }+\sin  \theta _{ { 1 } }{ y } }
}

※元論文では、 n_3 だけなぜかマイナスで定義されています。理由がちょっと定かではないのですが、この部分はさほど重要な点ではないように思いましたので、ここではシンプルにすべて+での定義を使いました。

u 方向の累積分布関数

いよいよコア部分に入っていきます。単位面積におけるx座標を u 、y座標をvとパラメータ化することを考えます。

f:id:ushiostarfish:20180922161555p:plain

ここでは u について考えます。 uは当然 0 ~ 1 の間を行き来することになります。そのようなシチュエーションを考えた時、

 u = 0 のとき、 A\left( Q_{ u } \right) =0

そして、

 u = 1 のとき、 A\left( Q_{ u } \right) =A\left( Q \right)

もっと進んで、任意のuで、

 
\displaystyle {
A\left( Q_{ u } \right) =A\left( Q \right) u
}

が成り立つような  Q_{ u } を考えます。ここではこれは ローカル座標系でのxに対応させて考えると、

f:id:ushiostarfish:20180922162545p:plain

とこのような領域を定義したいです。 uについてアニメーションさせると、以下のようになるでしょう。

f:id:ushiostarfish:20180922163018g:plain

ただ多くのシチュエーションでは上の図のように綺麗な形をしておらず、例えば以下のように歪んでいるということは、一応確認しておきたいところです。例えば以下のように。 f:id:ushiostarfish:20180922163501p:plain

なのでuの値が線形に変化するとしても、当然面積が線形にはならないということです。

では  Q_u の面積について考えてみます。元論文 Figure 2を見てみましょう。 今までの議論でイメージができていれば、とても立体的な図として見えるはずです。

f:id:ushiostarfish:20180922165047p:plain ※元論文 Figure 2

この中で、多くのものは定義済みですが、以下青で丸をつけた部分については未定義でした。そして uに依存して変化する部分でもあります。

f:id:ushiostarfish:20180922165345p:plain

 Q_u を計算したいということから考えると、 \gamma '_{ 0 },\gamma '_{ 1 } というのはやはり必要な値であり、それは  m_u から求めます。  m_u とは何か?といえば、uの値に応じて、 -n_3 から  n_1 へ回転する法線であり、境界線の法線でもあります。 上から見るとわかりやすいです。

f:id:ushiostarfish:20180922170435p:plain

以上より、 Q_u の面積は以下のように書き下せることがわかります。

 
\displaystyle {
A\left( Q_{ u } \right) =\gamma '_{ 0 }+\gamma '_{ 1 }+\gamma _{ 2 }+\gamma _{ 3 }-2\pi \\ A\left( Q_{ u } \right) =\arccos { \left( -{ n }_{ { 0 } }\cdot { m }_{ { u } } \right)  } +\arccos { \left( -{ n }_{ { 2 } }\cdot { m }_{ { u } } \right)  } +\gamma _{ 2 }+\gamma _{ 3 }-2\pi 
}

 n_1, n_3 の角度パラメータ1つによる表現はすでに上の方で触れました。

 
\displaystyle {
{ n }_{ { 0 } }=\cos  \theta _{ { 0 } }{ z }+\sin  \theta _{ { 0 } }{ y }\\ { n }_{ { 2 } }{ =\cos  \theta _{ { 1 } }{ z }+\sin  \theta _{ { 1 } }{ y } }
}

これと同様に、 m_u についてもやはり同様の表記が可能で、

 
\displaystyle {
m_{ { u } }=\cos  \varphi _{ { u } }{ z }+\sin  \varphi _{ { u } }{ x }
}

と、このようになります。ここで、内積  { n }_{ { 0 } }\cdot { m }_{ { u } } と、{ n }_{ { 2 } }\cdot { m }_{ { u } }について考えてみます。  n_0, n_1 は今回のローカル座標系においては、

 
\displaystyle {
{ n }_{ { 0 } }=\left( 0,a,b \right) \\ { n }_{ { 2 } }=\left( 0,c,d \right) \\ 
}

と、このような値になるでしょう。 同様に、 m_u もローカル座標系においては、

 
\displaystyle {
{ m }_{ { u } }=\left( g,0,h \right) 
}

となるわけです。つまり結局のところ、内積  { n }_{ { 0 } }\cdot { m }_{ { u } } と、{ n }_{ { 2 } }\cdot { m }_{ { u } }についてはローカル座標系でz成分のみの掛け算により、内積が得られます。 したがって、 A \left( Q_u \right) は、

 
\displaystyle {
A\left( Q_{ u } \right) =\arccos { \left( -\cos  \theta _{ { 0 } }\cos  \varphi _{ { u } } \right)  } +\arccos { \left( -\cos  \theta _{ { 1 } }\cos  \varphi _{ { u } } \right)  } +\gamma _{ 2 }+\gamma _{ 3 }-2\pi 
}

と記述することができ、元論文では、

 
\displaystyle {
b_{ 0 }=\cos  \theta _{ { 0 } }\\ b_{ 1 }=\cos  \theta _{ { 1 } }\\ c_{ u }=\cos  \varphi _{ { u } }
}

とおいており、

 
\displaystyle {
A\left( Q_{ u } \right) =\arccos { \left( -b_{ 0 }c_{ u } \right)  } +\arccos { \left( -b_{ 1 }c_{ u } \right)  } +\gamma _{ 2 }+\gamma _{ 3 }-2\pi 
}

と短く記述でき、さらに

 
\displaystyle {
A\left( Q_{ u } \right) =A\left( Q \right) u
}

から、

 
\displaystyle {

A\left( Q \right) u=\arccos { \left( -b_{ 0 }c_{ u } \right)  } +\arccos { \left( -b_{ 1 }c_{ u } \right)  } +\gamma _{ 2 }+\gamma _{ 3 }-2\pi 
}

これが元論文の式 (8) になります。これはまるで逆関数法における、累積分布関数のようにも感じられるでしょう。 ちなみに、

  •  b_0 は、 n_0 のローカル座標系のz成分
  •  b_1 は、 n_2 のローカル座標系のz成分

であるとも整理できます。

次に  c_u は、まだこの段階ではどういった表現で得られるかはわかりませんが、仮に都合良く  c_u が求まりさえすれば、射影する前の長方形の座標(ローカル座標系)の x成分  x_u を得ることができます。

 m_u は、上から見ると、次の図のようになります。 f:id:ushiostarfish:20180922184543p:plain

したがって、これを90度回転させると、原点から境界線へと向かうベクトルが手に入ります。

f:id:ushiostarfish:20180922185920p:plain

ここで、長方形までの距離  z_0 と、 求めたい x_u は、次の図のような配置になっており、

f:id:ushiostarfish:20180922190628p:plain

したがって、

 
\displaystyle {
\begin{align} x_{ u } & =z_{ 0 }\tan { \left( \varphi _{ { u } }+\frac { \pi  }{ 2 }  \right)  }  \\  & =-z_{ 0 }\frac { 1 }{ \tan { \left( \varphi _{ { u } } \right)  }  }  \\  & =-z_{ 0 }\frac { \cos { \left( \varphi _{ { u } } \right)  }  }{ \sin { \left( \varphi _{ { u } } \right)  }  }  \\  & =-z_{ 0 }\frac { \cos { \left( \varphi _{ { u } } \right)  }  }{ \sqrt { 1-\cos ^{ 2 }{ \left( \varphi _{ { u } } \right)  }  }  }  \\  & =-z_{ 0 }\frac { c_{ u } }{ \sqrt { 1-{ c_{ u } }^{ 2 } }  }  \end{align}
}

これで  x_u を得ることができました。 ただ、

 
\displaystyle {
\sin { \left( \varphi _{ { u } } \right)  } =\sqrt { 1-\cos ^{ 2 }{ \left( \varphi _{ { u } } \right)  }  } 
}

については、 -\pi \lt \varphi _{ { u } }\lt0 や、 \pi \lt\varphi _{ { u } }\lt2\pi  では符号が逆になってしまうので、注意したいところではあります。

f:id:ushiostarfish:20180922192857p:plain

とはいえ、 \varphi _{ { u } }+\frac { \pi  }{ 2 } は、明らかに、

 
\displaystyle {
\frac { \pi  }{ 2 } <\varphi _{ { u } }+\frac { \pi  }{ 2 } <\frac { \pi  }{ 2 } +\pi \\ 0<\varphi _{ { u } }<\pi 
}

なので、心配する必要はありませんでした。

というわけで、最終的には都合の良い  c_u を見つけることに焦点があたります。 ※  c_u と書いていますが、実際には  c_u \left(u \right) とuの関数として記述したほうが直観的かもしれません。

 
\displaystyle {
A\left( Q \right) u=\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  } +\arccos { \left( -b_{ 1 }c_{ u }\left( u \right)  \right)  } +\gamma _{ 2 }+\gamma _{ 3 }-2\pi 
}

 c_u \left(u \right) を求める

こちらは元論文の Appendices, "A. Expressing cu as a function of u" の部分になります。  A\left( Q \right), b_0, b_1, \gamma_2, \gamma_3 は定数であり、 c_u \left(u \right) だけが未知のため、これを求めることが目標となります。 論文の説明が十分わかりやすいので、ここはかなりストレートに追いかけます。

まず定数をまとめて表記を簡単にするため、関数  \phi \left( x \right) を導入します。

 
\displaystyle {
\phi \left( x \right) =xA\left( Q \right) -\gamma _{ 2 }-\gamma _{ 3 }+2\pi 
}

これにより、

 
\displaystyle {
A\left( Q \right) u=\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  } +\arccos { \left( -b_{ 1 }c_{ u }\left( u \right)  \right)  } +\gamma _{ 2 }+\gamma _{ 3 }-2\pi \\ A\left( Q \right) u-\gamma _{ 2 }-\gamma _{ 3 }+2\pi -\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  } =\arccos { \left( -b_{ 1 }c_{ u }\left( u \right)  \right)  } \\ \phi \left( u \right) -\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  } =\arccos { \left( -b_{ 1 }c_{ u }\left( u \right)  \right)  } 
}

 b_0, c_u はともにコサインであり、範囲は -1 ~ 1をはみ出ることはないので、

f:id:ushiostarfish:20180923114946p:plain

 
\displaystyle {
\cos { \left( \phi \left( u \right) -\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  } =\cos { \left( \arccos { \left( -b_{ 1 }c_{ u }\left( u \right)  \right)  }  \right)  } \\ \cos { \left( \phi \left( u \right) -\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  } =-b_{ 1 }c_{ u }\left( u \right) 
}

 \cos の中の引き算を外に引きずり出すため、ここで加法定理、

 
\displaystyle {
\cos { \left( \alpha -\beta  \right)  } =\cos { \alpha  } \cos { \beta  } +\sin { \alpha  } \sin { \beta  } 
}

で、

 
\displaystyle {
\begin{align} \cos { \left( \phi \left( u \right) -\arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  }  & =-b_{ 1 }c_{ u }\left( u \right)  \\ \cos { \left( \phi \left( u \right)  \right)  } \cos { \left( \arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  } +\sin { \left( \phi \left( u \right)  \right)  } \sin { \left( \arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  }  & =-b_{ 1 }c_{ u }\left( u \right)  \\ \cos { \left( \phi \left( u \right)  \right)  } \left( -b_{ 0 }c_{ u }\left( u \right)  \right) +\sin { \left( \phi \left( u \right)  \right)  } \sin { \left( \arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  }  & =-b_{ 1 }c_{ u }\left( u \right)  \\ -\cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) +\sin { \left( \phi \left( u \right)  \right)  } \sin { \left( \arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  }  & =-b_{ 1 }c_{ u }\left( u \right)  \end{align}
}

すこし複雑化してしまいましたが、ここで、 \sin\left(\arccos\left(x\right)\right) は、

f:id:ushiostarfish:20180923115955p:plain

コサインからサインを求める形になるため、単位円の上半分を表しており、

 
\displaystyle {
{ x }^{ 2 }+{ y }^{ 2 }=1
}

から、

 
\displaystyle {
\sin  \left( \arccos  \left( x \right)  \right) =\sqrt { 1-x^{ 2 } } 
}

f:id:ushiostarfish:20180923120709p:plain

であり、とすると、

 
\displaystyle {
\begin{align} -\cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) +\sin { \left( \phi \left( u \right)  \right)  } \sin { \left( \arccos { \left( -b_{ 0 }c_{ u }\left( u \right)  \right)  }  \right)  }  & =-b_{ 1 }c_{ u }\left( u \right)  \\ -\cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) +\sin { \left( \phi \left( u \right)  \right)  } \sqrt { 1-\left( -b_{ 0 }c_{ u }\left( u \right)  \right) ^{ 2 } }  & =-b_{ 1 }c_{ u }\left( u \right)  \\ -\cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) +\sin { \left( \phi \left( u \right)  \right)  } \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & =-b_{ 1 }c_{ u }\left( u \right)  \end{align}
}

これを少し変形して、

 
\displaystyle {
\begin{align} -\cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) +\sin { \left( \phi \left( u \right)  \right)  } \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & =-b_{ 1 }c_{ u }\left( u \right)  \\ \sin { \left( \phi \left( u \right)  \right)  } \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & =\cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) -b_{ 1 }c_{ u }\left( u \right)  \\ \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & =\frac { \cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }c_{ u }\left( u \right) -b_{ 1 }c_{ u }\left( u \right)  }{ \sin { \left( \phi \left( u \right)  \right)  }  }  \\ \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & =c_{ u }\left( u \right) \frac { \cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }-b_{ 1 } }{ \sin { \left( \phi \left( u \right)  \right)  }  }  \end{align}
}

こうすると、かなりの部分が  c_u \left(u \right) から分離でき、

 
\displaystyle {
f\left( u \right) =\frac { \cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }-b_{ 1 } }{ \sin { \left( \phi \left( u \right)  \right)  }  } 
}

とおいてしまうと、

 
\displaystyle {
\sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } } =c_{ u }\left( u \right) f\left( u \right) 
}

とかなり簡単になります。 あとは  c_u \left(u \right) について解きます。ここも逆関数法みたいな香りがしますね。

 
\displaystyle {
\begin{align} \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & =c_{ u }\left( u \right) f\left( u \right)  \\ 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } & =\left( c_{ u }\left( u \right)  \right) ^{ 2 }{ \left( f\left( u \right)  \right)  }^{ 2 } \\ \frac { 1 }{ \left( c_{ u }\left( u \right)  \right) ^{ 2 } } -b_{ 0 }^{ 2 } & ={ \left( f\left( u \right)  \right)  }^{ 2 } \\ \frac { 1 }{ \left( c_{ u }\left( u \right)  \right) ^{ 2 } }  & ={ \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } \\ \left( c_{ u }\left( u \right)  \right) ^{ 2 } & =\frac { 1 }{ { \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } }  \\ c_{ u }\left( u \right)  & =\pm \frac { 1 }{ \sqrt { { \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } }  }  \end{align}
}

おっと、符号が行方不明になってしまいました。 ただ、

 
\displaystyle {
\sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } } =c_{ u }\left( u \right) f\left( u \right) 
}

をよく見ると、

 
\displaystyle {
0\le \sqrt { 1-b_{ 0 }^{ 2 }\left( c_{ u }\left( u \right)  \right) ^{ 2 } } 
}

なのは明らかです。したがって、 c_{ u } \left( u \right)   f\left( u \right) は同じ符号でなければなりません。

加えて、

 
\displaystyle {
0\le \frac { 1 }{ \sqrt { { \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } }  } 
}

であるため、以上をふまえると、 c_{ u } \left( u \right)  は、

 
\displaystyle {
c_{ u }\left( u \right) =\frac { sign\left( f\left( u \right)  \right)  }{ \sqrt { { \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } }  } 
}

であるべきでしょう。 長くなってしまったため、以上をまとめると、

 
\displaystyle {
\begin{align} \phi \left( x \right)  & =xA\left( Q \right) -\gamma _{ 2 }-\gamma _{ 3 }+2\pi  \\ f\left( u \right)  & =\frac { \cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }-b_{ 1 } }{ \sin { \left( \phi \left( u \right)  \right)  }  }  \\ c_{ u }\left( u \right)  & =\frac { sign\left( f\left( u \right)  \right)  }{ \sqrt { { \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } }  }  \end{align}
}

となります。

 \sin { \left( \phi \left( u \right)  \right)  } の除算について

上記の式変形の中で、 \sin { \left( \phi \left( u \right)  \right)  } で除算する部分があります。元論文中では、 \sin { \left( \phi \left( u \right)  \right)  } は常に 0より小さいことが書かれています。しかしこれは事実ではありません。というのも、これの根拠にしているのが、 \gamma '_0, \gamma '_1 がそれぞれ  \frac { \pi  }{ 2 }  よりも大きいこと、と記述されておりますが、これが真実ではありません。反例として、例えば以下のようなシチュエーションがあります。

f:id:ushiostarfish:20180923131654p:plain

これは明らかに  \gamma _0 \lt \frac { \pi  }{ 2 }  , \gamma _1 \lt \frac { \pi  }{ 2 } です。ということは自然と \gamma '_0, \gamma '_1 \gamma' _0 \lt \frac { \pi  }{ 2 }  , \gamma' _1 \lt \frac { \pi  }{ 2 } となり得るということになります。

ただ、論文中で \sin { \left( \phi \left( u \right)  \right)  } \lt 0 を示したかった理由は単にゼロ除算を避けるためにあります。 では実際にゼロ除算は実装上問題になるのでしょうか?

 
\displaystyle {
f\left( u \right) =\frac { \cos { \left( \phi \left( u \right)  \right)  } b_{ 0 }-b_{ 1 } }{ \sin { \left( \phi \left( u \right)  \right)  }  } 
}

であるため、まず

 
\displaystyle {
f\left( u \right) =+inf
}

になります。次に、

 
\displaystyle {
c_{ u }\left( u \right) =\frac { sign\left( f\left( u \right)  \right)  }{ \sqrt { { \left( f\left( u \right)  \right)  }^{ 2 }+b_{ 0 }^{ 2 } }  } 
}

なので、

 
\displaystyle {
c_{ u }\left( u \right) = 0
}

になります。最後に、

 
\displaystyle {
x_{ u }=-z_{ 0 }\frac { c_{ u } }{ \sqrt { 1-{ c_{ u } }^{ 2 } }  } 
}

より、

 
\displaystyle {
x_{ u }=0
}

そう、これは結局単に x = 0 の地点であり、図示すると、

f:id:ushiostarfish:20180923133526p:plain

となり、異常な特異点というわけではないことがわかります。 ただ、ゼロ除算に近づいていく過程で0付近は数値的に誤差が生じやすいかもしれません。

この点については、知人の紹介で、Carlos Ureña氏とAlan King氏にメールで確認させて頂いたところ、確かにerrorであると確認していただけました。

なにが解決したか?

 u x_u へのマッピングが以上で明らかになりました。これで何が実現できたのかを再確認します。  u は連続的であり、イメージがややしにくいため、一旦均等に極めて細かく面積を分割することを考えます。 例えば極めて細かく、均等に4つに分割しますと、

f:id:ushiostarfish:20180924185614p:plain

この時立体角上に写像した a, b, c, dの面積(=立体角) も均等となります。

 
\displaystyle {
a'=b'=c'=d'\Leftrightarrow a=b=c=d
}

このようなマッピングが成立したことになります。

ということは、残る  v 方向については、以下のように経度方向に細かく分割した中で残りの縦方向の写像を考えれば良いことになります。 f:id:ushiostarfish:20180923152144p:plain

 vマッピングどうするか?

ではここで、θに対して  vマッピングするのは正しいでしょうか?

f:id:ushiostarfish:20180923153341p:plain

これは正しくありません。 なぜならθに均一に分割しても、それぞれの面積(=立体角)は一様ではないためです。 これは、

 
\displaystyle {
\int _{ \Omega  }^{  }{ d\omega  } =\int _{ 0 }^{ 2\pi  }{ \int _{ 0 }^{ \frac { \pi  }{ 2 }  }{ \sin { \theta  } d\theta d\phi  }  } 
}

のような立体角積分でも  \sin \theta として出現することからも明らかです。 これをなんとかするために、球とそれをすっぽりと覆う円柱の側面積の面積の関係性を使います。

f:id:ushiostarfish:20180923154604p:plain

ここでいう面積の関係性は、 横方向にスライスした部分の面積は、水平に切る限りどこでも一致する、というものです。(例えば赤い部分) ※これについて、単位球面に対してランダムに点を生成するでは、より詳細な説明をしています。

Y軸に対して面積は線形であり、そしてこの関係性は、経度方向に薄くスライスしても、同様の性質が保持されることは明らかです。さらに言えば、今回のケースは上下に切られている状況ではありますが、これも関係がありません。 ということは結局のところY軸に対して  v を線形にマッピングするだけで良いということになります。

 v の詳細

マッピングのアイディアがわかってしまえば、あとは幾何学的な部分を整理するだけになります。 元論文のFigure 3 を見てみましょう。

f:id:ushiostarfish:20180923160526p:plain ※元論文のFigure 3

d は、xz平面上のp までの距離であり、

 
\displaystyle {
d=\sqrt { { x_{ u } }^{ 2 }+{ z_{ 0 } }^{ 2 } } 
}

で求められます。

 h_0 h_1 はどんな値かというと、oから  a_0 および a_1 に向かうベクトルを正規化したあとのy成分で求めることができます。

 
\displaystyle {
h_{ 0 }=\frac { y_{ 0 } }{ \left\| a_{ 0 } \right\|  } =\frac { y_{ 0 } }{ \sqrt { { d }^{ 2 }+{ y_{ 0 } }^{ 2 } }  } \\ h_{ 1 }=\frac { y_{ 1 } }{ \left\| a_{ 1 } \right\|  } =\frac { y_{ 1 } }{ \sqrt { { d }^{ 2 }+{ y_{ 1 } }^{ 2 } }  } 
}

ここの間を線形にマッピングすれば良いため、

 
\displaystyle {
h_{ v }=h_{ 0 }+v\left( h_{ 1 }-h_{ 0 } \right) 
}

 h_{ v } が 確定します。 ここから、 c_u のときとほぼ同様に、

f:id:ushiostarfish:20180923163328p:plain

 
\displaystyle {
y_{ v }=d\tan { \theta  } =d\frac { \sin { \theta  }  }{ \cos { \theta  }  } =d\frac { \sin { \theta  }  }{ \sqrt { 1-\sin ^{ 2 }{ \theta  }  }  } \\ y_{ v }=d\frac { h_{ v } }{ \sqrt { 1-{ h_{ v } }^{ 2 } }  } 
}

 y_{ v } を求めることができました。

グローバル座標へ

 x_u, y_v はローカル座標系での値ですので、最後にグローバル座標へ戻して終了です。

 
\displaystyle {
o+x_{ u }x+y_{ v }y+z_{ 0 }z
}

実装

以上を踏まえての実装です。

まずはナイーヴなほうです。

次に最適化したバージョンです。まだできることが残っていたらすみません。。

PathTracing + Next Event Estimation 比較

ナイーヴなPathTracing + Next Event Estimation の実装で、 面積に一様な場合 ( 64 spp ) と、立体角に一様な場合 ( 64 spp ) の比較です。

PathTracing + Next Event Estimation + MIS(パワーヒューリスティック) 比較

BRDFのサンプリングと光源のサンプリングの2戦略でのMISをした場合、 面積に一様な場合 ( 32 spp ) と、立体角に一様な場合 ( 32 spp ) の比較です。

MISなしのに比べると、差は少ないですが、確かにこの場合でも分散が低くなっています。特に光源付近では、MISウエイトによって分散の高いサンプルを十分に潰しきれていないことがわかります。

サンプリング生成コストが少し増加するので、同じsppで比較するのは完全にはフェアではありません。しかし単純なシーンならともかく、複雑なシーンではサンプリング生成コストの増加は相対的に影響度は少なく、この分散の低下のメリットが遥かに大きいと思います。 論文では、極めて最適化された実装で、プロダクションのシーンで、数パーセントのパフォーマンスの低下が見られたが、本手法のノイズ低減効果を考えると十分回収できるものだったとありました。

サンプリング手法と組み合わせる

また、今回の手法では、他のサンプリング手法と組み合わせるのが容易です。 例えばBlueNoiseのようなサンプリングからマッピングすると以下のような結果が得られます。

f:id:ushiostarfish:20180929161549p:plain

f:id:ushiostarfish:20180929161557p:plain

こちらのサンプルデモは以下にあります。

github.com

まとめ

この手法は、法線や頂点の数がそこそこ多く出てくる上、3Dなのでイメージするがやや難しいですが、実際にプロットしつつぐるぐる3Dビューを回して確認さえできれば、あとはそれほど高度な数学も登場してこないので、それほど難易度も高くなく、実装もシンプルで最適化もすれば十分効率的です。座標系のとりかたがうまいことがとても良く効いているように感じます。また分散低減は効果もわかりやすく、まだやったことがない方は試してみる価値は十分にあるのではないでしょうか。ただいったん三角形でいいや、という方は先行研究である、James Arvo, "Stratified sampling of spherical triangles" のほうを先に試したほうが良いかもしれません。ただ長方形の場合にそれを三角形二枚にしてサンプリングするのは、本手法と等価ではなく、論文によるとわずかに本手法のほうが分散が少ないようですね。

サンプリング対象が長方形限定であるのは、純粋にリミテーションであり、今後拡張される余地もあるでしょう。(もちろん三角形ポリゴンのバージョンはすでにありますが)また、cos項まで考慮したり、放射輝度が均一でない光源や、BSDFなども同時に考慮に入れれたら、さらにすごいかもしれません。

参考文献

Carlos Ureña, Marcos Fajardo, Alan King , “An Area-Preserving Parametrization for Spherical Rectangles”