シェーダープログラミング入門:パート3

シェーダーの操作の基本を習得したので、現実的な動的照明のシステムを作成することにより、GPUのフルパワーを実際に抑制しようとします。







このシリーズの最初のレッスンでは、グラフィックシェーダー作成の基本について説明しました。 2番目の方法では、任意のプラットフォームにシェーダーをセットアップする際のアクション一般的なアルゴリズムを研究しました。 ここで、プラットフォームに縛られることなく、グラフィックシェーダーの分野の基本的な概念に対処します。 便宜上、例では引き続きJavaScript / WebGLを使用します。



先に進む前に、シェーダーを操作する最も便利な方法を選択してください。 最も簡単なオプションはJavaScript / WebGLですが、お気に入りのプラットフォームで試してみることをお勧めします。



目標



このレッスンの終わりまでに、照明システムの動作原理を深く理解できるようになるだけでなく、そのようなシステムを最初から最後まで作成します。



これが最終結果の表示方法です( CodePen移動して照明を切り替えます)。







多くのゲームエンジンには照明システムが組み込まれていますが、それらの作成プロセスを理解することで、より柔軟な設定を適用し、ゲームをよりユニークにすることができます。 シェーダー効果の役割を純粋に装飾的なものに限定すべきではありません。新しいゲームの仕組みを作成するために使用できます。



ダイナミックライティングの優れた例はChromaです。







はじめに:開始シーン



このすべては前のレッスンで説明したように、ほとんどの準備をスキップします。 テクスチャを使用した単純なフラグメントシェーダーから始めましょう。







まだ特別なことはありません。 JavaScriptコードはシーン設定を設定し、テクスチャをレンダーに送信し、画面設定をシェーダーに送信します。



var uniforms = { tex : {type:'t',value:texture},//The texture res : {type: 'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}//Keeps the resolution }
      
      







GLSLコードで変数を宣言します。

 uniform sampler2D tex; uniform vec2 res; void main() { vec2 pixel = gl_FragCoord.xy / res.xy; vec4 color = texture2D(tex,pixel); gl_FragColor = color; }
      
      







ピクセルを使用してテクスチャをペイントする前に、必ずピクセルの座標を調整してください。

しかし、まず、次のタスクを実行してウォームアップします。



目的:縦横比を維持しながらテクスチャを表示できますか? 以下のソリューションに進む前に、自分で試してみてください。



テクスチャが引き伸ばされる理由はかなり明白です。 しかし、ここに簡単なヒントがあります:座標が調整される行を見てください:



 vec2 pixel = gl_FragCoord.xy / res.xy;
      
      







vec2でvec2を除算します。これは、各コンポーネントを個別に除算するのに似ています。 つまり、上記の行は次の行と同等です。



 vec2 pixel = vec2(0.0,0.0); pixel.x = gl_FragCoord.x / res.x; pixel.y = gl_FragCoord.y / res.y;
      
      







xとyを異なる数(画面の幅と高さ)に分割するため、もちろんテクスチャが引き伸ばされます。

しかし、gl_FragCoordのxとyをx resで割っただけではどうなりますか? または、逆に、y resの値は?

実験を簡単にするために、レッスンの最後までそのままにしておきましょう。 しかし、何らかの形で、コード内で何が起こっているのか、そしてその理由を理解することは非常に重要です。



ステップ1.光源を追加する



まず、光源を追加します。 光源は、シェーダーに送信するポイントにすぎません。 このポイントの新しい均一変数を作成します。



 var uniforms = { //Add our light variable here light: {type:'v3', value:new THREE.Vector3()}, tex : {type:'t',value:texture},//The texture res : {type: 'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}//Keeps the resolution }
      
      







xとyの値を使用して画面上の光源の位置を示し、zをその半径として使用するため、3つのパラメーターを持つベクトルを作成しました。



JavaScriptコードで光源の値を設定します。



 uniforms.light.value.z = 0.2;//Our radius
      
      







画面サイズの20%に対応する半径を0.2に設定します。 ただし、単位は特別な役割を果たしません。サイズはピクセル単位で設定することもできます。 これは、GLSLコードになるまで何にも影響しません。



JavaScriptコードにイベントリスナーを追加して、マウスカーソルの位置を決定します。



 document.onmousemove = function(event){ //Update the light source to follow our mouse uniforms.light.value.x = event.clientX; uniforms.light.value.y = event.clientY; }
      
      







次に、光源を機能させるためのシェーダーコードを記述します。 簡単なものから始めましょう。ライトの半径内のすべてのピクセルが表示され、他のすべてのピクセルが黒であることを確認します。



GLSLコードでは、次のようになります。



 uniform sampler2D tex; uniform vec2 res; uniform vec3 light;//Remember to declare the uniform here! void main() { vec2 pixel = gl_FragCoord.xy / res.xy; vec4 color = texture2D(tex,pixel); //Distance of the current pixel from the light position float dist = distance(gl_FragCoord.xy,light.xy); if(light.z * res.x > dist){//Check if this pixel is without the range gl_FragColor = color; } else { gl_FragColor = vec4(0.0); } }
      
      







だから私たちがやったことです:



•光源の均一変数を宣言しました。

•内蔵距離機能を使用して、光源とこのピクセル間の距離を決定しました。

•距離関数の値を確認しました(ピクセル単位)。 画面幅の20%を超える場合は、指定されたピクセルの色を返し、そうでない場合は黒を返します。





アクションで表示-CodePenで。



おっと! 光の動きの論理に何かが間違っているようです。

タスク:修正できますか? 繰り返しますが、以下の答えを見る前に自分でやってみてください。



光の動きを修正する



最初のレッスンから覚えているように、ここではy軸が反転しています。 おそらく次のことを行うでしょう。



 light.y = res.y - light.y;
      
      







これは数学的な観点からは真実ですが、シェーダーはコンパイルされません。 実際には、均一変数は変更できません。 覚えておいてください:このコードは各ピクセルに対して並行して実行されます。 すべてのプロセッサコアが単一の変数を同時に変更しようとしていることを想像してください。 良くない!



この変数を変更するのではなく、新しい変数を作成することで問題を修正できます。 さらに良いことに、シェーダーに送信する前にそれを行います:





CodePenでは 、ソースコードのブランチを作成して編集できます。



 uniforms.light.value.y = window.innerHeight - event.clientY;
      
      







シーンの可視部分のパラメーターを正常に設定しました。 これで、この領域のエッジを少し滑らかにするのに問題はなくなりました。



グラデーションを追加



表示領域を黒で切り抜くのではなく、滑らかなグラデーションを作成してみてください。 このため、すでに計算している距離が役立ちます。



次のように、テクスチャの色を表示領域全体に戻す代わりに:



 gl_FragColor = color;
      
      







色に距離係数を掛けることができます:



 gl_FragColor = color * (1.0 - dist/(light.z * res.x));
      
      











これは、distが特定のピクセルと光源の間のピクセル単位の距離であるため機能します。 用語式(light.z * res.x)は、半径の長さです。 したがって、光源に正確に当たるピクセルを見ると、distは0です。その結果、ピクセルのフルカラーに対応する1を色に乗算します。







この図では、任意のピクセルのdistが計算されます。 distの値は、使用しているピクセルによって異なりますが、light.z * res.xの値は一定です。



円の端にあるピクセルの場合、distは円の半径に等しいため、色に0を乗算します。これは黒に相当します。



ステップ2.深さを追加する



そこで、テクスチャのグラデーションマスクを作成しました。 しかし、すべてがまだ平らに見えます。 これを修正する方法を理解するために、照明システムが現在何をしていて、原則として何をすべきかを見てみましょう。







この場合、セクションAは光源の真下に位置するため、セクションAが最も明るくなり、セクションBとセクションCは実際には光が入らないため暗くなります。

ただし、照明システムの動作は次のとおりです。







私たちが考慮する唯一の要素はxy平面上の距離であるため、すべての側面が等しく照らされます。 問題を解決するには、これらの各ポイントの高さを知るだけでよいように思えるかもしれませんが、これは完全に真実ではありません。 次の状況を考慮してください。







セクションAはブロックの上部で、BとCはその側面です。 D-ブロック近くの表面積。 ご覧のとおり、セクションAとDは最も明るいはずですが、Dは斜めに光が当たるため、少し暗くなります。 BとCは、光が実際には当たらないため、非常に暗くなります。



高さではなく、表面の前面の方向を知る必要があることがわかります。 これは、表面の法線と呼ばれます。

しかし、このデータをシェーダーに転送する方法は? 個々のピクセルごとに数千の数値を持つ巨大な配列を送信できますか? 実際、これはまさに私たちがやっていることです! 配列のみがテクスチャとして機能します。



これは法線マップです。各ピクセルの値r、g、bが色ではなく方向を示す画像です。







上記は単純な法線マップです。 カラーパレットを使用すると、標準の「フラット」方向が色(0.5、0.5、1)に対応していることがわかります。つまり、画像の大部分を占める青色です。 青いピクセルはまっすぐに見えます。 各ピクセルのすべてのr、g、b値は、x、y、z値に変換されます。



たとえば、ピンク色の斜めの面を見てみましょう。 それはそれぞれ右に向けられており、赤色に対応するx値は他の値よりも高くなっています。 同じことが他の関係者にも当てはまります。



おそらくこれはすべて奇妙に見えます。 ただし、法線マップはレンダリング用ではなく、法線値をサーフェスに変換するためだけのものです。



したがって、単純な法線マップをロードします。

 var normalURL = "https://raw.githubusercontent.com/tutsplus/Beginners-Guide-to-Shaders/master/Part3/normal_maps/normal_test.jpg" var normal = THREE.ImageUtils.loadTexture(normalURL);
      
      







そして、それを一様変数の1つとして追加します。



 var uniforms = { norm: {type:'t', value:normal}, //.. the rest of our stuff here }
      
      







すべてが正しくロードされたことを確認するには、GLSLコードを少し調整した後、テクスチャではなく法線マップをレンダリングしましょう(法線マップとしてではなく、背景テクスチャとして使用します)。







ステップ3.照明モデルを適用する



サーフェスの法線データが得られたので、照明モデルを実装する必要があります。 言い換えれば、ピクセルの最終的な明るさを計算するために利用可能なすべての要因を考慮する方法を表面に伝える必要があります。



最も簡単なオプションは、Phongモデルです。 これらの法線を持つそのような表面があると仮定します:







光源と表面の法線との間の角度を簡単に計算できます。







角度が小さいほど、ピクセルは明るくなります。

つまり、角度がゼロの光源の真下にあるピクセルが最も明るくなります。 そして最も暗いのは、オブジェクトの背面にあるピクセルです。

ここで、テストに単純な法線マップを使用しているため、テクスチャに単色を適用します。 次に、照明システムが機能しているかどうかを確認します。



代わりに:



 vec4 color = texture2D(...);
      
      







単色の白色を作成します(または他の任意の裁量で):



 vec4 color = vec4(1.0); //solid white
      
      







このGLSLの削減は、1.0に等しいすべてのコンポーネントでvec4を作成するために必要です。



アクションアルゴリズムは次のようになります。



1.このピクセルで法線ベクトルを取得します。

2.ライトの方向ベクトルを取得します。

3.ベクトルを正規化します。

4.ベクトル間の角度を考慮します。

5.最終的な色にこの係数を掛けます。



1.このピクセルで法線ベクトルを取得する

このピクセルに入射する光の量を決定するために、表面の前面の方向を知る必要があります。 この方向は法線マップに保存されているため、法線テクスチャ上の特定のピクセルの色を認識することで法線ベクトルを取得できます。



 vec3 NormalVector = texture2D(norm,pixel).xyz;
      
      







アルファ値は法線マップのどの要素にも関与しないため、最初の3つのコンポーネントのみが必要です。



2.光の方向ベクトルを取得する



次に、光源の方向を確認する必要があります。 マウスカーソルが置かれている場所の画面の前でホバリングする懐中電灯として想像してください。 光源とこのピクセル間の距離を使用して、光の方向ベクトルを決定できます。



 vec3 LightVector = vec3(light.x - gl_FragCoord.x,light.y - gl_FragCoord.y,60.0);
      
      







また、3次元の法線ベクトルに対する角度を決定するために、z座標が必要です。 値を変更してみてください。値が小さいほど、暗い部分と明るい部分のコントラストがはっきりしています。 ステージの上で懐中電灯を保持する高さと考えてください。遠くにあるほど、光はより均等に分配されます。



3.ベクトルの正規化



 NormalVector = normalize(NormalVector); LightVector = normalize(LightVector);
      
      







組み込みのnormalize関数を使用して、両方のベクトルの長さが1.0であることを確認します。 これは、スカラー積( ドット積 )を使用して角度を計算する必要があるため必要です。 これがどのように機能するかが完全にはわからない場合は、線形代数の知識を向上させるときです。 ただし、この場合、スカラー積が同じ長さの2つのベクトル間の角度のコサインを返すことを知るだけで済みます。



4.ベクトル間の角度を考慮します



これには、組み込みのドット関数が役立ちます。



 float diffuse = dot( NormalVector, LightVector );
      
      







Phongのライトモデルには、拡散光コンポーネントの概念があり、シーンの表面にどれだけの光が当たるかを考慮しているので、変数diffuseと呼びました。



5.最終的な色にこの係数を掛けます



以上です! 色に係数を掛けるだけです。 さらに進んで、distanceFactor変数を作成して、方程式の読みやすさを改善しました。



 float distanceFactor = (1.0 - dist/(light.z * res.x)); gl_FragColor = color * diffuse * distanceFactor;
      
      







その結果、実用的なライトモデルが完成しました。 結果をより見やすくするために、光源の半径を大きくすることができます。







うーん、何かがおかしかった。 光源の中心がずれているようです。



計算をもう一度確認しましょう。 ベクトルがあります:



 vec3 LightVector = vec3(light.x - gl_FragCoord.x,light.y - gl_FragCoord.y,60.0);
      
      







私たちが知っているように、このピクセルに光が直接当たると、(0、0、60)を返します。 正規化すると、(0、0、1)が得られます。

注意:最大の輝度を得るには、光源を直接指す法線が必要です。 真上を向いたサーフェスの法線の値は、デフォルトで(0.5、0.5、1)です。



タスク:問題の解決策はありますか? 直せますか?



実際には、負の数はテクスチャの色の値に格納できません。 たとえば、左を指すベクトルに値(-0.5、0、0)を設定することはできません。 したがって、通常のマップ作成者は各値に0.5を追加する必要があります。つまり、座標系をシフトします。 マップを使用する前に、各ピクセルからこれらの0.5を減算する必要があることに注意してください。

以下は、法線ベクトルのx値とy値から0.5を引いた結果です。







修正すべきことは1つだけです。 スカラー積は角度のコサインを返すため、結果の値は-1〜1の範囲になります。ただし、負の色の値は必要ありません。 WebGLは自動的に負の値を拒否しますが、問題はどこかで発生する可能性があります。 組み込みのmax関数を使用して、これを変更します。



 float diffuse = dot( NormalVector, LightVector );
      
      







これに:



 float diffuse = max(dot( NormalVector, LightVector ),0.0);
      
      







これで実用的な照明モデルが完成しました!

テクスチャを石で戻すことができます。 通常のマップは、GitHubのリポジトリで入手できます (または直接リンクしてください )。



JavaScriptコードでこのリンクを修正するだけです。



 var normalURL = "https://raw.githubusercontent.com/tutsplus/Beginners-Guide-to-Shaders/master/Part3/normal_maps/normal_test.jpg"
      
      







これについて:

 var normalURL = "https://raw.githubusercontent.com/tutsplus/Beginners-Guide-to-Shaders/master/Part3/normal_maps/blocks_normal.JPG"
      
      





GLSLコードの次の行:



 vec4 color = vec4(1.0);//solid white
      
      







テクスチャを単色の白い色に置き換える:



 vec4 color = texture2D(tex,pixel);
      
      







最後に、結果は次のとおりです。



prntscr.com/auy52h

CodePenでは 、ソースコードのブランチを作成して編集できます。



最適化のヒント



GPUは非常に効率的ですが、GPUの動作を妨げる要因を知ることは有用です。 いくつかの簡単なヒントを次に示します。



分岐

シェーダーを使用する場合は、可能な限り分岐を避けるのが最善です。 CPUのコードを使用する場合、多くのifステートメントを心配する必要はほとんどありません。 しかし、GPUにとっては、深刻な障害になる可能性があります。



理由を理解するために、GLSLコードは画面上の個々のピクセルごとに並行して実行されることを思い出します。 すべてのピクセルで同じ操作が必要な場合、ビデオカードの操作を大幅に最適化できます。 コード内に多数のif文がある場合、ピクセルごとに異なるコードを実行する必要があるため、最適化が低下する可能性があります。 もちろん、これは特定のコンポーネントの特性、およびビデオカードを使用する機能に依存します。 しかし、シェーダーを高速化するためにこれを覚えておくと便利です。



遅延レンダリング



これは、照明を扱うのに非常に便利なテクニックです。 1つの光源ではなく、2、3、または10の光源を作成する場合を想像してください。サーフェスの各法線と各光源の間の角度を計算する必要があります。 これにより、シェーダーの速度が非常に速く低下します。 遅延レンダリングは、シェーダーの作業を複数の動きに分割することにより、これを修正するのに役立ちます。 この記事では、この問題について詳しく説明します。 ここでは、このレッスンのトピックに関する断片のみを示します。



「照明は、いずれかの開発パスを選択する主な理由です。 標準の可視化パイプラインでは、照明に関連するすべての計算は、光源ごとに、各頂点および可視シーンの各フラグメントで実行する必要があります。



たとえば、光の点の配列を送信する代わりに、それらをテクスチャ上に円として配置できます。各ピクセルの色は光の強度を示します。 したがって、シーン内のすべての光源の合計効果を計算し、この最終テクスチャ(またはバッファとも呼ばれる)のみを送信し、それに基づいてライティングを計算できます。



シェーダーの作業を複数の動きに分割することは非常に便利なテクニックです。 ぼかし効果、水、煙を作成するために広く使用されています。 遅延レンダリングはこのレッスンのトピックには合いませんが、次のレッスンで説明します!



さらなるステップ



作業用の照明シェーダーができたので、次を試すことができます。



•光ベクトルの高さ(z値)を変更して、光ベクトルが生成する効果を確認します。

•光の強度を変更します。 これは、拡散コンポーネントに係数を掛けることで実行できます。

•光を計算するための方程式に周囲成分を追加します。 これは、シーンに最小限の(初期)照明を追加して、よりリアルに作成することを意味します。 実際の生活では、最小限の光がどの表面にも当たるため、絶対に暗い物体はありません。

•このWebGLチュートリアルで説明されているシェーダーのいずれかを作成してみください これらはThree.jsではなくBabylon.jsに基づいていますが、GLSL設定にすぐにアクセスできます。 特に、セルシェーディングとフォンシェーディングに興味があるかもしれません。

GLSL SandboxShaderToyの興味深い作品をご覧ください



参照資料

このチュートリアルで使用する石のテクスチャと法線マップは、 OpenGameArt Webサイトから取得したものです。 とりわけ、法線マップを作成するための多くのプログラムがあります。 法線マップの作成方法について詳しく知りたい場合は、 この記事も参照してください。




All Articles