概要
この記事では「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回でやった内容のおさらいです。
今回はマウス座標をシェーダーに渡すため、ウィンドウ座標系(左上原点)からテクスチャ座標系(左下原点)に変換します。
// 省略
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);
}
};
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クラスでwindowのmousemoveイベントが発生したときにcanvasのmouseMoved関数を呼びます。
import Canvas from './Canvas';
export default class Page00 {
constructor() {
const canvas = new Canvas();
window.addEventListener('mousemove', e => {
canvas.mouseMoved(e.clientX, e.clientY);
});
}
};
簡易イージングでなめらかに追いかける
普通にマウス座標を割り当てるだけだとちょっと味気ないので、簡易的なイージングを挟んでマウス座標をゆっくり追いかけるようにしましょう。
// 省略
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);
}
// 省略
};

GIFだとちょっとわかりづらいですが、マウス座標をぬるっと追いかけるようになりました。
vecA.lerp(vecB, 0.2);と書くと、vecAをvecBの位置に20%近づけるという意味になります。
この処理が毎フレーム呼ばれているので、現在位置から目標位置までの差分の20%ずつ近づいていきます。
目標位置までの距離が近くなるほど、当然差分の20%の量も小さくなるので、だんだんゆっくりになります。
上記はVectorクラスでの書き方ですが、float値で同じ処理を書く場合はval += (targetVal - val) * 0.2;になります。
クリック中に半径を大きくする
targetRadiusという変数を作り、mousePressedで最大値に、mouseReleasedでデフォルトに設定します。
uRadiusというuniform変数を作ってtargetRadiusの値を追いかけさせます。
// 省略
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に置き換えます。
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クラスにイベントリスナを追加して、mousedownでcanvas.mousePressed、mouseupでcanvas.mouseReleasedを呼びます。
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);
});
}
};
簡易イージングを使うと段階的な目標値の変化に対して値がスムーズに移動してくれるので便利ですね。
次回はシェーダーでテクスチャにエフェクトをかける方法を紹介します。

