WebGLスクール第4回の課題はRaycasterと板を使って何かを作るというものでした。
若干課題の趣旨とずれているかもしれませんがRaycasterとPlaneを使ってDOMの位置とThree.jsの描画位置の同期を取るというコンセプトでアヒルを並べるページを作りました。
DOM上の位置からRaycasterでxy平面との交点を取り、そこにアヒルを複数個描画しています。あとは最近学んだベジェ曲線を添えて怪Webサイトができあがりました。
Raycasterを使ったDOMとの位置調整
Raycastingはカメラから光線(レイ)を発射して、他のオブジェクトと交差判定をする方法です。
Three.jsにはRaycasterというユーティリティがあり、これを使うことで簡単にオブジェクトの交差判定を行うことができます。
今回のデモではRaycasterを使ってスクリーン座標からワールド座標系への変換を実現しました。
XY平面に大きい見えないポリゴンを配置してカメラからレイを飛ばして衝突判定をします。
カメラがXY平面を向いていると仮定するとスクリーン座標に対応するワールド座標を取得できます。
この点はスクリーンに投影したときに、最初に与えられたスクリーン座標に落ちるワールド座標なので、ここにMeshを配置すればDOMと重なるように表示されます。
private updateObjectPosition(key: string) {
const element = this.elements.get(key);
const object = this.duckObjects.get(key);
if (!element || !object) {
return;
}
const rect = element.getBoundingClientRect();
const ndcX = ((rect.left + rect.width / 2) / window.innerWidth) * 2 - 1;
const ndcY = -((rect.top + rect.height / 2) / window.innerHeight) * 2 + 1;
const ndcPosition = new THREE.Vector2(ndcX, ndcY);
this.raycaster.setFromCamera(ndcPosition, this.raycastCamera);
const hit = this.raycaster.ray.intersectPlane(
this.intersectPlane,
this.worldPosition,
);
if (hit) {
object.position.copy(hit);
}
}
スクロール時にDOMとの相対的な位置関係を保つ方法
Raycasterを使えばDOMに合わせた初期位置を設定することができますが、次の問題としてスクロールに追従させる方法を考える必要があります。
ここでは
position: fixedを使う方法とposition: absoluteを使う方法を紹介します。
今回作成したデモでは最初position: fixedで作成しましたが、あとからposition: absoluteを使った方法に書き直しています。position: fixedを利用した方法
scrollイベントが呼ばれる度に上述の方法ですべてのMeshの位置を再計算する方法です。
実装自体は非常にシンプルにscroll毎に位置の再計算を回すだけなので楽ですが以下のような問題点があります。
可能であれば後述の方法を使うほうが好ましそうです。
- scrollイベントの度に位置計算を挟む必要があり描画コストがそこそこ高い。
- DOMのスクロール後、次のrenderまでラグがあり若干Meshが遅れて見える。
position: absoluteを使った方法
WebGLスクールのDiscordで紹介されていた方法です。
position: fixedでcanvasを画面に固定するのではなく、position: absoluteを使いcanvasを画面上に配置します。
こうするとcanvasは画面のスクロールとともに動くので、scrollの瞬間にDOMとcanvas内のMeshの相対的な位置関係は変化しません。
しかしこのままだとcanvasがviewport外にズレてしまうので、requestAnimationFrameの中で以下2つの操作を同時に行います。- translateYなどでcanvasの位置をscrollした分だけ動かす。
- Meshの位置を再計算する。
こうすることでscroll~renderまでの瞬間も相対的な位置関係が保持されるのでチラつきを抑えることができます。
WebGLのMeshとDOM Elementの位置同期の図解したら理解が深まった。なるほどこれは確かに論理的にはずれない。
— Takumi HASEGAWA (unshift Inc.) (@_unshift) April 8, 2025
リポジトリのREADMEに
"It's not perfect, but sometimes, "good enough" is what gets the job done."
とあるので、だめなときも当然あるんだろう。 https://t.co/ejCCcOELc1 pic.twitter.com/07qDQTddAi
ベジェ曲線
Computer Graphicsの本を買って、ベジェ曲線の原理を学んだので試してみました。
あひるとあひるを繋いでいるのは3次ベジェ曲線で以下の要領で作成しています。
- 長さが1のランダムなベクトルを2つ生成する。
- 始点と終点にそれぞれ生成したランダムなベクトルを足して制御点とする。
- 時間経過に応じて生成したベクトルを回転させる。
シンプルなコードですが有機的に動くような線を簡単に生成できました。
まとめ
WebGLスクールの第4回の課題としてDOM要素に合わせてMeshを配置するデモを作りました。
Raycasterよりも曲線の描画のほうが体感としては難しかったです。
線の太さなどを動的に調整したり、長さなどのジオメトリ情報をリアルタイムに変化させたりなどの試してみたかったところが結局実装できませんでした。
この辺のプラクティスがどこかにまとまっていると嬉しいのですが...