three.jsのTrackballControlsが思った通りに動かないので


GWに田舎から出てきた母親を連れて京都嵐山に行ったらあまりの人の多さに「京都、凄ぇ!怖ぇ!」となっている九州は福岡から出てきて2年目の東です。

さて。この右側の3次元のグラフを描くために、JavaScriptで簡単に3Dグラフィックが描けるthree.jsというライブラリを使ってみました。
three.jsを使うと、3Dのオブジェクトを配置するのも簡単ですし、マウスを使って視点の移動も簡単にできます...のはずなんですが、どうもうまく動きません。視点の移動にはTrackballControlsというコンポーネントを使っているのですが、どうやらこれがこちらの想定通りの動きをしてくれないようです。
今回はTrackballControlsを改造してうまく動くようにしてみました、というお話です。

「細かい話はどうでもいいから結論が見たい」という方はこちらへ。
(註: 今回使用したthree.jsのリビジョンはr58です。)

■ まずは、うまく動く例を。

公式のサンプルを参考に、シンプルな例を作ってみたのがこちら(01.html)です。
TrackballControlsという名前の通り、トラックボールを操作するような感じで視点を動かすことができます。マウスの左ボタンでつまんで回す、右ボタンで移動、中ボタンでズームができます。

でも、これだと画面に3Dオブジェクトしかないのでちょっと面白くありません。記事の中に3Dオブジェクトを埋め込みたいんです。

■ 記事の中に埋め込んでみた。

単純な方法で記事の中に埋め込んでみます。下記のように変更します。

変更前(01.html)
<div id="container" style="background-color: #e0e0e0;"></div>
  function init() {
    var container = document.getElementById( 'container' );

    camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 );
    camera.position.z = 500;

    controls = new THREE.TrackballControls( camera );
    controls.addEventListener( 'change', render );

    scene = new THREE.Scene();
    var geometry = new THREE.SphereGeometry( 100 );
    var material =  new THREE.MeshNormalMaterial()
    var mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );

    renderer = new THREE.CanvasRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );

    container.appendChild( renderer.domElement );
  }
変更後(02.html)
<div id="container" style="background-color: #e0e0e0; width: 240px; height: 180px; float:left"></div>
Lorem ipsum dolor sit amet, (略)
  function init() {
    var container = document.getElementById( 'container' );

    camera = new THREE.PerspectiveCamera( 60, container.clientWidth / container.clientHeight, 1, 1000 );
    camera.position.z = 500;

    controls = new THREE.TrackballControls( camera, container );
    controls.addEventListener( 'change', render );

    scene = new THREE.Scene();
    var geometry = new THREE.SphereGeometry( 100 );
    var material =  new THREE.MeshNormalMaterial()
    var mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );

    renderer = new THREE.CanvasRenderer();
    renderer.setSize( container.clientWidth, container.clientHeight );

    container.appendChild( renderer.domElement );
  }

8行目では、3Dオブジェクトの描画対象となる<div>要素を用意しています。元々は<div>要素のサイズは指定していませんでしたが、サイズを明示するように変更しています。
21行目では、カメラ(視界)の縦横比を指定しています。元々はwindow.innerWidth/Heightつまり画面全体のサイズを使っていましたが、9行目で指定した<div>要素のサイズに変更しています。
24行目では、マウス操作のイベントを受け取る対象を<div>要素に変更しています。ここを変更しないと、画面のどこを触っても3Dオブジェクトが動いてしまいます。
34行目では、描画領域のサイズを指定しています。ここでも画面全体のサイズだったものを、<div>要素のサイズに変更しています。

以上の変更を行なったものがこちら(02.html)です。

記事の中に3Dオブジェクトが埋め込まれていい感じなんですが、マウスでつまんでみると、どうも動きがおかしいです。特に全画面表示にすると、思った通りに動いてくれません。

■ どうなっているのか。⇒改造してみるか。

TrackballControlsのソースを見てみると、どうやら画面全体をひとつの描画領域として考えているようです。
<div>要素が描画領域となるようにTrackballControlsを変更していきます。

変更前
	this.handleResize = function () {

		this.screen.width = window.innerWidth;
		this.screen.height = window.innerHeight;

		this.screen.offsetLeft = 0;
		this.screen.offsetTop = 0;

		this.radius = ( this.screen.width + this.screen.height ) / 4;

	};
変更後
	this.handleResize = function () {

		this.screen.width  = this.domElement.clientWidth  || window.innerWidth;
		this.screen.height = this.domElement.clientHeight || window.innerHeight;

		this.screen.offsetLeft = 0;
		this.screen.offsetTop = 0;

		this.radius = ( this.screen.width + this.screen.height ) / 4;

	};

ここではマウスの左ボタンを押したときの回転動作の中心点を決めています。window.innerWidth/Heightつまり画面全体のサイズから計算していたものを、this.domElement=コンストラクタに渡された描画対象のエレメントのサイズから計算するように変更します。

変更前
	function mousedown( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		if ( _state === STATE.NONE ) {

			_state = event.button;

		}

		if ( _state === STATE.ROTATE && !_this.noRotate ) {

			_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY );

		} else if ( _state === STATE.ZOOM && !_this.noZoom ) {

			_zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		} else if ( _state === STATE.PAN && !_this.noPan ) {

			_panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		}

		document.addEventListener( 'mousemove', mousemove, false );
		document.addEventListener( 'mouseup', mouseup, false );

	}
変更後
	function mousedown( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		if ( _state === STATE.NONE ) {

			_state = event.button;

		}

		if ( _state === STATE.ROTATE && !_this.noRotate ) {

			var offset = _this.offset(event);
			_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( offset.X, offset.Y );

		} else if ( _state === STATE.ZOOM && !_this.noZoom ) {

			_zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		} else if ( _state === STATE.PAN && !_this.noPan ) {

			_panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		}

		document.addEventListener( 'mousemove', mousemove, false );
		document.addEventListener( 'mouseup', mouseup, false );

	}
	this.offset = function( event ) {
		if ( event.offsetX ) {
			return { X: event.offsetX, Y: event.offsetY }
		}
		var box = event.target.getBoundingClientRect();
		return { X: event.pageX - box.left - window.pageXOffset, Y: event.pageY - box.top - window.pageYOffset };
	}

マウスの左ボタンが押されたときの動作です。event.clientX/Yを使っています。ここも画面全体の座標を使っているものを、eventを検知したエレメント上の座標を使うように変更します。event.offsetX/Yを使えばいいのですが、Firefoxではこれらが使えないので、少し面倒なことをしています。
マウスが動いたときの動作を定義するfunction mousemove(event)も同じように変更します。

以上の変更を行なったものがこちら(03.html)です。うまく動いてくれています。
記事中に描画領域を複数入れても(04.html)、それぞれキチンと動作します。

■ まとめ

JavaScript用3Dグラフィックライブラリthree.jsには簡単に視点移動が行なえるTrackballControlsというコンポーネントがありますが、どうやらこれは画面全体をひとつの描画領域と考えているようで、描画領域のサイズを限定すると視点移動が不自然になってしまいます。
描画領域のサイズを限定しても視点移動が自然になるようにTrackballControlsを改造してみました。これによりテキスト記事の中に3Dオブジェクトの描画領域を埋め込むことができるようになります。
改造後のTrackballControlsはこちらに置いておきます。

画面全体が描画領域の例(01.html)
改造前:記事に埋め込んだらうまく動かない例(02.html) 全画面表示にすると不自然さが顕著に現れます。
改造後:記事に埋め込んでもうまく動くように改造(03.html)
改造後:描画領域が複数存在する例(04.html)


This entry was posted in 技術 and tagged , . Bookmark the permalink.