コンピューターグラフィックスの短期コース:単純化されたOpenGLを自分で作成する、記事6/6

メインコースの内容







コード改善










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






シャドウマッピング



さて、短いコースは終わりに近づいています。今日の課題は、影の描画方法を学ぶことです(注意、部分的な影の計算は別のトピックです)。







いつものように、コードはgithubで利用可能です



これまで、サーフェスの法線により凸状オブジェクトを隠すことができましたが、非凸状オブジェクトのレンダリングは間違った結果をもたらしました。 左頬の角から影がないのはなぜですか? 混乱。







アイデアは非常に単純です。2つのパスでレンダリングします。 光源の代わりにカメラを置いて初めて画像をレンダリングすると、どの場所が照らされているかが正確にわかります。 そして、2番目のパスでは、最初のパスの結果を使用します。 ここにはほとんど困難はありません。 このシェーダーを書きましょう:

非表示のテキスト
struct DepthShader : public IShader { mat<3,3,float> varying_tri; DepthShader() : varying_tri() {} virtual Vec4f vertex(int iface, int nthvert) { Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file gl_Vertex = Viewport*Projection*ModelView*gl_Vertex; // transform it to screen coordinates varying_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3])); return gl_Vertex; } virtual bool fragment(Vec3f bar, TGAColor &color) { Vec3f p = varying_tri*bar; color = TGAColor(255, 255, 255)*(pz/depth); return false; } };
      
      







このシェーダーは、zバッファーの内容をフレームバッファーに単純に描画します。 main()からこのシェーダーを呼び出します:

非表示のテキスト
  { // rendering the shadow buffer TGAImage depth(width, height, TGAImage::RGB); lookat(light_dir, center, up); viewport(width/8, height/8, width*3/4, height*3/4); projection(0); DepthShader depthshader; Vec4f screen_coords[3]; for (int i=0; i<model->nfaces(); i++) { for (int j=0; j<3; j++) { screen_coords[j] = depthshader.vertex(i, j); } triangle(screen_coords, depthshader, depth, shadowbuffer); } depth.flip_vertically(); // to place the origin in the bottom left corner of the image depth.write_tga_file("depth.tga"); } Matrix M = Viewport*Projection*ModelView;
      
      









光源の代わりにカメラを置き(lookat(light_dir、center、up);)、レンダリングを行います。 このレンダリングパスのzバッファーは、シャドウバッファーポインターに格納されます。 最後の行で、オブジェクトの座標から画面座標への遷移行列を保存することに注意してください。



これがこのシェーダーの結果です。最初のレンダリングパスは終了しました。

非表示のテキスト








別のシェーダーを使用して2回目のパスを作成します。



非表示のテキスト
 struct Shader : public IShader { mat<4,4,float> uniform_M; // Projection*ModelView mat<4,4,float> uniform_MIT; // (Projection*ModelView).invert_transpose() mat<4,4,float> uniform_Mshadow; // transform framebuffer screen coordinates to shadowbuffer screen coordinates mat<2,3,float> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader mat<3,3,float> varying_tri; // triangle coordinates before Viewport transform, written by VS, read by FS Shader(Matrix M, Matrix MIT, Matrix MS) : uniform_M(M), uniform_MIT(MIT), uniform_Mshadow(MS), varying_uv(), varying_tri() {} virtual Vec4f vertex(int iface, int nthvert) { varying_uv.set_col(nthvert, model->uv(iface, nthvert)); Vec4f gl_Vertex = Viewport*Projection*ModelView*embed<4>(model->vert(iface, nthvert)); varying_tri.set_col(nthvert, proj<3>(gl_Vertex/gl_Vertex[3])); return gl_Vertex; } virtual bool fragment(Vec3f bar, TGAColor &color) { Vec4f sb_p = uniform_Mshadow*embed<4>(varying_tri*bar); // corresponding point in the shadow buffer sb_p = sb_p/sb_p[3]; int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]); Vec2f uv = varying_uv*bar; // interpolate uv for the current pixel Vec3f n = proj<3>(uniform_MIT*embed<4>(model->normal(uv))).normalize(); // normal Vec3f l = proj<3>(uniform_M *embed<4>(light_dir )).normalize(); // light vector 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); for (int i=0; i<3; i++) color[i] = std::min<float>(20 + c[i]*shadow*(1.2*diff + .6*spec), 255); return false; } };
      
      









これは、前の記事の最後の1対1のシェーダーですが、1つの例外があります。

頂点またはフラグメントシェーダーマット<4.4、float> uniform_Mshadowの操作中に変化しない定数行列を宣言しました。



このマトリックスを使用すると、現在のシェーダーの画面座標を、既に描画されているシャドウバッファーの画面座標に変換できます。



次の段落でそれをどう考えるかについて。 使用方法を見てみましょう。シェーダーの次の4行に注意してください。



  Vec4f sb_p = uniform_Mshadow*embed<4>(varying_tri*bar); // corresponding point in the shadow buffer sb_p = sb_p/sb_p[3]; int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]);
      
      







changing_tri * barは、描画する現在のフラグメントのスクリーン座標を提供します。 それらを同次座標に浸し、マジックマトリックスuniform_Mshadowで変換すると、最初のパスで使用したシャドウシェーダー空間のxyz座標がわかります。 さて、この点が照らされているかどうかを理解するために、最初のパスからそのz座標とzバッファーの値を比較するだけで十分です!



main()の2番目のシェーダー呼び出しはどのように見えますか? すべてがかなり標準です:



  Matrix M = Viewport*Projection*ModelView; { // rendering the frame buffer TGAImage frame(width, height, TGAImage::RGB); lookat(eye, center, up); viewport(width/8, height/8, width*3/4, height*3/4); projection(-1.f/(eye-center).norm()); Shader shader(ModelView, (Projection*ModelView).invert_transpose(), M*(Viewport*Projection*ModelView).invert()); Vec4f screen_coords[3]; for (int i=0; i<model->nfaces(); i++) { for (int j=0; j<3; j++) { screen_coords[j] = shader.vertex(i, j); } triangle(screen_coords, shader, frame, zbuffer); } frame.flip_vertically(); // to place the origin in the bottom left corner of the image frame.write_tga_file("framebuffer.tga"); }
      
      





マトリックスMは、オブジェクトの座標をシャドウバッファーの画面座標に変換するためのマトリックスであることを思い出してください。 カメラをあるべき場所に置き、ビューポートと遠近法の投影パラメーターを構成し、2番目のレンダリングパスのシェーダーを宣言します。



Viewport * Projection * ModelViewは、オブジェクトの座標を2番目のシェーダーのスクリーン座標に変換するためのマトリックスであることを知っています。 ただし、2番目のシェーダーの画面から最初のシェーダーの画面への変換マトリックスを知る必要があります。 簡単です:(Viewport * Projection * ModelView).invert()は、2番目のシェーダーの画面をオブジェクト座標に変換し、Mを乗算するだけで、最終変換マトリックスをM *(Viewport * Projection * ModelView).invert()として取得します。



些細なことではないとしても、すべてがうまくいきます。19の半分は、分割されていないようです。 2パスレンダーの結果は次のとおりです。

非表示のテキスト








これは何ですか このアーティファクトはz戦いとして知られています。 ピクセルを照らす必要がある場合、そのZ座標はシャドウシェーダーのZバッファー内にある必要があります。 または、それは隣接するピクセルのZ値である必要がありますか? 一般に、zバッファの解像度は、アーチファクトのない画像を提供するのに十分ではありません。 総当たりでこの問題と戦います。

  float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]+43.34); // magic coeff to avoid z-fighting
      
      





特定の定数だけ1つのzバッファを別のzバッファにシフトしています。これにより、このアーティファクトが消えます。 はい、これにより新しいアーティファクト(どれが発生しますか?)が発生しますが、目にはかなり目立ちません。 すべて、プログラムの結果はタイトル画像に表示されます。



おめでとうございます、私たちの短いコースは終わりました。 最初から、OpenGLの類似物であるかなり良いものを作成しました。



ボランティアとしてサインアップしましたか?



ショートコースのボーナスとして、次回は(接線空間で定義されたテクスチャを使用するために)表面への接線基底を計算する方法を示すと同時に、発光オブジェクトを扱うことができるシンプルなシェーダーを記述します(糖尿病の頭の結晶を参照)。







Samuel Sharitはこのモデルを非常に親切に提供してくれました。もちろん、このトレーニングコースの枠組み内でのみ彼の特別な許可なしに使用できます。また、Vidar Rapp製のNegroヘッドモデルも使用できます。



All Articles