古今東西のゲームで用いられている、フェードインやフェードアウトといったエフェクトは、three.jsではどのように実現するのでしょうか。ググったところ、2種類の方法があることが分かりました。すなわち、
- (HTML/CSS)div要素をcanvasの上に重ねて、opacityを変化させる
- (three.js)カメラの前に透明な板を置いて、opacityを変化させる
今回は両方実装してみることにしました。
結論
どっちでも実現はできるので、好きな方を使っていい。
div要素を重ねて実現する方法
こちらはCSSを使ってフェードを行います。
まずHTMLとしての要素構成は以下のようになります。
<div class="container">
<div id="curtain" class="child"></div>
<canvas id="canvas" class="child"></canvas>
</div>
このdiv#curtainをcanvasの上において、透明度を増減させます。
続いてCSSは以下のようになります。
.container {
position: relative;
width: 400px;
height: 300px;
}
.child {
position: absolute;
margin: auto;
width: 100%;
height: 100%;
left: 0px;
top: 0px;
}
# curtain {
z-index: 2;
background: black;
opacity: 50%;
}
# canvas {
z-index: 1;
}
重要なのは、canvas
とdiv#curtain
をposition: absolute
を使って重ねるところと、z-index
を指定してdiv要素をcanvasより前に表示させるところですね。
最後にjavascriptでdiv#curtainのopacityスタイルをいじって透明度を変化させます。
function animate() {
curtain_opacity += 0.01;
if(curtain_opacity >= 1.0) curtain_opacity = 1.0;
curtain.style.opacity = curtain_opacity;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
サンプル(jsFiddle)
実際に動くサンプルをjsFiddleに用意しました。
この方法のメリット
- 実装がかんたん!
この方法のデメリット
- ブラウザの実装の違い等の理由で、div要素とcanvasがずれたときに困る(かもしれない)
カメラの前に板を置いて実現する方法
こちらの方法ではthree.jsだけで完結することができます。一方で、行列やクォータニオンを取り扱うので、それらについての何の知識も持っていないと理解が難しいかもしれません。
さて、こちらの方法は以下のようなステップで行います。
- 板のメッシュを生成する
- 板の位置を決める
- 板の向きを決める
- 板の透明度を変化させる
板のメッシュを生成する
別にどんな板を作ってもよいのですが、ここはスタンダードに、THREE.PlaneGeometry
とTHREE.MeshBasicMaterial
を使った板を作ることにします。
コードは以下の通りです。
let curtain_opacity = 0.0;
const curtain_color = new THREE.Color("rgb(255, 255, 255)");
const geo_curtain = new THREE.PlaneGeometry(2.0, 2.0);
const mat_curtain = new THREE.MeshBasicMaterial({
color: curtain_color,
transparent: true,
opacity: curtain_opacity
});
const curtain = new THREE.Mesh(geo_curtain, mat_curtain);
説明では板というワードを使いつつ、コードではカーテンという語を使っているのはご容赦ください(板→カーテンと読み替えてください)。
板のサイズはカメラの画角と視錐台の奥行(near,far)、画面のアスペクト比等に応じて適宜変更してください。curtain_opacity
は透明度の初期値(0は100%透明)、curtain_color
は板の色を表しています。こちらも用途に応じて変更してください。
板の位置を決める
作った板をカメラの真正面に置きます。それにはまず、カメラを基準とした座標系で考えます。カメラの位置が(0,0,0)
となります。向きについては、特に何もいじっていないデフォルト状態の場合は、カメラから見て右側がX軸の正方向、上側がY軸の正方向、そして真正面がZ軸の負方向となります(OpenGL/webGLの場合)。
ということで、カメラのちょっと手前の座標は(0,0,-Δz)
と表せます。
次にこれをワールド座標系に変換します。これは簡単に得ることができます。カメラのObject3D.matrixWorld
プロパティを通じてカメラの位置と姿勢を表す行列(モデル行列)が得られます。これと、先ほどの座標(0,0,-Δz)
を掛けるだけです。
ただし、1点だけ注意点があります。matrixWorldはレンダリング時に生成されますが、それ以前はundefined
なので、updateMatrixWorldを使って手動で算出してやる必要があります。
const distance = 0.5; //カメラと板の距離
const pos = new THREE.Vector3(0.0, 0.0, -distance);
camera.updateMatrixWorld(true); //matrixWorldを更新
pos.applyMatrix4(camera.matrixWorld);
この位置に物体を置いてやれば、カメラの直前に表示されます。カメラと板の距離はカメラのnear-clipping-planeより大きくなるようにしてください。
板の向きを決める
しかし今回それだけでは十分ではありません。球体ならば問題ありませんが、置きたいのは板なので、板の向きもしっかり揃えてやる必要があります。
板はカメラの向きに対して真正面に構えて欲しいです。専門用語でいうところのビルボードというやつですね。これを実現するには、カメラの向きを板にそのままコピーしてやればOKです。具体的にはObject3D.quaternion
とObject3D.setRotationFromQuaternion
を使います。
curtain.setRotationFromQuaternion(camera.quaternion);
これだけで板はカメラに正対した状態になります。
板の透明度を変える
最後に、時間経過に応じて、板の透明度を変化させます。メッシュの.material.opacity
を操作します。
curtain.material.opacity = curtain_opacity;
サンプル(jsFiddle)
ここまでのコードをまとめたのものをjsFiddleに用意しました。
この方法のメリット
- three.jsだけで完結できる
この方法のデメリット
- 板よりオブジェクトが手前に来ると写り込んでしまう
- カメラが動くたびに板の位置・姿勢を更新する必要がある(あまり大した問題ではない)
余談
今回初めてjsFiddleでthree.jsを使ったのですが、予め用意されているthree.jsのバージョン105ではなぜかdocument.getElementById
が機能しなくて、結局CDNからバージョン109をインポートして使用することにしました。jsFiddleでthree.jsをやる人はご注意ください。