レイトレーサーとは何ですか? これは、画面に表示される3次元のシーンを描画するプログラムです。 もちろん、そうではありませんが、たとえばAvatarのように、一部のレイトレーサーは非常に信頼できる画像を描画できます。
レイトレーサーのアイデアは非常に単純です。この記事では、このアルゴリズムの仕組みを説明し、さらにJavaScriptで記述します。 写真と例が添付されています。
3次元シーンを描く方法は?
今日、私が知る限り、フラットスクリーンで3次元シーンを設計する方法は2つあります。 最初の方法は、マトリックス変換に基づいています。 彼のアイデアもシンプルで、高速に動作しますが、彼が描くものは写真のようには見えず、ゲームにのみ適しています。 2番目の方法はレイトレーシングです。 単純に配置されているため、シャドウ、反射、屈折、その他の照明効果を簡単に描くことができますが、動作が非常に遅いため、ゲームには適していません。 さらに、レイトレーシングアルゴリズムは簡単に並列化できます。プロセッサがいくつあるか、正確に何回も加速されます。
アルゴリズムのアイデア
あなたが座っているモニターが窓であり、窓の後ろにある種のシーンがあると想像してください。 モニターの各ピクセルの色は、目から出て、そのピクセルを通過してシーンと衝突する光線の色です。 各ピクセルの色を調べるには、各ピクセルから光線を開始し、この光線がシーンと衝突する場所を見つける必要があります。 したがって、アルゴリズムの名前は、レイトレーシング-レイトレーシングです。
ビームの座標(空間内の2点)に応じて、このビームが落ちる表面の色を計算する関数を記述するだけで十分であることがわかります。 どのような状況を考慮する必要がありますか? 少なくとも3つあります。
- 平らな表面。 ビームがこれと衝突するとき、ビームの色はこの表面の色であると言えます。 これは最も単純なケースです。
- リフレクション。 光線は鏡に当たり、同じ角度で跳ね返ることができます。 この状況に対処するには、ビームを反射できる必要があります。
- 屈折。 光線は、2つの媒体の底部を通過できます。たとえば、空気から水になります。 ある媒体から別の媒体に移動すると、ビームは屈折します。 この現象は屈折と呼ばれます。
これらの各状況は簡単に処理できるため、ラスターの書き込みは難しくありません。
シーン
ステージ上には、画面に描画する必要があるオブジェクトと光源の2つのタイプのオブジェクトがあります。 簡単にするために、ボール、立方体(平行六面体)、およびそれらの周囲のすべての方向に均一に輝く点光源のみがあります。 被験者は3つのことを実行できる必要があります。つまり、次の3つの方法が必要です。
- norm (p)は、pでオブジェクトの表面の法線を見つけます。 法線は外側で、長さは1です。
- color (p)は、ポイントpでのオブジェクトの表面の色を示します。
- トレース (光線)は光線に沿って進み、光線がオブジェクトの表面と交差する場所で停止します。 このメソッドは、交差点の座標と、ビームの始点から交差点までの距離を返します。
これは、これらのメソッドが球体でどのように見えるかです。
sphere.norm = function(at) { return vec.mul(1 / this.r, vec.sub(at, this.q)) } sphere.trace = function(ray) { var a = ray.from var aq = vec.sub(a, this.q) var ba = ray.dir var aqba = vec.dot(aq, ba) if (aqba > 0) return var aq2 = vec.dot(aq, aq) var qd = aq2 - this.r * this.r var D = aqba * aqba - qd if (D < 0) return var t = qd > 0 ? -aqba - Math.sqrt(D) : -aqba + Math.sqrt(D) var sqrdist = t * t var at = vec.add(a, vec.mul(t, ba)) return {at:at, sqrdist:sqrdist} } sphere.color = function(p) { return [1, 0, 0] // red color }
this.qのような個々の表記法の意味は今では重要ではありません。sphere.trace関数を簡単に書くことができます。 重要なのは、これら3つのメソッドを記述するのが非常に簡単なことです。 同様に、キューブについても説明します。
レイトレーサー
それでは、レイトレーサーコードに移りましょう。 いくつかの基本的な機能があります。
- トレース (光線)は光線に沿って進み、光線がオブジェクトを横切る場所で停止します。 つまり、この関数は、ビームと対象物の最も近い交差点を見つけます。 traceは、交差点の座標とその距離、および交差したオブジェクトへのリンクを返します。 この関数は次のように書きました。
rt.trace = function(ray) { var p for (var i in rt.objects) { var obj = rt.objects[i] var ep = obj.trace(ray) if (ep && (!p || ep.sqrdist < p.sqrdist)) { p = ep p.owner = obj } } return p }
- inshadow (p、lightpos)は、pがlightposの光源の影にあるかどうかをチェックします。 言い換えると、この関数は、lightposがpを照らすかどうかをチェックします。 彼女のコードは次のとおりです。
rt.inshadow = function(p, lightpos) { var q = rt.trace(rt.ray(lightpos, p)) return !q || vec.sqrdist(q.at, p) > math.eps }
最初のステップで、関数は光線をlightposからpに解放し、この光線がオブジェクトと交差する場所を調べます。 2番目のステップでは、関数は交点が点pと一致するかどうかをチェックします。 一致しない場合、光線はpに達していません。
- カラー (レイ)はレイレイを放出し、オブジェクトと衝突する場所を探します。 衝突点で、表面の色を認識して返します。 彼女のコードは次のとおりです。
rt.color = function( r ) { var hit = rt.trace( r ) if (!hit) return rt.bgcolor hit.norm = hit.owner.norm(hit.at) var surfcol = rt.diffuse(r, hit) || [0, 0, 0] var reflcol = rt.reflection(r, hit) || [0, 0, 0] var refrcol = rt.refraction(r, hit) || [0, 0, 0] var m = hit.owner.mat // material return vec.sum ( vec.mul(m.reflection, reflcol), vec.mul(m.transparency, refrcol), vec.mul(m.surface, surfcol) ) }
最初に、関数は光線と最も近いオブジェクトとの衝突点を見つけ、この点で表面の法線を計算します(そのような点が見つからなかった場合、光線はすべてのオブジェクトを通過し、黒などの背景色を返すことができます)。 衝突点で、色は3つの部分にまとめられます。
- Diffuse-このポイントが光源によって照らされる角度と光線rがそこに当たる角度を考慮した表面自体の色。
- reflection-反射光線の色。
- 屈折 -屈折した光線の色。
これらの3つの部分は重みで合計されます:サーフコルの表面の色は重みm.surface、反射ビームの反射色はm.reflection、屈折ビームの色はm.transparencyです。 重み係数の合計は1です。たとえば、透明度がm.transparency = 0の場合、屈折を考慮することは意味がありません。
ある点での色の計算方法を検討する必要があります。 拡散、反射、屈折機能を実装するには、さまざまなアプローチがあります。 それらのいくつかを検討します。
ランバートモデル
これは、カラーソースがどのように輝くかによってサーフェスの色を計算するためのモデルです。 このモデルによると、ポイントの照度は、光源の強度と、ポイントに照射される角度の余弦の積に等しくなります。 Lambertモデルを使用して拡散関数を記述しましょう。
rt.diffuse = function(r, hit) { var obj = hit.owner var m = obj.mat var sumlight = 0 for (var j in rt.lights) { var light = rt.lights[j] if (rt.inshadow(hit.at, light.at)) continue var dir = vec.norm(vec.sub(hit.at, light.at)) var cos = -vec.dot(dir, hit.norm) sumlight += light.power * cos } return vec.mul(sumlight, obj.color) }
この関数は、すべての光源を反復処理し、ヒットポイントがシャドウ内にあるかどうかを確認します。 照らされた領域にある場合、ベクトルdir-光源ライトからヒットポイントへの方向が計算されます。 次に、ヒット時のサーフェスに対するhit.normの法線とdirの方向との間の角度のコサインを求めます。 このコサインは、スカラー積dir•hit.normと等しくなります。 最後に、関数はLambertに従ってイルミネーションを見つけます:light.power•cos。
この照明モデルのみを適用すると、次のようになります。
フォンモデル
Phongモデルは、Lambertモデルと同様に、点の照明を表します。 ランバートモデルとは異なり、このモデルでは、サーフェスをどの角度で見るかを考慮します。 フォンイルミネーションは次のように計算されます。
- 光源から表面上の問題のポイントまで光線を描き、この光線を表面から反射します。
- 反射ビームと表面を見る方向との間の角度の余弦を見つけます。
- この余弦をある程度上げ、結果の数値に光源のパワーを掛けます。
このモデルによれば、表面上の光源の反射が見られる場合、表面上の点の見かけの照明は最大になります。 それは目に直接反映されます。 関連する拡散コード:
rt.diffuse = function(r, hit) { var obj = hit.owner var m = obj.mat var sumlight = 0 for (var j in rt.lights) { var light = rt.lights[j] if (rt.inshadow(hit.at, light.at)) continue var dir = vec.norm(vec.sub(hit.at, light.at)) var lr = vec.reflect(dir, hit.norm) var vcos = -vec.dot(lr, r.dir) if (vcos > 0) { var phong = Math.pow(vcos, m.phongpower) sumlight += light.power * phong } } return vec.mul(sumlight, obj.color) }
これはどのように見えるかです:
Phongだけでは良い照明には不十分であることがわかりますが、ある重み係数のPhong照明を使用し、異なる重み係数のLambert照明を追加すると、次の図が得られます。
対応する拡散関数のコードは示しません。これは、前の2つの拡散の組み合わせであり、例のrt.jsファイルにあります。
リフレクション
反射光線の色を計算するには、法線ベクトルを使用して表面からこの光線を反射し、反射光線に対して既に記述されている関数rt.colorを実行する必要があります。 微妙な点は1つだけです。表面はビームのすべてのエネルギーを反射するのではなく、特定の割合のみを反射するため、開始と方向の座標に加えて、ビームにエネルギーを追加します。 エネルギーが小さい場合、ビームの色は、それが何であれ、rt.colorで得られる合計色にわずかに寄与するため、このパラメーターは、ビームの色の計算にまだ関連があるかどうかを示します。
rt.reflection = function(r, hit) { var refl = hit.owner.mat.reflection if (refl * r.power < math.eps) return var q = {} q.dir = vec.reflect(r.dir, hit.norm) q.from = hit.at q.power = refl * r.power return rt.color(q) } vec.reflect = function(a, n) { var an = vec.dot(a, n) return vec.add(a, vec.mul(-2 * an, n)) }
これで、各オブジェクトには反射係数が必要になります。これは、ビームのエネルギーが表面からどのくらい反射されるかを示しています。 この関数を記述した後、次の図を取得します。
屈折
光線が1つの媒体から別の媒体を通過すると、屈折します。 これはウィキペディアで読むことができます。 屈折の実装は、反射とほぼ同じです。
rt.refraction = function(r, hit) { var m = hit.owner.mat var t = m.transparency if (t * r.power < math.eps) return var dir = vec.refract(r.dir, hit.norm, m.refrcoeff) if (!dir) return var q = {} q.dir = dir q.from = hit.at q.power = t * r.power return rt.color(q) } vec.refract = function(v, n, q) { var nv = vec.dot(n, v) if (nv > 0) return vec.refract(v, vec.mul(-1, n), 1/q) var a = 1 / q var D = 1 - a * a * (1 - nv * nv) var b = nv * a + Math.sqrt(D) return D < 0 ? undefined : vec.sub(vec.mul(a, v), vec.mul(b, n)) }
これで、すべてのオブジェクトには、表面を通過する光の割合である透明度係数と、屈折した光線の方向の計算に関係する屈折率があります。
フレネル係数
反射光の量は、ビームが表面に当たる角度と屈折率に依存します。 ウィキペディアで公式を見ることができます。 目に見えない変更を加えたため、レーサーではこの効果を考慮しませんでした。
スムージング
各ピクセルを介して1つの光線が発射されると、3次元空間の滑らかな線が設計後に画面上に階段状に表示されます。 これを回避するには、各ピクセルに複数の光線を照射し、それぞれの色を数え、それらの間の平均を見つけます。
例
ここで、画像は1000×1000 (RPSはRays Per Second-ブラウザが1秒間に計算する光線の数を意味します)であり、ここで他の画像は800×800です。 このリンクからサンプルをダウンロードできます。 さまざまなブラウザーでレンダリング速度を比較しました。 次のことが判明しました。
オペラ | 33,000 RPS |
クロム | 38,000 RPS |
Firefox | 16,000 RPS |
探検家 | 20,000 RPS |
サファリ | 13,000 RPS |
2011年2月5日に最新のブラウザを使用しました。
このレイトレーサーにはないものは何ですか?
レイザーの基本的な機能を調べました。 被写体が鏡の前に立ち、鏡を照らすとどうなりますか? 被写体の裏側が反射光で照らされます。 ガラス玉に光を当てるとどうなりますか? 彼はレンズのように光線を集め、その下のスタンドに明るいドットができます。 部屋に光が入る小さな窓しかない場合はどうなりますか? 部屋全体が薄暗くなります。 考慮されるレイトレーサーはこれを行うことはできませんが、レイトレーサーの基本的な考え方により許可されるため、追加することは難しくありません。
すべての関数(ランバート照明、Fong照明、反射、屈折)を計算するには、ベクトルを追加し、それらを数値で乗算し、スカラー積を求める機能のみが必要です。 ベクトルに対するこれらの操作は、空間の次元に依存しません。つまり、4次元空間のレイトレーサーを記述して、コードにいくつかの変更を加えることができます。