トップ 差分 一覧 ソース 検索 ヘルプ PDF RSS ログイン

独り言日記(2008/04)

独り言日記

続・Shadeと比較してみる(2008/04/30)

今月最後の更新。今作っているレンダラとShade10での結果を質と時間にて比べてみました。Pentium4 2.8GHz/Mem512MBのWinXPマシンにて。

Shade10パストレーシングにてイラディアンスキャッシュOff、レンダリング時の画質50、視線追跡レベル20(透明体があるため大きくしました)。レンダリング時間165秒。ただし、hdrのガンマをあわせるために、Shadeの色補正にてガンマ2.2にしています。

↓独自レンダラでは、50samples/pixel、追跡レベル20でレンダリング。レンダリング時間は64秒。

↓これを300samples/pixelにした場合は以下な感じ。レンダリング時間は268秒。

空気感というか、リアルな感じが伝わってるかな。Shadeでいう光沢(スペキュラ)は、独自レンダラではわずかな反射としています(Schlickのglossyに1/20倍して割り当て)。

しかし、Shadeの場合はシーンによってはかなり時間がかかりますね。この場合はダントツShadeのほうが遅くなってます。実はちょっとGI的には嫌がらせしてまして、お分かりのとおりhdrだけの光源ですが、左側と後ろは壁で覆ってます。後、右端のほうは中段に板をおいて光が届きにくいようにしました。独自レンダラに間違いがなければ、Shadeは途中で間接照明のためのパストレ(モンテカルロ)を打ち切っている?光が届いていない場所もありそうです。

共有ライブラリ(dll)でのクラスの隠蔽化(2008/04/30)

内部処理を見せないようにするために、プログラムではコア部分を分離する処理を共有ライブラリ(dll)にて実現します。

ヘッダにもprivate関数・変数などを定義していたため、このあたりの分離をどうしようかと調べてみたのですが、なかなかいいアイデアがいろんなサイトに記載してました。

クラスを使う場合の処理は継承を使って、virtualにて宣言だけをヘッダに記載する(それを公開ヘッダとする)。クラスの作成のみDLLで行ってクラス自身は、その公開ヘッダで宣言した関数を使って生成(new)する。

例として、「test.dll」という共有ライブラリを作成するとする(foo.h/naibu.h/naibu.cppを使い、プリプロセッサに「E_EXPORTS」を指定)。

で、作成された「test.dll」と「test.lib」を使って、「main.cpp/foo.h」を使って「mainApp.exe」を生成する(プリプロセッサ指定はなし)。

以下のような感じ。

<< test.dll / testApp.exe の両方で使用 >>

[ foo.h (公開ヘッダ) ]

#ifdef E_EXPORTS
#define E_API __declspec(dllexport) 
#else
#define E_API __declspec(dllimport) 
#endif

class CFoo
{
public:
    explicit CFoo() { }
    virtual ~CFoo() { }

    virtual void SetData(const int i) = 0;
    virtual int GetData() = 0;
};

/**
 * 外部関数。クラスCFooをnewする。
 */
E_API CFoo* __stdcall CreateFoo();
<< test.dll で使用 >>

[ naibu.h(非公開部) ]

class CNaibu : public CFoo
{
private:
    int m_dat1;

public:
    CNaibu() { }
    virtual ~CNaibu() { }

    virtual void SetData(const int i) {
        m_dat1 = i;
    }

    virtual int GetData() { return m_dat1; }
};

[ naibu.cpp(非公開部) ]

#include "naibu.h"

/**
 * 外部関数。クラスCFooをnewする。
 */
E_API CFoo* __stdcall CreateFoo()
{
    return new CNaibu();
}
<< testApp.exe で使用 >>

[ main.cpp ]

#include "foo.h"

int main(int argc, char *argv[])
{
    CFoo *pFoo;

    // 作成した外部クラスを生成
    pFoo = CreateFoo();

    // 関数にアクセス
    pFoo->SetData(10);

    // 外部クラスを破棄
    delete pFoo;

    return 0;
}

これで、このdllを使う側から見ると、関数が入り口になってそれ以外の情報は隠蔽されます。

レンダラみたいに規模が大きくなってくると、

E_API CFoo* __stdcall CreateFoo();

みたいな外部クラスの生成用関数、参照用関数が増えてきます。

そこでDLL内の実装にて、公開している継承元(上記例ではクラスCFoo)から定義先(上記例ではクラスCNaibu)を参照するとして

CNaibu *pNaibu = dynamic_cast<CNaibu *>(CreateFoo());

みたいな「dynamic_cast」を使う場合が出てくるかもしれません。

レンダラでのライブラリ分離でdynamic_castでのキャストを多用していたのですが、これ、速度的な負荷が高いですね。137秒のレンダリングが183秒になってしまいました。ということで、dynamic_castは使用してはいけない!!、というのが教訓としてありそうです。内部処理は内部クラスまたは関数で完結させるべき、ってことでしょうね。楽して、外部に出した関数・クラスでアクセスしている部分は地道に内部用と外部用に分けて扱うほうがいいなと思いました。

ノイズを減らす第一歩(2008/04/29)

あんまり攻めるのもあれなんで、ノイズを減らす方法を書いてみます。すでにJensen氏のフォトンマッピング本に書いてあることですので、既出ではありますが。

ランダムについて

Mersenne Twisterを使用する。

http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html

ANSI Cのrandは結構偏りがあるので有名ですが(さすがにこれは使わないと思うけど(^_^;;)、Mersenne Twisterはほどよく散ってくれます。なんと、この理論を考えた方は日本人です。私の中では、ランダムルーチンはこれが決定打になってます(過去は自分の作ったランダムルーチンを使ってたのですが、数年前に乗り換えました)。これだと、再現性も出せますので、クラス化するのが吉です。ただし、マルチスレッドで使う場合は、それぞれのスレッドごとにランダムクラスを保持しておかないとえらいことになります(乱数発生時にカウンタを使うため)。

拡散反射の半球上のサンプリングにて

極座標のθとφを求めるときに、

θ = acos(sqrt(r1))
φ = 2π(r2)

を使うようにする。r1、r2は0.0〜1.0の乱数です。プロットしてみれば分かりますが、単純にθ、Φを

θ =  π * r1
Φ = 2π * r2

としたら大きく偏ります。これは球だから、ですね(RRT本(Realistic Ray Tracing)にもっと詳しく書いてます)。

層化サンプリングまたはQMCを使う

以前日記で書いたとおりです。ランダムだけではダメで、「ある程度規則性がある」もノイズ除去には重要です。

と、hdrを使ったレンダリングでは重点的サンプリングを使用するにも判定が難しい部分があるので、結局はこれだけなんですよね(^_^;;。実はまだ重点的サンプリングは取り入れてないです。さぁ、実装する作業に取りかかるんだっ。

Shadeと比較してみる(2008/04/28)

ページ開くの重い・・・画像貼りすぎですね(汗)。今作っているレンダラとShade10での結果を質と時間にて比べてみました。Pentium4 2.8GHz/Mem512MBのWinXPマシンにて。

Shade10パストレーシングにてイラディアンスキャッシュOff、レンダリング時の画質50、視線追跡レベル5。レンダリング時間42秒。Shade9なら同じ画質で51秒でした。ただし、hdrのガンマをあわせるために、Shadeの色補正にてガンマ2.2にしています。

独自レンダラ。パストレーシングにて50samples/pixel、追跡レベル5。レンダリング時間60秒。

ところで、ShadeってスペキュラがGIに反映されないのかな?反映すればもっとリアルになるように思います。

後、やはりShadeの場合はノイズが強いような気がします。独自レンダラだと、サンプリング数をあげていくとhdrのみのシーンだとすぐに収束していくので、結果的に速度と品質のバランスがShadeとは逆転します。50samples/pixelくらいだとShadeに負けてますけどね(^_^;;。

でも、Shadeはやっぱり暗いのかな。どうも吸収の考え方が自分とは違うのかもしれない。表に出せないシーンもレンダリングしているのですが(表に出したくてウズウズしますが)、空気感という面で言うとShadeのはしょりすぎる処理自身は一部もったいないことをしてるのでは、と感じてます。

実験してみると、Shadeの「画質」はサンプリング数/ピクセルとイコールではない感じですね。100を越えたあたりで急激に重くなりますし。

で、独自レンダラにて200samples/pixelでは以下のような感じに。

これで216秒。サンプリング数が4倍になったのですが時間は3.6倍です。

ダイナミックレンジを持つピクセルでのアンチエイリアス(2008/04/26)

0.0〜1.0の範囲を超えるRGBを持つダイナミックレンジな状態を ローレンジに持ってくるときにクランプを行います。が、普通に視点からのレイトレースで1サンプリングごとにクランプすると、1ピクセル内でのサンプルした色の平均をとってもうまくいかない、というのは以前説明した理由です(2008/04/22の日記にて)。明るいピクセルが勝ってしまうので、平均をとっても偏りが出てしまいます。下の画像のような感じ。これで50samples/pixelですが、面光源と背景(黒)の部分ははっきりとジャギってます。

ではどうすればいいか、というのでフィルタも試したのですがボケては意味がないので別の方法を考えます。誰でも考え付くと思うのでやり方を記載すると、「オーバーサンプリングすればいいじゃない」というものです。

大きくレンダリングしてそれを縮小すれば、エッジにアンチエイリアスがかかる。

という古典的な理論です。ただ、これは条件があって「整列されているピクセルで」使える方法です。たとえば、1ピクセルを4x4の領域に分割します。

この0〜15の領域はそれぞれ0.25 x 0.25ピクセルになります。そこでまずはこの領域におさまるようにサンプリング位置に制限をかけます。で、その個々の領域内でサンプリングします。要はある意味層化サンプリングですね。

  • 1ピクセル内を4x4の領域に分けるとします。その個々の1ピクセルで80サンプリングするとします。
  • 80 / 16 = 5サンプリングずつを、0.25x0.25ピクセル内でサンプリングします。
  • 5サンプリング分のトレース後のRGBの合計〜平均を求めます。これを16点分行います。
  • 16点分のRGBにて、それぞれが0.0〜1.0の間に収まるようにクランプします。
  • 16点分のRGBの合計から平均を求めます。

これで、1ピクセル内のアンチエイリアスを考慮したRGBが求まります。結果的に4x4のオーバーサンプリングをかけたことになり、1ピクセルでのサンプリング数は80で変化しません。

ただし、クランプした段階でローレンジになりますので、露出や発光のフィルタをかける場合はクランプ前に行う必要があります。

結果は以下の通り。

色はそのままでアンチエイリアスがかかりました。これで明るいノイズも安心です。

じゃあ、「もしかしたらこれでノイズも劇的に消えない?」という淡い期待を抱くかもしれませんが、残念!!ノイズに関しては、レンダリング画像サイズが縦横4倍(4x4サンプリング/ピクセル)になったとしても、サンプリング数を「80 / 16 = 5サンプリング」みたいにしている段階で16で割ってしまってるので、プラスマイナス0です。ようは、ノイズは据え置き。レンダリング時間も据え置き。

やり方としては、結局1ピクセルごとにサンプリングしたRGBの合計を取って平均してクランプしても変わらないんじゃない?と思うかもしれません。が、それは以前の日記に書いた結果になります。この差はなぜか、というと

これは条件があって「整列されているピクセルで」使える方法です。

と書いたのがその理由になります。ダイナミックレンジを持つため、1ピクセルよりも細かい領域であっても、そのピクセルでの位置が結構大事です。MCRTではランダムも大事なんですが、整列されているというのも大事な部分ではありますね。

ただ、この処理(クランプ)を行う段階でダイナミックレンジな情報は失われるため、あくまでもトーンマップの一貫的な処理とご理解いただければと(レンダラでそのままの色をhdrに焼き付けたい場合など、このクランプを行わないオプションもいるでしょうね)。

続・面光源実験のバグ(2008/04/26)

机部分に暗い部分とやたらと明るい反射がありますが、はて、何でバグってるんだろう?

の部分は、レンダリング時の問題ではなくて法線の影響でした。先日の日記の画像では、机の表面にて明るい部分(面光源の映りこみ)と暗い部分が混じっています。

これのポリゴンメッシュのスムージングの角度を見てみると、50(度)としておりました。机の角が丸くなるようにモデリングしているため、結局は頂点法線が以下のようになってたんですね。

で、机の表面は平面なはずなのに大きく曲面になっていたと。各サンプル点では法線をゲットしたときにゆるやかに楕円の表面に沿うかのごとく、レイ方向に反射するためにその先(二次レイ以降)が机に反映されていたのでした。

ということで、机でのスムージング角度を0度にした場合は以下の通り。なお、200samples/pixelです。ノイズも割と減ってます(レンダリング時間は前回の50samples/pixel時に比べて約4倍です)。

続・面光源での実験(2008/04/25)

Pentium4 2.8GHz/Mem512MBのWinXPマシンにて、512x512 pixel/50samples/pixelにて323秒(5.3分)。去年の12月にMaxwellやWinOSiにて実験していたシーンです。

教室っぽいシーンで寸法が適当なんですが(というつっこみもいただいたのですが)、そのへんはスルーを(^_^;;。

以前はMaxwellで2時間でなんかCGっぽい感じ(ただし、マシンはCeleron1GHzのノートPCでした。CGくさいのはマテリアルの設定もあるかもしれません)、WinOSiはきれいなんですが114時間くらいレンダリングしてましたね、日記を読むと。マシンと速度などを書いているのは、自分の過去の検証と比較するためですが改めて記録しておくのは大事だなぁと思いました。

やっぱりWinOSiのほうが抜群にきれいなんですが、それに質は近づきつつ速度はなぜかMaxwellよりもかなり速くなりました。ノイズを消していくと時間はかかるでしょうけど。でも、なんか抜けてるような・・・(Maxwellよりも速いってのは今のところないはずなので)。でもこのシーンも結局は直接照明が強いのであんまりテストにはなりませんね。

机部分に暗い部分とやたらと明るい反射がありますが、はて、何でバグってるんだろう?

CoreDuoの64ビットマシンにて300samples/pixelにて1783秒(29分)。

ライトは外から2つの面光源、天井に弱めの4つの面光源を配置してます。300サンプリングでもまだノイジーです。吸収が速度に影響している感じ。とりあえず指向性のあるライト(面光源も含む)は、重点的サンプリングなどでノイズを減らす工夫がいりそうです(hdrは全球なので厳しいですが、できなくはないと思われます。SHあたりを使えば(<未だに理解できてないです))。

続・面光源での実験(2008/04/24)

面光源を配置した場合に、あまりにも収束に時間がかかるので、直接照明と間接照明を分離してみました。

上記はCeleron1GHz/Mem512MBのノートPCにて、400x300pixelのレンダリングで50samples/pixelで524秒(8.7分)。発光する光源(面光源)は4つ配置です。と速度アップ、、、したように見えてます(レイのトレース最大は15にしてます)。直接照明部分は近似的になるため、純粋なパストレースとは微妙に差が出ますね。でも、気づかない程度かなとは思うのでこれでいくのが無難かなぁ。

MaxwellやWinOSiと比較しながら実験を進めてますが、直接照明と間接照明を分離したあたりからMaxwellっぽい陰影が出てたりして、「ああ、こうやってはしょってるんだな」と思える点がいくつかありますねぇ。WinOSiがきれいなのは純粋なパストレだからかもしれませんね。

ちょっとマテリアルが暗いのですが、ロシアンルーレットの割り振りルールをどうしようか、というのを試行錯誤しつつ調整入れてるので前のとは比較できないです。ですが、ノイズがちらほら。これは間接照明時のノイズです(間接照明は単なるパストレです)。これを消すにはやっぱり数百sampleくらいいるかも。フォトンのモヤモヤは極力抑えたいので、二次か三次レイで回収するか、以前から練っているフォトンを使ったレイ追跡(輝度計算では使わない、ようは双方向になるのかな)をやってみよう。単純な考えだと、直接照明は数十のサンプリングで収束するので(もちろんシーンによりますが)間接照明とは別にして、間接照明では別のサンプリング数を与えて詰めていくほうがいろいろはしょれていい感じかもしれません。

Bilateral Filter(2008/04/23)

Bilateral Filterを実装してみました。

もう2年も前にimagireさんのところで検証されてましたので、参考にしました。

http://www.t-pot.com/
の「Microsoft DirectX 9.0 SDK (August 2006) :バイラテラルフィルタ」

先人の知恵、バンザイ!!

ということで。

オリジナルは512x512pixelの100samples/pixelで、Pentium4 2.8GHz/Mem512MBのWinXPにて72秒でレンダリングしたオリジナルは以下になります。

以降フィルタをかけても時間は秒単位も変わりません。フィルタ処理は、ボケ具合が変わっても0.5秒くらいかな。

トーンマップ処理として、「露出調整」→「Bilateral Filter」→「ガンマ補正」で、ようやくディスプレイ上のRGBに変換してます。

↓距離の敷居値2.0、輝度の敷居値0.2。

地面と背景の輪郭はまだガタガタです。

↓距離の敷居値2.0、輝度の敷居値2.0。

ようやく輪郭にぼかしがかかった感じ。

↓距離の敷居値5.0、輝度の敷居値4.0。

なんかぼけぼけです。

なんとなく、輝度差が激しい状態が残っているガンマ補正前にフィルタリングするのが悪いのかも。ということで、ガンマ補正後にBilateral Filterをかけてみました。

↓ガンマ補正後のフィルタ。距離の敷居値2.0、輝度の敷居値0.5。

この場合は、輝度の敷居値を1.0にするとすべてがぼけるはず。ガンマ後のほうがいいのかな。でも、これでもボケが気になるのでもっと範囲を狭めたほうがいいかもしれない。

ノイズ除去に使えそうなのですが、エイリアス問題の解決にはちと方向性が違うか(どちらかというと激しい変化の部分を平滑化したい)。ただ、この2つの「距離差」「輝度差」の調整の組み合わせでいろいろできそうなので、これはこれで研究の必要がありそうです。

float型のRGB情報を持つ場合の圧縮(2008/04/23)

HDRでやっている、float型のRGBをRGBEの計4バイトであらわす方法。ソースはhdrのやつそのまんまです。

float x 3だと4x3=12バイトになり、画像サイズに合わせてかなりメモリを食ってしまいます。RGBE4バイトで持つと、三倍の圧縮になりますね。後、隣接するピクセルの色が似通っているとすると、指数部のEは圧縮にもつながりそうです(解凍を速くするのならランレングスでも使ったらよさげ)。

float型のRGBからRGBE(4バイト)への変換は以下のような感じです。

(red, green, blue)にfloat型のRGB値が入っているとする
double dVal;
int eVal;
int iR, iG, iB, iE;
   
if(red > green) dVal = red;
else dVal = green;
if(blue > dVal) dVal = blue;

if(dVal <= 1e-32) {
    iR = iG = iB = iE = 0;
} else {
    dVal = frexp(dVal, &eVal) * 255.9999 / dVal;

    iR = (int)(col.red   * dVal);
    iG = (int)(col.green * dVal);
    iB = (int)(col.blue  * dVal);
    iE = (int)(eVal + 128);
}

RGBE(4バイト)からfloat型のRGBへの変換は以下のような感じです。

(iR, iG, iB, iE)に0〜255の値が入ってるとする。

double fVal;
float red, green, blue;
    
if(iE == 0) {
    red = green = blue = 0.0f;
} else {
    fVal = ldexp(1.0, (int)(iE - (128 + 8)));
    red   = (float)((iRed   + 0.5) * fVal);
    green = (float)((iGreen + 0.5) * fVal);
    blue  = (float)((iBlue  + 0.5) * fVal);
}

相互に行き来して変換可能です。

トーンマップに向けて(2008/04/22)

メモ書きです。

hdrのフォーマットではexposure(露出)を保持できますが、これ自身はhdr写真を撮影したカメラマンまたは見る人のいわゆる感情みたいなもんですので、数値にあらわすものではないのかもしれませんね。exposureなし(0がデフォルト)のhdrを用意するのが一般的なのかは分かりませんが、少なくとも生データにexposureの補正値を入れてしまってRGBEのE(指数部)を変えてしまっていいものか悩むところ。レンダリングでは結局は理想の画像になるように変換するんですが(いわゆるトーンマップ)、たぶん物理的により正しく、という用途でhdrを使うのなら変換を加えたらアウトだと思う。もし、精確な光の強さ情報をhdrに保持しているのだとすると、晴れた夏の昼どきに外でhdrを作成すると真っ白に飛んでしまうような(そして、おそらくそれが正しいのかも)。それを「うぉっ、まぶしっ!」ってならないように自動補正している目の露出機能ってすごいですね。後、それを模倣しているカメラも。かなり企業秘密な世界なんだろうなぁ。

で、レンダラでの「exposure(露出)」って何?というところから。強引に解釈すると「暗くするか明るくするか」という感じ。C言語的な解釈だと、「<< 1」なら+1(2^1)のシフト、「>> 1」なら-1(-2^1)のシフト、みたいな感じで指数としてシフト処理するのが「露出(exposure)」ってやつみたいです。この-1や+1がそのままhdrで使える露出の値。0でデフォルトなのでシフトなし。-1だと一段階暗くなります。+1だと一段階明るくなります。

露出(exposure)についての式など

http://freespace.virgin.net/hugo.elias/graphics/x_posure.htm

露出の処理は、ダイナミックレンジを持つ生の光情報をディスプレイ上で表現する(劣化させる、という表現でもいいかな)ときの第一歩の作業の1つであります。ディスプレイに表現できるように変換すること、それが「トーンマップ」といわれているものになります。

ところで気づいている方もいるかと思いますが、ここで載せている実験画像にて 特に背景とオブジェクトのエッジ部分がガタガタに見えているものがあります。原因はダイナミックレンジを持つ(1.0以上の値を持つ色も存在する)からなんです。

hdrを使ったレンダリングなどでは、ダイナミックレンジからの情報を扱うため1.0より明るい部分も存在します。また、点光源などでもかなり明るい部分は1.0越えがあります。

最終的には、スクリーン上の1ピクセルにてレイのトレースを行い、例えば50サンプル分のRGB情報を加算して平均を取るわけですが、ものすごい明るい色が混じっていると平均してもその明るい色が勝ってしまって結局は白くなります。対して、ほとんどが1.0を超えない色をサンプリングして平均を取った場合(間接照明でやわらかい光が届いている場所など)は、白よりは明度は低いです。結果、境界部分にはエイリアスとしてガタガタが出ます。これはサンプル数をいくらあげても緩和できません。

では、1サンプルごとに0.0〜1.0の値におさまるようにクランプしたらどうだろう?それで合計した色の平均を取るとエイリアスっぽくなるところも解消されるはず(1ピクセルで50サンプリングとかしているわけなので)。実験すると以下の画像のようになってます。

たしかにエッジ部分は緩和されたのですが、暗くなりましたよね。特にGIでは大量のサンプリングをしますので、両極端の黒寄りの成分と白寄り(だけど、1.0よりも大きい値を含む)の成分が交ざっていい感じに調和されることがあります。ここでディスプレイで表現できる最大の1.0の色を超えた部分をカットすると、、、。サンプリングした色の平均を取ると「(黒+白)÷2 = 灰色」になります。これはいけない!!ということに。

ということで何を言いたいのかというと、露出もそうですがレンダリング中は色は加工せずに扱う、最後の最後、ディスプレイに表示するときに加工する必要がある、それが「トーンマップ」だ、ということです(<うまいことまとめたつもり(^_^;;)。

じゃあ、エイリアスをどうすれば緩和できるの?というと、思いつくのはフィルタですがググッて見ると「Bilateral Filter」というがあるみたいです。

Bilateral Filterについて

http://lucille.sourceforge.net/blog/archives/2004_02.html
http://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Filtering.html

ちょっと実装してみようかな。Shadeのレンダリングもやたら白い部分でエイリアスが目立つことがありますが、原因は同じであると思われます。これは、ダイナミックレンジを持つ媒体が現れたので表面に出てきた問題、であるのかもしれませんね。

微小立体角での訂正(2008/04/21)

影の部分が白くなることがあったため、何でかなと思っていたらどうも拡散反射の計算で重大な間違いをしてたようでした。

2008/04/09の「放射輝度(raddiance)での微小立体角」の日記にて、

Lo(x, \vec{w}) = Le(x, \vec{w}) + \int_{\Omega} fr(x, \vec{w}', \vec{w}) Li(x, \vec{w}') (\vec{w}' \cdot \vec{n}) d\vec{w}'

計算では積分になりますので、この部分は半径1.0の半球の表面積を
入れることになります。つまりは4π * 0.5 = 2π。
球の表面積は「4πr^2」ですのでこれの半分です。

の部分d\vec{w}'を考えると、「2π」を入れるんでなくてこれをさらに半分にした「π」を入れるのが正解なのかも。というのも、積分した最終的なものはその中間値になるから、なんだろうか。まじめに検証したほうがいいかな、というか数学の勉強を再度したほうが(汗)。

間違いについてはすみません、修正しつつ実験出来ればと。またテストレンダリングしてみます。

DualCoreの64ビット環境での実験(2008/04/20)

作っているレンダラにて、512x512pixel、500samples/pixelのシーンにて負荷テスト。

Pentium4 2.8GHz/Mem512MBのWinXPマシンにて「655秒」(4スレッド使用)かかったシーンが、Dell Precision PWS490(Intel Xeon5110 1.6GHz(DualCore))/Mem2048MBのWinVista(64ビット)にて「569秒」(1スレッド使用)、「287秒」(4スレッド使用、2スレッドでもほぼ同じくらいの時間)、となりました。

DualCoreの場合は、見事にコア数にあわせて倍速化しました。このへん完全に2CPUあるのと同じ動きをするのかぁ、レンダリングには結構有効に使えますね。ということでマルチスレッド対応については問題なさそうです。

面光源での実験(2008/04/20)

面光源には対応するまでもないのですが(emissionなんでhdrと同じに扱えます)、Shadeからシーンデータをエクスポートできるようにしたので、過去にWinOSiで実験していた結構間接照明が重要なシーンをレンダリング。視点の手前(背景)は黒になってますので、パストレとしては厳しいシーンではあります。

テストマシンは自宅のノートPCなのでスペックは低く、Celeron 1GHz/Mem 512MBのWinXPです。1つのレイの最大追跡回数は10としてます。

50samples/pixelで204秒(3.4分)

500samples/pixelで1567秒(26分)

ノイズが消えないですね(^_^;;。これはどうも視点の手前にある背景黒の部分がかなり影響してそうです。ここで重点的サンプリングがいかに重要になるかが分かりますねぇ(まだ実装してない)。

ということで一晩寝かせてみて怒涛の10000samples/pixelです。WinOSiのときの実験を彷彿させる作業です。

10000samples/pixelで30180秒(503分 = 8.3時間)

おお、なんかMaxwellっぽい。別段10000サンプリングもいらなかったかな。で、いろいろ分かったことがあります。

  • 視点からのパストレースだけでも間接照明やコースティクスはきっちり再現できる(光源から追わないと再現できないのでは、というのは間違い)。ただし、シーンによってはかなりのサンプリングが必要。理想は無限のサンプリングになるため、光源からの追跡でショートカットしないとやってらんない、というのが結論っぽい。
  • 重点的サンプリングは、光源からだけではなくて視点からの追跡も考慮しないといけないかなぁ。
  • なんだかんだいってMaxwellは速い。

Maxwellの場合は、これくらいだと1時間もあれば収束するかと。と考えると、「はしょり方」はまだ研究の余地がありそうです。このシーンは結構面光源の前に物体を配置していて光が届きにくい状態にしているので、フォトンを光源から飛ばした場合どこまで機能するかな。ということで、そろそろ重点的サンプリングを考えていかないと。

妙に明るくなるピクセルの原因(2008/04/20)

WinOSiでテストしていたときもですが、今作っているレンダラにて妙に明るくなるピクセルが点在する、という現象が起こってました。

原因はというと、拡散反射の面にぶつかって合わせ鏡のようにいつまでたっても抜け出ることができずに最大追跡回数までいってしまった、という部分でした。

で、これの回避策として拡散反射での減衰が考えられますが単純に減衰すると品質を下げるといけませんので、以下のルールを設けてみました。

1つのレイを視点からトレースしているとします。拡散反射では半球状にランダムに反射します。これを1レイで再帰的に繰り返します。

  • 拡散反射の反射回数を記録し、3回目まではそのまま。
  • 拡散反射の反射が3回以上の場合、かつcosθ(法線と出射ベクトルの内積)が0.5以上の場合は、拡散反射にて0.6をかけて減衰させる。

値は適当です。決して鏡面反射や透過で減衰してはいけません、拡散反射のみ。拡散反射の微妙な変化も全体的に見ると重要ですので、極力「妙に明るいピクセルだけ除外」するように細工してます。結果として以下みたいな感じになります。ミクのほっぺ部分が無限反射を繰り返しやすい部分になるのですが、自然になりました。

同様に、鏡面反射物体のエッジで白くなっていた部分も回避できてますね(これも、ロシアンルーレットしたときの拡散反射成分での無限反射が原因でした)。

ということで、hdrでのIBLについてはこれでそれっぽくなったかな。

ノイズ低減での品質アップ、層化サンプリングでの最適化(2008/04/19)

拡散反射時にまだ偏りがあったため(しかも結構レンダリング結果に影響してました)、それを解消したら層化サンプリングの影響が効いたのか、少ないサンプリング数でのノイズ低減につながったようです。後、光沢反射で層化サンプリングしていなかったため、対応。先日の1000samples/pixelのを500サンプルに落としてもそんなに劣化しなくなりました。jpeg画像なので粗いですが、以下のような比較になります。ほんとはpngにしようかと思ったのですが、そろそろこのページも重くなってきてますのでこれにて勘弁。

GIでの嫌がらせのようなhdr(2008/04/18)

Debevecさんのところにて、光量差(イラディアンス値)が100以上もあるhdrをダウンロードできるのですが、これくらい明るい部分と暗い部分が混在する場合は欠点が見えてしまうGIレンダラも多いと思います。以下にhdrとソースがあります。この空気感を(ブラーでの後処理で)出したいと思ったので掘っていたのですが、hdr自身が結構テストに使えそうです。

http://www.debevec.org/RNL/Source/

しかし、10年前の研究っすか、、、私は10年遅れですね(^_^;;。hdrをあさってみて分かったのですが、素材集のhdrは数十くらいの光量差しかないものや曇りのシーンが意外と多く、「GIとしては優しいhdr」であるのかなとなんとなく思いました。Debevecさんのところのhdrは容赦なしっす、500くらい差があるものもあるし(汗)。

まずその光量差がノイズとして出ます。それを消すためにサンプリング数を増やして、、、となると今度はレンダリング時間がかかります。

今作ってるレンダラでのレンダリング実験画像。上記サイトのhdrを拝借してみました。

Pentium4 2.8GHz/Mem512MBのWinXPマシンにて、512x512ピクセルを1000samples/pixelでレンダリングしました。レンダリング時間は1194sec(約20分)。300ピクセルではノイズが強すぎて厳しかったのでサンプリング数をあげたのですが、レンダリング時間は2倍増加したくらいです(そのための独自アルゴリズムを実装していたりします。実はサンプリング数の増加に強いレンダラだったりする)。

Shadeだと、これくらいのhdrになるとやっぱり1000サンプリングくらいいりますが、イラディアンスキャッシュを使わないともっとレンダリング時間がかかりそうでした(逆にこのようなhdrを使う場合は、光量の変化が激しいのでイラディアンスキャッシュは使えないかと思います)。Shadeの場合はイメージウィンドウの「レイトレーシング画質」がサンプリング数に相当するようです(数値では1-100になってますが、1000と入力可能です)。Shadeって、サンプリングは層化もQMCもしてないのかな、もっと品質は上げることができそうですが。。。

光沢と粗さ(2008/04/18)

Schlickのパラメータの光沢と粗さを調整した床の表現。

部分的にえらい明るいピクセルが出るのはなぜだろう・・・。気になります(レイトレの再帰処理を省きたいために展開したですが、そのときに反射物体のエッジにも出るようになった)。上記は床のマテリアルとして光沢値0.3、粗さ0.01です。ワックスがけしたような床の表現は出るかな。床部分は木目調のテクスチャをShadeでレンダリングしたものをはき出して、それをUVで貼り付けてます。ということで単なるイメージテクスチャ表現です。方言を生むのでプロシージャルには対応してません。プロシージャルを使いたい場合のため、将来的にシェーダー機能でカスタマイズできるようにする予定。

これはPentium4 2.8GHz/Mem512のWinXPマシンにて、512 x 512 pixel(400samples/pixel)で550sec(9.1分)です。2スレッド使用。

ただ、床などの大きい領域で反射が入るとノイズが目立ちますね。これ(強い反射)は最適化できるのだろうか、フォトンを使っても散る元ですので難しいのかなぁ。また速度アップの法則として、色を吸収する物体(黒に近い物体)が多いとノイズが出にくいというか収束しやすい、というのがありそうです。考えれば当たり前ではあるのですが、黒色に近い物体だとそこで光が吸収される可能性が高く、それ以上はレイの追跡が不要になります(ロシアンルーレットで打ち切りの確率が高くなります)。上記シーンは床を黒っぽくしたものですので、レンダリング時間は以前よりも速くなってます。

この吸収という概念も速度アップのきっかけになりそうです。

後、独自ファイルフォーマットを考えてシーンを読むようにしました。面倒なのでメタセコのフォーマット(mqo)をパクってみました(^_^;;。マテリアル部分は複数層を持てる仕組みにしたかったのと光沢や粗さなどがありますので独自ではあります。これは予定通りなのですが、別途メタセコのmqoファイルをinclude(外部ファイルとして参照)できるフォーマット構成にしています。もちろん、新しい形状も頂点座標と面情報を列挙して記述可能です。面ごとの頂点数は最大196頂点としてます(メタセコは4ですね)。でも、196頂点/ポリゴンだと一行に1つの面の情報を記載できるのだろうか、、、ただおそらくプログラムでデータを流し込むことになるのでいいですよね。

と、これでテスト効率が上がりそうです。

速度アップ(2008/04/16)

レンダラをスレッドに対応しました。Pentium4のハイパースレッド環境などで速度アップしましたが、純粋2倍はにはならなかったです、残念。

Pentium4 2.8GHz/Mem512MBのWinXPマシンにて、512x512Pixelで、300サンプリング/ピクセルのデータ。過去1スレッドで677sec(11分)だったのが4スレッド使用にて518sec(8.6分)、2スレッドでも同じくらいでした。スレッドの上限はなしです(いくらでもOK)。で、念願の10分のラインを切りました!まだスケジューリングで最適化できるのかな。他の最適化のアイデアは思いついてますが、しばらくは棚に置いてそろそろまとめに入らなくては。SSS対応と光の吸収に関する部分の実装と重点的サンプリングなどがありますので。

しかし、スレッド化の際での共有データの分離に時間がかかりました。今度はCore Duoの64ビット環境ででもチェックしてみることにします。

層化サンプリング(Stratified sampling)(2008/04/14)

デフューズ反射は、θφの2つの回転からなる極座標上で半球方向にサンプリングされて反射されます。この際に、普通に0.0〜1.0の間のランダムを使うと「偏り」が起こります。Mersenne Twisterを使ってもそれは変わりません。

ですが、デフューズ反射のように満遍なく反射させたい場合はランダムではなくて、「隔たりなく」散っているほうがノイズは目立ちません(強引に言うと、ランダムがなくなると模様になり、ノイズには見えにくくなります)。

そこで層化サンプリング(Stratified sampling)という理論が有効です。この方法は、「サンプリングがかなり多い」部分で結構有効です。サンプリングを1ピクセルに100回とか、全体で何万回か分からないくらい繰り返すレンダラではうってつけですね。

理論としては以下のようなもの。0.0〜1.0内の乱数を返すものとする。

  • 0.0〜1.0の区間を4分割して、0.0〜0.25、0.25〜0.5、0.5〜0.75、0.75〜1.0の4つの空間に分ける。double aOffset[4] = {0.0, 0.25, 0.5, 0.75};
  • 4つの空間を順番に移動していく。offsetPos = 0,1,2,3,0,1,2,3...のように繰り返す。
  • 「(randF() * 0.25) + aOffset[offsetPos];」のように計算したランダム値を返す。randF()は0.0〜1.0の間の実数を返す関数とする。

区間の分割数は用途にあわせて変えてあげるといいです。これで、順番に0.0〜0.25、0.25〜0.5、0.5〜0.75、0.75〜1.0の間の実数を返していくことになり、「割と満遍なく順番に」散らばることになります。θφの極座標の半球上を向くベクトルだと、ちょっと工夫はいりますが同様に「満遍なく」散らすように実装できます。これがレンダラでは結構重要です。

極座標を表す2つの乱数を返したい場合は以下のような感じ。

int m_aPos;            ///< 層のカウント用(最大4 x 8)
double m_aOffset[20];  ///< 1.0 / 8 = 0.125のカウント用
double m_aOffset2[20]; ///< 1.0 / 4 = 0.25のカウント用
...
// 初期化処理
int loop;
double dPos, dt;
m_aPos  = 0;
dPos = 0.0;
dt   = 1.0 / (double)8.0;
for(loop = 0; loop < 8; loop++) {
    m_aOffset[loop] = dPos;
    dPos += dt;
}
dPos = 0.0;
dt = 1.0 / (double)4.0;
for(loop = 0; loop < 4; loop++) {
    m_aOffset2[loop] = dPos;
    dPos += dt;
}

/**
 * 層化サンプリング(Stratified sampling)にて
 * 0.0 - 1.0の隔たりの少ないサンプルを生成する。2つのdouble値を返す。
 * ただし、この処理は大量に呼ぶものとする。
 * @param[out]  pRetD1  ランダム値1が返る(0.0 - 1.0)
 * @param[out]  pRetD2  ランダム値2が返る(0.0 - 1.0)
 */
void RandF2(double *pRetD1, double *pRetD2)
{
    double rVal1, rVal2;

    // randF()は0.0 - 1.0の間の実数を返す関数とする
    rVal1 = (randF()) * 0.25;
    rVal1 += m_aOffset2[(m_aPos >> 3)]; // 4分割の配列
    rVal2 = (randF()) * 0.125;
    rVal2 += m_aOffset[(m_aPos & 7)];   // 8分割の配列

    m_aPos++;
    m_aPos = m_aPos & 31;

    *pRetD1 = rVal1;
    *pRetD2 = rVal2;
}

「RandF2」関数の第一引数にて垂直方向(θ)の乱数のための0.0〜1.0の値が返ります。第二引数にてぐるっと円状(φ)を回る0.0〜1.0の乱数値が返ります。φのほうが距離が長いので、層の精度はθの乱数の2倍としています。

double r1, r2, s_sita, c_sita, s_phi, c_phi;

// 層化サンプリング用のランダムを返す
RandF2(&r1, &r2);

r1 = acos(sqrt(r1));
r2 = PI * 2.0 * r2;   // PIは3.1415926535
s_sita = sin(r1);
c_sita = cos(r1);
s_phi  = sin(r2);
c_phi  = cos(r2);

// Z軸を基点とした半球上の方向ベクトルを計算
vDir.x = (float)(s_sita * c_phi);
vDir.y = (float)(s_sita * s_phi);
vDir.z = (float)(c_sita);

な感じで極座標からデカルトに持ってこれますね。

参考までに、今までの試していたシーンを100sample/pixelでレンダリングしてみました。

1つ前の日記に300sample/pixelのをあげてましたが、見比べてみて雰囲気はだいたい似てまよすね(でも白いノイズが気になるけど)。これでレンダリング時間は300sampleのときと比べて約1/3です。なんとか見ることのできるレベルだと、もっとサンプリング数が低くてもそれなりになるのでサンプリングに関しては一応この方向で。層化サンプリングしないのと並べて見比べてみたのですが、実はあんまり変わらない、全体で見渡してみるとたしかにノイズが減ってる、という状態でした(微妙だったので比較はなしです(^_^;;)。

300sample/pixelで層化サンプリングの場合は以下のようになりました。1つ前の日記の内容と見比べても気持ち的にきれいになってますね。

その他にQMCなどもあるのですが、同じようにランダムを隔たりなくするための手法になります(ソース読んでも内容をまだ理解できてないので実装してません)。後、ランダムシードをうまいこと制御すると、アニメーションしてもまったくちらつかないレンダラを作る、ということも可能だったりします(過去、このサイトでもちらっとムービーなどを乗っけてましたけど理論は非公開としてました)。

ということで、ランダムと仲良くなればノイズの軽減につながるという話でした。

速度アップ(2008/04/14)

サンプリング方法を若干調整して、品質据え置きで過去比で1.4倍ほど速度アップです。でもまだ300サンプリング/ピクセルでは、512 x 512 pixelのレンダリングで20分くらいかかってますので、まだまだ絞り込まなくては。レイのキャッシュっぽいものと重み付けの概念を取り入れてみました(まだunbiasedです)。

以下のレンダリングにて、Celeron1GHz/Mem512MBのWinXPマシンにて、512x512pixel、300sampling/pixelにて1277秒(21分)です。最適化前は同一マシンにて30分くらいかかってました。

手前の3つの球体は、左から「光沢1.0、粗さ0.0」「光沢1.0、粗さ0.05」「光沢1.0、粗さ0.1」です。粗さはちと効き過ぎかな。

できる限りトラバースが発生しないようにレイの動きの特徴を探ってますが、直接照明がほとんどのシーンではどのように最適化するのが妥当なのだろう。極力Blinn-Phongのようなシェーディングモデルを使わない方法で対処したいのですが、速度を求めると妥協がいるかなぁ。

キーワードとして「GIでは結構同じ動きがかぶってる」というのがあります(≒トラバースの無駄が実は多い)。

この部分はレイ自身をキャッシュすることで最適化に結びつけることができそうです。今回はunbiasedでいきたいので、イラディアンスキャッシュ的な補間はパス。フォトンも光源からのレイの動きを追跡するためのアタリ(またはキャッシュ)とすると、たぶん品質と速度の両方に使えるかと思ってます。ただ、微妙なずれの累積が最終品質に結びつくので、そのままキャッシュだけを使う、というわけにはいかなそうです(そのまま使う、を実験したのですが結局フォトンマップのようなムラを生んでしまいました。ランダムって大事ですね〜)。

後、まだマルチスレッドを考慮した実装はしてないので(内部的にはスレッド処理してますが、レンダラには使ってません)、これも速度アップにどれほど貢献するか楽しみです。

追記:

上記のサンプリングにおける最適化アルゴリズムにて、仕事場のPentium4 2.8GHz/Mem512MBのWinXPマシンにて、1245sec(20.75分)→ 677sec(11分)に速度アップです。環境によりけりですが、2倍ほど速くなりました。hdrを使った大部分が拡散反射による計算になりますので、この部分はさらに最適化できそうです。

これで若干Maxwellに近づいたかな(Maxwellも実際は結構速いと思います)。あえてhdrのみのレンダリングにこだわってるのですが、これをクリアしてから平行光源・点光源・スポットライトを置く、emissionの物体(面光源など)を置く、などのミッションをクリア予定です。分かる方は分かるとは思いますが、hdrのみだと結構「おお、きれい」でごまかせるんですよね。もっと明るい面光源を置いたりすると、ノイズが一気に増えたり間接照明の重要度が上がったりするので、まだまだ道は遠そうです。

続・unbiasedなフォトンマップ(2008/04/13)

メモ書きの意味もありますので分かりにくくてすみません。

フォトンにて「パストレ+フォトンをする場合に、何次レイからフォトンを採用するかを(レイごとに)可変にしたい」というのは、フォトン自体に光源からの反射回数を入れてあげて光源方向にさかのぼる仕組みを入れると、とりあえずはどこでフォトンをゲットしてもつじつまが合いそうです。(単純に言うと、パストレでの視点から光源に向かう反射の最適化と反射処理自身のキャッシュがしたい。そのためには、まずは光源からの反射をどこかに格納しておくのが近道かなと)

入射方向のベクトルωと法線Nの内積(cosθ)をどうしようかというのがあったのですが、さかのぼることができればフォトン自体には累積したラディアンスを入れてもいけそう(普通のフォトンマップでは個々のフォトンの情報として累積した光の強さを入れてます。ただ、さかのぼる仕組みは入れてないですが)。何を書いているやらですが、また説明入れることにします。

後、パストレの(拡散反射での)二次レイ目でフォトンを集める、なんてしてみたのですがあんまり速度アップ・品質アップしませんでした。ほとんどがhdrから当たる直接照明だからかなぁ。

分かったこととして、

  • 間接照明が弱いシーンで、間接照明の最適化をしてもあんまり効果がないかも(当たり前か(^_^;;)。
  • パストレは、交点xから半球状に四方八方にレイを飛ばした段階でノイジーになるので、実はこの一次レイとぶつかった交点x上でフォトンなどを集めないといくら二次・三次反射部分の先端でうまいこと光を集めてもあんまり目立った効果がない。親から絶たないと。

後、光源からのフォトンだけでは背景全体からの放射となると精度が足りないです(特に直接照明)。直接照明部分と間接照明部分は最適化手段も分けて考えたほうがよさそうです。直接照明を普通のレイトレで、間接照明部分はフォトンで、なんてことではないですよ。背景にhdrを使ってるとそうもいかないですので。

ということで、流れ的には少ないサンプリング数でノイズを抑える、という重点的サンプリングに偏っていきそうです。あれ?すでに教科書どおりの流れになっていってる予感・・・。

unbiasedなフォトンマップ(2008/04/12)

「unbiased」とは純粋パストレーシングのような補間を伴わないレンダラのことを言います。市販のものですと、Maxwellが代表的(どこまでunbiasedかは分からないですが)。レンダラ屋が好むのもこれが多いかも。対抗する「biased」なレンダラはv-rayなどの補間(イラディアンスキャッシュとかフォトンマップ)を伴うもの。biasedのものは高速にレンダリングできるのですが、全体的に(補間を行うので)低周波ノイズになる反面のっぺりと間接照明が消えてしまいます。速くレンダリングすればするほど、のっぺりになります。

実は仕事で使えるのは後者。品質はMaxwellのほうが断然いいのになぜ?というのは、速度が遅いからというのにあります。仕事ではレンダリング速度が命、品質は二の次になります(と書くと語弊がありますが、速度と品質ともにキリキリのところまで詰めるとは思います)。私だとWinOSiでも気長に待てるんだけどなぁ、という流暢な世界ではないみたい(^_^;;。

で、biasedなレンダラのための手法としてフォトンマッピングがありますが、たいていのレンダラはアタリとしてフォトンを使うか(重点的サンプリングなど)、二次・三次の間接照明をフォトンで代用します。

先日書いた

  • フォトンを集める輝度推定で精度が足りなければパストレを使うハイブリッド

ですが、実はフォトンマップでは直接照明と間接照明、コースティクスなどはフォトンの部分と普通のレイトレで分離できるのですが、その追跡回数を可変にしたい(1つのレイの再帰の途中でフォトンを利用したりパストレに変えたり)は難しい問題です。なぜならフォトンの集積時は、ラディアンス(放射輝度)を半球内でかき集めるために光量にはすでに「いつのときの光か」が内包されてしまってます。また、個々のフォトン自身が持つラディアンスでも、すでに計算済みの情報が入ってしまってます。

ですが妄想でunbiasedなフォトンマップというものが思い浮かびました。これなら基本的にフォトンマップ並みの速度でレンダリングでき、品質は純粋パストレース並、かつコースティクスにも強い、というのが実現できるかもしれません(MLTじゃないです)。アイデアが思い浮かんでいるうちに実装しよう。これだと、パストレのトレース処理の最中に(間接照明計算のどの段階ででも)フォトンに切り替える、フォトンの精度が悪ければパストレースですすめる、などのハイブリッド実装も制限なくいけそう。と、夢のようなことができるかチャレンジです。

hdrの読み込みを高速化する(2008/04/12)

HDRのフォーマットは以下のページのソースがそのまんま使えます。読み書き両方可能なコードが入ってます。

http://radsite.lbl.gov/radiance/refer/Notes/picture_format.html

が、実はもっと速度アップできまして とりあえずファイル読み込みのバッファリングを自前で行うと、

6MB(2000 x 1000 pixel)の読み込みが1.8秒→0.6秒に速度アップ、50MB(6000 x 3000 pixel)の読み込みが10秒→4秒に速度アップです。

実はCarraraのhdr読み込みが速いとのことを聞きましたので(Carraraは全体的に読み込みが速いですよね)、対抗意識を燃やして速くしてみました。

上記ページでは、fgetcで一バイトずつ読み込みなんてことをしてますのでこの部分は32760バイトほどのメモリを先に確保してキャッシュするようにします(内部的にはブロックごとにfread関数で読み込んでます)。ファイル読み込みのバッファリング処理部分のみ、そのままソースを貼り付けました。

Read関数でバイナリデータをブロック読み込み(ただし1024バイト以内ずつ)、ReadLineにてテキストとして1行読み込みを行い改行で区切る(ただし1024バイト以内ずつ)です。ReadLine関数では改行コードはOS依存しないように処理してます。hdrの読み込みでは、テキストとしてヘッダを1行ずつ読み込んで、データ部分はバイナリで1バイトずつ読み込んで解析してます(ちょっと変則的なファイル構成ですね)。サンプルソースでは、テキストでも最大512バイト・バイナリは常に1バイトずつ読み込み、という書き方ではありますので1024バイトの上限でも問題なしです。

FILE *m_fp;   ///< これにfopenしてるとする
unsigned char *m_pFileBuffer = NULL;   ///< ファイル情報を一度蓄えるバッファ
int m_FileBufferSize, m_FileBufferMaxSize;  ///< ファイルバッファのサイズ
int m_FileReadOffset;   ///< ファイル読み込み時の作業バッファのオフセット
...
// 作業用のバッファを確保。
// エラーチェックは自前でしてください。
// なお、freadでまとめて読み込みできる最大は32767までなので
// バッファはそれより少し少なめの32760バイト分を確保。
if(!m_pFileBuffer) {
    int cou = 32760;
    m_pFileBuffer = (unsigned char *)malloc(cou + 16);
    m_FileBufferMaxSize = cou;
}
m_FileBufferSize = 0;
m_FileReadOffset = 0;

/**
 * 指定の内容をまとめて読み込み
 * @param[out]  pRetBuffer   情報を読み込むバッファ
 * @param[in]   readSize     読み込みサイズ
 * @return 処理に成功すれば読み込みサイズが返る。
 *         eofの場合、処理が失敗した場合は-1が返る。
 */ 
int Read(char *pRetBuffer, const int readSize)
{
    int cou, len;
    unsigned char *pPos;
    int readSize2;

    if(!pRetBuffer || readSize <= 0) return -1;
    if(!m_fp || !m_pFileBuffer) return -1;

    // バッファに一括して情報を読み込み
    if(!m_FileBufferSize) {
        if(feof(m_fp)) return -1;
        cou = (int)fread(m_pFileBuffer, 1, m_FileBufferMaxSize, m_fp);
        m_FileBufferSize = cou;
        m_FileReadOffset = 0;
        if(!cou) return -1;

    } else {
        if(m_FileReadOffset + 1024 > m_FileBufferMaxSize) {
            memmove(m_pFileBuffer, m_pFileBuffer + m_FileReadOffset,
               m_FileBufferSize - m_FileReadOffset);
            m_FileBufferSize = m_FileBufferSize - m_FileReadOffset;
            m_FileReadOffset = 0;

            len = m_FileBufferMaxSize - m_FileBufferSize;
            if(!feof(m_fp)) {
                cou = (int)fread(m_pFileBuffer + m_FileBufferSize, 1, len, m_fp);
                m_FileBufferSize += cou;
            }
        }
    }
    if(m_FileReadOffset + 1 >= m_FileBufferSize) {
        return -1;
    }

    // 読み込むバッファサイズ(メモリ上のもののみ読み込めるとする)
    readSize2 = readSize;
    if(m_FileReadOffset + readSize >= m_FileBufferSize) {
        readSize2 = m_FileBufferSize - m_FileReadOffset;
    }

    pPos = m_pFileBuffer + m_FileReadOffset;
    if(readSize2 == 1) {
        *pRetBuffer = *pPos;
        m_FileReadOffset++;
    } else {
        memcpy(pRetBuffer, pPos, readSize2);
        m_FileReadOffset += readSize2;
    }

    return readSize2;
}
 
/**
 * テキストとして開いている場合に、1行分の情報を取得
 * @param[out]  pRetLine  1ラインの文字列が返る
 * @param[in]   MaxSize   読み込める最大サイズ
 * @return 処理に成功すれば文字列長が返る。
 *         eofの場合、処理が失敗した場合は-1が返る。
 */
int ReadLine(char *pRetLine, const int MaxSize)
{
   int cou, len;
   unsigned char *pPos, *pPos2;
   unsigned char chDat, chDat2;
   int offsetPos;

   if(!pRetLine) return -1;
   if(!m_fp || !m_pFileBuffer) return -1;

   // バッファに一括して情報を読み込み
   if(!m_FileBufferSize) {
       if(feof(m_fp)) return -1;
       cou = (int)fread(m_pFileBuffer, 1, m_FileBufferMaxSize, m_fp);
       m_FileBufferSize = cou;
       m_FileReadOffset = 0;
       if(!cou) return -1;

   } else {
       if(m_FileReadOffset + 1024 > m_FileBufferMaxSize) {
           memmove(m_pFileBuffer, m_pFileBuffer + m_FileReadOffset, 
              m_FileBufferSize - m_FileReadOffset);
           m_FileBufferSize = m_FileBufferSize - m_FileReadOffset;
           m_FileReadOffset = 0;

           len = m_FileBufferMaxSize - m_FileBufferSize;
           if(!feof(m_fp)) {
               cou = (int)fread(m_pFileBuffer + m_FileBufferSize, 1, len, m_fp);
               m_FileBufferSize += cou;
           }
       }
   }

   if(m_FileReadOffset + 1 >= m_FileBufferSize) {
       return -1;
   }

   // 改行コードの位置まで進める
   pPos = m_pFileBuffer + m_FileReadOffset;
   
   len       = 0;
   offsetPos = m_FileReadOffset;
   pPos2     = pPos;
   while(offsetPos < m_FileBufferSize && len < MaxSize) {
       // 改行コードの検出
       chDat = *pPos2;
       if(chDat == '\r') {
           chDat2 = *(pPos2 + 1);
           if(chDat2 == '\n') {
               offsetPos += 2;
               len += 2;
               break;
           }
           offsetPos++;
           len++;
           break;
       } else if(chDat == '\n') {
           chDat2 = *(pPos2 + 1);
           if(chDat2 == '\r') {
               offsetPos += 2;
               len += 2;
               break;
           }
           offsetPos++;
           len++;
           break;
       }
       pPos2++;
       offsetPos++;
       len++;
   }
   if(len >= MaxSize) return -1;

   m_FileReadOffset = offsetPos;

   if(pPos == pPos2) {
       *pRetLine = '\0';
       return 0;
   }

   // 文字列をコピー
   len = (int)(pPos2 - pPos);
   memcpy(pRetLine, pPos, len);
   *(pRetLine + len) = '\0';

   return len;
}

バッファリングはよく使うテクニックではありますが、ファイル読み込みなどのI/Oが発生するものは極力ハードウェアアクセスが頻繁に起こらないようにする(ようはキャッシュする)、が結構有効です。

Shadeでのファイル読み込みが軒並み遅いのも、おそらくここが原因の1つかと思われます(hdr読み込みは上記のradsite.lbl.govのサイトを参考にして実装してるんですかね、それ以上に重い気がしますが・・・)。

ためしにdxf読み込みなどを自前のインポータプラグインとして実装すると、標準読み込みの10倍速とかできてましたので全体的にもっと速度アップが可能なはず。このサイトで公開しているmqoインポータfor Shade9/10も、自前バッファリングしたほうが速そうだなぁ。そんな大きなデータの読み込みはないので放置してましたが、また対応しておきます。

光沢反射と粗さ(2008/04/11)

昔作ってたものを焼き回しただけですが、反射モデルのSchlickでの光沢と粗さの表現。

手前の3つの球が左から「光沢1.0 / 粗さ0.0」「光沢1.0 / 粗さ0.2」「光沢1.0 / 粗さ0.5」になってます。reflectionで「反射」という表現にすると区別がややこしくなるので、光沢(glossy)で鏡面も含む反射を表現、粗さ(Roughness)はツヤ消し効果、鏡面反射(specular)で正反射、をロシアンルーレット。

このへんの反射パターンはレンダラによってルールは違うと思うので、シェーダー的にカスタマイズできるようにしておけばいいかな。これで鈍い反射と磨りガラスはOKです。後は、、、SSSか・・・。

上記のレンダリング時間は、Pentium4 2.8GHz/Mem 512MBのWinXPマシンにて512x512ピクセルで1ピクセル300サンプリング、で1511秒(25分)になってます。純粋パストレですが正直遅い(^_^;;。ぜんぜん最適化してないので、これから間接照明部分にてはしょっていきます。背景のIBLをフォトンで表現してみたのですが、、、厳しいか。背景の反映はもうちょっときれいに出そうな理論を練ってみよう。

なんとなくできそうなこととして、

  • 分光
  • フォトンを集める輝度推定で精度が足りなければパストレを使うハイブリッド

2番目は、一次・二次レイくらいはパストレでその後は間接照明部分でフォトンを拾おうと思ってますが、フォトンでまかないきれていない空間がどうしても出てしまう場合に、それを判断してその部分だけパストレで補えないかなぁと考えてます(というよりも逆で、基本パストレで最適化のためにちょっとフォトンを集める程度)。

うまいこと空気間がGIパラメータをいじらずに出すことができればしめたものですが、、、フォトンの設定は手間過ぎる(直感的でない)のでなんとか除外したいところではあります。

アンビエントオクルージョン(2008/04/10)

アンビエントオクルージョン(AO)の実験です。以下はマニュアルどおりに 遮られる濃度を「ヒット数/サンプリング数」のfloat値で与えたもの。AOにおける1レイでのサンプリングは100。512x512ピクセルにて、AO計算で100秒かかりました(Pentium4 2.8GHz/Mem512MBのWinXPにて)。レンダリング全体では190秒ほど(1ピクセルでのサンプリングは16)。結構かかってますね。左がアンビエントオクルージョンマップ、右が合成結果です。

アンビエントオクルージョンは、スクリーンの1ピクセルに対してレイを飛ばし、その交差した交点位置(交差位置X、法線N)より半球上に複数のランダムな向きのレイを飛ばして背景にたどり着いたか物体にぶつかったか、の割合を最終的なピクセル色にかけて影をやわらかくする手法です。ですので、間接照明は考慮してません。

AOの前処理として、スクリーンの縦x横ピクセル分のバッファを作成してそれに対して濃度値を入れていきます。その後、直接照明のみのレンダリングを普通のレイトレースにておこないます。ピクセル色が決定した段階で、AOバッファの情報と合成(乗算)します。

今度は、背景のhdrのRGBを累積したAOバッファ(1ピクセルでRGB要素を持つ)を計算して、それを最終結果に反映してみました。同じく左がアンビエントオクルージョンマップ、右が合成結果です。

光源は背景のhdrのみです。GIの結果に多少近づいてますね。レンダリング時間は210秒ほど。

ただ、思ったよりも速くないです。AO計算で半分以上は負荷がかかってますねぇ。で、おそらくですがマニュアル通りのAOはしないのが普通かと。やるのなら、各形状の頂点ごとに半球上にレイを飛ばして環境色を計算して蓄えておく、それでレンダリング時に環境色を補間、がスマートかもしれません(ノイズも軽減できるし計算も速い)。これなら反射・透過物体がないのなら、AO計算後はリアルタイムで(ラスタライズで)対処できますしね。

なので、頂点色を持たせる構成というのがフェイクでは必要かなぁと思ったりしてます。

このAOに関しては実験ですので実際は入れ込まないですが、簡単ですので(後、フェイクGIではよく使うので)表現手段としてはアリかなぁとは思います。Blenderで最近流行り?のGIもAOですね。

テストレンダリング(2008/04/09)

hdrもお借りしたもの、形状もお借りしたものですがレンダリング。

IBLを使えば背景次第でそれっぽくなるのであんまりGIの参考にはならないですが、これをリファレンスにして速度アップをしていくことに。

今は単純なパストレなんで、これだけでもものすごく時間がかかってます。Maxwellだと10分もかからないかな、たぶん。追いつけるだろうか、それよりも背景のIBLをフォトンで表現するためのサンプリング方法はCG Magicにも乗ってましたが、どこまでをはしょって品質を維持するかが難しいところです。基本的にパストレのノイズは好きなので、この方向で進めますが何次レイから先をフォトンに置き換えるかは試行錯誤がいりそう。フォトンはあんまり使いたくないのですが・・・。

ちなみにマルチレイヤでテクスチャを持つことができ、かつ複数のUV層を1つのオブジェクトで持てる構成にしています。また、マテリアルはオブジェクト単位にも持てますし、ポリゴン単位にも持てる構成にしました。ですので、メタセコのインポートがスムーズにできるようになってます(レンダラにmqoのインポート機能を内蔵)。

上記も形状はmqoから読み込んでます。片面のみ可視にするというのができないか試したのですが、GIの場合はデフューズ反射も頻繁に行われるため面の裏からのレイの衝突は普通に起こるんですよね。なので、整合性を考えると片面だけ素通りさせる、の対応は難しいかな。

後、見て分かるように白い部分(地面は実は真っ白な設定)にて背景の色が反映されてしまうのが目に付きます。本来ならそうはならないと思いますので、デフューズ(物体色)以外で別途デフューズアルベド(拡散反射としての反射率)はパラメータで持たすほうがいいかなぁ。

放射輝度(raddiance)での微小立体角(2008/04/09)

今までパストレ計算で多々勘違いしているところがあったのでメモ書き。パストレースレンダリングで出てくる計算式にて以下があります。点xにおける、入射するレイのベクトルが\vec{w}'のときの出ていく放射輝度(raddiance)の計算です。

Lo(x, \vec{w}) = Le(x, \vec{w}) + \int_{\Omega} fr(x, \vec{w}', \vec{w}) Li(x, \vec{w}') (\vec{w}' \cdot \vec{n}) d\vec{w}'

Leが発光(emission)の放射輝度、frはランバート面でのBRDF関数、Liは入射する放射輝度、\vec{w}'は入射するベクトル、\vec{w}は出ていくベクトル、\vec{n}は対象点xでの法線。

まずはランバート面でのBRDFですが、それぞれRGBの要素別に考えるとして

fr(x, \vec{w}', \vec{w}) = DiffuseAlbedo / \pi

「DiffuseAlbedo」は、デフューズ面における反射率になります。1.0だとすべて反射。これは色にも依存しますので、今はDiffuseAlbedo = Diffuse色(物体色)として考えます。真っ白だと全反射、真っ黒だと全吸収。あくまでもデフューズでの半球での反射の反射率になるので、鏡面反射または全反射のものとは別と考えてください。これをπで割ったものが、デフューズでのBRDFになります。

で「d\vec{w}'」の存在を今まで忘れて実装してました。これは「微小立体角」というものです。パストレースでは、1つの点xに入る光(raddiance)を無限方向から取り入れるとすると半球上に面ができることになります。計算では積分になりますので、この部分は半径1.0の半球の表面積を入れることになります。つまりは4π * 0.5 = 2π。球の表面積は「4πr^2」ですのでこれの半分です。

ということで1つのレイにおけるraddianceの計算式は

L' = fr(x, \vec{w}', \vec{w}) Li(x, \vec{w}') (\vec{w}' \cdot \vec{n}) 2\pi = DiffuseAlbedo \cdot Li(x, \vec{w}') \cdot (\vec{w}' \cdot \vec{n}) \cdot 2.0

になります。これを再帰的に乗算すると1つのレイにおけるraddianceが求まります。でも、BRDFは分離して考えたほうがすっきりしそうではありますね。

ごくごく単純なパストレースのレンダリング画像です。光源はhdrでのIBLのみ、ガンマを2.2にしてみました。

これで正しいかなぁ?とりあえずリファレンスがほしいがための実装ではあります。コーレルボックスもやってみないと。

hdrとgamma(2008/04/06)

仕様を勘違いしていて、間違えて報告入れてしまったものを掘り返してみます(^_^;;。ガンマについて(実は今の今まで理解はしてませんでした)。

IBLまたは背景テクスチャとしてダイナミックレンジを持つhdrを採用場合に、Shadeで全体的に暗い、という現象に出くわした人が多いはず。hdrをビュワーで見たらもっと明るいのになぜ?というのが疑問の出発点でした。

hdrフォーマットの場合はガンマは入っていない生データ(RGBと指数部のEで表現されます)で表現されています。これをそのままディスプレイで表現してしまうと、Shadeの背景にhdr画像を貼り付けたように「なんか暗い」となってしまいます。RGBEは4バイトのデータでこれで1ピクセルをあらわします。内部的にRGBEからfloat型のRGB(各要素が1.0以上の値を持つこともありうる)に変換されます。レンダラ内部ではこのfloat型RGBを使うと、計算的には正しいはず。

では、なぜ暗くなるのかというとhdrフォーマットではガンマを入れてないから。自前レンダラでの実験画像です。直接照明のみのガンマ無考慮のもの(わかりやすいように標準レイトレース+アンチエイリアスなしです)。

暗いですよね。

ガンマの計算は元の画像によりますが、2.2の補正値を与える場合が多く これは大元の画像を撮影したカメラによって1.8であることもありえるそうです。

1ピクセルの色をRGBで与えた場合、2.2のガンマ補正をかけるとすると

R' = R ^ (1.0 / 2.2)
G' = G ^ (1.0 / 2.2)
B' = B ^ (1.0 / 2.2)

での(R', G', B')が最終的にディスプレイに表示する色になります。hdrを採用したレンダラはこれを加える必要があります。

hdrの画像のみをガンマ補正してレンダリングした例(これは間違いの例です)。

背景のみガンマ補正したので、せっかくのダイナミックレンジの情報は吹っ飛んでしまいます。直接照明だけのシーンなら不都合は起こらないですが、間接照明を入れたときは明るい部分が死んでしまいます。

では、レンダリングの最後の最後、1ピクセルのRGBを反映するときにフィルタ的にガンマ補正をかけたらどうなるでしょうか?

背景はガンマ補正した上記のものと一致しましたが、レンダリング結果も全体的に白くなってます。Shadeの色補正でガンマがありますが、それを2.2にしたらおそらくこれと同じようになると思います。なんで、Shadeでhdrを使ったIBLを行う場合は、ガンマをいじる必要が出てきます(というのに最近気づいた)。

じゃあ、テクスチャを貼り付けた場合にそのテクスチャはガンマ補正前のもの?後のもの?と考えていくと、いたちごっこの予感(おそらくディスプレイに表示したものと一致するように用意すると思うので後者が多いかもね)。ということで、基準が定まらないです。

Blinn-Phongシェーディングでは輝度計算は近似ではありますが、そもそもガンマを内包しているのかどうかは知らないです。見た目上で色合いが合うように調整されているとしたら、ガンマを考慮しているといえるのかな。そうすると、hdrを使った場合に矛盾が出てしまいます。妥協案は、元のデフューズやスペキュラのRGBもガンマなしのものを指定すべし、というなんともローテクな話になりそうです(^_^;;。

しかし、こんなところでガンマに右往左往するとは考えもしませんでした(汗)。自前レンダラではガンマ計算の機能を持たせてますが、デフォルトを2.2にしよう。しかし、理論上の色とディスプレイにトーンマップされた色や写真や目で見た「色合い」などを考えると、色変換を複数通るためなかなか難しい問題ではありますね。

ライブラリ(2008/04/05)

最近は集中しているので土日もプログラムしてます。出来る限り会話を抑えてバリアをはりたい・・・・。仕事に関してはなんとか一本化して無駄を省いた感じです(複数持ってはいるのですが、スケジュールに沿ってシリアルにすすめるようにしてます。しばらく数年は予定が入っている・・・・ありがたいことではありますけど(^_^;;)。つくづく自分は複数のことを並列してできないなぁと思ったり。

ということで、今までの知識の集大成的にライブラリを作成中。fbxはマテリアル部分がどうも自分の作ってるものと整合性が取れないので(UVの持ち方がどうも気になります)、ちょっと離れて考えてみることにしました。

個人的にはシーン構成は

  • マテリアルリスト
  • テクスチャリスト
  • オブジェクト階層(光源、メッシュ、スケルトン、カメラなど)
  • レンダリング情報
  • 背景情報
  • アニメーション情報

のように分かれていて、マテリアルはオブジェクトのメッシュからインデックス参照される、がいいと思ってたりします。

ただ、問題がUVの存在なんです(もちろん、1オブジェクトに複数のUVレイヤを持てるようにしてます)。UV自身はメッシュのジオメトリとして持たせるのがいいと思ってるのですが、マテリアルがマルチレイヤだとするとマテリアル内にてオブジェクト内のUVインデックスをふる必要が出てしまいます。こうすると、マテリアルとジオメトリが密になりすぎますので、マテリアルは他のメッシュで共有できないほうがいいのではというfbx方式が妥当なのかなぁ。

ちなみにShadeのfbxインポータ、エクスポータはかなり機能を限定してどうにか動くのはできてます。が、あまりにもカスタム情報が多くなり汎用的じゃないので内部的に使うことにします。もっといいシーンフォーマットがあればいいのですが・・・、fbxも足りない機能をユーザパラメータを追加して補っていくとどうも方言が生まれてしまいますね。

後、形状としてはメタセコのmqoをインポートできる仕組みも入れてます。mqoはすっきりしてるので入れやすいです。

ちなみに今回は仕事と私事を混ぜたライブラリですので、忙しくなってストップすることはない、はず。どういう運用ですすめようかというのはまだ未定です。進行次第ある程度は情報公開していくようにします(特に真新しいことはしてませんので、クローズドにする必要もないですし)。

Future's Laboratory 技術格納庫 2004-2013 Yutaka Yoshisaka.