Help us understand the problem. What is going on with this article?

three.js超入門 第8回 シェーダーをインタラクティブに動かす

More than 1 year has passed since last update.

概要

この記事では「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回 シェーダーでテクスチャにエフェクトをかける

リポジトリ

今回は、前回書いた発光する円をマウス座標やクリックイベントでインタラクティブにうごかしてみます。

uniform変数でマウス座標を渡す

前回第3回でやった内容のおさらいです。
今回はマウス座標をシェーダーに渡すため、ウィンドウ座標系(左上原点)からテクスチャ座標系(左下原点)に変換します。

Canvas/index.js
// 省略
export default class Canvas {
  constructor() {
    // 省略

    // マウス座標
    this.mouse = new Vector2(0.5, 0.5);

    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: this.mouse
      }
    };

    // 省略
  }

  // 省略

  mouseMoved(x, y) {
    // 左上原点から左下原点に変換
    this.mouse.x = x / this.w;
    this.mouse.y = 1.0 - (y / this.h);
  }
};
shader.frag
varying vec2 vUv;

uniform float uAspect;
uniform float uTime;
uniform vec2  uMouse;// マウス座標

void main() {
  vec2 uv = vec2( vUv.x * uAspect, vUv.y );
  vec2 center = vec2( uMouse.x * uAspect, uMouse.y );// アスペクト補正したマウス座標
  float radius = 0.05 + sin( uTime ) * 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;
}

Pageクラスでwindowmousemoveイベントが発生したときにcanvasmouseMoved関数を呼びます。

00_shader_empty/index.js
import Canvas from './Canvas';

export default class Page00 {
  constructor() {
    const canvas = new Canvas();

    window.addEventListener('mousemove', e => {
      canvas.mouseMoved(e.clientX, e.clientY);
    });
  }
};

Kapture 2019-03-21 at 16.00.33.gif
円がマウスについてくるようになりました。

簡易イージングでなめらかに追いかける

普通にマウス座標を割り当てるだけだとちょっと味気ないので、簡易的なイージングを挟んでマウス座標をゆっくり追いかけるようにしましょう。

Canvas/index.js
// 省略
export default class Canvas {
  constructor() {
    // 省略

    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: new Vector2(0.5, 0.5)// this.mouse とは別のベクトル
      }
    };

    // 省略
  }

  render() {
    requestAnimationFrame(() => { this.render(); });

    const sec = performance.now() / 1000;

    this.uniforms.uTime.value = sec;

    // シェーダーに渡すマウスを更新
    this.uniforms.uMouse.value.lerp(this.mouse, 0.2);

    this.renderer.render(this.scene, this.camera);
  }

  // 省略
};

Kapture 2019-03-21 at 15.40.19.gif
GIFだとちょっとわかりづらいですが、マウス座標をぬるっと追いかけるようになりました。
vecA.lerp(vecB, 0.2);と書くと、vecAvecBの位置に20%近づけるという意味になります。
この処理が毎フレーム呼ばれているので、現在位置から目標位置までの差分の20%ずつ近づいていきます。
目標位置までの距離が近くなるほど、当然差分の20%の量も小さくなるので、だんだんゆっくりになります。
上記はVectorクラスでの書き方ですが、float値で同じ処理を書く場合はval += (targetVal - val) * 0.2;になります。

クリック中に半径を大きくする

targetRadiusという変数を作り、mousePressedで最大値に、mouseReleasedでデフォルトに設定します。
uRadiusというuniform変数を作ってtargetRadiusの値を追いかけさせます。

Canvas/index.js
// 省略
export default class Canvas {
  constructor() {
    // 省略

    this.mouse = new Vector2(0.5, 0.5);
    this.targetRadius = 0.005;// 半径の目標値

    // uniform変数を定義
    this.uniforms = {
      uAspect: {
        value: this.w / this.h
      },
      uTime: {
        value: 0.0
      },
      uMouse: {
        value: new Vector2(0.5, 0.5)
      },
      uRadius: {
        value: this.targetRadius
      }
    };

    // 省略
  }

  render() {
    requestAnimationFrame(() => { this.render(); });

    const sec = performance.now() / 1000;

    this.uniforms.uTime.value = sec;

    this.uniforms.uMouse.value.lerp(this.mouse, 0.2);

    // シェーダーに渡す半径を更新
    this.uniforms.uRadius.value += (this.targetRadius - this.uniforms.uRadius.value) * 0.2;

    this.renderer.render(this.scene, this.camera);
  }

  // 省略
  mousePressed(x, y) {
    this.mouseMoved(x, y);
    this.targetRadius = 0.25;// マウスを押したら半径の目標値を大きく
  }
  mouseReleased(x, y) {
    this.mouseMoved(x, y);
    this.targetRadius = 0.005;// マウスを押したら半径の目標値をデフォルト値に
  }
};

シェーダー側では、radiusだったところをuRadiusに置き換えます。

glslshader.frag
varying vec2 vUv;

uniform float uAspect;
uniform float uTime;
uniform vec2  uMouse;
uniform float uRadius;// 半径

void main() {
  vec2 uv = vec2( vUv.x * uAspect, vUv.y );
  vec2 center = vec2( uMouse.x * uAspect, uMouse.y );
  float lightness = uRadius / length( uv - center );// 半径を uRadius に変更
  // 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;
}

Pageクラスにイベントリスナを追加して、mousedowncanvas.mousePressedmouseupcanvas.mouseReleasedを呼びます。

00_shader_empty/index.js
import Canvas from './Canvas';

export default class Page00 {
  constructor() {
    const canvas = new Canvas();

    window.addEventListener('mousemove', e => {
      canvas.mouseMoved(e.clientX, e.clientY);
    });
    window.addEventListener('mousedown', e => {
      canvas.mousePressed(e.clientX, e.clientY);
    });
    window.addEventListener('mouseup', e => {
      canvas.mouseReleased(e.clientX, e.clientY);
    });
  }
};

Kapture 2019-03-21 at 17.01.10.gif

簡易イージングを使うと段階的な目標値の変化に対して値がスムーズに移動してくれるので便利ですね。
次回はシェーダーでテクスチャにエフェクトをかける方法を紹介します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした