1764255404
2025-11-27 07:31:00
免責事項: このページのデモでは、一部のモバイル デバイスでは利用できない WebGL 機能が使用されています。
数週間前、私はおもちゃのグラフィック プロジェクトのビデオをツイートしました (下)。まだ完成していませんが、多くの人が気に入ってくれて、驚きで楽しかったです。何人かがそれがどのように機能するか尋ねたので、それがこの投稿の内容です。
内部では距離フィールドと呼ばれるものが使用されます。距離フィールドは、各ピクセルが形状からどのくらい離れているかを示す以下のような画像です。明るいグレーのピクセルは形状に近く、濃いグレーのピクセルは形状から遠いです。
デモが起動すると、2D キャンバス上にテキストが描画され、その距離フィールドが生成されます。使用します 私が書いたライブラリ 距離フィールドを非常に迅速に生成します。ライブラリがどのように機能するか興味がある場合は、それについて書きました ここ。
私たちの照明スキームは次のように機能します。特定のピクセルを処理するときに、そのピクセルからライトへの光線を考慮します。
光線がグリフと交差する場合、シェーディングしているピクセルは影の中にあるはずです。ピクセルと光の間に何かがあるからです。
これを確認する最も簡単な方法は、レイに沿って 1 ピクセルずつ移動し、シェーディングしているピクセルから開始してライトで終了し、形状からの距離が 0 であるかどうかを距離フィールドに繰り返し問い合わせることです。これは機能しますが、非常に遅くなります。
30 ピクセルなどの特定の長さを選択し、そのサイズの増分で移動することもできますが、その場合、30 ピクセルより小さいグリフを飛び越える危険があります。私たちは、影にいるべきときに影にいないと思うかもしれません。
レイ マーチングの中心となるアイデアは次のとおりです。距離フィールドは、最も近いグリフからどれだけ離れているかを示します。グリフを飛び越えることなく、その距離だけ光線に沿って安全に進むことができます。
例を見てみましょう。上の図のように開始して、距離フィールドにグリフからどのくらい離れているかを尋ねます。この場合、答えは 95px であることがわかります (左の図)。これは、何もスキップせずに光線に沿って 95 ピクセル移動できることを意味します。
今、私たちは光に少し近づいています。 b のアセンダーに到達するまでこのプロセスを繰り返します。もしbのグリフがなかったら、光に当たるまで進み続けただろう。
以下は、特定のピクセルのレイ マーチング ステップを示すデモです。赤いボックスはシェーディングしているピクセルで、レイに沿った各円はレイ マーチング ステップとそのステップでのシーンからの距離を表します。
ライトとピクセルをドラッグして、直感を確立してみてください。
以下は、この手法を実装するための GLSL です。関数が定義されていることを前提としています getDistance 距離フィールドをサンプリングします。
vec2 rayOrigin = ...;
vec2 rayDirection = ...;
float rayProgress = 0;
while (true) {
if (rayProgress > distance(rayOrigin, lightPosition)) {
// We hit the light! This pixel is not in shadow.
return 1.;
}
float sceneDist = getDistance(
rayOrigin + rayProgress * rayDirection);
if (sceneDist 0.) {
// We hit a shape! This pixel is in shadow.
return 0.;
}
rayProgress += sceneDist;
}
一部のピクセルは処理に非常にコストがかかることが判明しました。したがって、実際には、while ループの代わりに for ループを使用します。そうすることで、実行したステップが多すぎる場合に救済します。レイ マーチングでよくある「遅いケース」は、レイがシーン内のシェイプのエッジと平行である場合です。
これまで説明してきたアプローチでは、次のようなシーンが得られます。
カッコいいのですが、影がシャープであまり見栄えが良くありません。デモでの影はこんな感じです…
大きな免責事項の 1 つは、それらは物理的に現実的ではないということです。実際の影はエッジがぼやけた硬い影のように見えます。このアプローチでは、少し異なる処理が行われます。以前は影になっていたすべてのピクセルが完全に影になったままになります。周囲に部分的に影がついたピクセルの半影を追加しました。
利点は、それらが美しく、計算が速いということであり、それが私が気にしていることです。それらの計算には 3 つの「ルール」が関係します。
ルール 1: 光線が形状との交差に近づくほど、そのピクセルの影が大きくなります。下の画像には、2 つの同様の光線があります (黄色と緑色で示された形状までの距離)。角に近づくものほど影が多くなるようにしたいと考えています。
これは、変数が次のとおりであるため、計算コストが低くなります。 sceneDist レイ マーチングの各ステップで最も近い形状からどれだけ離れているかを示します。したがって、の最小値は sceneDist すべてのステップにわたるこの値は、上の画像の黄色と緑色の線の近似値となります。
ルール 2: シェーディングしているピクセルがシェイプとほぼ交差する点から遠く離れている場合は、シャドウをさらに広げる必要があります。
上の光線に沿った 2 つのピクセルを考えてみましょう。 1 つはほぼ交差点に近く、より明るいです (その距離は緑の線です)。もう 1 つはさらに遠くて暗いです (その距離は黄色の線です)。一般に、ピクセルがそのほぼ交差点から遠ざかるほど、そのピクセルをより「影の中に」作成する必要があります。
これは、変数が次のとおりであるため、計算コストが低くなります。 rayProgress は、上の画像の緑と黄色の線の長さです。
つまり、私たちは以前に戻ってきました 1.0 影の中になかったピクセルの場合。ルール 1 と 2 を実装するには、次のように計算します。 sceneDist / rayProgress 各レイ マーチング ステップで、その最小値を追跡し、代わりにそれを返します。
vec2 rayOrigin = ...;
vec2 rayDirection = ...;
float rayProgress = 0.;
float stopAt = distance(samplePt, lightPosition);
float lightContribution = 1.;
for (int i = 0; i 64; i++) {
if (rayProgress > stopAt) {
return lightContribution;
}
// `getDistance` samples our distance field texture.
float sceneDist = getDistance(
rayOrigin + rayProgress * rayDirection);
if (sceneDist 0.) {
// We hit a shape! This pixel is in shadow.
return 0.;
}
lightContribution = min(
lightContribution,
sceneDist / rayProgress
);
rayProgress += sceneDist;
}
// Ray-marching took more than 64 steps!
return 0.;
この比率は物理的な値に対応していないため、私にとっては一種の魔法のように感じられます。それでは、なぜそれが特定の値を取るのかを考えて、それに対する直感を構築しましょう…
-
もし
sceneDist / rayProgress >= 1、その後、どちらかsceneDist大きいか、rayProgressは(相互に比べて)小さいです。前者の場合、どの図形からも遠く離れており、影になるべきではないため、軽い値は次のようになります。1理にかなっています。後者の場合、影を付けているピクセルは影を落としているオブジェクトに非常に近く、影はまだぼやけていないため、ライトの値は次のようになります。1理にかなっています。 -
比率は
0そのときだけsceneDistは0。これは、オブジェクトと交差し、そのピクセルが影になっている光線に対応します。
そして、これが私たちがこれまでに持っているもののデモです…
ルール #3 最も単純なものは、光は遠ざかるほど弱くなるということです。
の最小値を返す代わりに、 sceneDist / rayProgress 文字通りに、それに a を掛けます。 distanceFactor それは 1 光のすぐそばで、 0 遠ざかるにつれて二次関数的に小さくなります。
これまでのアプローチのコードをまとめると、次のようになります…
vec2 rayOrigin = ...;
vec2 rayDirection = ...;
float rayProgress = 0.;
float stopAt = distance(samplePt, lightPosition);
float lightContribution = 1.;
for (int i = 0; i 64; i++) {
if (rayProgress > stopAt) {
// We hit the light!
float LIGHT_RADIUS_PX = 800.;
// fadeRatio is 1.0 next to the light and 0. at
// LIGHT_RADIUS_PX away.
float fadeRatio =
1.0 - clamp(stopAt / LIGHT_RADIUS_PX, 0., 1.);
// We'd like the light to fade off quadratically instead of
// linearly.
float distanceFactor = pow(fadeRatio, 2.);
return lightContribution * distanceFactor;
}
// `getDistance` samples our distance field texture.
float sceneDist = getDistance(rayOrigin + rayProgress * rayDirection);
if (sceneDist 0.) {
// We hit a shape! This pixel is in shadow.
return 0.;
}
lightContribution = min(
lightContribution,
sceneDist / rayProgress
);
rayProgress += sceneDist;
}
// Ray-marching took more than 64 steps!
return 0.;
このソフト シャドウのテクニックをどこで見つけたか忘れましたが、私が発明したわけではありません。イニゴ・クレス 素晴らしい投稿があります そこで彼は 3D での使用について話しています。
Inigo の投稿では、上記のデモで気づいたかもしれないこのアプローチの落とし穴についても触れています。それは、バンディング アーティファクトの原因となります。これは、ルール 1 が次の最小値を想定しているためです。 sceneDist すべてのステップにわたるこの値は、レイからシーンまでの距離の適切な近似値となります。レイ マーチング ステップが非常に少ない場合もあるため、これは常に真実であるとは限りません。
したがって、私のデモでは、Inigo が投稿で書いている改良された近似を使用します。また、より効果的ですがパフォーマンスは低い別のトリックも使用します。 sceneDist レイマーチングの各ステップで、次のような感じで進みます。 sceneDist * randomJitter どこ randomJitter の間にあります 0 そして 1。
これにより、レイ マーチにさらに多くのステップが追加されるため、近似が改善されます。しかし、次のステップで前進することでそれを実現できます。 sceneDist * .3。ランダム ジッターにより、隣り合うピクセルが同じバンドに属さないことが保証されます。これにより、結果が少し粗くなり、あまり良くありません。でも、バンディングよりは見栄えが良いと思います…これはデモの点でまだ満足していないので、改善方法のアイデアがあれば教えてください。
全体として、私のデモには、将来書くかもしれないいくつかの追加の調整が含まれていますが、これがデモの核心です。読んでいただきありがとうございます!ご質問やご意見がございましたら、お知らせください ツイッターで。
この投稿の初期草案にフィードバックを提供してくださった Jessica Liu、Susan Wang、Matt Nichols、Kenrick Rilee に感謝します。また、この投稿が気に入っていただけた場合は、私と一緒に仕事を楽しんでいただけるかもしれません フィグマ!
#のレイ #マーチング #ソフト #シャドウ #Ryan #Kaplan