最新のグラフィックシミュレーションのほとんどは、ビデオプロセッサ用に記述されたコードを使用しています。ハイテクAAAゲームのリアルな照明効果から、後処理と流体シミュレーションの2次元効果まで。
複数のシェーダーを追加する前後のMinecraftのシーン。
このチュートリアルの目的
シェーダープログラミングは、神秘的なブラックマジックのように思われ、しばしば誤解されます。 信じられないほどの効果の作成を示す多くのコード例がありますが、説明はほとんどありません。 私のガイドでは、このギャップを埋めたいと思っています。 シェーダーコードの作成と理解の基本原則に主に焦点を当て、独自のシェーダーをゼロから簡単にカスタマイズ、組み合わせ、または作成できるようにします!
これは一般的なガイドであるため、ここで学習するすべてのものは、シェーダーを使用するあらゆるテクノロジーで使用できます。
シェーダーとは何ですか?
シェーダーは、グラフィックスパイプラインで実行される単なるプログラムです。 各ピクセルのレンダリング方法をコンピューターに伝えます。 これらのプログラムは、 照明やシェーディングの効果を制御するためによく使用されるため、シェーダー(「シェーダー」)と呼ばれますが、他の特殊効果に使用されることを妨げるものはありません。
シェーダーは特別なシェーダー言語で記述します。 まったく新しい言語を学ぶ必要はありません。Cに似たGLSL(OpenGLシェーディング言語)を使用します(異なるプラットフォーム用のシェーダーを記述するための言語はいくつかありますが、それらはすべてビデオプロセッサでの実行に適合しているため、互いに類似しています友人。)
注:この記事は、フラグメントシェーダー専用です。 他の種類のシェーダーがあるかどうかに興味がある場合は、OpenGL Wikiでグラフィックパイプラインのさまざまな段階について読むことができます。
さあ始めましょう!
このチュートリアルでは、 ShaderToyを使用します 。 これにより、インストールと構成に煩わされることなく、ブラウザーで直接シェーダーのプログラミングを開始できます! (レンダリングにはWebGLを使用するため、このテクノロジーをサポートするブラウザーが必要です。) アカウントを作成する必要はありませんが、コードを保存するには便利です。
注:執筆時点では、ShaderToyはベータ版でした[おおよそ。 trans .: 2015年の記事] 。 一部のインターフェイス/構文の詳細は若干異なる場合があります。
New Shaderをクリックすると、次のようなものが表示されます。
登録していない場合、インターフェースは若干異なる場合があります。
以下の小さな黒い矢印は、コードをコンパイルするのに役立ちます。
何が起こっているの?
シェーダーがどのように機能するかを1つの文で説明します。 準備はいい? ここにある!
シェーダーの唯一の目的は、
r
、
g
、
b
4つの数値を返すことです 。
これはまさにシェーダーができることであり、それだけです。 この関数は、画面上の各ピクセルに対して実行されます。 これらの4つの色の値が返され、ピクセルの色になります。 これは、 ピクセルシェーダー ( フラグメントシェーダーとも呼ばれます)と呼ばれるものです。
これを念頭に置いて、画面を赤一色で塗りましょう。 RGBA値(透明度を決定する赤、緑、青、アルファ)は
0
から
1
まで変化するため、行う必要があるのは
r,g,b,a = 1,0,0,1
返すことだけです。 ShaderToyは、ピクセルの最終色が
fragColor
に格納されることを
fragColor
ます。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { fragColor = vec4(1.0,0.0,0.0,1.0); }
おめでとうございます! これが最初の完成したシェーダーです!
タスク:色を純灰色に変更してみてください。
vec4
は単なるデータ型なので、次のように色を変数として宣言できます。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec4 solidRed = vec4(1.0,0.0,0.0,1.0); fragColor = solidRed; }
それはあまり刺激的ではありません。 数十万ピクセルのコードを同時に実行する能力があり、1つの色でペイントします。
画面にグラデーションをレンダリングしてみましょう。 画面上の位置など、影響を与えるピクセルについていくつかのことを知らなければ、多くを達成することはできません...
シェーダー入力
ピクセルシェーダーは、使用できるいくつかの変数を渡します。 最も有用なものは
fragCoord
。これには、ピクセル(および3Dで作業している場合はZ)のXおよびY座標が含まれます。 画面の左側のすべてのピクセルを黒に、右側のすべてのピクセルを赤に変えてみましょう。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 xy = fragCoord.xy; // vec4 solidRed = vec4(0,0.0,0.0,1.0);// if(xy.x > 300.0){// , ! solidRed.r = 1.0;// 1.0 } fragColor = solidRed; }
注:
obj.x
、
obj.y
、
obj.z
および
obj.w
を使用して、 または
obj.r
、
obj.g
、
obj.b
、
obj.a
を使用して、
obj.z
のコンポーネントにアクセスできます。 それらは同等であり、コードの読み取りを単純化する便利な命名方法です。なぜなら、人々は
obj.r
見ると、
obj
が色であることを理解するからです。
上記のコードに問題がありましたか? プレビューウィンドウの右下隅にある[ 全画面表示]ボタンをクリックしてみてください。
画面の赤い部分のサイズは異なり、画面のサイズによって異なります。 画面のちょうど半分を赤くペイントするには、そのサイズを知る必要があります。 通常、画面サイズはアプリケーションを作成するプログラマーによって選択されるため、ピクセルの位置のようなインライン変数ではありません 。 この場合、画面サイズはShaderToy開発者によって設定されます。
何かが組み込み変数でない場合、この情報を中央処理装置(メインプログラムから)からビデオプロセッサ(シェーダー)に送信できます。 ShaderToyはこのタスクを引き受けます。 シェーダーに渡されるすべての変数は、「 シェーダー入力」タブで指定されます。 このようにしてCPUからビデオプロセッサに転送される変数は、GLSL ユニフォーム(グローバル)と呼ばれます。
画面の中心を正しく決定するためにコードを変更しましょう。
iResolution
シェーダー
iResolution
を使用する必要があり
iResolution
。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 xy = fragCoord.xy; // xy.x = xy.x / iResolution.x; // xy.y = xy.y / iResolution.y; // x 0, 1 vec4 solidRed = vec4(0,0.0,0.0,1.0); // if(xy.x > 0.5){ solidRed.r = 1.0; // 1.0 } fragColor = solidRed; }
プレビューウィンドウのサイズを変更しようとすると、色によって画面が正確に半分に分割されます。
スプリットからグラデーションへ
これをグラデーションに変えるのは非常に簡単です。 色の値は
0
から
1
まで変化し、座標も
0
から
1
まで変化し
0
。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 xy = fragCoord.xy; // xy.x = xy.x / iResolution.x; // xy.y = xy.y / iResolution.y; // x 0, 1 vec4 solidRed = vec4(0,0.0,0.0,1.0); // solidRed.r = xy.x; // x fragColor = solidRed; }
出来上がり!
タスク:この写真を垂直方向のグラデーションに変えることができますか? 対角線はどうですか? いくつかの色のグラデーションはどうですか?
コードを試してみると、左上隅の座標が
(0,0)
ではなく
(0,0)
(0,1)
であることに気付くでしょう。 これは覚えておくことが重要です。
画像レンダリング
色を試してみるのは面白いですが、印象的なことをしたい場合、シェーダーは画像を受け取り、入力として変更する方法を学ぶ必要があります。 したがって、ゲーム画面全体に影響するシェーダー(「水中」エフェクトや色補正など)を取得したり、特定のオブジェクトに対してのみ必要な方法で動作したりすることができます(例えば、現実的な照明システムを作成するため)。
通常のプラットフォームでプログラミングした場合、画面解像度を送信するのと同じように、画像プロセッサ(またはテクスチャ)を均一としてビデオプロセッサに転送する必要があります。 ShaderToyがこれを処理してくれます。 画面の下部に4つの入力チャネルがあります。
4つの入力チャンネルShaderToy
iChannel0をクリックして、テクスチャ(画像)を選択します。
その結果、シェーダーに渡されるイメージがあります。 ただし、1つの問題があります
DrawImage()
関数の欠如です。 ピクセルシェーダーでできることは、各ピクセルの色を変更することだけです。
色のみを返すことができる場合、どのようにして画面にテクスチャを描画できますか? シェーダーの現在のピクセルをテクスチャの対応するピクセルに何らかの方法でバインドする必要があります。
画面上の位置(0,0)によっては、テクスチャを適切にスナップするためにY軸を反転する必要がある場合があります。 執筆時点では、ShaderToyは更新されており、開始点は左上隅にあるため、何も反映する必要はありません。
texture(textureData,coordinates)
関数を使用してバインドできます。この関数は入力でテクスチャデータと座標のペア
(x, y)
を受け取り、これらの座標のテクスチャカラーを
vec4
として
vec4
ます。
座標を好きなように画面にスナップできます。 画面全体の4分の1にテクスチャ全体を描画する(ピクセルをスキップする、つまり縮尺を小さくする)か、テクスチャの一部のみを描画することができます。
画像を見たいだけなので、ピクセルは1:1スケールで一致します:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 xy = fragCoord.xy / iResolution.xy;// vec4 texColor = texture(iChannel0,xy);// iChannel0 xy fragColor = texColor;// }
最初の画像ができました!
これで、テクスチャからデータを正しく受け取る方法を学習したので、それらを使って何でもできます! それらを引き伸ばしたり、拡大縮小したり、色を実験したりできます。
前にやったように、画像にグラデーションを適用してみましょう:
texColor.b = xy.x;
おめでとうございます、あなたはあなたの最初の後処理効果を書いたばかりです!
タスク:イメージを白黒にするシェーダーを作成できますか?
静止画像を選択しましたが、表示されるものはすべてリアルタイムで実行されることに注意してください。 これを確認するには、静止画像をビデオに置き換えます。iChannel0をクリックして、任意のビデオを選択します。
モーションを追加
これまでのところ、エフェクトはすべて静的です。 ShaderToyが提供する入力を使用して、さらに興味深いことができます。
iGlobalTime
は増え続ける変数です。 これをシードとして使用して、周期的な効果を作成できます。 色を少し試してみましょう。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 xy = fragCoord.xy / iResolution.xy; // vec4 texColor = texture(iChannel0,xy); // iChannel0 xy texColor.r *= abs(sin(iGlobalTime)); texColor.g *= abs(cos(iGlobalTime)); texColor.b *= abs(sin(iGlobalTime) * cos(iGlobalTime)); fragColor = texColor; // }
GLSLにはサイン関数とコサイン関数が組み込まれているほか、他の多くの便利な関数(ベクトルの長さや2つのベクトル間の距離の取得など)があります。 色の値を負にすることはできないため、
abs
関数を使用して絶対値を取得する必要があります。
タスク:イメージを白黒からカラー、またはその逆に変更するシェーダーを作成できますか?
シェーダーのデバッグに関する注意
コードをデバッグする場合、ステップごとにコードを実行したり、値を印刷したりできますが、シェーダーを作成する場合は不可能です。 プラットフォームのデバッグツールを検索できますが、一般的なケースでは、チェックされた値を表示可能な何らかのグラフィック情報にバインドするのが最善です。
まとめると
これらはシェーダーを操作するための非常に基本的なものですが、それらをマスターすれば、さらに多くを達成できます。 ShaderToyエフェクトをチェックして、理解または再現できるかどうかを確認してください !
このチュートリアルでは、 頂点シェーダーについては言及しませんでした。 それらは同じ言語で書かれていますが、ピクセルではなく各頂点に対して実行され、色とともに位置を返します。 通常、頂点シェーダーは3Dシーンを画面に投影します(この機能はほとんどのグラフィックパイプラインに組み込まれています)。 ピクセルシェーダーは、画面に表示されるほとんどの複雑な効果を提供するので、それらを確認します。
別のタスク: ShaderToyを使用してビデオから緑を削除し、最初のビデオの背景として別のビデオを追加するシェーダーを作成できますか?
以前に使用したShaderToyは、迅速なテストと実験に最適ですが、その機能はかなり制限されています。 たとえば、シェーダーに渡されるデータを制御することはできません。 シェーダーを起動するための独自の環境があれば、あらゆる種類の興味深いエフェクトを作成し、独自のプロジェクトで使用できます! ブラウザーでシェーダーを実行するには、フレームワークとしてThree.jsを使用します。 WebGLはシェーダーをレンダリングできるJavascript APIです。Three.jsは作業を簡素化するためにのみ必要です。
JavaScriptまたはWebプラットフォームに興味がない場合は、心配しないでください。Webレンダリングの機能については説明しません(ただし、フレームワークについて詳しく知りたい場合は、 このチュートリアルを参照してください )。 ブラウザーでシェーダーをセットアップするのが最速の方法ですが、このプロセスをマスターすれば、どのプラットフォームでもシェーダーを簡単に構成して使用できます。
カスタマイズ
このセクションでは、シェーダーのローカル構成について検討します。 このビルトインCodePenウィジェットのおかげで、何かをダウンロードすることなく、私の後に繰り返すことができます :
フォークを作成し、 CodePenでプロジェクトを編集できます 。
こんにちはThree.js!
Three.jsは、シェーダーのレンダリングに必要なWebGLのボイラープレートコードの大部分を引き継ぐJavaScriptフレームワークです。 最も簡単な開始方法は、CDNに投稿されているバージョンを使用することです。
HTMLファイルをダウンロードできます 。これは、単純なThreejsシーンです。
ファイルをディスクに保存し、ブラウザで開きます。 黒い画面が表示されるはずです。 まだあまり面白くないので、すべてが機能することを確認するためだけにキューブを追加してみましょう。
キューブを作成するには、ジオメトリとマテリアルを決定し、それをシーンに追加する必要があります。 次のコードスニペットを[
Add your code here
フィールドに
Add your code here
。
var geometry = new THREE.BoxGeometry( 1, 1, 1 ); var material = new THREE.MeshBasicMaterial( { color: 0x00ff00} );// var cube = new THREE.Mesh( geometry, material ); // scene.add( cube ); cube.position.z = -3;// ,
シェーダーにもっと興味があるので、キューブを詳細に検討しません。 しかし、すべてが正しく行われると、画面の中央に緑色の立方体が表示されます。
ここにいる間に、スピンさせてみましょう。
render
関数はフレームごとに実行されます。
cube.rotation.x
(または
.y
または
.z
)を介してキューブの回転にアクセスできます。 値を増やして、レンダリング関数が次のようになるようにしてください。
function render() { cube.rotation.y += 0.02; requestAnimationFrame( render ); renderer.render( scene, camera ); }
課題:キューブを別の軸で回転できますか? 同時に2つの車軸はどうですか?
これで準備が整いました。シェーダーを追加しましょう!
シェーダーを追加する
この段階で、シェーダーを実装するプロセスについて考え始めることができます。 ほとんどの場合、プラットフォームに関係なく同じような状況になります。すべてが構成され、オブジェクトが画面に描画されます。ビデオプロセッサにアクセスするにはどうすればよいでしょうか。
ステップ1:GLSLコードをダウンロードする
JavaScriptを使用してシーンを作成します。 他の状況では、C ++、Lua、またはその他の言語を使用できます。 これに関係なく、シェーダーは特別なシェーダー言語で記述されます 。 OpenGLシェーダー言語はGLSL (Open GL S had L anguage)と呼ばれます。 OpenGLベースのWebGLを使用しているため、GLSLで記述します。
GLSLコードはどこでどのように書かれていますか? 基本ルール:GLSLコードは
としてロードされ
。 その後、ビデオプロセッサで解析と実行のために転送できます。
JavaScriptでは、次のように変数内にコードを渡すだけでこれを実行できます。
var shaderCode = " ;"
これは機能しますが、JavaScriptで複数行の文字列を作成する簡単な方法はないため、これはあまり便利ではありません。 ほとんどのプログラマーは、テキストファイルにシェーダーコードを記述し、拡張子
.glsl
または
.frag
( fragment shaderの略)を付けてから、ファイルをダウンロードするだけです。
可能ですが、このチュートリアルでは、シェーダーコードを新しい
<script>
内に記述し、そこからJavaScriptにロードして、すべてのコードが1つのファイルに収まるようにします。
次のようなHTMLで新しい
<script>
を作成します。
<script id="fragShader" type="shader-code">; </script>
後でアクセスできる
fragShader
、彼にfragShader IDを割り当てます。
shader-code
タイプは、実際には存在しない、架空のタイプのスクリプトです。 (他の名前を選択できます)。 これは、コードが実行されず、HTMLで表示されないようにするためです。
次に、白を返す非常にシンプルなシェーダーを挿入しましょう。
<script id="fragShader" type="shader-code"> void main() { gl_FragColor = vec4(1.0,1.0,1.0,1.0); } </script>
(この場合の
vec4
コンポーネントは、チュートリアルの冒頭で説明したように、RGBA値に対応しています。)
次に、このコードをダウンロードする必要があります。 これを、HTML要素を見つけて内部テキストを受け取る単純なJavaScript文字列にします。
var shaderCode = document.getElementById("fragShader").innerHTML;
キューブコードの下にある必要があります。
忘れないでください:文字列としてロードされたコードのみが有効なGLSLコードとして解析されます(つまり、
void main() {...}
。残りは単なるボイラープレートHTMLです。)
フォークを作成し、 CodePenでプロジェクトを編集できます 。
ステップ2:シェーダーを適用する
シェーダーの使用方法は異なる場合があります。使用するプラットフォームとビデオプロセッサとのインターフェイスによって異なります。 これに問題はありません。簡単な検索で、オブジェクトを作成し、Three.jsを使用してシェーダーを適用する方法を見つけることができます 。
特別なマテリアルを作成し、シェーダーコードに渡す必要があります。 シェーダーオブジェクトとして、プレーンを作成します(ただし、キューブも使用できます)。 必要なことは次のとおりです。
// , var material = new THREE.ShaderMaterial({fragmentShader:shaderCode}) var geometry = new THREE.PlaneGeometry( 10, 10 ); var sprite = new THREE.Mesh( geometry,material ); scene.add( sprite ); sprite.position.z = -1;// ,
この時点で、白い画面が表示されるはずです。
フォークを作成し、 CodePenでプロジェクトを編集できます 。
シェーダーコードの色を他の色に置き換えてページを更新すると、新しい色が表示されます。
目的:画面の一部を赤、もう一方を青にできますか? (うまくいかない場合は、次のステップでヒントを示します!)
ステップ3:データ転送
この時点で、既にシェーダーで何でもできますが、何ができるかはまだわかりません。 ピクセル
gl_FragCoord
位置を決定する組み込み機能のみがあり、覚えている場合、この位置は正規化されます。 少なくとも、画面のサイズを知る必要があります。
データをシェーダーに転送するには、 ユニフォーム変数と呼ばれるものを送信する必要があります。 これを行うには、
uniforms
オブジェクトを作成し、それに変数を追加します。 画面解像度を渡すための構文は次のとおりです。
var uniforms = {}; uniforms.resolution = {type:'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)};
各均一変数には、
と
が必要
。 この場合、それはウィンドウの幅と高さを座標とする2次元ベクトルです。 次の表( Three.jsドキュメントから取得)は、送信可能なすべてのタイプのデータとその識別子を示しています。
均一な文字列 | タイプGLSL | Javascriptタイプ |
---|---|---|
'i', '1i'
| int
| Number
|
'f', '1f'
| float
| Number
|
'v2'
| vec2
| THREE.Vector2
|
'v3'
| vec3
| THREE.Vector3
|
'c'
| vec3
| THREE.Color
|
'v4'
| vec4
| THREE.Vector4
|
'm3'
| mat3
| THREE.Matrix3
|
'm4'
| mat4
| THREE.Matrix4
|
't'
| sampler2D
| THREE.Texture
|
't'
| samplerCube
| THREE.CubeTexture
|
ShaderMaterial
の
ShaderMaterial
インスタンスを変更します。
var material = new THREE.ShaderMaterial({uniforms:uniforms,fragmentShader:shaderCode})
まだ完了していません! シェーダーがこの変数を取得したので、それで何かをする必要があります。 前と同じようにグラデーションを作成しましょう。座標を正規化し、それを使用して色の値を作成します。
シェーダーコードを次のように変更します。
uniform vec2 resolution;// uniform- void main() { // vec2 pos = gl_FragCoord.xy / resolution.xy; // ! gl_FragColor = vec4(1.0,pos.x,pos.y,1.0); }
その結果、美しいグラデーションが得られます!
フォークを作成し、 CodePenでプロジェクトを編集できます 。
タスク:画面を異なる色の4つの等しい部分に分割してみます。 このようなもの:
ステップ4:データの更新
データをシェーダーに送信する方法を学んだのは良いことですが、データを更新する必要がある場合はどうでしょうか? たとえば、 前の例を新しいタブで開き、ウィンドウのサイズを変更した場合、グラデーションは以前の画面サイズを使用するため更新されません。
変数を更新するには、通常、均一変数が単に再送信されます。 ただし、Three.jsでは、
render
関数でuniformsオブジェクトを更新するのは非常に簡単です。シェーダーはデータを再度送信する必要はありません。
変更後のレンダリング関数は次のようになります。
function render() { cube.rotation.y += 0.02; uniforms.resolution.value.x = window.innerWidth; uniforms.resolution.value.y = window.innerHeight; requestAnimationFrame( render ); renderer.render( scene, camera ); }
新しいCodePenを開いてウィンドウのサイズを変更すると、ビューポートの元のサイズが同じままであるにもかかわらず、色がどのように変化するかがわかります。 これに気付く最も簡単な方法は、角の色を見て、それらが変わらないことを確認することです。
注:通常、ビデオプロセッサへのデータ送信はコストのかかる作業です。フレームごとに複数の変数を送信することは非常に正常ですが、フレームごとに数百の変数を送信すると、フレームレートが大幅に低下します。これはありそうもないようですが、画面上に数百のオブジェクトがあり、それらすべてに異なるプロパティの照明が適用されると、すべてがすぐに制御不能になります。後でシェーダーの最適化について学びます。
タスク:色を徐々に変えてみてください。
ステップ5:テクスチャの使用
すべてのプラットフォームでの読み込み方法とテクスチャ形式に関係なく、これらは均一な変数としてシェーダーに転送されます。
JavaScriptでのファイルのダウンロードに関する小さなメモ:外部URLから画像を簡単にアップロードできます(これがまさに私たちが行うことです)。ただし、画像をローカルにアップロードする場合、JavaScriptは通常システム上のファイルにアクセスできないため、アクセスできないため、アクセス許可に問題があります。最も簡単な解決策は、ローカルPythonサーバーを起動することです。これは実際には思ったより簡単です。
Three.jsには、画像をテクスチャとしてロードするための小さな便利な機能があります。
THREE.ImageUtils.crossOrigin = '';// var tex = THREE.ImageUtils.loadTexture( "https://tutsplus.github.io/Beginners-Guide-to-Shaders/Part2/SIPI_Jelly_Beans.jpg" );
最初の行は一度だけ設定されます。任意の画像URLを挿入できます。
次に、オブジェクトにテクスチャを追加する必要があります
uniforms
。
uniforms.texture = {type:'t',value:tex};
そして最後に、シェーダーコードで均一変数を宣言し、関数を使用して以前と同じ方法でそれを描画する必要があります
texture2D
。
uniform vec2 resolution; uniform sampler2D texture; void main() { vec2 pos = gl_FragCoord.xy / resolution.xy; gl_FragColor = texture2D(texture,pos); }
お菓子の引き伸ばされた画像が画面に表示されます:
(これは、コンピュータグラフィックスで標準テスト画像である、から取られる信号と画像処理研究所(SIPI)南カリフォルニア大学は(したがって、それはIPIの略を示して)。私たちはただのグラフィックシェーダを勉強しているので、それは、私たちに合うと思います!)
タスク:テクスチャの色をフルカラーからグレースケールに徐々に変更してみてください。
追加の手順:シェーダーを他のオブジェクトに適用する
作成した飛行機には特別なものはありません。シェーダーをキューブにも適用できます。実際、単純に線を平面のジオメトリに置き換えることができます。
var geometry = new THREE.PlaneGeometry( 10, 10 );
に:
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
出来上がり、お菓子は立方体に描かれています:
あなたは反対するかもしれません:「待ってください、これは立方体へのテクスチャの正しい投影ではないようです!」あなたは正しいです:シェーダーを見ると、実際に彼に「この画像のすべてのピクセルを画面に配置する」と伝えていることがわかります。キューブ上にあるからといって、単にその外側のすべてのピクセルが破棄されることを意味します。
キューブに物理的に描かれるようにテクスチャを適用したい場合、3Dエンジンの発明に似た作業になります(3Dエンジンを既に使用しているので、それを要求するだけでかなりバカになります)両側に個別にテクスチャを描画します)。このチュートリアルでは、それ以外の場合は不可能なものにシェーダーを使用するため、そのような詳細には触れません。 (詳細を知りたい場合は、Udacity に3Dグラフィックスの基礎に関する優れたコースがあります!)
次のステップ
この段階で、ShaderToyで行ったことはすべて実行できますが、現在では、任意のプラットフォームで任意のテクスチャとオブジェクトを使用できます。
こうした自由度があれば、リアルな影と光源を備えた照明システムのようなことができます。これは以下で行います。さらに、シェーダー最適化手法についても説明します。
シェーダーの基本を習得したので、実際にはビデオプロセッサのパワーを使用してリアルなダイナミックライティングを作成します。
この時点から、特定のプラットフォームに縛られることなく、グラフィックシェーダーの一般的な概念を検討します。 (便宜上、すべてのコード例は引き続きJavaScript / WebGLを使用します。)
開始するには、シェーダーを実行する適切な方法を見つけてください。(JavaScript / WebGLが最も簡単な方法ですが、お気に入りのプラットフォームで実験することをお勧めします!)
目標
このチュートリアルの終わりまでに、照明システムでうまくナビゲートし始めるだけでなく、ゼロから独自に作成することもできます。
最終結果は次のようになります(クリックしてライトをオンにします)。
多くのゲームエンジンには既製の照明システムがありますが、その作成方法と独自の照明システムを作成する方法を理解することで、ゲームにユニークな外観を与える機会が増えます。さらに、シェーダーエフェクトは「化粧」だけでなく、驚くべき新しいゲームメカニクスへの扉を開きます!
この良い例がクロマです。プレーヤーは、リアルタイムで作成された動的なシャドウに沿って実行できます。
はじめに:開始シーン
上記で詳細に説明されているため、初期セットアップでは多くのことを逃します。テクスチャをレンダリングする単純なフラグメントシェーダから始めましょう。
ここでは複雑なことは何も起こりません。JavaScriptコードはシーンを設定し、レンダリングと画面サイズのためにシェーダーテクスチャを送信します。
var uniforms = { tex : {type:'t',value:texture},// res : {type: 'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}// }
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を異なる数に分割します(画面の幅と高さで)。当然、画像は引き伸ばされます。
xとy
gl_FragCoord
をxだけで除算するとどうなり
res
ますか?それともただ?
簡単にするために、チュートリアルでは、正規化されたコードは同じままにしておきます。ここで何が起こっているのかを把握しておくといいでしょう。
ステップ1:光源を追加する
面白いものを作成する前に、光源が必要です。「光源」は、シェーダーに渡される単なるポイントです。この点について、新しいユニフォームを作成します。
var uniforms = { // light: {type:'v3', value:new THREE.Vector3()}, tex : {type:'t',value:texture},// res : {type: 'v2',value:new THREE.Vector2(window.innerWidth,window.innerHeight)}// }
画面上のソースの位置として、およびradiusとして-の
x
両方を使用するため、3次元のベクトルを作成しました。JavaScriptで光源に値を割り当てましょう:
y
z
uniforms.light.value.z = 0.2;//
半径は画面サイズの割合として使用するため
0.2
、画面の20%になります。(この選択に関して特別なことは何もありません。サイズをピクセル単位で設定できます。GLSLコードで何かを開始するまで、この数値は何の意味もありません。)
マウス位置を取得するには、イベントリスナーを追加するだけです。 ):
document.onmousemove = function(event){ // , uniforms.light.value.x = event.clientX; uniforms.light.value.y = event.clientY; }
次に、光源のこの座標を利用するシェーダーコードを記述しましょう。単純なタスクから始めましょう。光源の半径内の各ピクセルが表示され、残りが黒であることを確認してください。
GLSLでは、次のようになります。
uniform sampler2D tex; uniform vec2 res; uniform vec3 light;// uniform! void main() { vec2 pixel = gl_FragCoord.xy / res.xy; vec4 color = texture2D(tex,pixel); // float dist = distance(gl_FragCoord.xy,light.xy); if(light.z * res.x > dist){// , gl_FragColor = color; } else { gl_FragColor = vec4(0.0); } }
ここでは、次のことを行いました。
- 光源の均一変数を宣言しました。
- 組み込みの距離関数を使用して、光源と現在のピクセル間の距離を計算しました。
- この距離(ピクセル単位)が画面幅の20%より大きいかどうかを確認しました。そうであれば、ピクセルの色を返し、そうでなければ黒を返します。
痛い!光源は奇妙な方法でマウスに追従します。
タスク:修正できますか?(以下でこの問題を解決する前に、自分でもう一度試してみてください。)
光源移動補正
ここで、Y軸が反転していることを覚えているかもしれません。次のように入力するだけで急ぐことができます。
light.y = res.y - light.y;
これは数学的には正しいですが、そうするとシェーダーはコンパイルされません!問題は、均一変数を変更できないことです。理由を理解するには、このコードが個々のピクセルごとに並行して実行されることを覚えておく必要があります。すべてのプロセッサコアが一度に1つの変数を変更しようとすることを想像してください。悪い状況!
ユニフォームの代わりに新しい変数を作成することにより、エラーを修正できます。またはさらに良い- データをシェーダーに転送する前に次の手順を実行できます。
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
。つまり、乗算
color
して
1
フルカラーを取得します。
この図では
dist
、任意のピクセルに対して計算されます。
dist
どのピクセルにいるかによって変化し、値は
light.z * res.x
一定です。
円の境界にあるピクセルを見る
dist
と、半径の長さと等しくなります。つまり、結果として、乗算
color
し
0
て黒色を取得します。
ステップ2:深さを追加する
これまでのところ、特別なことは何もせず、テクスチャにグラデーションマスクを追加しました。すべてがまだ平らに見えます。これを修正する方法を理解するために、照明システムが現在何をしているのかを見て、何をすべきかと比較しましょう。
上記の場合、点Aは光源がその真上にあるため最も強く点灯し、BとCは実際には側面に光線がないため暗くなります。
ただし、ここに照明システムが表示するもの
があります。システムが考慮する唯一の要素はxy平面上の距離であるため、すべてのポイントは同じ方法で処理されます。。これらの各ポイントの高さが必要だと思うかもしれませんが、これは完全に真実ではありません。理由を確認するには、この図を検討してください
。Aは図の上部にあり、BとCは側面にあります。Dは地球上の別のポイントです。AとDが最も明るく、Dが斜めに到達するため少し暗くなることがわかります。一方、BとCは光源からの光であるため、ほとんど光が届かないため、非常に暗いはずです。
高さは、表面が回転する方向ほど重要ではありません。と呼ばれます表面に対して垂直。
しかし、この情報をシェーダーに転送する方法は?おそらく、個々のピクセルごとに膨大な数の数の配列を送信することはできませんか?実際、それを行います!配列ではなく、テクスチャと呼びます。
これは、通常のマップを作るものである:それは単にの値画像であり
r
、
g
かつ
b
各画素には色、及び方向を表します。
上の図は、単純な法線マップを示しています。スポイトツールを使用すると、デフォルトの方向(「フラット」)が色で表されることがわかります。
(0.5, 0.5, 1)
(画像の大部分を占める青)。これはまっすぐ上を指す方向です。x、y、およびzの値は、r、g、およびbの値に割り当てられます。
右側の傾斜側は右に回転しているため、そのx値は高くなります。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}, //.. }
正しくダウンロードしたことを確認するには、コードをGLSLに変更して、テクスチャの代わりにレンダリングしてみます(この段階では、通常のマップではなく背景テクスチャのみを使用することに注意してください)。
ステップ3:照明モデルを適用する
表面の法線データが得られたので、照明モデルを実装する必要があります。言い換えれば、最終的な明るさを計算するために利用可能なすべての要因を考慮する方法を表面に伝える必要があります。
実装する最も簡単なモデルはPhongモデルです。仕組みは次のとおりです。
法線データを持つサーフェスがあるとします。光源とサーフェス法線の間の角度を単純に計算します。角度が
小さいほど、ピクセルは明るくなります。
これは、角度差が0であるピクセルが光源の真下にあるとき、最も明るいことを意味します。最も暗いピクセルは、光源と同じ方向を指します(これはオブジェクトの背面のように見えます)。
このモデルを実装しましょう。
検証には単純な法線マップを使用するため、テクスチャが単色で塗りつぶされて、すべてがうまくいくかどうかを明確に理解しましょう。
したがって、代わりに:
vec4 color = texture2D(...);
単色の白色(またはその他の色)を作成しましょう。
vec4 color = vec4(1.0); //
これは、
vec4
すべてのコンポーネントが等しいGLSLを作成するための速記です
1.0
。
アルゴリズムは次のようになります。
- 現在のピクセルの法線ベクトルを取得します。
- ライトの方向ベクトルを取得します。
- ベクトルを正規化します。
- それらの間の角度を計算します。
- 最終的な色にこの係数を掛けます。
1.現在のピクセルの法線ベクトルを取得する
このピクセルに当たる光の量を計算できるように、表面がどの方向に「見える」かを知る必要があります。この方向は法線マップに保存されるため、法線ベクトルを取得するには、法線テクスチャの現在のピクセルの色を取得します。
vec3 NormalVector = texture2D(norm,pixel).xyz;
アルファ値は法線マップ上では何も意味しないため、最初の3つのコンポーネントのみが必要です。
2.光の方向ベクトルを取得する
次に、光がどの方向を指しているかを知る必要があります。照明面は、マウスカーソルの位置で画面に向けられた懐中電灯であると想像できます。したがって、光源とピクセル間の距離を使用するだけで、光の方向ベクトルを計算できます。
vec3 LightVector = vec3(light.x - gl_FragCoord.x,light.y - gl_FragCoord.y,60.0);
また、Z座標も必要です(3次元の表面法線ベクトルに対する角度を計算できるようにするため)。この値を試すことができます。小さいほど、明るい部分と暗い部分のコントラストがはっきりしていることがわかります。これは、ステージ上の懐中電灯の高さであると想像できます。遠くにあるほど、光はより均等に広がります。
3.ベクトルの正規化
次に、正規化する必要があります。
NormalVector = normalize(NormalVector); LightVector = normalize(LightVector);
両方のベクトルが長さ
1.0
を持つように、組み込みのnormalize関数を使用します。これは、スカラー積を使用して角度を計算するために必要です。仕組みがよくわからない場合は、線形代数について少し勉強する価値があります。この目的のために、スカラー積が同じ長さのベクトル間の角度のコサインを返すことを知る必要があるだけです。
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
。
以下
0.5
は、xおよびy座標から法線ベクトルを減算した後のデモの様子です。
別の修正を行う必要があります。スカラー積は角度のコサインを返すことに注意してください。これは、出力が-1から1の範囲に制限されることを意味します。色の値を負にすることはできません。また、WebGLは負の値を自動的に拒否するため、場合によっては動作がおかしくなります。この問題を解決するには、組み込みのmax関数を使用してこれを有効にします。
float diffuse = dot( NormalVector, LightVector );
これに:
float diffuse = max(dot( NormalVector, LightVector ),0.0);
そして、実用的な照明モデルを得ました!
背景に石のテクスチャを配置し、GitHubのこのチュートリアルのリポジトリにある実際の法線マップを取得することができます(つまり、ここ):
1行だけを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の1行:
vec4 color = vec4(1.0);//
単色の白色は不要になりました。次のように実際のテクスチャをロードします。
vec4 color = texture2D(tex,pixel);
そして、最終結果は次のとおりです。
最適化のヒント
ビデオプロセッサは非常に効率的に作業を実行しますが、何が遅くなるかを知ることは非常に重要です。 以下にいくつかの提案を示します。
分岐
シェーダーでは、可能な場合は通常、分岐を回避することが望ましいです。
if
CPUのコードでは多くのデザインが問題になることはめったにありませんが、ビデオプロセッサのシェーダーではボトルネックになる可能性があります。
理由を理解するには、GLSLコードが画面上の各ピクセルに対して並列に実行されることを思い出してください。グラフィックスカードは、すべてのピクセルが同じ操作を実行する必要があるという事実に基づいて、多くの最適化を実行できます。ただし、codeに多くのコードがある場合
if
、ピクセルごとに異なるコードが実行されるため、一部の最適化は失敗します。デザインはありますか
if
実行を遅くするかどうかは、特定のハードウェアとグラフィックカードの実装に依存しますが、シェーダーを高速化する場合はこれをよく覚えておいてください。
遅延レンダリング
これは、照明を扱うときに非常に便利な概念です。2つの光源、3つ、または1ダースが必要だと想像してください。各表面の法線と光源の各点の間の角度を計算する必要があります。その結果、シェーダーはタートル速度で実行されます。遅延レンダリングは、シェーダーの作業を複数のパスに分割することにより、このようなプロセスを最適化する方法です。これが何を意味するかを詳しく説明している記事です。私たちにとって重要な部分を引用します。
— . .
たとえば、光源のポイントの配列を送信する代わりに、テクスチャ上に円の形でそれらを描画できます。円の各ピクセルの色は光の強度を表します。このようにして、シーン内のすべての光源の複合効果を計算し、それを最終的なテクスチャ(または呼ばれることもあるバッファ)に渡すだけで、ライティングを計算できます。
作業を複数のパスに分割する機能は、シェーダーを作成するときに非常に便利な手法です。たとえば、液体/煙シェーダーだけでなく、ぼかし効果の計算でシェーダーを加速するために使用されます。
次のステップ
実用的なライティングシェーダーができたので、次のことを試してみてください。
z
照明ベクトルの高さ(値)を変更して、その効果を確認してください。- . ( , diffuse .)
- ambient ( ). ( , , . , , )
- WebGL . Babylon.js, Three.js, , GLSL. , - .
- GLSL Sandbox ShaderToy
参照資料
このチュートリアルの石のテクスチャと法線マップは、OpenGameArt:http ://opengameart.org/content/50-free-textures-4-normalmapsから取得されます。
法線マップの作成に役立つ多くのプログラムがあります。独自の法線マップの作成について詳しく知りたい場合は、この記事が役立ちます。