Pre-Integrated SkinのLUTを作ってみた。

Share

こんにちわ。
Pocolです。

会社でキャラの肌が調整しずらいからどうにかしてくれ!
…と,言われてしまったので,とりあえずPre-Integrated Skinを調べてみようと思いました。

LUTテーブル作るところまでは実装してみました。
出来上がったLUTのテクスチャはこちら。

で,これを作るソースコードは以下のような感じ。


#define STBI_MSC_SECURE_CRT
#define STB_IMAGE_WRITE_IMPLEMENTATION

//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include <cstdio>
#include <stb_image_write.h>
#include <vector>
#include <asdxMath.h>

//-----------------------------------------------------------------------------
//      リニアからSRGBに変換.
//-----------------------------------------------------------------------------
inline float ToSRGB(float value)
{
    return (value <= 0.0031308) 
        ? 12.92f * value 
        : (1.0f + 0.055f) * pow(abs(value), 1.0f / 2.4f) - 0.055f;
}

//-----------------------------------------------------------------------------
//      ガウス分布.
//-----------------------------------------------------------------------------
inline float Gaussian(float v, float r)
{ return 1.0f / sqrtf(2.0f * asdx::F_PI * v) * exp(-(r * r) / (2.0f * v)); }

//-----------------------------------------------------------------------------
//      散乱計算
//-----------------------------------------------------------------------------
inline asdx::Vector3 Scatter(float r)
{
    // GPU Pro 360 Guide to Rendering, "5. Pre-Integrated Skin Shading", Appendix A.
    return 
          Gaussian(0.0064f * 1.414f, r) * asdx::Vector3(0.233f, 0.455f, 0.649f)
        + Gaussian(0.0484f * 1.414f, r) * asdx::Vector3(0.100f, 0.336f, 0.344f)
        + Gaussian(0.1870f * 1.414f, r) * asdx::Vector3(0.118f, 0.198f, 0.000f)
        + Gaussian(0.5670f * 1.414f, r) * asdx::Vector3(0.113f, 0.007f, 0.007f)
        + Gaussian(1.9900f * 1.414f, r) * asdx::Vector3(0.358f, 0.004f, 0.00001f)
        + Gaussian(7.4100f * 1.414f, r) * asdx::Vector3(0.078f, 0.00001f, 0.00001f);
}

//-------------------------------------------------------------------------------
//      Diffusionプロファイルを求める.
//-------------------------------------------------------------------------------
asdx::Vector3 IntegrateDiffuseScatterOnRing(float cosTheta, float skinRadius, int sampleCount)
{
    auto theta = acosf(cosTheta);
    asdx::Vector3 totalWeight(0.0f, 0.0f, 0.0f);
    asdx::Vector3 totalLight (0.0f, 0.0f, 0.0f);

    const auto inc = asdx::F_PI / float(sampleCount);
    auto a = -asdx::F_PIDIV2;

    while(a <= asdx::F_PIDIV2)
    {
        auto sampleAngle = theta + a;
        auto diffuse     = asdx::Saturate(cosf(sampleAngle));
        auto sampleDist  = abs(2.0f * skinRadius * sinf(a * 0.5f));
        auto weights     = Scatter(sampleDist);

        totalWeight += weights;
        totalLight  += weights * diffuse;
        a += inc;
    }

    return asdx::Vector3(
        totalLight.x / totalWeight.x,
        totalLight.y / totalWeight.y,
        totalLight.z / totalWeight.z);
}

//-----------------------------------------------------------------------------
//      ルックアップテーブル書き出し.
//-----------------------------------------------------------------------------
bool WriteLUT(const char* path, int w, int h, int s)
{
    std::vector<uint8_t> pixels;
    pixels.resize(w * h * 3);

    float stepR = 1.0f / float(h);
    float stepT = 2.0f / float(w);

    for(auto j=0; j<h; ++j)
    {
        for(auto i=0; i<w; ++i)
        {
            auto radius    = float(j + 0.5f) * stepR;
            auto curvature = 1.0f / radius;
            auto cosTheta  = -1.0f + float(i) * stepT;
            auto val       = IntegrateDiffuseScatterOnRing(cosTheta, curvature, s);

            // 書き出しを考慮して, 上下逆にして正しく出力されるようにする.
            auto idx = ((h - 1 - j) * w * 3) + (i * 3);

            pixels[idx + 0] = static_cast<uint8_t>(ToSRGB(val.x) * 255.0f + 0.5f);
            pixels[idx + 1] = static_cast<uint8_t>(ToSRGB(val.y) * 255.0f + 0.5f);
            pixels[idx + 2] = static_cast<uint8_t>(ToSRGB(val.z) * 255.0f + 0.5f);
        }
    }

    auto ret = stbi_write_tga(path, w, h, 3, pixels.data()) != 0;
    pixels.clear();

    return ret;
}

//-----------------------------------------------------------------------------
//      メインエントリーポイントです.
//-----------------------------------------------------------------------------
int main(int argc, char** argv)
{
    std::string path = "preintegrated_skin_lut.tga";
    int w = 256;
    int h = 256;
    int s = 4096;

    for(auto i=0; i<argc; ++i)
    {
        if (_stricmp(argv[i], "-w") == 0)
        {
            i++;
            auto iw = atoi(argv[i]);
            w = asdx::Clamp(iw, 1, 8192);
        }
        else if (_stricmp(argv[i], "-h") == 0)
        {
            i++;
            auto ih = atoi(argv[i]);
            h = asdx::Clamp(ih, 1, 8192);
        }
        else if (_stricmp(argv[i], "-s") == 0)
        {
            i++;
            auto is = atoi(argv[i]);
            s = asdx::Max(is, 1);
        }
    }

    return WriteLUT(path.c_str(), w, h, s) ? 0 : -1;
}

ちょっとググって調べた感じだと,https://j1jeong.wordpress.com/2018/06/20/pre-intergrated-skin-shading-in-colorspace/というBlog記事をみて,検証もせずにとりあえず鵜呑みで,sRGBで書き出してみました。
シェーダで使う場合は補正入れてリニアになるようにしてから使ってください。

NaNをなくす

Share

良くライティング計算結果がNaNになってポストエフェクトでバグるというのが頻発していて困っていたのですが,
ようやく最近回避方法を知りました。
StackOverflowとかみていたら,

step(value, value) * value;

みたいにすると,NaNである場合には0になり,NaN以外の場合にはvalueで返すことが出来るらしい…。
と書いてあったのですが,よくよく考えてみるとstep(value, value)のところは確かにゼロとなるので問題ないなさそうに思えるのですが,NaNとの四則演算結果はNaNになるような気がします。実際に試してみたのですが,やっぱり駄目でした。

あとは,Googleのfilamentに

#define MEDIUMP_FLT_MAX    65504.0
#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)

みたいなのが書いてあったので,下記のような実装を試してみました。

static const float HALF_MAX = 65504.0;
clamp(value, 0.0f, HALF_MAX);

結果として,NaNが発生しなくなりバグが解消されるようになりました。

特に物理ベースレンダリングに移行する際に,GGXモデルなどを使うことが多いと思うのですが,定式化されているものが除算を含む形で定義されているので,実装する際に除算をしなくてはならないということが発生します。
GGXの計算する際に,分母がゼロになることにより,ゼロ除算が発生し,計算結果がNaNになる可能性があります。
また,pow()関数を使用している場合は,MSDNのドキュメント にも書いてあるように第1引数が0未満の場合はNaNになり,第1引数・第2引数がともにゼロの場合はハードウェア依存によりNaNが発生する可能性があります。

昔からよくある方法としてゼロ除算を発生させないために,例えば1e-7fなどのような小さな値を分母側に足しておき,ゼロにならないようにオフセットを掛けるという回避方法もあるのですが,この方法は除算には適用できるのだけれども,pow()関数には適用できません。

GGXを用いたライティング計算の場合は,ゼロ除算が発生する可能性があるため最初はこのオフセットを足し込むというやり方を取っていたのですが,これをやってしまうと計算結果が変わってしまい,ハイライトが鈍くなるなど見た目上に影響を及ぼすことが分かりました。
PBRやっているはずなのに,何故かなまった感じの画が出る場合には,下駄をはかせてハイライトを潰してしまっていないかどうか確認するのを強くお勧めします。
下駄をはかせている箇所を下駄を取って普通に除算し,最後に上記のようなNaNをはじく処理をいれることで,ハイライトがなまったり,ライティング以降のレンダリングパスでバグるという問題を解決することが出来ます。また,pow()関数を用いる場合にも適用でき,NaNをなくすることができるので,NaNで困っている方はぜひ試してみてください。

※もっとより良い方法ご存知であれば,是非教えてください!

超雑訳 Fast Ray Tracing by Ray Classification

Share

前回はレイトレ合宿用に論文を読んだのですが,肝心のところが書いてなかったので,今回はレイの分類について詳細が書いてある1987年の論文を読むことにします。
毎度のごとく,誤字・誤訳が多々ありますので,間違いを指摘して頂ける場合は正しい翻訳例と共にご指摘いただけると幸いです。

(さらに…)

最近悩んでいること。

Share

こんにちわ,Pocolです。

たまには日記っぽいことを書いてみようかと思います。
最近の悩みは,シェーダバリエーションの生成とシェーダのマルチプラットフォーム対応どうしようかな?ってことで悩んでいます。
仕事で使っているものは他の方が書いてくれたもので,特に問題なく使っています。
で,「プライベートで使う方はどうしようかな?」ってのが今回の悩みです。
仕事で使っているのと同じものを作るのが手間というのと,
#ifdefのオンパレードが死ぬほど嫌いなので,趣味のコーディングではそういった手法は使いたくないです。
ソースコードが汚くなる。

で,比較的に綺麗に書けたなぁっていうのが,fxファイル。
前職で有名な方もいまだに使っていたというのと,バンジーのスライドだったような気がするのですが,やっぱりfxっぽい記述できるようにしていたんですよね。あとunityもcgfxっぽい感じなので,「やっぱりfxファイルでいいんじゃないの?」って気がして,fxシステム欲しいなぁ・欲しいなぁと思って,ずっと放置していました。
ようやく仕事がひと段落付きそうなので,ちろっと作ってみました。
https://github.com/ProjectAsura/asfxc

あんまり,凝ったツールにすると面倒なので,シェーダの組み合わせだけをとりあえず作るようにしてみました。
組み合わせはxmlファイルで吐き出すので,あとで別ツールにかまして… みたいなことができます。…というかその目的で作っています。
あくまでも,バリエーション生成用のHLSLソースコードと,バリエーションの羅列を出力だけする簡単なツールです。
雑ですが,とりあえずバリエーション生成はこれで解決!

あと残ったのは,マルチプラットフォーム対応ですね。
会社ではHLSLcc使っているみたいなんですが,HLSLccのフラグに関するドキュメントっぽいものがあんまりなくて,ちと不親切。
で他にないかなぁ…って探していたら,ありました。
https://github.com/microsoft/ShaderConductor

Microsoftが作ってくれているみたいなのです。
Metalにも対応しているみたいなので,「あ、これでいいじゃん!」って思ったので,今のところこれで行こうと思います。
これで,マルチプラットフォーム対応も(ほぼ)問題ないです。

シェーダのワークフローは下図みたいな感じになります。

「ほぼ」と言ったのにはワケがって,それでまた悩み中です。
悩み解決の進展があったら,またなんか書こうとかと思います。

深度バイアスについて

Share

お久しぶりです。Pocolです。
最近お仕事の関係で,たまたま他社さんのソースコードを観る機会があったのですが,そのソースコードのコメントに自分のホームページのURLが記載してありました。
で,見てみたら深度バイアスの説明している個所だったのですが…
「これ正確じゃないな…」と今更ながら気づいてしまったので,訂正を兼ねてここで説明することにします。

深度バイアスについては,”Real-Time Rendering Forth Edition”の7.5にまとめられています。

(さらに…)

レイトレ合宿6に参加しました。

Share

こんにちわ。
Pocolです。

今年もレイトレ合宿に参加しました。

今回の開催地は神津島で,海がもの凄く青くてとてもよかったです。

昼はholeさん達とバーガー食いに行きました。


バケツポテトは残念ながら上げ底されています。
夕方になると,ホテルからは夕陽が見られ,とても綺麗でした。

さて,レンダリングの方ですが
順位はブービーでしたが,エースコンバット5好きなので,ブービーでもいいかなと思っています。…というのもの、今年準備が間に合いませんでした。個人的には順位はどうでもいいんです。参加することに意義がある!!
ちなみに描画はこんな感じです。

いくつかのフレームで黒画面が出るのはレンダリングが間に合っていないという仕様です(設計バグ)。

本当はボリュームレンダリングをする予定でしたが,間に合わず提出日当日になって,急遽方針を転換してパストレで行くことにしました。
最終提出日にエラーが発生してレンダリングされないなどの不具合があったので,レンダリングされただけもでも御の字かなと個人的には思っています。(本当黒い画像しか表示されないと思っていました)
Next Event Estimationの実装や交差判定処理箇所の設計見直しなど,1日で出来そうなことはとりあえずやりました。所々バグがあるのはまあ実力無さなので,そんなものでしょう。

今年はアニメーション部門に応募しました。
1フレーム12秒でレンダリングして60枚を出力することに目標にしました。
他の静止画部門の方々と比べるとレンダリング時間は1/10程度なので,ノイズまみれだし,特に何にもやっていないので汚いっちゃ~汚いです。(むしろ,こっちは10倍近くサンプルが少ないので,他の人の方が10倍近く綺麗じゃないとそもそもおかしい)
アニメーション部門に応募してみて,色々と課題とか問題があって難しいなと感じる部分を「面白いな」と錯覚してしまったので,来年もアニメーション部門に応募してみようかと思います。人と同じことをしていても面白みがないので,他の人がやらない分野には積極的に力を注ぐひねくれた人ですね。何よりも静止画よりも動画の方が個人的には血が騒ぎます。やりがいがあるし,やっぱり動くものは面白い。

あと今年はシークレットイベントとして,「レイトレ検定」が行われました。

特に難しかったのはサンプリングを選ばせる問題です。皆さん,これ分かります?

自分は,6問中3問正解できたようでした。まだまだ勉強が足りませんね。
検定の最終結果は77点で,何とか赤点は免れることができました!

来年はもう少し落ち着いてレイトレ合宿に時間を割けると思うので,来年こそは頑張りたいなぁと思います。GPUレイトレーサー書きたいなぁ…
日々是精進ですね。

来年も是非参加したい!
レイトレ合宿運営の皆さん,どうもありがとうございました。

レイトレ合宿6Ω

Share

こんにちわ。
Pocolです。

明日からレイトレ合宿6です。
今年の開催地は神津島ということで,ワクワクしています。

合宿終了後にレポートを上げる予定なので,
楽しみにしていてください。