概要
この記事では「three.js超入門」と題して、three.jsの基礎からシェーダーの利用までをやっていきます。
ターゲットは主に「canvas表現を触ったことがないフロントエンドエンジニア」を想定しているので、jsの構文などの説明は省略しています。
three.jsのバージョンは執筆時点で最新のr98を使用します。
three.js超入門 第0回 3Dコンピュータグラフィックスの基礎
three.js超入門 第1回 レンダリングまでの流れ
three.js超入門 第2回 アニメーションと時間ベースでの制御
three.js超入門 第3回 マウスやスクロールでのインタラクション
three.js超入門 第4回 DOM要素との連携
three.js超入門 第5回 シェーダー(GLSL)の基礎
three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する
three.js超入門 第7回 シェーダーに変数を渡す
three.js超入門 第8回 シェーダーをインタラクティブに動かす
three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける
前回は、シェーダーを使ってメッシュを変形させたり、メッシュ全体の色を変えたりしました。
今回はさらに細かい制御ができるように、頂点シェーダーからピクセルシェーダーに変数を送ったり、CPU(JavaScript)からGPU(シェーダー)に変数を送る方法を紹介します。
varying
変数
main()
関数の前(グローバル)に変数を宣言する際に、行頭にvarying
と記述するとその変数が頂点シェーダーからピクセルシェーダーに送られます。
頂点シェーダーとピクセルシェーダー両方で同じvarying
変数の記述が必要です。
変数名に特に制限はありませんが、頭に接頭辞のv
をつけるとわかりやすいです。
varying
変数は初期化時に代入はできません。(初期化代入で定数にする場合はピクセルシェーダーに書けばいいので。)
値の代入はmain()
関数の中に記述します。
サンプルコード
varying float vSample;// varying: 頂点シェーダーからピクセルシェーダーに変数を送るための装飾子
void main() {
vSample = 1.0;// main() の中で値を代入する
gl_Position = vec4( position, 1.0 );
}
varying float vSample;// 頂点シェーダーから送られてきた varying 変数を受け取る
void main() {
gl_FragColor = vec4( vSample, 0.0, 0.0, 1.0 );
}
メッシュのテクスチャ座標をピクセルシェーダーに送る
テクスチャ座標とは
文字通りメッシュにテクスチャを貼る際に参照される座標です。
頂点ごとにテクスチャのどの部分を対応させるのかを設定するものです。
頂点座標とテクスチャ座標
今回のコードでの対応関係は以下のようになっています。
テクスチャの座標系では左下が原点になることに注意しましょう。
varying vec2 vUv;
void main() {
vUv = uv;// uv: ShaderMaterialで補完される vec2 型(xy)の変数。テクスチャ座標のこと。
gl_Position = vec4( position, 1.0 );
}
varying vec2 vUv;
void main() {
vec4 color = vec4( vUv.x, vUv.y, 0.0, 1.0 );// テクスチャ座標を r g に入れる
gl_FragColor = color;
}
赤緑黄色のグラデーションが表示されました。
左下が黒で、右上が黄色になっているので、テクスチャ座標が左下原点で0.0 ~ 1.0
になっていることがわかります。
テクスチャ座標を使って発光する円を書いてみる
GLSLはグラデーション表現が得意なので、発光しているような表現もほんの数行で書くことができます。
画面中心から現在のピクセルまでの距離を求める
varying vec2 vUv;// 頂点シェーダーから varying 変数を受け取る
void main() {
vec2 center = vec2( .5, .5 );// 画面の中心
float dist = length( vUv - center );// 中心から現在のピクセルへの距離を取得
vec4 color = vec4( vec3( dist ), 1.0 );// 距離を rgb に変換
gl_FragColor = color;
}
画面中心から外側に広がる円形グラデーションができました。
画面中心から各ピクセルまでの距離をビジュアライズしているので中心が暗く、外側が明るくなっています。
(画面比率で伸びてしまっていますが、後ほど修正するのでこのまま進めます。)
内接円の半径を距離で割る
テクスチャ座標のx
y
がそれぞれ0.0 ~ 1.0
の範囲なので、ウィンドウに内接する円の半径は0.5
になります。
dist
をlightness
に変更して、半径の0.5
を距離で割ります。
float lightness = 0.5 / length( vUv - center );// 半径を、中心から現在のピクセルへの距離で割る
vec4 color = vec4( vec3( lightness ), 1.0 );
画面にぴったり収まる発光した円ができました。
なぜ半径を距離で割ると発光するのでしょうか?
仮にこの部分を数学の式にするとy = 0.5 / x
となります。
x
(中心からの距離)が0.5
より小さくなるとき、y
(明るさ)は1.0
よりおおきくなります。
円の内側ではrgb
の値が1.0
を超えているので、真っ白になるわけです。
半径を小さくする
さきほど0.5
を入れていた値を変更することで、任意の半径に変更できます。
float lightness = 0.05 / length( vUv - center );// 割られる値が円の半径になる
円に色をつける
シェーダーでは色を0.0 ~ 1.0
で表現するので、乗せたい色を乗算することで白い部分に色を乗せることができます。
color *= vec4( 0.2, 1.0, 0.5, 1.0 );// 着色する
円の中心に近いほど、lightness
が1.0
以上の大きな値になるため、乗算した色が発光しているような見た目になります。
着色する色をそのまま乗せたい場合は、lightness
の値の範囲をclamp()
で0.0 ~ 1.0
に制限します。
lightness = clamp( lightness, 0.0, 1.0 );// 値の範囲を 0.0 ~ 1.0 に制限
vec4 color = vec4( vec3( lightness ), 1.0 );
uniform
変数
main()
関数の前(グローバル)に変数を宣言する際に、行頭にuniform
と記述するとその変数はCPUから値を受け取ります。
uniform
変数は、頂点シェーダーとピクセルシェーダー両方に送られますが、変数を使用しないシェーダーには記述する必要はありません。
変数名に特に制限はありませんが、頭に接頭辞のu
をつけるとわかりやすいです。
uniform
変数はCPUから値を受け取るため、代入はできません。
アスペクト比による画像の伸びを補正する
Canvas
クラスでuniforms
というuniform
変数を格納しておくためのオブジェクトを作成して、ShaderMaterial
を作成する際に渡します。
任意の変数名のオブジェクトのvalue
要素にシェーダーに渡す数値が入ります。
float
を渡す場合はそのまま数値を入れ、vec
系で渡す場合はthree.jsのVector
クラスを入れます。
// uniform変数を定義
this.uniforms = {
uAspect: {
value: this.w / this.h
}
};
// uniform変数とシェーダーソースを渡してマテリアルを作成
const mat = new ShaderMaterial({
uniforms: this.uniforms,
vertexShader: vertexSource,
fragmentShader: fragmentSource
});
varying vec2 vUv;
uniform float uAspect;// 画面のアスペクト比
void main() {
vec2 uv = vec2( vUv.x * uAspect, vUv.y );// xをアスペクト補正したテクスチャ座標
vec2 center = vec2( .5 * uAspect, .5 );// xをアスペクト補正した画面の中心
float lightness = 0.05 / length( uv - center );
// lightness = clamp( lightness, 0.0, 1.0 );
vec4 color = vec4( vec3( lightness ), 1.0 );
color *= vec4( 0.2, 1.0, 0.5, 1.0 );
gl_FragColor = color;
}
伸びが補正されて正円になりました。
アニメーションさせる
アニメーションをさせるには時間の数値が必要なので、CPUからuniform
変数で渡します。
// 省略
export default class Canvas {
constructor() {
// 省略
// uniform変数を定義
this.uniforms = {
uAspect: {
value: this.w / this.h
},
uTime: {
value: 0.0
}
};
// 省略
}
render() {
requestAnimationFrame(() => { this.render(); });
const sec = performance.now() / 1000;
this.uniforms.uTime.value = sec;// シェーダーに渡す時間を更新
this.renderer.render(this.scene, this.camera);
}
}
varying vec2 vUv;
uniform float uAspect;
uniform float uTime;// 時間
void main() {
vec2 uv = vec2( vUv.x * uAspect, vUv.y );
vec2 center = vec2( .5 * uAspect, .5 );
float radius = 0.05 + sin( uTime * 2.0 ) * 0.025;// 時間で半径をアニメーションさせる
float lightness = radius / length( uv - center );// 半径を距離で割る
// lightness = clamp( lightness, 0.0, 1.0 );
vec4 color = vec4( vec3( lightness ), 1.0 );
color *= vec4( 0.2, 1.0, 0.5, 1.0 );
gl_FragColor = color;
}
ピクセルシェーダーでアニメーションができました。
同様に頂点シェーダーでもuTime
を受け取ることで頂点をアニメーションさせることができます。
次回はマウスをつかってシェーダーをインタラクティブに動かします。