コンピュータグラフィックスの短期コース:簡略化されたDIY OpenGLの記事5/6

メインコースの内容







コード改善










公式の翻訳(少し磨き上げたもの)はこちらから入手できます。






楽しみの時間です。まず、 現在のコードのサイズを見てみましょう。





合計525行 。 コースの最初に約束したとおりです。 our_glとmainでのレンダリングのみに関与していることに注意してください。これはたった168行であり、サードパーティライブラリを呼び出すところはありません。すべてのレンダリングはゼロから行われました。

私のコードは、作業中のコードとの最終的な比較にのみ必要であることを思い出してください! この一連の記事に従う場合は、良い方法ですべてをゼロから書く必要があります。 最もクレイジーなシェーダーを実行し、コメントに写真を投稿してください!







角の黒い三角形はやや打たれたモデルです。老黒人の頭にうんざりしましたが、それを修正したくありません。






OpenGLフレームワークに似るようにコードをリファクタリングする



したがって、main.cppが少し大きくなり始めたので、2つの部分に分けましょう。





our_gl出力した内容を詳しく見てみましょう。 投影マトリックス、タイプ、およびスクリーン座標への遷移を構築する機能、およびマトリックス自体は、単にグローバル変数です。 さて、三角形関数ラスタライザ。 それだけです!



our_gl.hファイルの内容は次のとおりです(少し後のIShaderの目的について)。

#include "tgaimage.h" #include "geometry.h" extern Matrix ModelView; extern Matrix Viewport; extern Matrix Projection; void viewport(int x, int y, int w, int h); void projection(float coeff=0.f); // coeff = -1/c void lookat(Vec3f eye, Vec3f center, Vec3f up); struct IShader { virtual ~IShader(); virtual Vec3i vertex(int iface, int nthvert) = 0; virtual bool fragment(Vec3f bar, TGAColor &color) = 0; }; void triangle(Vec4f *pts, IShader &shader, TGAImage &image, TGAImage &zbuffer);
      
      







main.cppファイルには66行しか残っていないので、その全体を提供します(シートについては申し訳ありませんが、このファイルが好きなので、スポイラーの下に隠しません)。

 #include <vector> #include <iostream> #include "tgaimage.h" #include "model.h" #include "geometry.h" #include "our_gl.h" Model *model = NULL; const int width = 800; const int height = 800; Vec3f light_dir(1,1,1); Vec3f eye(1,1,3); Vec3f center(0,0,0); Vec3f up(0,1,0); struct GouraudShader : public IShader { Vec3f varying_intensity; // written by vertex shader, read by fragment shader virtual Vec4f vertex(int iface, int nthvert) { varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates } virtual bool fragment(Vec3f bar, TGAColor &color) { float intensity = varying_intensity*bar; // interpolate intensity for the current pixel color = TGAColor(255, 255, 255)*intensity; // well duh return false; // no, we do not discard this pixel } }; int main(int argc, char** argv) { if (2==argc) { model = new Model(argv[1]); } else { model = new Model("obj/african_head.obj"); } lookat(eye, center, up); viewport(width/8, height/8, width*3/4, height*3/4); projection(-1.f/(eye-center).norm()); light_dir.normalize(); TGAImage image (width, height, TGAImage::RGB); TGAImage zbuffer(width, height, TGAImage::GRAYSCALE); GouraudShader shader; for (int i=0; i<model->nfaces(); i++) { Vec4f screen_coords[3]; for (int j=0; j<3; j++) { screen_coords[j] = shader.vertex(i, j); } triangle(screen_coords, shader, image, zbuffer); } image. flip_vertically(); // to place the origin in the bottom left corner of the image zbuffer.flip_vertically(); image. write_tga_file("output.tga"); zbuffer.write_tga_file("zbuffer.tga"); delete model; return 0; }
      
      







詳細に分析しましょう。 ヘッダーをスキップしてから、グローバル定数(画面サイズ、カメラの設置場所など)に進みます。

次の段落でGouraudShaderの構造を分析し、スキップします。 それから直接main()になります:





最後の段落では、楽しみが始まります。 外側のサイクルはすべての三角形を通過します。

内側のループは三角形のすべての頂点を通過し、それらのそれぞれに対して頂点シェーダーを呼び出します。



頂点シェーダーの主な目的は、変換された頂点の座標を計算することです。 2番目は、フラグメントシェーダーのデータを準備することです。



三角形のすべての頂点に対して頂点シェーダーを呼び出した後はどうなりますか? 三角形のラスタライザーを呼び出すことができます。 内部で何が起こっているのかわかりません(もちろん、自分で書いたわけではありません)。 1つの興味深いことを除いて。 三角形ラスタライザーは関数を呼び出し、これをフラグメントシェーダーに渡します。 つまり、三角形内の各ピクセルに対して、ラスタライザーはフラグメントシェーダーを呼び出します。



フラグメントシェーダーの主な目的は、現在のピクセルの色を決定することです。 セカンダリ-trueを返すことにより、このピクセルの描画を拒否することもできます。



OpenGL 2ピップラインは次のようになります。





短いグラフィックコースがあるため、ここではこれら2つのシェーダーに限定します。 OpenGLの新しいバージョンでは、ジオメトリをその場で作成できる新しいタイプのシェーダーが登場しました。 この図では、青いステージはプログラムできないステージですが、赤いステージはプログラム可能です。 実際、メイン()はプリミティブ処理です。 頂点シェーダーを呼び出します。 プリミティブビルダーはありません。 愚かな三角形を直接描画します(プリミティブ処理で接着しました)。 triangle()関数はラスタライザーであり、各ポイントに対してフラグメントシェーダーを呼び出し、z-bufferなどで深度チェックを行います。



それだけです シェーダーが何であるかを知っていて、それらのプログラミングを開始できます。



シェーダーの私の具体化がGuro着色の例でどのように機能するか





main.cppコードで持ってきたシェーダーを見てみましょう。 ご想像のとおり、最初のシェーダーはGuroの色合いです。



非表示のテキスト








頂点シェーダーは、.objファイルから頂点を読み取り、4次元空間に浸し(前の記事を参照)、その画面座標を見つけます。 3Dで投影されたポイントを返しますが、その前に、指定された頂点の拡散照明係数を考慮して、variing_intensityベクトルの対応するコンポーネントに保存します。



もう一度、便宜上のコード:

非表示のテキスト
  Vec3f varying_intensity; // written by vertex shader, read by fragment shader virtual Vec4f vertex(int iface, int nthvert) { varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates }
      
      









GLSLでは、variingは予約語です。それらの類似点を強調するために、variing_intensityを名前として使用しました(GLSLについては、7番目の記事で説明します)。 三角形の内部で補間されるデータをさまざまな構造に保存し、フラグメントシェーダーが補間されたデータを受け取ります。



フラグメントシェーダーを分析してみましょう。もう一度、便宜上コードを見てみましょう。

非表示のテキスト
  Vec3f varying_intensity; // written by vertex shader, read by fragment shader // [...] virtual bool fragment(Vec3f bar, TGAColor &color) { float intensity = varying_intensity*bar; // interpolate intensity for the current pixel color = TGAColor(255, 255, 255)*intensity; // well duh return false; // no, we do not discard this pixel }
      
      









三角形内の各ピクセルに対してラスタライザーによって呼び出されます。 重心座標の入力を受け取り、variing_データを補間します。



つまり、補間強度は、variing_intensity [0] * bar [0] + changing_intensity [1] * bar [1] + changing_intensity [2] * bar [2]、または単にvariing_intensity * barベクトル間のスカラー積として計算できます。 もちろん、実際のGLSLでは、シェーダーはその値を準備します。



フラグメントシェーダーはブール値を返すことに注意してください。 ラスタライザーの内部our_gl.cpp 、三角形())を見ると、その意味は簡単に理解できます。

非表示のテキスト
  TGAColor color; bool discard = shader.fragment(c, color); if (!discard) { zbuffer.set(Px, Py, TGAColor(Pz)); image.set(Px, Py, color); }
      
      







シェーダーは指定されたピクセルの描画を拒否する場合があり、その後、ラスタライザーはzバッファーを更新せずにz座標も無視します。 バイナリマスクを作成したい場合や、他に思いついたことがある場合に便利です。



もちろん、ラスタライザはあなたの頭に浮かぶことを考えることすらできないので、事前にシェーダでコンパイルすることはできません。 ここでは、抽象クラスIShaderが役立ちます。 Uff、私はめったに抽象クラスを使用しませんが、それがなければ抽象クラスが悪い場合があります。 関数へのポインターをまったく渡したくありません!








最初の修正



非表示のテキスト
  virtual bool fragment(Vec3f bar, TGAColor &color) { float intensity = varying_intensity*bar; if (intensity>.85) intensity = 1; else if (intensity>.60) intensity = .80; else if (intensity>.45) intensity = .60; else if (intensity>.30) intensity = .45; else if (intensity>.15) intensity = .30; else intensity = 0; color = TGAColor(255, 155, 0)*intensity; return false; }
      
      









照明強度の固定セットを許可します。 これが彼の仕事の結果です。

非表示のテキスト













モデルのテクスチャリング



Phongの色合いをスキップし、コメントで詳細に調べました。テクスチャを課しましょう。 これを行うには、UV座標を補間する必要があります。 何も新しいことはありません。2行(uv)と3列(3つの頂点のテクスチャ座標)にマトリックスを追加するだけです。

非表示のテキスト
 struct Shader : public IShader { Vec3f varying_intensity; // written by vertex shader, read by fragment shader mat<2,3,float> varying_uv; // same as above virtual Vec4f vertex(int iface, int nthvert) { varying_uv.set_col(nthvert, model->uv(iface, nthvert)); varying_intensity[nthvert] = std::max(0.f, model->normal(iface, nthvert)*light_dir); // get diffuse lighting intensity Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates } virtual bool fragment(Vec3f bar, TGAColor &color) { float intensity = varying_intensity*bar; // interpolate intensity for the current pixel Vec2f uv = varying_uv*bar; // interpolate uv for the current pixel color = model->diffuse(uv)*intensity; // well duh return false; // no, we do not discard this pixel } };
      
      









非表示のテキスト









ノーマルマッピング





さて、これでテクスチャ座標ができました。 しかし、色だけでなくテクスチャにも保存できます。RGBはxyzを表すのに十分です。

画像のピクセルごとに(以前のように頂点だけでなく!)通常のベクトルを与えるようなテクスチャをロードしましょう。

非表示のテキスト






ちなみに、このような写真と比較してください。これは同じ情報ですが、異なるフレームにあります。

非表示のテキスト








これらの画像の1つは、グローバル座標系の法線ベクトルを提供し、もう1つは、オブジェクトの各ポイントに対して決定される接線のベクトルを提供します。 このテクスチャでは、ベクトルzはオブジェクトの法線、ベクトルxはサーフェスの曲率の主方向のベクトル、yはそれらのベクトル積です。

演習1

これらのテクスチャのどれがグローバル座標で与えられ、どの接線でオブジェクトに接しているのか教えてください。

演習2

接線またはグローバルのどちらのテクスチャー形式が推奨されますか? なんで?



コメントでこれらの質問への回答を提供するために(コメントを事前に読むことなく)お気軽に!



非表示のテキスト
 struct Shader : public IShader { mat<2,3,float> varying_uv; // same as above mat<4,4,float> uniform_M; // Projection*ModelView mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose() virtual Vec4f vertex(int iface, int nthvert) { varying_uv.set_col(nthvert, model->uv(iface, nthvert)); Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates } virtual bool fragment(Vec3f bar, TGAColor &color) { Vec2f uv = varying_uv*bar; // interpolate uv for the current pixel Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize(); Vec3f l = proj<3>(uniform_M *embed<4>(light_dir )).normalize(); float intensity = std::max(0.f, n*l); color = model->diffuse(uv)*intensity; // well duh return false; // no, we do not discard this pixel } }; [...] Shader shader; shader.uniform_M = Projection*ModelView; shader.uniform_MIT = (Projection*ModelView).invert_transpose(); for (int i=0; i<model->nfaces(); i++) { Vec4f screen_coords[3]; for (int j=0; j<3; j++) { screen_coords[j] = shader.vertex(i, j); } triangle(screen_coords, shader, image, zbuffer); }
      
      









GLSLのuniformキーワードを使用すると、シェーダーに定数を渡すことができます。ここでは、法線ベクトルを変換するためにProjection * Modelviewマトリックスとその逆転置マトリックスをシェーダーに渡しました(前の記事を参照)。

つまり、すべてが以前と同じで、法線ベクトルを補間しないだけですが、準備されたテクスチャから、それぞれ光と法線ベクトルの方向ベクトルを忘れずに取得します。



非表示のテキスト













光沢のある表面または鏡面反射マッピング





会話を続けます! (安い)アイトリックの場合、モデルを照らすためにPhong近似を使用ます。 この領域の全体の照明は、シーン全体の一定の照明(アンビエント照明)、これまで検討してきたマットサーフェスの照明(拡散照明)、光沢のある表面の照明(鏡面照明)で構成されます。







マットサーフェスの輝度は、法線ベクトルとライトベクトルの間の角度の余弦と見なされました。 つまり、表面がほぼすべての方向に光を散乱すると仮定しました。 光沢のある表面はどうなりますか? 極端な場合(鏡面の場合)、このピクセルからの光源を見ると光があります。



写真は次のとおりです。



この点について、マットサーフェスの照明をベクトルnlの間の角度のコサインと考えた場合、ベクトルr (反射光)とv (ビューの方向)の間の角度のコサインに関心があります。



演習3:ベクトルnとlを持つベクトルrを見つける



非表示のテキスト
nとlが正規化されている場合、 r = 2 n < nl > -l




マットサーフェスのライトを角度の余弦と見なしたことを思い出してください。 しかし、光沢のある光源は、はるかに焦点の合ったビームで光源を反映します! 同じことを行って、このコサインを10乗した場合はどうなりますか? ユニティ未満の数は、自分に比べて10分の1に減少することを思い出してください! つまり、10度では照明の半径が大幅に小さくなります。 そして、100分の1はさらに小さくなります。 この度合いは、表面のすべてのポイントに光沢を与えるテクスチャに保存されます。



非表示のテキスト
 struct Shader : public IShader { mat<2,3,float> varying_uv; // same as above mat<4,4,float> uniform_M; // Projection*ModelView mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose() virtual Vec4f vertex(int iface, int nthvert) { varying_uv.set_col(nthvert, model->uv(iface, nthvert)); Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file return Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates } virtual bool fragment(Vec3f bar, TGAColor &color) { Vec2f uv = varying_uv*bar; Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize(); Vec3f l = proj<3>(uniform_M *embed<4>(light_dir )).normalize(); Vec3f r = (n*(n*l*2.f) - l).normalize(); // reflected light float spec = pow(std::max(rz, 0.0f), model->specular(uv)); float diff = std::max(0.f, n*l); TGAColor c = model->diffuse(uv); color = c; for (int i=0; i<3; i++) color[i] = std::min<float>(5 + c[i]*(diff + .6*spec), 255); return false; } };
      
      









実際、係数を除いて、ここで説明するものはありません。 並んで

         for(int i = 0; i <3; i ++)color [i] = std :: min <float>(5 + c [i] *(diff + .6 * spec)、255);


アンビエントに5、ディフューズに1、スペキュラに0.6を取りました。 どちらを取るかはあなた次第です。 これは、さまざまな素材の印象を与えます。 ほとんどの場合、アーティストから与えられましたが、この場合は持っていませんので、ブルドーザーから取り出しました。



非表示のテキスト








おわりに



信じられないようなシーンをレンダリングする方法を学びましたが、照明はまだ理想からはほど遠いです。 次の記事では、シャドウマッピングとは何かについて説明します。 直交する記事の1つで、新しいラスタライザの動作について説明します(古いラスタライザで同じコードを実行することを妨げるものは何もありません!)。



All Articles