レイトレーシング 02 メッシュレンダリング

<<チュートリアル01
>>03
GitHubリポジトリ

概要

前回は, 球とレイの交差判定を行い, 球のレンダリングをするところまで行いました.
今回は, 球ではつまらないモチベーションを維持できないため, メッシュのレンダリングまで行います.
まず前段階として三角形とレイの交差判定を説明し, 次に総当たりでメッシュのレンダリングを行ってみます.
好きなものをレンダリングできるので, やる気が出ると思います.

三角形とレイの交差判定

いくつか方法はありますが, ベースは以下のアルゴリズムです.
[Wikipedia]Möller–Trumbore intersection algorithm
[PDF]Fast, Minimum Storage Ray/Triangle Intersection
より高速なものもあるかと思いますが, 重心座標(Barycentric coordinate)を同時に計算できると後で便利なため採用しています.
レイが当たったからといって, 一番手前の三角形かどうかはわからないため, 余計な計算は後回しにするのも設計判断だと思います.
ゲームなどだと特に重心座標は必要ないと思われます.
#define LRAY_TEST_RAY_TRIAGNLE_HELPER_FRONT(ret_fail) \
    Vector3 tvec = ray.origin_-v0;\
    f32 v = dot(tvec, c);\
    if(v<0.0f || discr<v){\
        return ret_fail;\
    }\
    qvec = cross(tvec, d0);\
    f32 w = dot(qvec, ray.direction_);\
    if(w<0.0f || discr<(v+w)){\
        return ret_fail;\
    }\

#define LRAY_TEST_RAY_TRIAGNLE_HELPER_BACK(ret_fail) \
    Vector3 tvec = ray.origin_-v0;\
    f32 v = dot(tvec, c);\
    if(0.0f<v || v<discr){\
        return ret_fail;\
    }\
    qvec = cross(tvec, d0);\
    f32 w = dot(qvec, ray.direction_);\
    if(0.0f<w || (v+w)<discr){\
        return ret_fail;\
    }\

Result testRayTriangleBoth(f32& t, const Ray& ray, const Vector3& v0, const Vector3& v1, const Vector3& v2)
{
    Vector3 d0 = v1-v0;
    Vector3 d1 = v2-v0;
    Vector3 c = cross(ray.direction_, d1);

    Result result = Result_Fail;
    f32 discr = dot(c, d0);
    Vector3 qvec;
    if(F32_EPSILON<discr){
        //Front
        result = Result_Front;
        LRAY_TEST_RAY_TRIAGNLE_HELPER_FRONT(Result_Fail)

    }else if(discr < -F32_EPSILON){
        //Back
        result = Result_Back;
        LRAY_TEST_RAY_TRIAGNLE_HELPER_BACK(Result_Fail)
    }else{
        return Result_Fail;
    }

    f32 invDiscr = 1.0f/discr;
    t = dot(d1, qvec);
    t *= invDiscr;
    return (Result)(Result_Success | result);
}

数学の説明はPDFを確認していただきたいですが, 雑な説明をすると, まずレイが表裏どちらからくるか, または三角形と平行かを判定します.
次に, レイの方向が, レイの原点と三角形の各辺がつくる平面のどちら側にあるか判定します.
下図では, レイの方向は線分$p_{0}p_{1}$とレイの原点$o$が作る平面$op_{0}p_{1}$の裏表どちら側にあるか($ray_{0}$か$ray_{1}$か)判定します.
これは, 3次元なら外積と内積で判定できます. 表か裏か三角形の3辺で全て同じ側ならレイは三角形内部を通っています.

Cプリプロセッサの乱用は避けたいところですが, 表面・裏面だけ判定バージョンを作成するために使用しています.
今時のコンパイラならよろしくやってくれそうではありますが.

前回の球との判定を入れ替えれば, とりあえずの三角形レンダリングです.

メッシュとレイの交差判定

三角形ができれば, 三角形の集合のポリゴンメッシュはできたようなものです.

と言いたいのですが, できるだけ楽にデータを用意したいとなると, よく知られたデータ形式をロードする必要があり大変です.
FBX, Wavefront obj, Stanford PLYあたりをロードできるようにしておくとテストデータには困らないと思います.Open Asset Import Library (Assimp)を使ってもいいでしょう.
今回は, glTF 2.0を使ってみます. この選択に特に意味はないですが, 自作のローダのテストでもあります.

メッシュとの判定部分だけは以下になります. 鬼の総当たりですが, 最初はこんなものだと思います, 次回高速化しましょう.
重心座標は法線の計算に使用しています.
Result Mesh::test(Intersection& intersection, Ray& ray)
{
    for(s32 prim = 0; prim<primitives_.size(); ++prim){

        Primitive& primitive = primitives_[prim];
        for(s32 tri=0; tri<primitive.getNumTriangles(); ++tri){
            const Triangle& triangle = primitive.getTriangle(tri);
            const Vector3& p0 = primitive.getPosition(triangle.indices_[0]);
            const Vector3& p1 = primitive.getPosition(triangle.indices_[1]);
            const Vector3& p2 = primitive.getPosition(triangle.indices_[2]);

            f32 tmpT = F32_INFINITY;
            f32 tmpV;
            f32 tmpW;
            Result tmpResult = testRayTriangleBoth(tmpT, tmpV, tmpW, ray, p0, p1, p2);
            if(tmpResult != Result_Fail && tmpT<intersection.t_){
                intersection.result_ = tmpResult;
                ray.t_ = intersection.t_ = tmpT;
                intersection.b0_ = 1.0f-tmpV-tmpW;
                intersection.b1_ = tmpV;
                intersection.b2_ = tmpW;

                //Calc normal
                f32 w0 = intersection.b0_;
                f32 w1 = intersection.b1_;
                f32 w2 = intersection.b2_;
                const Vector3& n0 = primitive.getNormal(triangle.indices_[0]);
                const Vector3& n1 = primitive.getNormal(triangle.indices_[1]);
                const Vector3& n2 = primitive.getNormal(triangle.indices_[2]);
                intersection.shadingNormal_ = weightedAverage(w0, w1, w2, n0, n1, n2);
            }//if(tmpResult
        }//for(s32 tri=0
    }//for(s32 prim = 0
    return intersection.result_;
}

データはSketchfabから,
Hatsune Miku (Chibi) w/ Stand by Senpai-3689 is licensed under CC Attribution
を選びました.

まとめ

三角形とレイの交差判定を行い, メッシュをレンダリングするところまで行いました.
データ形式に拘ると時間がかかると思いますが, Wavefront objやStanford PLYなら苦労しないと思います.
任意のメッシュのレンダリングができるようになるとやる気がでるのではないでしょうか.

下回り

CMakeでファイル収集

そろそろ共通ファイルが増えてきたため, 雑ですがファイルを集めてグループ化する関数を作成しました.
"file (GLOB"と正規表現でファイルを集めて, "source_group"でいいかんじにグループ化します.
凝る必要もないので, やりたいことができればよいのだと思います.
function(gather_files FILES SOURCE)
    file (GLOB SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${SOURCE})
    set(${FILES} ${SOURCES} PARENT_SCOPE)
endfunction(gather_files)

function(gather_lib_files COMMON_HEADERS COMMON_SOURCES ROOT LIBNAME MODULES)
    set(ROOT_INCLUDE "${ROOT}/${LIBNAME}/")
    set(ROOT_SOURCE "${ROOT}/${LIBNAME}/src")

    set(CMN_HEADERS "")
    set(CMN_SOURCES "")

    foreach(MODULE IN LISTS MODULES)
        set(HFILES "")
        set(CPPFILES "")
        gather_files(HFILES "${ROOT_INCLUDE}${MODULE}/*.h")
        gather_files(CPPFILES "${ROOT_SOURCE}${MODULE}/*.cpp")

        # Group files
        string(REPLACE "/" "\\" FILTER "include/${LIBNAME}/${MODULE}")
        source_group("${FILTER}" FILES ${HFILES})
        string(REPLACE "/" "\\" FILTER "src/${LIBNAME}/${MODULE}")
        source_group("${FILTER}" FILES ${CPPFILES})
        set(CMN_HEADERS ${CMN_HEADERS} ${HFILES})
        set(CMN_SOURCES ${CMN_SOURCES} ${CPPFILES})
    endforeach()
    set(${COMMON_HEADERS} ${CMN_HEADERS} PARENT_SCOPE)
    set(${COMMON_SOURCES} ${CMN_SOURCES} PARENT_SCOPE)
endfunction(gather_lib_files)

0 件のコメント:

コメントを投稿