LoginSignup
27
19

More than 3 years have passed since last update.

three.js超入門 第7回 シェーダーに変数を渡す

Last updated at Posted at 2019-03-20

概要

この記事では「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()関数の中に記述します。

サンプルコード

shader.vert
varying float vSample;// varying: 頂点シェーダーからピクセルシェーダーに変数を送るための装飾子

void main() {
  vSample = 1.0;// main() の中で値を代入する

  gl_Position = vec4( position, 1.0 );
}
shader.frag
varying float vSample;// 頂点シェーダーから送られてきた varying 変数を受け取る

void main() {
  gl_FragColor = vec4( vSample, 0.0, 0.0, 1.0 );
}

メッシュのテクスチャ座標をピクセルシェーダーに送る

テクスチャ座標とは

文字通りメッシュにテクスチャを貼る際に参照される座標です。
頂点ごとにテクスチャのどの部分を対応させるのかを設定するものです。

頂点座標とテクスチャ座標

今回のコードでの対応関係は以下のようになっています。
テクスチャの座標系では左下が原点になることに注意しましょう。
coordinate_uv.png

shader.vert
varying vec2 vUv;

void main() {
  vUv = uv;// uv: ShaderMaterialで補完される vec2 型(xy)の変数。テクスチャ座標のこと。

  gl_Position = vec4( position, 1.0 );
}
shader.frag
varying vec2 vUv;

void main() {
  vec4 color = vec4( vUv.x, vUv.y, 0.0, 1.0 );// テクスチャ座標を r g に入れる

  gl_FragColor = color;
}

スクリーンショット 2019-03-20 14.38.42.png
赤緑黄色のグラデーションが表示されました。
左下が黒で、右上が黄色になっているので、テクスチャ座標が左下原点で0.0 ~ 1.0になっていることがわかります。

テクスチャ座標を使って発光する円を書いてみる

GLSLはグラデーション表現が得意なので、発光しているような表現もほんの数行で書くことができます。

画面中心から現在のピクセルまでの距離を求める

shader.frag
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;
}

スクリーンショット 2019-03-20 15.07.29.png
画面中心から外側に広がる円形グラデーションができました。
画面中心から各ピクセルまでの距離をビジュアライズしているので中心が暗く、外側が明るくなっています。
(画面比率で伸びてしまっていますが、後ほど修正するのでこのまま進めます。)

内接円の半径を距離で割る

テクスチャ座標のx yがそれぞれ0.0 ~ 1.0の範囲なので、ウィンドウに内接する円の半径は0.5になります。
distlightnessに変更して、半径の0.5を距離で割ります。

float lightness = 0.5 / length( vUv - center );// 半径を、中心から現在のピクセルへの距離で割る 
vec4 color = vec4( vec3( lightness ), 1.0 );

スクリーンショット 2019-03-20 15.29.34.png
画面にぴったり収まる発光した円ができました。
なぜ半径を距離で割ると発光するのでしょうか?
仮にこの部分を数学の式にするとy = 0.5 / xとなります。
スクリーンショット 2019-03-20 15.42.27.png
x(中心からの距離)が0.5より小さくなるとき、y(明るさ)は1.0よりおおきくなります。
円の内側ではrgbの値が1.0を超えているので、真っ白になるわけです。

半径を小さくする

さきほど0.5を入れていた値を変更することで、任意の半径に変更できます。

float lightness = 0.05 / length( vUv - center );// 割られる値が円の半径になる

スクリーンショット 2019-03-20 16.04.06.png

円に色をつける

シェーダーでは色を0.0 ~ 1.0で表現するので、乗せたい色を乗算することで白い部分に色を乗せることができます。

color *= vec4( 0.2, 1.0, 0.5, 1.0 );// 着色する

スクリーンショット 2019-03-21 12.01.50.png

円の中心に近いほど、lightness1.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 );

スクリーンショット 2019-03-21 12.02.07.png

uniform変数

main()関数の前(グローバル)に変数を宣言する際に、行頭にuniformと記述するとその変数はCPUから値を受け取ります。
uniform変数は、頂点シェーダーとピクセルシェーダー両方に送られますが、変数を使用しないシェーダーには記述する必要はありません。
変数名に特に制限はありませんが、頭に接頭辞のuをつけるとわかりやすいです。
uniform変数はCPUから値を受け取るため、代入はできません。

アスペクト比による画像の伸びを補正する

Canvasクラスでuniformsというuniform変数を格納しておくためのオブジェクトを作成して、ShaderMaterialを作成する際に渡します。
任意の変数名のオブジェクトのvalue要素にシェーダーに渡す数値が入ります。
floatを渡す場合はそのまま数値を入れ、vec系で渡す場合はthree.jsのVectorクラスを入れます。

Canvas/index.js
// uniform変数を定義
this.uniforms = {
  uAspect: {
    value: this.w / this.h
  }
};

// uniform変数とシェーダーソースを渡してマテリアルを作成
const mat = new ShaderMaterial({
  uniforms: this.uniforms,
  vertexShader: vertexSource,
  fragmentShader: fragmentSource
});
shader.frag
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;
}

スクリーンショット 2019-03-21 12.09.22.png

伸びが補正されて正円になりました。

アニメーションさせる

アニメーションをさせるには時間の数値が必要なので、CPUからuniform変数で渡します。

Canvas/index.js
// 省略
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);
  }
}
shader.frag
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;
}

Kapture 2019-03-21 at 12.12.02.gif

ピクセルシェーダーでアニメーションができました。
同様に頂点シェーダーでもuTimeを受け取ることで頂点をアニメーションさせることができます。

次回はマウスをつかってシェーダーをインタラクティブに動かします。

27
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
19