three.js
TypeScript
NIJIBOXDay 24

Three.jsでマウスイベントに応じてカメラが球面上を動くようにする(クォータニオンを使って)

はじめに

カメラが球面を回って、視線は常に中心を向いている
そんな動作をThree.jsでやりたくなりました

数式

x,y,z座標から角度を求めるには

var radius = 1000; // 半径

var theta = Math.atan2(y, x);
var phi = Math.asin(z / radius);

角度からx,y,z座標を求めるには

var radius = 1000; // 半径

var x = radius * Math.cos(phi) * Math.cos(theta);
var y = radius * Math.cos(phi) * Math.sin(theta);
var z = radius * Math.sin(phi);

まずは簡単にやってみる

そういうことなら、マウスで回転させたければ、マウスの移動量をとって
phiとthetaを増減させれば、いい感じになりそうです

ということで、やってみます。まずは上記の変数thetaを単純に増加させています
デモ1
うまくいった気がします

ですが、今度は変数phiを単純増加させると、、
デモ2
なんでしょう、、跳ね返っています

マウスで下にぐーっとドラッグしている最中に跳ね返るなんて、挙動としておかしいですぅぅ

クォータニオンを使ったコードに修正する

軸が薄く表示されていますが、跳ね返る瞬間、よ〜く見ると、赤と緑がパッと逆になっています
これが原因です
(赤道から北に向かっていって、北極点を過ぎた時にくるっと回るイメージというと伝わるでしょうか。。。)

ということで、解決すべくクォータニオンを使います
抜粋すると以下な感じ

class Main
{
    constructor() {
         this.renderer.domElement.addEventListener('mousedown', (e) => this.mouseDownHandler(e));
         this.renderer.domElement.addEventListener('mousemove', (e) => this.mouseMoveHandler(e));
         this.renderer.domElement.addEventListener('mouseup', (e) => this.mouseUpHandler(e));

    }

    private mouseDownHandler(e){
        this.isMouseDown = true;
        this.setBaseInfo(e.clientX , e.clientY);
    }
    private mouseMoveHandler(e){
        if(! this.isMouseDown){
            return;
        }
        let mx = e.clientX - this.mouseBaseX;
        let my = e.clientY - this.mouseBaseY;

        let newPos = this.cameraBasePosition.clone();
        let deltaQuat = new THREE.Quaternion();
        let deltaQuatX = new THREE.Quaternion();
        let deltaQuatZ = new THREE.Quaternion();
        deltaQuatX.setFromAxisAngle(this.cameraBaseUp , -mx * Math.PI / 180);
        deltaQuatZ.setFromAxisAngle(this.cameraBaseUpCross , my * Math.PI / 180);
        deltaQuat.multiply(deltaQuatX).multiply(deltaQuatZ);
        newPos.applyQuaternion(deltaQuat);

        this.camera.position.set(newPos.x,newPos.y,newPos.z);

        let newUp = this.cameraBaseUp.clone();
        deltaQuat.setFromAxisAngle(this.cameraBaseUpCross , my * Math.PI / 180);
        newUp.applyQuaternion(deltaQuat);
        this.camera.up.set(newUp.x,newUp.y,newUp.z);
        this.camera.lookAt(new THREE.Vector3(0,0,0));

        this.setBaseInfo(e.clientX , e.clientY);
    }
    private setBaseInfo(x,y){
        this.mouseBaseX = x;
        this.mouseBaseY = y;
        this.cameraBasePosition = this.camera.position.clone();
        this.cameraBaseUp = this.camera.up.clone();
        this.cameraBaseUpCross = this.cameraBasePosition.clone().cross(this.cameraBaseUp).normalize();
    }
    private mouseUpHandler(e){
        this.isMouseDown = false;
    }


}

デモ3
ソースはこちら
https://github.com/nbfujiwara/okiagariStructure

できたー
普通の感覚ではオブジェクトを回転してるようにしか見えないと思いますが
カメラが球面を回っております

ちなみに、setBaseInfo()というメソッドをmousemove時にも呼んでいます。
つまり、マウス差分の元をmousedownの時をベースにするのではなく
一つ直前のmousemove時をベースにしています。
これは、こうしておかないと、ドラッグ操作が長い場合に違和感を覚えます

なぜかというと、X軸が回転ゼロの時と、X軸を90度回転させた時とで、Z軸の回転は
それぞれで意味が違ってくる。。。。みたいなことがおきるからです

最後に

ちなみに、このデモで用いた妙な3Dオブジェクトについてなんですが
これは「おきあがりこぼし」というベビー向けの玩具でして
Google画像検索-おきあがりこぼし

これを義母からプレゼントされたのですが、私の愛娘がものの見事にボコボコにぶっ壊してしまったんですね。

その時に、中身の構造を見て、あまりのシンプルさに驚愕したんです
このシンプルさで、あの複雑で耳障りな音がなるのかと!!

(ちなみに、中には筒が入ってて、筒の下部分に重りが固定されています(デモの紫の部分)。その重りには長さの違う金属の棒(デモの緑の部分)が刺さっています。そして筒の上から振り子のように可動する重り(デモの赤の部分)がついており、これが揺れて緑の棒とぶつかって、けたたましい音を立てるのです)

というとこから、エンジニアはシンプルなことが大好き!みたいな下りで、ブログチックな内容を書こうとしたんですね
その過程で久しぶりにThree.jsを触り、オブジェクトの回転は、実際にこの玩具が揺れる表現で使うと思って、まずカメラを回そうとしたら、そこで予想以上に戸惑ってしまい、このソースコードをコピペしようっとという方針転換をしたのでした

ではではメリクリ〜