概要
この記事では「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回 getBoundingClientRect()を使ったDOM要素との連携
three.js超入門 第5回 シェーダー(GLSL)の基礎
three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する
three.js超入門 第7回 シェーダーに変数を渡す
three.js超入門 第8回 シェーダーをインタラクティブに動かす
three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける
前回はrequestAnimationFrame
を使って3Dオブジェクトをアニメーションさせました。
今回はマウスやスクロールなどのイベントから取れる値を使って3Dオブジェクトをインタラクティブに動かしてみます。
下準備
ウィンドウとWebGLの座標を統一させる
前回のコードでは、以下のようにカメラの距離とメッシュのサイズを目測で決めていましたが、今回はウィンドウのマウスイベントなどを使ってインタラクションを実装していくため、WebGL
の座標の単位をウィンドウと同じpx
に統一します。
↓カメラの撮影範囲や位置、ジオメトリのサイズが目測値になっていた。
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera';
// カメラを作成 (視野角, 画面のアスペクト比, カメラに映る最短距離, カメラに映る最遠距離)
this.camera = new PerspectiveCamera(60, this.w / this.h, 1, 10);
this.camera.position.z = 3;// カメラを遠ざける
// 立方体のジオメトリを作成(幅, 高さ, 奥行き)
const geo = new BoxGeometry(1, 1, 1);
ウィンドウサイズの平面がぴったり収まるカメラ距離
直角三角形のひとつの角(fov/2
)と、角の反対側の辺の長さ(height/2
)がわかっていて、底辺のdist
を求めたいので、三角関数のtan
の定義に当てはめて値を求めます。
// 視野角をラジアンに変換
const fov = 60;
const fovRad = (fov / 2) * (Math.PI / 180);
// 途中式
// Math.tan(fovRad) = (height / 2) / dist;
// Math.tan(fovRad) * dist = (height / 2);
const dist = (height / 2) / Math.tan(fovRad);
ピクセル座標で3Dオブジェクトを再配置
カメラを作成するところを以下のように変更して、ジオメトリの大きさとライトの位置もあわせて変更します。
const fov = 60;
const fovRad = (fov / 2) * (Math.PI / 180);// 視野角をラジアンに変換
const dist = (this.h / 2) / Math.tan(fovRad);// ウィンドウぴったりのカメラ距離
// カメラを作成 (視野角, 画面のアスペクト比, カメラに映る最短距離, カメラに映る最遠距離)
this.camera = new PerspectiveCamera(fov, this.w / this.h, 1, dist * 2);
this.camera.position.z = dist;// カメラを遠ざける
this.light.position.set(400, 400, 400);// ライトの位置を設定
// 立方体のジオメトリを作成(幅, 高さ, 奥行き)
const geo = new BoxGeometry(300, 300, 300);
見た目に大きな変化はありませんが、大きさや座標をpx
指定できるようになりました。
マウスでのインタラクション
コードを書く前に現時点でのwindow
座標とWebGL
座標の対応関係を確認しましょう。
PerspectiveCamera
を作る際に位置の指定をしていないので、原点(0, 0, 0)
を見据えたまま、ウィンドウサイズがぴったり収まるところまで後ろに下がっているので、以下のようになっています。
マウス座標の取得
まず、Canvas
クラスにマウス座標を保存しておく変数と、マウスが動いた時に呼ばれるmouseMoved
関数を作成します。
// ~ 省略 ~
import { Vector2 } from 'three/src/math/Vector2';
export default class Canvas {
constructor() {
// マウス座標
this.mouse = new Vector2(0, 0);
// ~ 省略 ~
}
// ~ 省略 ~
mouseMoved(x, y) {
this.mouse.x = x - (this.w / 2);// 原点を中心に持ってくる
this.mouse.y = -y + (this.h / 2);// 軸を反転して原点を中心に持ってくる
}
};
次に、Page00
クラスでwindow
のmousemove
イベントにイベントリスナーを登録し、その中で↑で作ったcanvas.mouseMoved
を呼びます。
mousemove
イベントから取得できる値(e.clientX
とe.clientY
)がwindow
座標なので、canvas.mouseMoved
関数の中でWebGL
座標に変換する必要があるというわけです。
import Canvas from './Canvas';
export default class Page00 {
constructor() {
const canvas = new Canvas();
window.addEventListener('mousemove', e => {
canvas.mouseMoved(e.clientX, e.clientY);
});
}
};
マウス座標でライトを動かす
まず、ライト座標の初期値のxy
を0
にしておきます。
this.light.position.set(0, 0, 400);// ライトの位置を設定
mouseMoved
関数内で、マウス座標が更新された後にライトの座標にマウス座標をそのまま代入します。
mouseMoved(x, y) {
this.mouse.x = x - (this.w / 2);// 原点を中心に持ってくる
this.mouse.y = -y + (this.h / 2);// 軸を反転して原点を中心に持ってくる
// ライトの xy座標 をマウス位置にする
this.light.position.x = this.mouse.x;
this.light.position.y = this.mouse.y;
}
3DCGが勝手に動いているだけでなく、自分の操作に反応するだけでちょっと楽しいですね。
スクロールでのインタラクション
スクロール量の取得
まず、スクロールさせるためにpublic/00_empty/index.html
にスクロール用のコンテナを追加します。
<body>
<!-- この中にcanvasが入ります -->
<div id="canvas-container"></div>
<!-- スクロール用コンテナ -->
<div id="scroll-container"></div>
<script src="/resource/js/vendor.bundle.js"></script>
<script src="/resource/js/common.bundle.js"></script>
</body>
スクロールコンテナ用のスタイルはすでにpublic/resource/css/common.css
に記述済みなので、追加する必要はありません。対応する箇所は以下になります。
#scroll-container {
position: relative;
width: 100%;
height: 200%;
}
Canvas
クラスには現在のスクロール量を保存しておく変数と、スクロールしたときに呼ばれるscrolled
関数を用意しておきます。
// ~ 省略 ~
export default class Canvas {
constructor() {
// スクロール量
this.scrollY = 0;
// ~ 省略 ~
}
// ~ 省略 ~
scrolled(y) {
this.scrollY = y;
}
};
そして、Page00
クラスでウィンドウのscroll
イベントで↑で作ったcanvas.scrolled
関数を呼び出します。
ページを表示したときにすでに前回のスクロール位置まで移動しているケースがあるので、イベント発生時だけでなく、ページ表示時にもcanvas.scrolled
関数を呼んでスクロール位置を更新しておくことを忘れないようにしましょう。
import Canvas from './Canvas';
export default class Page00 {
constructor() {
const canvas = new Canvas();
canvas.scrolled(window.scrollY);
window.addEventListener('mousemove', e => {
canvas.mouseMoved(e.clientX, e.clientY);
});
window.addEventListener('scroll', e => {
canvas.scrolled(window.scrollY);
});
}
};
スクロール量でメッシュを動かす
Canvas
クラスのrender
関数内でthis.mesh.position.y
にスクロール量を代入してメッシュの位置を更新します。
render() {
// ~ 省略 ~
// スクロールに追従させる
this.mesh.position.y = this.scrollY;
// 画面に表示
this.renderer.render(this.scene, this.camera);
}
スクロールに追従するようになりました!
ちなみに、ここでthis.mesh.position.y
に反映させるスクロール量を増やしたり減らしたりすることで簡単にパララックス的な動きを作ることができます。
DOM要素がないとパララックスがわかりにくいので、先にindex.html
にh1
を追加しておきます。
<!-- スクロール用コンテナ -->
<div id="scroll-container">
<h1 id="scroll-container_title">THREEJS<br>TUTORIAL<br>03</h1>
</div>
こちらもスタイルはcommon.css
に記述済みです。
#scroll-container_title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 7em;
letter-spacing: 0.05em;
font-family: sans-serif;
font-style: italic;
font-weight: bold;
}
// スクロール量より多く動く
this.mesh.position.y = this.scrollY * 1.5;
// スクロール量より少なく動く
this.mesh.position.y = this.scrollY * 0.5;
今回はマウス座標やスクロール量をそのままライトのxy
座標とメッシュのy
座標に反映させましたが、サイズや回転などに入れてみてもおもしろいとおもいます。