レイトレーシング チュートリアル01

<<チュートリアル00
>>02 メッシュレンダリング

概要

前回は環境構築と画像の保存まで行いました.
画像フォーマットの詳細には触れませんでしたが,
今後も本質ではない部分はライブラリなりを用意しようと思います.
コードは, GitHubリポジトリにまとめます.

今回はレイを飛ばして、それっぽく球を描画します.

レイを飛ばす

レイトレースはカメラ(視点)からレイを飛ばして画を作ります.
順番に画(スクリーン)からカメラ, ワールドへ座標変換してレイを生成していきます.

スクリーンから正規化デバイス座標へは問題ないと思います.


カメラからワールドへは, ゲームのように行列でもいいのですが, 今回はベクトルで計算します.
上図は視垂台と関連する計算式です.
どういうやり方でもさほど変わらないので, 好みでいいと思います.
右図は上面図です. RayDxとRayDyが少し特徴的ですが,
視垂台の角へのベクトルが, カメラ視線方向へ1進むと, それぞれカメラ右方向,
カメラ上方向へどれだけ進むかを表します.
この方式の方が後で各変換の間に処理を入れやすくてこのみです.
Ray&& Camera::generateRay(f32 screenX, f32 screenY) const
{
    //正規化デバイス座標
    f32 nx = invHalfWidth_*screenX - 1.0f;
    f32 ny = 1.0f-invHalfHeight_*screenY;

    //視点からスクリーン(screenX, screenY)を通る線分
    Vector3 dx = mul(nx*rayDx_, right_);
    Vector3 dy = mul(ny*rayDy_, up_);
    Vector3 direction = normalize(dx+dy+forward_);

    return move(Ray(position_, direction, nearClip_, farClip_));
}


レイは, 始点, 方向, 長さで表していますが, このあたりの方法も好みでいいと思います.

球との交差

中学校?の数学で習ったと思いますが, スカラと掛け算がベクトルと内積に代わっていますが
2次方程式の解の公式で解くことができます.
下図のo+tdのレイが, 半径r中心pの球と接する場合の等式になります.


bool testRaySphere(f32& t, const Ray& ray, const Sphere& sphere)
{
    Vector3 po = ray.origin_ - sphere.position_;

    f32 b = dot(po, ray.direction_);
    f32 c = dot(po, po) - sphere.radius_ * sphere.radius_;

    if(0.0f<c){
        if(0.0f<b){
            return false;
        }
    }else{
        t = 0.0f;
        return true;
    }

    //Check discriminant
    f32 discr = b*b - c;
    if(discr < 0.0f){
        return false;
    }

    discr = lray::sqrt(discr);
    b = -b;
    t = b-discr;
    f32 tmax = b + discr;
    return tmax<=ray.t_;
}

描画

ここまでのコードをまとめます. 球に当たればそれっぽいグラデーションにするために
適当な方向と内積をとっています. この内積計算は意味のない確認用です.
//Add object
Sphere sphere;
sphere.position_ = Vector3(0.0f, 0.0f, 1.0f);
sphere.radius_ = 0.25f;

//Set light environment
Vector3 lightDirection = normalize(Vector3(0.5f, 0.5f, 0.0f));

//Loop over pixels
for(s32 y=0; y<Height; ++y){
    for(s32 x=0; x<Width; ++x){
        Ray ray = camera.generateRay(static_cast<f32>(x), static_cast<f32>(y));
        f32 t;
        u8 r,g,b;
        if(testRaySphere(t, ray, sphere)){
            Vector3 hitPosition = muladd(t, ray.direction_, ray.origin_);
            Vector3 normal = normalize(hitPosition - sphere.position_);
            f32 d = maximum(dot(normal, lightDirection), 0.0f);
            r = g = b = static_cast<u8>(minimum(clamp01(d)*256, 255.0f));
        }else{
            r = g = b = 128;
        }
        s32 pixel = (y*Width + x)*Bpp;
        image[pixel+0] = r;
        image[pixel+1] = g;
        image[pixel+2] = b;
    }
}


まとめ

球とレイの交差判定を行い, 球をレンダリングするところまで行いました.
やはり球を描画してもつまらないなという感想です.
TODO:雑すぎるので書き足します.

C++と下回り

SIMD

ベクトルや行列の計算には, SSEの組み込み関数を用いています.
Visual Studioを使用する場合は2010以降でお願いします.
2008では, コンパイラによって_mm_loadu_ps, _mm_storeu_psが
_mm_load_ps, _mm_store_psに変更される不具合があります.
"u"付きがメモリアライン制約のない転送命令ですので, _mm_loadu_psと書いているのに
メモリアクセス例外でクラッシュすることがありました.
C++11で追加された機能を利用しているため, 2008を使うことはないと思いますが.

clamp01

UnityにClamp01という関数があるのはご存じでしょうか?
実装は見ていませんが, もしかしたらわざわざ特別に作るだけの意味があるのかもしれません.
いちおう, こちらの方がどんな環境でも高速だという保証はないです.
f32 clamp01(f32 v)
{
    UnionS32F32 u;
    u.f32_ = v;

    s32 s = u.s32_ >> 31;
    s = ~s;
    u.s32_ &= s;

    u.f32_ -= 1.0f;
    s = u.s32_ >> 31;
    u.s32_ &= s;
    u.f32_ += 1.0f;
    return u.f32_;
}

Trivially Copyable

memcpyでコピーできる型のことを, Trivially Copyableな型をいいます.
trivialであるとは, ユーザ定義でなくdeleteされていないということ,
Trivially Copyableの条件は以下だそうです.
  • trivialでない, コピーコンストラクタ, ムーブコンストラクタ, コピー代入演算子, ムーブ代入演算子を持たないこと.
  • trivialなデストラクタを持っていること.
std::is_trivially_copyableでチェックできます.
static_assert(std::is_trivially_copyable<T>::value == true, "T must be trivially copyable.");
cpprefjp is_trivially_copyable

ベクトルや行列の配列などは, memcpyですませたいことが多いのでTrivially Copyableにしておきます.

0 件のコメント:

コメントを投稿