オブジェクトの外観 (明るさ、色など) に影響を与えるパラメータを確認したので、簡単なシェーディング手法をいくつか見ていきましょう。
推奨事項: NSDT エディターを使用して、プログラム可能な 3D シーンを迅速に構築する
1. ノーマル
法線はシェーディングにおいて中心的な役割を果たします。物体を光源に向けると明るくなるということは誰もが知っています。物体の表面の向きは、物体が反射する光の量 (およびどれだけ明るく見えるか) に重要な役割を果たします。図 1 に示すように、物体の表面上の任意の点 P におけるこの方向は、表面に垂直な法線 N で表すことができます。
図 1: 法線方向と光の方向の間の角度が増加するにつれて、球がどのように暗くなるかに注目してください。
図 1 では、光の方向と法線方向の間の角度が増加するにつれて、球の明るさがどのように減少するかに注目してください。この明るさの低下は私たちが毎日見ている現象ですが、なぜそれが起こるのかを知っている人はおそらくほとんどいません。この現象の理由については後ほど説明します。とりあえず、次のことだけ覚えておいてください。
- 法線 (大文字の N で表します) と呼ばれるものは、点 P におけるサーフェスの接線に垂直なベクトルです。言い換えれば、点 P の法線を見つけるには、サーフェスに接する線をトレースし、その接線に垂直なベクトルを取る必要があります (3D ではこれが接平面になることに注意してください)。
- オブジェクトの表面上の点の明るさは法線方向に依存します。法線方向は、光に対するその点におけるオブジェクトの表面の方向を定義します。これを別の言い方で言えば、物体の表面上の任意の点の明るさは、その点の法線と光の方向との間の角度に依存するということです。
ここで問題は、この正規をどのように計算するかです。この問題に対する解決策の複雑さは、レンダリングされるジオメトリのタイプに応じて大きく異なります。球の法線は通常、簡単に見つけることができます。球の表面上の点の位置と中心がわかっている場合は、中心から点の位置を引くことによって、その点への法線を計算できます。
Vec3f N = P - sphereCenter;
オブジェクトが三角形のメッシュである場合、各三角形は平面を定義し、その平面に垂直なベクトルは、その三角形の表面上にある任意の点の法線になります。三角形の平面に垂直なベクトルは、三角形の 2 つの辺の外積によって簡単に取得できます。v1xv2 = -v2xv1 に注意してください。したがって、エッジの選択は法線の方向に影響します。三角形の頂点を反時計回りの順序で宣言する場合は、次のコードを使用できます。
Vec3f N = (v1-v0).crossProduct(v2-v0);
図 2: 三角形の面法線は、三角形の 2 つの辺の外積を取ることで計算できます。
三角形が xz 平面内にある場合、図 2 に示すように、結果の法線は (0,-1,0) ではなく (0,1,0) になります。
この方法で法線を計算すると、いわゆる面法線が得られます (面または三角形のどの点を選択しても、法線は面全体で同じであるため)。三角形メッシュの法線は、三角形の頂点で定義することもできます。その場合、これらの法線を頂点法線と呼びます。頂点法線は、スムース シェーディングと呼ばれる手法で使用されます。これについては、この章の最後で説明します。現時点では、面法線のみを扱います。
プログラム内でシェーディングされる点の表面法線がいつどのように計算されるかは関係ありません。これに色を付けるときは、この情報を手元に用意しておくことが重要です。基本的なシェーディングを行うこのセクションでは、各ジオメトリ クラスに getSurfaceProperties() と呼ばれる特別なメソッドを実装し、交点の法線 (レイトレーシングを使用している場合) やテクスチャ座標などのその他の変数を計算します。これについては後ほど説明します。このレッスン。球および三角形のメッシュ ジオメトリ タイプの場合、これらのメソッドの実装は次のとおりです。
class Sphere : public Object
{
...
public:
...
void getSurfaceProperties(
const Vec3f &hitPoint,
const Vec3f &viewDirection,
const uint32_t &triIndex,
const Vec2f &uv,
Vec3f &hitNormal,
Vec2f &hitTextureCoordinates) const
{
hitNormal= Phit - center;
hitNormal.normalize();
...
}
...
};
class TriangleMesh : public Object
{
...
public:
void getSurfaceProperties(
const Vec3f &hitPoint,
const Vec3f &viewDirection,
const uint32_t &triIndex,
const Vec2f &uv,
Vec3f &hitNormal,
Vec2f &hitTextureCoordinates) const
{
// face normal
const Vec3f &v0 = P[trisIndex[triIndex * 3]];
const Vec3f &v1 = P[trisIndex[triIndex * 3 + 1]];
const Vec3f &v2 = P[trisIndex[triIndex * 3 + 2]];
hitNormal = (v1 - v0).crossProduct(v2 - v0);
hitNormal.normalize();
...
}
...
};
2. シンプルな着色効果:表面積比
オブジェクトの表面上の点の法線を計算する方法がわかったので、フェーシング率と呼ばれる単純なシェーディング効果を作成するのに十分な情報が得られました。この手法は、シェーディングしたい点の法線と視線方向の内積を計算することで構成されます。視線方向の計算も非常に簡単です。レイ トレーシングを使用する場合、サーフェスと交差する P でのレイの方向がちょうど反対になります。レイ トレーシングを使用せずに、表面上の点 P から目までの線をトレースするだけで視線方向を見つけることもできます。
Vec3f V = (E - P).normalize(); // or -ray.dir if you use ray-tracing
2 つのベクトルの内積は、それらが平行で同じ方向を向いている場合は 1 を返し、2 つのベクトルが互いに垂直である場合は 0 を返すことに注意してください。ベクトルが反対の方向を向いている場合、内積は負になりますが、その内積の結果を色として使用している場合は、とにかく負の値には興味がありません。ドット積の入門が必要な場合は、幾何学コースをチェックしてください。負の結果を避けるには、結果を 0 に制限する必要があります。
float facingRatio = std::max(0, N.dotProduct(V));
法線とベクトル V が同じ方向を向いている場合、内積は 1 を返します。2 つのベクトルが垂直の場合、結果は 0 になります。この簡単なテクニックを使用してフレームの中央にある球にシェーディングを適用すると、以下に示すように、球の中心は白く、中心から端に向かって遠ざかるにつれて球は暗くなります。
Vec3f castRay(
const Vec3f &orig, const Vec3f &dir,
const std::vector<std::unique_ptr<Object>> &objects,
const Options &options)
{
Vec3f hitColor = options.backgroundColor;
float tnear = kInfinity;
Vec2f uv;
uint32_t index = 0;
Object *hitObject = nullptr;
if (trace(orig, dir, objects, tnear, index, uv, &hitObject)) {
Vec3f hitPoint = orig + dir * tnear; //shaded point
Vec3f hitNormal;
Vec2f hitTexCoordinates;
// compute the normal of the point we want to shade
hitObject->getSurfaceProperties(hitPoint, dir, index, uv, hitNormal, ...);
hitColor = std::max(0.f, hitNormal.dotProduct(-dir)); //facing ratio
}
return hitColor;
}
おめでとう!最初のシェーディングテクニックについて学びました。次に、拡散オブジェクトに対する光の効果をシミュレートする、シェーディングへのより現実的なアプローチを見てみましょう。ただし、この方法を理解する前に、まず光の概念を導入して理解する必要があります。
3. フラット シェーディング、スムース シェーディング、頂点法線
三角形メッシュの問題は、(三角形が非常に小さい場合を除き) 完全に滑らかな表面を表現できないことです。先ほど説明したアスペクト比テクニックをポリゴン メッシュに適用したい場合は、光線が交差する三角形の法線を計算し、面の法線とビュー方向の内積としてアスペクト比を計算する必要があります。このアプローチの問題は、次の図に示すように、オブジェクトにファセットのある外観が与えられることです。したがって、このシェーディング方法はフラット シェーディングと呼ばれます。
前のレッスンで何度も述べたように、三角形の法線は、ベクトル v0v1 と v0v2 の外積を計算するだけで求めることができます。ここで、v0、v1、v2 は三角形の頂点を表します。この問題を解決するために、Henri Gouraud は 1971 年に、現在スムース シェーディングまたはグーロー シェーディングと呼ばれる方法を導入しました。
このテクニックの背後にある考え方は、メッシュで表されるオブジェクトが平面 (ポリゴンまたは三角形) の集合から構築されているため連続していない場合でも、ポリゴン メッシュの表面に連続的な影を生成することです。この目的のために、グーローは頂点法線の概念を導入しました。考え方はシンプルです。面の法線を計算または保存する代わりに、メッシュの各頂点に法線を保存します。法線の方向は、三角形メッシュの変換元となった基礎となる滑らかな表面によって決まります。三角形の表面上の点の色を計算する場合、面法線を使用する代わりに、ヒット ポイントの重心座標を使用して三角形の頂点で定義された頂点法線を線形補間することにより、「疑似スムーズ」法線を計算できます。
このテクニックを上の図に示します。頂点法線は三角形の頂点で定義されます。三角形メッシュが構築される滑らかな下層サーフェスに対して垂直に配向されていることがわかります。場合によっては、三角形メッシュが滑らかなサーフェスから直接変換されず、頂点法線をその場で計算する必要があります。頂点法線を計算するための滑らかなサーフェスがない場合に頂点法線を計算するにはさまざまな手法がありますが、このレッスンではそれらについては説明しません。ここで、Maya や Blender などのソフトウェアを使用してこれを行います。Maya では、ポリゴン メッシュを選択し、法線メニューでエッジをソフト化オプションを選択できます。
実際、実用的および技術的な観点から、各三角形には 3 つの頂点法線の独自のセットがあります。これは、三角形メッシュの頂点法線の総数が三角形の数に 3 を乗じたものに等しいことを意味します。場合によっては、2 つ、3 つ、またはそれ以上の三角形によって共有される頂点に定義された頂点法線が同じ (同じ方向を向いている) 場合がありますが、異なる方向のエフェクトを与えることで異なる方向を実現できます。たとえば、一部のハード エッジを表面に偽装することができます。
三角形の表面上の任意の点の補間された法線を計算するソース コードは、三角形の頂点法線、三角形上の点の重心座標、および三角形のインデックスがわかっていれば非常に簡単です。ラスタライゼーションとレイ トレーシングの両方でこの情報が得られます。頂点法線は、モデルの作成に使用した 3D プログラムによってモデル上に生成されます。これらは、三角形の接続情報、頂点の位置、三角形のテクスチャ座標を含むジオメトリ ファイルにエクスポートされます。次に、点の重心座標と三角形の頂点法線を組み合わせて、点補間された滑らかな法線を計算するだけです (以下の 17 ~ 20 行目)。
void getSurfaceProperties(
const Vec3f &hitPoint,
const Vec3f &viewDirection,
const uint32_t &triIndex,
const Vec2f &uv,
Vec3f &hitNormal,
Vec2f &hitTextureCoordinates) const
{
// face normal
const Vec3f &v0 = P[trisIndex[triIndex * 3]];
const Vec3f &v1 = P[trisIndex[triIndex * 3 + 1]];
const Vec3f &v2 = P[trisIndex[triIndex * 3 + 2]];
hitNormal = (v1 - v0).crossProduct(v2 - v0);
#if 1
// compute "smooth" normal using Gouraud's technique (interpolate vertex normals)
const Vec3f &n0 = N[trisIndex[triIndex * 3]];
const Vec3f &n1 = N[trisIndex[triIndex * 3 + 1]];
const Vec3f &n2 = N[trisIndex[triIndex * 3 + 2]];
hitNormal = (1 - uv.x - uv.y) * n0 + uv.x * n1 + uv.y * n2;
#endif
// doesn't need to be normalized as the N's are normalized but just for safety
hitNormal.normalize();
// texture coordinates
const Vec2f &st0 = texCoordinates[trisIndex[triIndex * 3]];
const Vec2f &st1 = texCoordinates[trisIndex[triIndex * 3 + 1]];
const Vec2f &st2 = texCoordinates[trisIndex[triIndex * 3 + 2]];
hitTextureCoordinates = (1 - uv.x - uv.y) * st0 + uv.x * st1 + uv.y * st2;
}
これは滑らかな表面の印象を与えるだけであることに注意してください。下の画像の多角形の球を見ると、内面は滑らかに見えますが、輪郭がファセット化されていることがわかります。この技術は三角形メッシュの外観を改善しますが、もちろん、ファセット状の外観の問題を完全に解決するわけではありません。この問題の唯一の解決策は、サブディビジョン サーフェス (これについては別のセクションで説明します) を使用するか、もちろん、滑らかなサーフェスを三角形メッシュに変換するときに使用する三角形の数を増やすことです。
拡散サーフェスの外観を再現する方法を学ぶ準備ができていません。ただし、拡散面では光が見える必要があります。したがって、このテクニックを検討する前に、まず 3D エンジンで光源の概念を扱う方法を理解する必要があります。
元のリンク:表面法線と頂点法線 - BimAnt