Hashed-Octree(ハッシュ化八分木)による空間分割を実装した

そろそろレイトレがしたい。具体的に言えばフォトンマッピングでGIを実装したい。

だが、フォトンマッピングを実装するには現状あまりにも足りないものが多すぎる。

その最たるものが、高速な衝突判定アルゴリズムだ。

シンプルに設計されたレイトレ専用のアプリケーションなら、レイの衝突判定は、シーン中の全てのオブジェクトに対して総当たり的にやってもいいだろう。メッシュも、プリミティブな形状に限定すれば、高速に処理できる。

しかし、現在私が作ってるのは曲がりなりにもリアルタイムレンダリングエンジンだ。シーンの大きさや配置されるオブジェクトの数は固定ではない。現実的な処理時間で動作させなくてはならない。また、メッシュも多くのポリゴンを有する複雑なモデルが存在するかもしれない。
そのためには、レイの衝突判定において、衝突しないことが自明なオブジェクトに対しては処理をスキップするような仕組みが必要である。

このような仕組みで最も直感的なものは、ユニフォームグリッドによる空間分割だろう。
シーン全体を一様なグリッドで分割し、所属するグリッド内のオブジェクトのみで衝突判定を行う。
ただし、この分割法には多くの問題がある。まず、グリッドの横断が想定されていない。空間の分割の粒度をどれくらいにするにせよ、位置、サイズによっては必ず複数のグリッドにまたがるオブジェクトが出現するはずである。基本的には一つのオブジェクトが複数のグリッドから参照される形になるだろう。また、メモリ効率も悪い。一般に、空間というものは疎なもので、基本的には何もない。そして、偏りがあるものである。例えば、空中にオブジェクトがあることはまれだが、あるテーブルに着目した時、そのテーブルの上には多くのオブジェクトがあることが期待される。このような分布の空間において、一様なグリッドで分割するのはあまりにも非効率である。

このようなケースにおける最適な空間分割法として、八分木(Octree)がある。
まず、ルートの空間を定義する。次に、X-Y-Zについてそれぞれ2分割する。分割後の空間を空間レベル1(L=1)として、同様の操作を任意の空間レベルまで行う。

ユニフォームグリッドとの大きな違いは、空間レベルという概念があること。「グリッドをまたぐ」というのは存在せず、仮にグリッドをまたぐ場合は、それは1ランク上の「親空間」と呼ばれる、より大きな分割単位の空間に所属する仕組みになる。また、グリッドの番号にはモートンオーダーを用いることで、ビット演算を駆使してあるオブジェクトの属するすべての空間レベルのグリッド番号を取得することも可能である。


この辺の細かい説明は、すでに必要十分な説明を記載しているサイトがいくつもあるので省略することにする。
その8 4分木空間分割を最適化する!

ちなみに、アルゴリズム的には木構造であるが、モートンオーダーに対して空間レベルの等比数列の総和のオフセットを付加することで、任意の空間レベルの任意のグリッドのオーダーをハッシュ化することができる。
上記のサイトでもそれは行われているが、実装が配列になっているため、結局常に (8^L - 1 / 7)の要素数の配列の動的確保を要求していて効率が悪い。 std::unordred_mapのようなハッシュマップを使えば、疎な空間における効率的なメモリ確保が実現できるだろう。



実装にあたって、八分木分割後のビジュアライザーが必要だと感じたので、そちらもついでに作った。
空間座標から所属するモートンオーダーの算出方法は上記サイト様に記載のある通りだが、モートンオーダーから空間座標を逆算する計算が不明だったので、ここに記しておく。

AABB CalculateOctreeBoxAABBFromMortonNumber(uint32_t number) {
    int level = 0;
   // ハッシュ値から所属する最小空間のモートンオーダーに変換
    while (number >= std::pow(8, level)) {
        number -= std::pow(8, level);
        level++;
    }

    uint32_t s = 0;

    for (int i = level; i > 0; i--) {
        s = s | (number >> (3 * i - 2 - i) & (1 << i - 1));
    }
    uint32_t x = s;

    s = 0;
    for (int i = level; i > 0; i--) {
        s = s | (number >> (3 * i - 1 - i) & (1 << i - 1));
    }
    uint32_t y = s;

    s = 0;
    for (int i = level; i > 0; i--) {
        s = s | (number >> (3 * i - i) & (1 << i - 1));
    }
    uint32_t z = s;

    // _rootAABB.size: ルート空間のサイズ。空間レベルで割って所属する空間レベルの分割サイズを求める
    Size3D boxSize = _rootAABB.size() / (1 << level);
    // _rootAABB.bpos: ルート空間の開始座標
    Vector3D bpos = Vector3D(x * boxSize.w, y * boxSize.h, z * boxSize.d) + _rootAABB.bpos;
    // 所属するAABB
    return AABB(bpos, Vector3D(bpos.x + boxSize.w, bpos.y + boxSize.h, bpos.z + boxSize.d));
}


これでハッシュ値から、所属するAABBを求めることができる。
戦略的には3bitごとに区切られたX-Y-Zのビットに着目して、特定の次元について切り詰める。例えば、110111001というモートンオーダーをXについて切り詰めると、011だ。今回の場合は空間レベルは最大8に設定したので、24桁のビットがある。もう少し効率的な方法がある気もするが....

実際に計算して可視化してみた。
各オブジェクトが所属する最小八分木空間を計算した後は、それと同じサイズのボックスメッシュを生成し、ワイヤーフレーム描画してそれっぽく見せている。


f:id:riyaaaaasan:20180328232059p:plain


同じオブジェクトでも、位置によってはグリッドをまたいでしまうため、より大きなAABB(より高いレベルの空間)に所属している様子が分かる。


これで空間内の衝突判定の準備は整った。次は、レイが空間を通過するときの衝突リストの作成だが...いまいち効率的な方法が思いついていない。

Variance Shadow Maps(分散シャドウマップ)を実装した

Variance Shadow Maps(分散シャドウマップ)を実装しようと思う。
これはソフトシャドウの実装の一つで、シャドウのエッジをなんかいい感じにする技術だ。

まず、普通のシャドウマップの実装を再掲する。

bool IsVisibleFromDirectionalLight(float4 shadowCoord) {
    float w = 1.0f / shadowCoord.w;
    float2 stex = float2((1.0f + shadowCoord.x * w) * 0.5f, (1.0f - shadowCoord.y * w) * 0.5f);
    float depth = DirectionalShadowMap.Sample(ShadowSampler, stex.xy).x;

    if (shadowCoord.z * w <= depth + 0.00005f) {
        return true;
    }
    return false;
}


まず、ライト視点のカメラを仮定してパースペクティブ・ビュー行列を作成し、シーンの深度を保存する。
そして、実際のレンダリングパスで対象の頂点を再度ライト視点に復元、格納済みの深度と比較してシャドウ判定を行う。

格納された深度よりも描画対象の深度が低ければシャドウなし、高ければシャドウありだ。

しかし、この2値的なアルゴリズムだと、どうしてもエッジが硬い違和感のあるシャドウになってしまう。これは、シャドウマップの解像度が低くなればよりジャギーが発生し違和感が顕著になる。

リアルの世界のシャドウと言えば、硬いエッジものはそうは見かけない。なぜなら、光源が面積を持っていたり、あるいはシャドウに間接光が入射するなど、様々な影響が存在するからだ。しかし、残念ながら現在はそのどちらも実装できていないので、シャドウのエッジを”誤魔化す”ことにする。

そこで登場するのがVariance Syadow Mapsだ。この手法は、物理的には正しくないが、それなりに見栄えの良いソフトシャドウを作ることができる。更に、これの優れた点は、現状多く存在するシャドウの実装と容易に組み合わせることが可能な点だ。

以下に示すのは、Variance Shadow Maps(以下VSM)の実装に使われる、確率論分野のチェビシェフの不等式である。

{ \displaystyle
E(x) = \int_∞^{-∞} xp(x)dx
\\
E(x^2) = \int_∞^{-∞} x^2p(x)dx
\\
μ = E(x)
\\
σ^2 = E(x^2) - E(x)^2
\\
P(x{\geq}t) {\leq} P_max(t) {\equiv} \frac{σ^2} {σ^2 + (t - μ)}
}

ここでtが確率論でいう標本、μが平均値、σが分散である。
これをシャドウマップに適用するのが、Variance Shadow Mapsだ。確率論の不等式をシャドウマップに適用というと突拍子もない話だが、この式によって得られる「0~1に正規化された確率」「影の濃さとして適用する」と言えばなんとなく想像はつくだろうか。

ここで、チェビシェフの不等式に使う平均値(期待値)、分散だが、これは局所平均値を用いることで計算できる。
その計算方法とは、すなわちシャドウマップにぼかしフィルタを適用した後の値だ。例えば、移動平均フィルタのカーネルは一様分布であるので、そのフィルタを適用した後の各テクセルの値は局所平均値になることは自明であろう。(元論文)http://www.punkuser.net/vsm/vsm_paper.pdfでは、ガウシアンフィルタを使うようだ。

シャドウマップには現状、深度ステンシルバッファを使っている。残念ながらこれは最も最適な手法だが融通が利かないので、深度バッファではなくレンダーターゲットに焼き付ける手法でシャドウマップを作成する。レンダーターゲットを使えば、1つのピクセルにつきrgbaの4つのスロットを使うことができる。rに深度を、ついでにgに深度の2乗を格納しておくことで、まとめてE(x), E(x^2)を計算することができる。

というわけで、DirectXで実装してみよう。DirectXには、GPU上のリソースに手を加えるという軟弱な機能はないので(CPUにマップすれば可能だが無駄すぎる)、ガウスフィルタを適用したいテクスチャと同じサイズのビューポートを作り、そこに画面いっぱいに板ポリゴンを配置、ガウスフィルタを適用しながらその板ポリゴンを描画すると、レンダーターゲットにガウスフィルタの適用したテクスチャのできあがりだ。

レンダーターゲットに深度を焼きこむピクセルシェーダは省略でいいだろう。zとz^2を出力するだけだ。

ガウスフィルタは、X-PassとY-Passに分離できるので、2つのシェーダーを作り2パスに分けて描画することにする。

{ \displaystyle
p(x_a, y_j) = \frac{1}{N} \sum_{i} exp(-\frac{(x_i - x_a)^2}{2σ^2}p(x_i, y_j))
\\
p(x_a, y_b) = \frac{1}{N} \sum_{j} exp(-\frac{(y_j - y_b)^2}{2σ^2}p(x_a, y_j))
}

X-Pass

struct pixcelIn
{
    float4 pos : SV_POSITION;
    float2 tex : TEXCOORD;
};

Texture2D txDiffuse : register(t0);
SamplerState samLinear : register(s0);

cbuffer ConstantBuffer : register(b0)
{
    float4 weight1;
    float4 weight2;
    float2 texsize;
}

float4 main(pixcelIn In) : SV_Target
{
    float MAP_WIDTH = texsize.x;

    float3 col = weight1.x * txDiffuse.Sample(samLinear, float2(In.tex) + float2(+1.0f / MAP_WIDTH, 0));
    col += weight1.y * (txDiffuse.Sample(samLinear, In.tex + float2(+3.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-3.0f / MAP_WIDTH, 0)));
    col += weight1.z * (txDiffuse.Sample(samLinear, In.tex + float2(+5.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-5.0f / MAP_WIDTH, 0)));
    col += weight1.w * (txDiffuse.Sample(samLinear, In.tex + float2(+7.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-7.0f / MAP_WIDTH, 0)));
    col += weight2.x * (txDiffuse.Sample(samLinear, In.tex + float2(+9.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-9.0f / MAP_WIDTH, 0)));
    col += weight2.y * (txDiffuse.Sample(samLinear, In.tex + float2(+11.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-11.0f / MAP_WIDTH, 0)));
    col += weight2.z * (txDiffuse.Sample(samLinear, In.tex + float2(+13.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-13.0f / MAP_WIDTH, 0)));
    col += weight2.w * (txDiffuse.Sample(samLinear, In.tex + float2(+15.0f / MAP_WIDTH, 0)) + txDiffuse.Sample(samLinear, In.tex + float2(-15.0f / MAP_WIDTH, 0)));

    return float4(col, 1.0f);
}

Y-Pass

struct pixcelIn
{
    float4 pos : SV_POSITION;
    float2 tex : TEXCOORD;
};

Texture2D txDiffuse : register(t0);
SamplerState samLinear : register(s0);

cbuffer ConstantBuffer : register(b0)
{
    float4 weight1;
    float4 weight2;
    float2 texsize;
}

float4 main(pixcelIn In) : SV_Target
{
    float MAP_HEIGHT = texsize.y;

    float3 col = weight1.x * txDiffuse.Sample(samLinear, float2(In.tex));
    col += weight1.y * (txDiffuse.Sample(samLinear, In.tex + float2(0, +2.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -2.0f / MAP_HEIGHT)));
    col += weight1.z * (txDiffuse.Sample(samLinear, In.tex + float2(0, +4.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -4.0f / MAP_HEIGHT)));
    col += weight1.w * (txDiffuse.Sample(samLinear, In.tex + float2(0, +6.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -6.0f / MAP_HEIGHT)));
    col += weight2.x * (txDiffuse.Sample(samLinear, In.tex + float2(0, +8.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -8.0f / MAP_HEIGHT)));
    col += weight2.y * (txDiffuse.Sample(samLinear, In.tex + float2(0, +10.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -10.0f / MAP_HEIGHT)));
    col += weight2.z * (txDiffuse.Sample(samLinear, In.tex + float2(0, +12.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -12.0f / MAP_HEIGHT)));
    col += weight2.w * (txDiffuse.Sample(samLinear, In.tex + float2(0, +14.0f / MAP_HEIGHT)) + txDiffuse.Sample(samLinear, In.tex + float2(0, -14.0f / MAP_HEIGHT)));

    return float4(col, 1.0f);
}

フィルタの重みは以下のように計算できる。

struct GaussianCBuffer {
        float weight[8];
        Size texsize;
    };
    AlignedBuffer<GaussianCBuffer> buf;

    float total = 0;
    constexpr float disperision = 10.0f;
    for (int i = 0; i < 8; i++) {
        float pos = 1.0f + 2.0f * (float)i;
        buf.weight[i] = std::expf(-0.5f * pos * pos / disperision);
        if (i == 0) {
            total += buf.weight[i];
        }
        else {
            total += 2.0f * buf.weight[i];
        }
    }

    for (int i = 0; i < 8; i++) {
        buf.weight[i] /= total;
    }

ここでdisperisionパラメータは、シャドウの見栄えがよくなる適切なパラメータを指定する。

このシェーダーで描画されたシャドウマップのrとbを使って、"影になる確率"を求める。(実際には確率を濃度として使うのでこの表現には語弊がある)

float GetVarianceDirectionalShadowFactor(float4 shadowCoord) {
    float w = 1.0f / shadowCoord.w; // 頂点シェーダでGPUは勝手にwでx, y, zを割る。その再現
    float2 stex = float2((1.0f + shadowCoord.x * w) * 0.5f, (1.0f - shadowCoord.y * w) * 0.5f); // -1 ~ 1を 0 ~ 1にマッピング
    
    float2 depth = DirectionalShadowMap.Sample(ShadowSampler, stex.xy).xy;

    float depth_sq = depth.x * depth.x; // E(x)^2
    float variance = depth.y - depth_sq; // σ^2 = E(x^2) - E(x^2)
    variance = min(1.0f, max(0.0f, variance + 0.0001f));

    float fragDepth = shadowCoord.z * w;
    float md = fragDepth - depth.x; // t - μ
    float p = variance / (variance + (md * md)); // σ^2 / (σ^2 + (t - μ)^2)

    return max(p, fragDepth <= depth.x); // P(x >= t)を満たすときのみ
}


あとはこれで得られるファクターを、ライティング結果に掛けるだけだ。


VSM実装前と実装後の比較画像を貼る。

VSM
f:id:riyaaaaasan:20180315215626p:plain

Not VSM
f:id:riyaaaaasan:20180315215615p:plain


うむ。ソフトになった....うん? うん...(論文とちょっと違う感じになってるのが気になる。ソフトというより残像っぽい)

今回はここまで。

Cube Map Reflectionを実装した

Cube Map Reflectionを実装した。

前回、スカイボックスを実装したが、そのスカイボックスのリソースであるキューブテクスチャを環境マップとして用いて、リフレクションを実装することにする。

といっても、一応、拡張性を持たせるために、シーン内に複数の環境マップが存在し、最も近く、小さく精度が高いと期待できる環境マップをメッシュごとに探索し、レンダリング時のリフレクションリソースとして使用する、という仕組みを作った。

その際に、オブジェクトにUUIDを付与する必要性が出てきたので、Boost Libraryにお世話になりオブジェクトのIDを一意に定められるようにした。

#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/random_generator.hpp>

//~~~

std::size_t uuid = std::hash<std::string>()(boost::lexical_cast<std::string>(boost::uuids::random_generator()()));

UUID -> std::string -> std::size_t

の変換を噛ませている。UUIDのフォーマットはXXXXX-XXXXXXX-XXXXXのような形式(桁数は適当)だが、これをそのまま使うにはC++はあまりにも文字列に対して無力なので、ハッシュ化することにする。SHAアルゴリズムを使っているはずなので、 (少なくともVC++はFNV-1アルゴリズムだった。)UUIDの一意性は失われ危険がある気はするが、まあ別にプロダクトを作っているわけではないのでこの辺はざっくり適当でいいだろう。問題が起こったらどうにかする。


さて、肝心のリフレクションだが、すでに実装しているスペキュラ反射の一部を流用しつつ実装する。

{ \displaystyle
F_r ≈ F_ 0 + (1 - F_0)(1 - cosθ)^5
}


これはSchlickの近似を使ったフレネルの公式である。
ここで {F_0} は垂直入射時の反射係数(物理的な正確さを求めるなら、RGBそれぞれの反射能の定数ベクトル)である。マテリアルのパラメータとして与える。cosθは正規化された法線・視線ベクトルの内積で求まる。

さて、ここでリフレクションが起きている頂点の色というのは、屈折方向の色と反射方向の色の合成で求まる。先ほど求めた反射係数と使うと

TextureCube EnviromentMap : register(t2);
SamplerState EnviromentSampler : register(s2);

float FrenselEquations(float reflectionCoef, float3 H, float3 V) {
    return (reflectionCoef + (1.0f - reflectionCoef) * pow(1.0 - saturate(dot(V, H)), 5.0));
}

float3 ReflectionFrensel(float4 posw, float4 norw, float4 eye, float eta)
{
    float3 N = norw;
    float3 I = normalize(posw.xyz - eye);
    float3 R = reflect(I, N);
    float3 T = refract(I, N, eta);
    float fresnel = FrenselEquations(pow(eta - 1 / eta + 1, 2), N, I);

    float3 reflecColor = EnviromentMap.Sample(EnviromentSampler, R);
    float3 refracColor = EnviromentMap.Sample(EnviromentSampler, T);

    float3 col = lerp(refracColor, reflecColor, fresnel);

    return col;
}

このように求まる。ここで、etaというのは物質の屈折率だ。大気の屈折率は1として反射係数を計算している。

さて、これで求まる反射の色だが、これをどのようにPBRに当てはめるか?
一応作っているのは、物理ベースであるので、レンダリング方程式に乗っ取って適用したい。

結論から言うと、現時点での実装では物理的な正しさをこのリフレクションにもたせるのは不可能だ。なぜなら、基本的に現実の物質は多少なりとも光を拡散するからである。

まず、環境マップによるリフレクションとは何か?という定義から考えなくてはならない。
反射が発生するとしている以上は、環境マップは光を放っているということにして、その光が物体に入射しているのだという前提で環境マップリフレクションは実装されている。つまり、環境マップは光源なのである。しかも、テクセル一つ一つが、光源として色をもって光を放っている。その数は横*縦*高さ。

このように、テクスチャを光源と定義してライティングをすることをImage Based Lightingという。

ここで一度原点に戻ってレンダリング方程式を掲載してみよう。

{ \displaystyle
L_{o}(x,\vec{\omega}) = L_{e}(x,\vec{\omega}) + \int_{\Omega} f_{r}(x,\vec{\omega}',\vec{\omega}) L_{i}(x,\vec{\omega}') (\vec{\omega}' \cdot \vec{n}) d\vec{\omega}
}

ここで、右辺第一項は自身が光源として放つ光のことなので今回は無視してよい。
右辺第二項は、(否透過材質であると仮定すれば)入射してくる光と、法線ベクトルと入射ベクトルの内積、そして反射を表すBSDF関数の積について、半球積分したものだ。そう、半球上のあらゆる入射方向についての総和だ。

この理論でいくならば、全テクセルを光源と定義した環境マップにおいては、その半球上に存在する全テクセルについて、ライティングの計算をしなくてはならない。喜ばしいことに、直接光だけの計算で考えるならば環境マップは離散データなので、モンテカルロ法とかそういう手法で頭を悩ませずとも数値積分することができる。

うむ。解決。

とはならない。

非現実的だからだ。

なぜなら、ライティング処理はピクセルシェーダーでピクセルごとに処理するのが最も一般的で精度が高くなるので、全ピクセルで行われる。つまり、メッシュ上の全てのピクセルについて、かつ環境マップの解像度に比例した膨大な量の光源についてライティングをしなくてはならない。そんなもの、今日のGPUでもFPS60/30を保って処理するのは不可能である。

そのため、今回は、屈折ベクトル及び反射ベクトルの先にあるテクセルの2つを光源として、しかもそのまま雑にピクセルカラーに加算するという、PBRもへったくれもない近似手法でごまかすことにしよう。

この近似は、完全鏡面でかつ、全ての光源が無限遠に存在しかつ減衰しないという仮定に即したものとなる。

というわけで、既存のディレクショナル・ポイントライトの拡散・鏡面反射の計算の後に大胆に以下のコードを挿入することにする。

specular += ReflectionFrensel(IN.posw, IN.norw, Eye, 0.2f);


実行すると以下のようになる。


f:id:riyaaaaasan:20180311013600p:plain



球のモデルが実はサッカーボールなせいで地味に凹凸がうまれている。

さて、次回の予定は未定だ。PRTか、ランタイムシェーダーコンパイルか....うーむ。何を実装しよう。

SkyBoxを実装した

 SkyBoxを実装した。

 といっても、リフレクションの前哨戦みたいなものだが。

 リフレクションは基本的には環境マップで実装される。理想的には、そのオブジェクトを中心とする環境マップを一個一個作ることだが、動的な環境マップとなるとあまりにも現実的ではない。そのため、環境マップを入れ子構造にし、リフレクションの精度を高めたいところに小さな環境マップを作成するという方針が、近年のゲームエンジンの基本戦略である。

 さて、そんな入れ子構造の最も外側の環境マップ、それがスカイボックスだ。そして、環境マップとしてだけではなく、シーンの背景としても活用される。

 実装としてはいたって単純で、キューブテクスチャ―と、頂点法線と面の向きを反転した内側向きのボックスを用意する。
 それをシーンを包むように巨大にスケールさせ、ボックスにキューブテクスチャをマッピングすれば完成だ。とても簡単である。今回の実装にあたって一番苦労したのはボックスモデルの用意だ。手元にちょうど良い.xモデルを吐き出せる3Dモデリングソフトがなかったので、手打ちで作った。

 
 にしても、書くことがないので、仕方なしに文字数を稼ぐために、DirectXのキューブテクスチャの初期化処理でも張ることにする。こんな手続きじみたもの、なんの技術的な価値もないが....。適当に抜粋する。

   // device: ID3D11Device  width: テクスチャ一枚の横幅 height: 縦幅 textures: 6枚のテクスチャ param: テクスチャ

    ComPtr<ID3D11Texture2D> mTexture;
    ComPtr<ID3D11ShaderResourceView> mView;
    ComPtr<ID3D11SamplerState> mSampler;
    std::vector<D3D11_SUBRESOURCE_DATA> initData;


    param.width = textures[0].Width();
    param.height = textures[0].Height();
    param.arraySize = textures.size();

   initData.resize(6);

        for (int i = 0; i < param.arraySize; i++) {
            initData[i].pSysMem = textures[i].get();
            initData[i].SysMemPitch = textures[i].Stride();
        }
    
    D3D11_TEXTURE2D_DESC desc;
    desc.Width = width;
    desc.Height = height;
    desc.MipLevels = 1;
    desc.ArraySize = 6;
    desc.Format = CastToD3D11Format<DXGI_FORMAT>(param.format);
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.Usage = CastToD3D11Format<D3D11_USAGE>(param.usage);
    desc.BindFlags = CastToD3D11Format<UINT>(param.bindFlag);
    desc.CPUAccessFlags = CastToD3D11Format<UINT>(param.accessFlag);
    desc.MiscFlags |= D3D11_RESOURCE_MISC_FLAG::D3D11_RESOURCE_MISC_TEXTURECUBE;

    auto hr = device->CreateTexture2D(&desc, &initData[0], mTexture.ToCreator());
    if (FAILED(hr)) {
        return false;
    }

    D3D11_SHADER_RESOURCE_VIEW_DESC SRVDesc = {};
    SRVDesc.Format = GetShaderResourceFormat(desc.Format);
    SRVDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
    SRVDesc.TextureCube.MostDetailedMip = 0;
    SRVDesc.TextureCube.MipLevels = 1;

    hr = device->CreateShaderResourceView(mTexture.Get(), &SRVDesc, mView.ToCreator());
    if (FAILED(hr))
    {
        return false;
    }

    D3D11_SAMPLER_DESC samplerDesc;
    samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    samplerDesc.MipLODBias = 0.0f;
    samplerDesc.MaxAnisotropy = 1;
    samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
    samplerDesc.BorderColor[0] = 0;
    samplerDesc.BorderColor[1] = 0;
    samplerDesc.BorderColor[2] = 0;
    samplerDesc.BorderColor[3] = 0;
    samplerDesc.MinLOD = 0;
    samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

    // Create the texture sampler state.
    hr = device->CreateSamplerState(&samplerDesc, mSampler.ToCreator());
    if (FAILED(hr))
    {
        return false;
    }

 

ああ...パラメータをDirectXの列挙体やフラグに変換する関数の説明が面倒くさい...(開発中のエンジンからコピペってきたので)
....ので省略する。大事なのはサブリソースの配列にそれぞれのテクスチャの先頭アドレスを突っ込み、それでテクスチャを初期化することだ。
また、テクスチャのMiscFlag、サブリソースビューのViewDimension、それぞれにCubeTextureの設定をする必要がある。そのほかは普通のテクスチャの初期化と一緒だろうか?

ちなみにHLSLでは前回も説明した通り、TextureCubeには位置ベクトルでアクセスする。

TextureCube TextureMap : register(t10);
SamplerState samLinear : register(s10);

float4 main(pixcelIn IN) : SV_Target
{
    return TextureMap.Sample(samLinear, IN.posw.xyz);
}

スカイボックスにはライティングもなにもないので、これだけになる。poswはワールド変換後の頂点座標だ。スカイボックスはワールドの原点に配置するので、ワールド座標がそのまま位置ベクトルとして使える。

レンダリングするとこうなる。


f:id:riyaaaaasan:20180309225919p:plain


360度どこを向いても背景がついた。
現在ライティングがまだまだPBRのレベルに達していないので、実際の写真を使っているスカイボックスを使うと違和感がある....まあ仕方なし。

宙に浮いている謎の球体は、次回スカイボックスのリフレクションに使う。この球体にスカイボックスを映していこうと思う。


 

Omnidirectional Shadow Mapping(全方位シャドウマップ)を実装した

 全方位シャドウマップを実装した。

 資料が少なくて実装が結構手間だった。
 こういうときよく頼りにしているもんしょという方のブログでは、全方位マップは双方物曲面かつディファードレンダリングでの実装だったので、参考にできなかった。私はキューブマップとフォワードレンダリングで実装している。
 双方物曲面で実装したほうが効率的だが、キューブマップの方がアルゴリズムとしては単純なのでこちらを優先して実装した。気が向いたら差し替えようと思う。

 前回も触りだけ紹介したが、全方位シャドウマップとは、ポイントライトのシャドウ実装に使われるアルゴリズムだ。

 ディレクショナルライトやスポットライトは、指向性があるので、そのライトの影響下にある全てのオブジェクトを内含するAABBを定義してテクスチャに深度を書き込み、実際のレンダリングパスでそのテクスチャから深度をフェッチしてきて比較することでシャドウを実装できる。

 一方、ポイントライトは全方向に光を放つので、ライトの影響下のオブジェクトを一つのテクスチャに収めるということはできない。
 そのため、どうにかして全方向の深度を書き込む必要があるのだが、その手法としてキューブマップを使う方法がある。ポイントライトを中心とする立方体の6面のテクスチャを定義して、X軸、Y軸、Z軸それぞれの方向に向かって描画し、全方向の深度情報を保存する方法だ。

 そのため一連のレンダリングのパスは

1. ライトから+X方向のビュー行列を使いステシンシルバッファに深度を描画(レンダーターゲットは必要ない)
2. ライトから-X方向の...
3. 略
4. 略
5. 略
6. ライトから-Z方向の...
7. 実際のカメラから描画、1~6で作ったキューブテクスチャを使いライティング

 となる。つまりポイントライトを置くだけで本来1回の描画キックで済んでいたパスが一気に7倍に膨れ上がる。ちなみに双方物曲面を使った場合はもっと少なく済む。


 ライティングでシャドウを計算するときは、各軸に垂直な正立方体テクスチャという性質を利用して、テクスチャからフェッチしてきた深度と、ライトから対象のピクセルのワールド座標の差分ベクトルの、X、Y、Zの最大値と比較すればよい。ただし、パースペクティブ行列による視推台の対応を考慮する。

 HLSLで実装するとなると、以下のようになる。

bool IsVisibleFromPointLight(float3 posw, int index) {
    float3 pointDir = posw - PLightParams[index].pos;
    float depth = PointShadowMap.Sample(PShadowSampler, normalize(pointDir)).x;
    float3 absVec = abs(pointDir);
    float z = max(absVec.x, max(absVec.y, absVec.z));

    float normZComp = 100.0f / (100.0f - 0.10f) - (100.0f * 0.10f) / (100.0f - 0.10f) / z;
    return normZComp <= depth + 0.005f;
}

 相変わらずいきなり出てくる変数名は雰囲気で察してほしい。
 デプスステンシルバッファに書き込まれる深度は、その際に使ったパースペクティブ行列により圧縮された深度が格納されるので、再現する必要がある。計算式上では、normZCompの計算式がそれにあたる。100がfar-planeで、0.1がnear-planeだ。この計算は厳密にやるなら定数バッファからビュー・パースペクティブ行列を与え計算すべきではあるだろうが、深度のみの計算だけで十分な上、ディレクショナルライトとは違い、この値は基本一定である(と勝手に思っている)ので、直接計算式をぶち込んだ。この方が高速であろう。計算式の意味が分からん人は実際にDirectXパースペクティブ行列を導出してみるべし。

 このブーリアンを返す関数を使ってライティング処理をするかしないかをピクセルごとに判定すれば、全方位シャドウの実装は完了だ。実際にライトの周辺にいろいろおいてレンダリングしてみるとこうなる。

f:id:riyaaaaasan:20180307222323p:plain

 蛇足になるが、テクスチャからフェッチしてきた深度と、光源とピクセルの距離のXYZの最大値を比較するという発想に至るまでもそこそこ時間はかかったのだが、それ以上に昔からずっと存在していたっぽいバグにはまった。

 頂点シェーダで頂点情報を描きだす際、ワールド座標からビュー座標に変換するのはいつも通りなのだが
法線も雑にワールド変換行列をかけて計算してしまっていた。

    float3 nor;
    float4 pos = float4(IN.pos, 1.0f);

    OUT.posw = mul(pos, World);
    pos = mul(OUT.posw, View);
    pos = mul(pos, Projection);
    nor = mul(float4(IN.nor, 1.0f), World).xyz;
    nor = normalize(nor);

    OUT.pos = pos;
    OUT.norw = float4(nor, 1.0f);

 (やたらとfloat4とfloat3を相互変換しているのは、wの値が不定な可能性があるため。そのうちちゃんと1.0fで初期化されるよう定数バッファの処理をリファクタしたい)
 これがバグを起こしたダメなコード。法線をワールド変換しているが、これは明らかに間違いである。
 法線はそもそも純粋な「向き」である。つまり、ワールド変換における、平行移動・拡大縮小・回転のうち、影響を受けるべきなのは拡大縮小・回転だけだ。頂点をワールド変換するとき、平行移動行列をT、拡大縮小をS、 回転をRとしたとき、その頂点の法線は(1/S * R)をかけることで正しく変換される。この辺の説明はほかのしっかりしたサイトを見たほうが早い。
 さて、ワールド変換行列から都合よくそんな成分を抜き出せるかどうか、と言われると、実はめっちゃ簡単なのだ。

 ワールド変換行列の転置行列の逆行列がそれにあたる。

 端的に言えば、転置行列で平行移動成分が消え、逆行列でスケール成分が逆数になる。回転成分はそのまま。

 この行列を定数バッファから与えて法線にかけてあげればよい。

 考えれば当たり前だが、こんな致命的な頂点シェーダのバグを残したまま大きな機能を実装していたせいで、発生したバグの原因の追究に無駄に時間を取られた。反省。もっと基礎の実装のデバッグを重点的にやっていこう。


 次はスカイボックスとリフレクションを実装したいと思う。
 ただし、そろそろコードをカオスになってきたので、リファクタも本格的にやっていきたいところだ。
 もっともネックになっているのは、やはりシェーダか。その性質ゆえ、シェーダの頂点レイアウトやバッファ変更が大きくプログラムに影響してしまう。最も生産的なのは、「プログラム側でシェーダを生成しランタイムコンパイルすること」だ。このシステム構築にはとんでもない手間がかかるが、必ず必要となるはずだ。

 やるべきことが山積みだが、一つ大きな機能の実装を終えたことでとりあえずひと段落ついた感じはある。この全方位シャドウのFeatureブランチをマージしたらDiffがAddition1300 Delete300くらいになってた。結構書いたなぁ。まあ差分が膨れ上がるのも、拡張性が低いクソ設計になっているのが原因なのだが。あーリファクタしたい。

HLSLのTextureCubeの情報が少なすぎる

ランス10やってたら気づいたら土日が終わっていた

さて、ポイントライトの全方位シャドウや、リフレクション用の環境マップの実装をしているのだが

HLSLのTextureCubeの情報が少なすぎる

c++ - Direct3D 11: How to access cube map faces in memory on the CPU side (should ID3D11DeviceContext::Map work with subresources?)? - Stack Overflow

A: CPUでCubeMap読み取りたいんだけどバグる。そもそもマイクロソフトの資料がうんちすぎて、そもそもTextureCubeの6面がメモリ用で連続なのかもわからん。でも常識的に考えて連続やと思うんだけど。誰か教えて。

B:連続やで。多分あんさんのコード間違ってる。



うーん皆苦労してんだなあ。
ただ、GPUメモリ内で連続である確証が得られたのはよかった。私も同様のことをしたいと思っていたから。

環境マップの作成のためには、当然ながら立方体六面のすべての面の描画が必要になるわけだが、残念ながらTextureCubeを使ってバーン!と描画するわけにはいかない。
なぜならば、そもそも根本的に描画する対象が違うからだ。法線、深度、もろもろの情報をMRTを使って一度の描画キックで描画するディファードレンダリングとは根本的に用途が違う。6面描画するなら6回描画しなければならない。つまり、個別のテクスチャとして描画する必要がある。

具体的には6方向のビュー行列を作って、それを定数バッファに格納して6回頂点シェーダを起動する。一般的にはZバッファ...Directx11ではDepthStencilViewの描画テクスチャを、シェーダーリソースとして再利用する形式が一般的。ピクセルシェーダとレンダーターゲットはnullptrを渡せばよい。

適当に一部抜粋

DirectX::XMVECTOR CubeTexture::lookAt[6] = {
XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f), // +X
XMVectorSet(-1.0f, 0.0f, 0.0f, 0.0f), // -X
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // +Y
XMVectorSet(0.0f, -1.0f, 0.0f, 0.0f), // -Y
XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f), // +Z
XMVectorSet(0.0f, 0.0f, -1.0f, 0.0f) // -Z
};
DirectX::XMVECTOR CubeTexture::up[6] = {
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // +X(Up = +Y)
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // -X(Up = +Y)
XMVectorSet(0.0f, 0.0f, -1.0f, 0.0f), // +Y(Up = -Z)
XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f), // -X(Up = +Z)
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // +Z(Up = +Y)
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // -Z(Up = +Y)
};
 
DirectX::XMMATRIX* PointLight::GetViewMatrixes() {
if (_isDirtyMatrix) {
  for (int i = 0; i < 6; i++) {
    DirectX::XMVECTOR pos = XMVectorSet(_point.x, _point.y, _point.z, 0.0f);
    auto dir = static_cast<CUBE_DIRECTION>(i);
    _viewMatrixes[i] = XMMatrixLookToLH(pos, CubeTexture::lookAt[dir],   CubeTexture::up[dir]);
  }
  _isDirtyMatrix = false;
}
return _viewMatrixes;
}


で、あとはこれで得られるビュー行列の配列と、共通の視野角90度・アスペクト比1:1のパースペクティブ行列で描画すれば、6枚分の深度テクスチャが得られるが....
これをCubeTextureに再パックしたい。

D3D11_TEXTURE2D_DESC texElementDesc;
depthTexture[0]->GetDesc(&texElementDesc);
D3D11_TEXTURE2D_DESC texArrayDesc;
texArrayDesc.Width = texElementDesc.Width;
texArrayDesc.Height = texElementDesc.Height;
texArrayDesc.MipLevels = texElementDesc.MipLevels;
texArrayDesc.ArraySize = 6;
texArrayDesc.Format = texElementDesc.Format;
texArrayDesc.SampleDesc.Count = 1;
texArrayDesc.SampleDesc.Quality = 0;
texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texArrayDesc.CPUAccessFlags = 0;
texArrayDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;
ComPtr<ID3D11Texture2D> texArray = 0;
if (FAILED(hpDevice->CreateTexture2D(&texArrayDesc, 0, texArray.ToCreator())))
return;
 
for (UINT i = 0; i < 6; i++)
{
  hpDeviceContext->CopySubresourceRegion(texArray.Get(), D3D11CalcSubresource(0, i, texArrayDesc.MipLevels), 0, 0, 0, depthTexture[i], 0, nullptr);
}


唐突に出てくる変数名とかは雰囲気で。
つまるところ、CopySubresourceRegionを使ってテクスチャを一枚一枚TextureCubeに先頭からコピーしていく。
正直TextureCubeのミップレベルの構造がよくわからんかったので自信がなかったが、うまく動いた。+X-X+Y-Y...と一通りのミップレベル0のテクスチャの後に、ミップレベル1...2...と続いていく形式じゃないのかと不安だった。どうやらただの6枚のテクスチャ配列らしい。

これでCubeTexture自体は実装できたので、さっそくシャドウを実装したいところだが、先にTextureCubeビューワーを作る。兎にも角にも、バグ発生時にシャドウの計算と、深度環境マップの作成のどちらでバグっているのかが分からなかったら話にならないからだ。

HLSLでは、TextureCubeのSampleメソッドはこういうシグニチャになっている。
float4 Sample(Sampler, float3);
Texture2DArrayと同じシグニチャ。つまり、第二引数の三次元ベクトルは、x、yはテクスチャUVで、Zがインデックスだな?と思ったら全然違った

あまりのも情報が少なすぎるので、DirectxのサンプルをあさりまくってTextureCubeを実際に使っているプログラムを探す。すると、引数に渡している引数の変数に代入している式に見覚えのある数式が....あ、これ反射ベクトルの計算式だ。

というわけで、TextureCubeの第二引数はUVではなく反射ベクトルだった。わ、わかりづれぇ..
ちがった。
反射ベクトルを使うのはリフレクション用途で環境マップにアクセスするとき。もっとシンプルに、立方体の中心から面に向かう3次元ベクトルでアクセスする。

これは、単純なテクスチャの表示には不便そうだ。なので、テクスチャビューアーはTextureCubeが渡された時には内部で展開して一枚のテクスチャにする機能を加えることにする。さっきのプログラムの真逆のことをするだけでいい。

D3D11_TEXTURE2D_DESC faceDesc(desc);
D3D11Texture faceTexture;
ComPtr<ID3D11Texture2D> faceTextureSrc;
faceDesc.ArraySize = 1;
faceDesc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_SHADER_RESOURCE;
faceDesc.MiscFlags = 0;
hpDevice->CreateTexture2D(&faceDesc, nullptr, faceTextureSrc.ToCreator());
hpDeviceContext->CopySubresourceRegion(faceTextureSrc.Get(), 
0, 0, 0, 0, texture, 
D3D11CalcSubresource(0, index, desc.MipLevels), nullptr);


これで深度環境マップが正しく描画されているか確認できるようになった。

f:id:riyaaaaasan:20180306004132p:plain


下六枚が部屋の中心に置いたポイントライトのシャドウマップ。
左上のは遥か上空にあるディレクショナルライトのシャドウマップ。
単純計算で全方位シャドウは6倍のコストがかかる。メモリも6倍。うーん、これはほいほいと毎フレーム計算したくないな。

左から順に+X, -X, +Y, -Y, +Z, -Zの面なので、雰囲気的にそれらしく描画はできているようだ。まだシャドウそのものを実装したわけではないので、確信はないが。
今週中にポイントライトの実際のシャドウの計算をHLSLを使って実装していきたい。

今回はこの辺で。









レンダリングエンジン進捗まとめ

 前回のポエムが長くなり過ぎたので二回に分けた。

 

 現在、レンダリングエンジンに対するコミット数は104。

 簡単に振り返ってみる。

 

1日目あたり

 最初の方のコミットは、なんというか、コピペコードばかりだ。酷過ぎる。とりあえず動くところ見るためとはいえ、よそ様のブログのコードをコピペって動かしているのがよくわかる。一応は自分なりに考えてレンダラ、シーン、ビューのクラスを作って雰囲気的に設計を考えてるようだが、かなりお粗末だ。

 

 まず最初に、メッシュという概念を定義して、その派生クラスとして三角形プリミティブメッシュを作ったようだ。そして、それを描画するための一通りの初期化コードと、コンパイルしたシェーダを読み込むためのバイナリローダ、およびリソースハンドラを作っている。リソースの所有権を誰が持つかは当時まったく決めていなかったようで、なぜかレンダラがシェーダの所有権を持っている。

 

 このレベルだと3Dですらない。画面上に三角形を出すだけだ。シェーダも、頂点シェーダは頂点情報をピクセルシェーダに渡すだけだし、ピクセルシェーダも頂点カラーを出力しているだけである。まあ、初期のレンダリングエンジンの足掛かりなんてこんなものだろう。

 

2~5日目あたり

 一つのオブジェクトにつき一つ作られるドローエレメントという描画用クラスを定義したり、シーンクラスを真面目につくったり、少しずつ地盤を固めている。

 そして、そろそろテクスチャを描画したくなったのか、libpngライブラリを導入し、pngローダを作成した。どうやらこの時期にようやくカメラという概念を導入し、ビュー変換、パースペクティブ変換を実装し、3D的な描画ができるようになったみたいだ。といっても、2Dポリゴンを傾けるようなレベルだが。

 

 この次辺りでなぜかファイルキャッシュの仕組みを作っている。他にやるべきことがあるだろうに。

 

6日目あたり

 ここでようやく、インデックスバッファを使った描画に対応し始めた。

 

 これ以降からやっとレンダリングエンジンらしい開発を始めている。これまでは、レンダリングエンジンというか、ただのDirectXチュートリアルみたいな状態だった。

 

7日目あたり

 3D描画のテストのためには、描画用のモデルローダを作る必要がある。最も多くの無料モデルが配布されていると言っても過言ではない、PMDモデル(MikuMikuDanceモデル)に着目し、最初にローダを実装した。現在の主流はFBXだが、アレは仕様が複雑怪奇すぎるので、いまだに実装をしていない。Autodeskが配布しているライブラリに依存するのも嫌なので、後回しにしている。

 

 とりあえず頂点とインデックスリストだけを読み込むだけなら、30分程度で作れた記憶がある。意外と簡単にモデルのディテールが表示されて、謎の達成感に包まれた。なにせ、やっと3Dの世界にきたという実感があったからだ。(一応、立方体の描画とかはやっていたが)

 

7日目あたり

 次に、やっとライティングに手をつけた。まずディレクショナルライトを追加し、最も簡単なランバート拡散反射を実装している。正規化頂点法線と正規化光源ベクトルの内積で、そのなす角が求まるので、直接光が当たってるかどうかわかるよねってだけのアルゴリズムだ。ポイントライトも追加しているが、ディレクショナルライトに距離減衰と光源位置を追加しただけのものなので、拡張は楽だった。

 

8~11日目あたり

 最初の挫折はここで味わった記憶がある。ディレクショナルライトのシャドウの実装だ。驚くほど動かなかった。

 アルゴリズムとしては、シーンを覆える視野のライトカメラを仮定して、ライトカメラから見えるシーンすべてのオブジェクトの深度を、ステンシルビューを使って描画する。あとは、実際のレンダリングで、対象の頂点とライトカメラの距離と、深度バッファの保存してある深度を比較し、必要ならシャドウをつける。

 GPU処理の基本、すべてのデータをテクスチャに焼きこんで、複数のパスで共有して使う。その最初の一歩だった。テクスチャには16ビット型無しフォーマットとして深度を描画し、それをシェーダーリソースとしてR8G8符号なしテクスチャとして読み込む。またビュー座標系からテクスチャ座標系へのマッピングも忘れてはならない。

 他にも、カメラから深度バッファを書き込むときはもちろん、実際のレンダリングのときにも、再度頂点をカメラの位置からビュー変換しなくてはならないので、定数バッファにライトの数だけライトのビュー・パースペクティブ行列を格納しなければならない。

 今まで雰囲気でやっていたシェーダ処理に、途端に厳密な実装を要求され、手探りでいろいろと試していた記憶がある。結果的に3~4日かかってしまった。動いたときはそれはもう感動したものだ。

 

14日目あたり

 今更ポイントライトの修正をしたり、あとはマウス操作によるカメラ移動を実装している。インタラクティブな要素を入れた途端、リアルタイムレンダリング感が出た。

 

15日目あたり

 DirectXリソースのメモリリークを解消している。実はこれをするまで、尋常じゃないくらいメモリリークを起こしていて、Warningを吐いていた。全部無視していたが、やっと観念して修正したようだ。

 

 そして、PMDモデルの材質リストに着手するとともに、一つのメッシュが複数のマテリアルを持つという当たり前のことが想定できていなかったせいで、大幅なレンダラのリファクタリングを強いられている。PMDモデルのテクスチャは基本BMPなので、BMPローダの実装も行った。無圧縮フォーマットなので実装は楽勝だった。やっと色付きの初音ミクの表示ができたのである。

 

17~20日目あたり

 鏡面反射を実装している。ここで、ようやく本格的な物理ベースレンダリングのコードを描き始めたことになる。基本的にはクック・トランスの反射モデルを採用し、今まで通り直接光しか計算していない。材質のメタリック値を上げると、スペキュラが3Dモデルに現れるようになった。割と、現実に近い挙動をする。真面目な物理モデルを実装したので。だが、実際のそのアルゴリズムを紹介するには、ここの余白は狭すぎる。というか、自分が参考にしたQiita記事が最強だったので、DirectXの実装的な補足を加えつつその記事を紹介する記事を書きたいものだ。

 

基礎からはじめる物理ベースレンダリング - Qiita

 

 21日目あたり

 PMDモデルだけでは限界を感じてきたので、.xモデルのローダも作成した。バイナリフォーマットは面倒くさそうだし、MSDNの資料も微妙だったので、テキストフォーマットにだけ対応することにした。テキストフォーマットなら、仕様は実際のファイルから読み取ることができる。ダーティーなハックになるが、いろんなファイルをかき集めて、ロードに失敗するたびにファイルの中身を確認し、想定漏れの仕様等を毎回カバーする形で実装していけば、いずれ完全なローダになる。

 

 C++のクソみたいな文字列処理で頑張って実装してもよかったのだが、いい機会だったのでパーサコンビネータを作成した。Boost.Spirit.Qiを採用したが、最終的なアウトプットはまあそこそこ美しい反面、あまりにも開発が苦痛だった。なにせちょっと構文や型を間違えるだけで尋常じゃない量のコンパイルエラーを吐くからだ。人類には早すぎるライブラリな気もする。ただ、ユーザー定義構造体をタプルにバインドして、美しく型安全な構文ルールを定義し、パースする仕組み。なるほど、うまく使えればとても気持ちが良い。私自身はTMPが大好きなので、正直使ってて興奮した。

 

22日目~35日目

 リアルが立て込んだせいで日付が一気に飛んでいるが、主にひたすら.xモデルの仕様網羅と最適化をしていた。

 .xモデルの仕様として、頂点リスト-面リスト-材質リストという順に定義されている。面リストは、一つの面につき複数の頂点への参照を持つ。つまりインデックスだ。そして、材質リストは、面リストすべてのマテリアルを定義する。つまり、面一つ一つが独自のマテリアルを持つことが可能(仕様上)であり、そのまま愚直に実装すると、ドローコールが面の数だけ発生してしまう。

 描画最適化のために、まず面リストを同種のマテリアルでグループ化し、グループ化された面リストを一つのインデックスリストに展開するようにした。こうすれば、ドローコールはマテリアルの数だけで済む。PMDモデルと一緒だ。ただし、面の順序を変える形になるので、面が連続ではなくなる。そのため、ストリップ描画をすると、離れた面へ描画していくとき、その軌跡に謎の三角形が生じてしまう。なので、頂点数4以上の面は、複数の三角形に分解し、三角形リストとして描画することにした。描画が爆速になった。

 

 あとは、モデルによっては頂点法線がデータに入っていないことがあったので、その際の計算も行うようにした。頂点が所属するすべての面の法線を合成した正規化ベクトルが頂点法線になる。ただし、三角形の面の法線はある点から他の二点への位置ベクトルの外積で求まるが、四角形はそうはいかない。対角ベクトル二本の外積を求める必要がある。五角形以上は知らん。

 

現在

 環境キューブマップの実装をしている。描画に使う、6面の周囲情報をベイクしたテクスチャのことだ。これを使うと、ポイントライトの全方位シャドウや、リフレクションが実装可能になる。ポイントライトのシャドウは結構面倒なのだ。また、リフレクションが実装できれば、鏡面に周りのオブジェクトの映り込みが実現できるので、おそらく更に”それっぽさ”が向上するだろう。

 

 

 一か月ちょっとを駆け抜けてみた。結構いろんなことをやったようで、まだ何もしてないようにも思える。シャドウの品質を上げるためのキャスケードシャドウや、間接光計算実現のためのフォトンマッピングアンビエントオクルージョン、そもそもディファードレンダリングすら実装できていない。そのほか、ライトの色情報や、その輝度だったり、そもそもライトが面積を持つことも想定していない。というか、こういう細かいことを挙げていたらキリがない。まともな見栄えのレンダリングができるまで、少なく見積もってもあと半年はかかると思っている。

 

 最終的な目標は、国産のPMX・PMDデフォルト対応の高品質レンダリングエンジンだが、まあ、数年がかりになるだろうし、そこまで到達できるとは思っていない。

 

 あくまで、今は自分が楽しんで、学ぶために開発している。そして、それがほかのレンダリングに興味のある人間の刺激になればなおよい。

 

 振り返りはここまで。次からは、ちゃんとした開発日記をつけていこうと思う。