#まずはじめに
###お前は誰だ
Jey-enと申します。業務でC#を、JavaScriptは業務と趣味で合わせて1年という、素人と知ったかぶりの狭間を生きております。
####こんなん作った
Three.jsでゲームっぽいのが作れるかなと思い、このくらいまでやってみました。
大体ね、「3DぐらふぃっくがWebで出来るようになった!おっしゃゲーム作ったろ!」なんて、脳筋すぎると思いませんかね
そのゲームっぽいものを作りながら思いついた、使えそうな小技をお伝えできればと思います。
他の方がやっているように、一つのテーマで完結まで行くという形ではなく、タイトルにもあるように【逆引き辞典風】な何かです。
#対象としている人:
Three.jsを使って、【何かモデルを表示する】とかできて、次は何をしようかと考えてる人
よせばいいのにUn●tyじゃなくてThree.jsでゲームっぽいのを作ろうとしてる人
せめて「Scene」に「カメラ」と「モデル」を追加すれば表示できる、という所までわかってる人
というように、多くの人に必要かとか、そういうことは一切考えずに「独断と偏見」のみでTipsを分類しました。
全部読もうと思わず、気になるところをつまみ食いをするのが一番いいかと思います。見出し一覧からGoです。全部知っていたらBackボタンGoです。
それでは、ごゆるりとお付き合いくださいませ。
Lv1:ゲーム以外のアプリ・サイト等でも(多分)使えそうなもの&初歩的なもの
##☆キャラに、カメラの方向を向いてほしい(簡易版)
「あの子を振り向かせたい」、プログラムなら確実に出来ます。Three.jsなら2次元じゃなくて3次元だぞ!って言っていいですか。ダメですか。そうですか。
手順としましては、
1・視点の位置から、あの子(被写体)に対して向かうベクトルを用意する
2・それを単位ベクトルにする(normalize)
3・魔法の呪文を唱える(atan2)
4・キャラの向きにセットする(rotation.y)
// charaMesh が、向かせたいキャラのメッシュオブジェクトとします
var tmpV = new THREE.Vector3();
tmpV.copy(camera.position);
tmpV.sub(charaMesh.position); //1.向かうベクトルの用意
tmpV.normalize(); //2.単位ベクトル化
var angle = Math.atan2(tmpV.x, tmpV.z); //3.魔法の呪文
charaMesh.rotation.y = angle; //4.向きに適用
Vector3.angleTo が使えるかな~と思ってたのですが、 angleTo は、値が「角度の絶対値」しか返ってこないんですよね。ダメでした。アレは罠です。向かせたいなら、Math.atan2 を使いましょう。
ここで検出できる向きは、「Y軸」の向きです。コレを応用すると、「進行方向にキャラを向かせる」とかも実現できるようになります。簡単かつ出来ることが多いのがこいつです。
##☆地形の上に立たせたい(メッシュからの高さ検出)
ド定番ですね。これを1つずつ必要な要素を切り出すと、
1.「立たせたい(上に置きたいもの)の、はるか上空のポイント(Vector3)を用意する
2.「1」から、真下に向かう「線」を用意する(Raycaster)
3.その「線」と、「地面となるもの」の、ピンポイントの衝突点を探す(Raycaster.intersectObject)
という流れになります。
XとZは元の位置を利用し、Yだけ「はるか上空」に置くのは、「指定した一方向に対してしか、衝突を調べてくれない」からです。ここに元位置のYをそのままセットしてしまうと、「自分が今いる場所から、下の場所の高さ」しか正確な位置が調べられず、階段で言うと「降りる」しかできなくなります。それは困るため、「下向きに調べれば全部分かる」ようにするため、Yだけ大きい位置にしてあります。
// basePos というVector3型に、「立たせたいもの」のPosが入ってるとします
var TopOverPos = new THREE.Vector3(basePos.x, 65535, basePos.z); //1.はるか上空のポイントを用意
var downVect = new THREE.Vector3(0,-1,0); //下向きのベクトルのみが入ったVector3を用意
var ray = new THREE.Raycaster(TopOverPos, downVect.normalize()); //2.真下に向かう線がコレ
var maxY = 0; //衝突対象が複数あった場合、一番「高い」ものを取得するようにします
var objs = ray.intersectObjects(scene.children, true); //衝突点検出!
for (var i = 0; i < objs.length; i++) {
if(maxY < objs[i].point.y)
maxY = objs[i].point.y;
}
//return maxY; //当然、この maxY に、検出できた高さが入ってきます。
basePos.y = maxY;
なお、上のサンプルは「scene全部にあるもの片っ端から高さを検出する」ものとなっています。計算は「メッシュの頂点量」に比例して時間がかかりますので、sceneの中に人物用のメッシュとかあったら当然ソコからも調べて、まぁソレは重くなります。
「コレが地面!」と、決めたものがある場合は、
var objs = ray.intersectObject(zimen_mesh);
というように、メッシュ単品からも取得できます。こちらのほうがより実践的で、現実的でしょう。
##☆3D上のポイントを画面に出したときの2D位置を知りたい(スクリーン座標)
ゲームを良くする人であれば、「キャラのすぐ上や下に表示されてるHP」とかが、ピンとくる感じでしょうか。
画面は3D、でもHPは2Dでさくっと表示したい。そういうときに「3D上のとある位置は、2D表示にするとドコに来るのか、が知りたくなります。
コレは、しばしば【スクリーン座標】という呼ばれ方をするので、3Dプログラムをするなら覚えておくと幸せかもしれません。
そのスクリーン座標をThree.jsで得るためには、こんな感じになります。
// basePos というVector3型に、「2Dにしたい位置」のPosが入ってるとします
var screenV = new THREE.Vector3(); //入れものを用意
screenV.copy(screenV); //値を複製。こうしないと、値を乗っ取ってしまうので。
screenV.project(camera); //この project というメソッドで、あっさり取得できます。だがしかし!
//この値は、 -1 ~ +1 で返ってくる(左&下が-1、中心がゼロ、右&上が+1)ので、画面サイズに直すにはもう一工夫
if(screenV.z < 1.0) //1.0を超える=カメラの裏側にある、ということ!
{
var scr_pos_x, scr_pos_y;
scr_pos_x = screenSize.Width * 0.5 + (screenV.x * 0.5 * screenSize.Width);
scr_pos_y = screenSize.Height * 0.5 + (screenV.y * -0.5 * screenSize.Height);
}
##☆逆に、クリックした位置を3D座標に変えたい
これはもう、非常にわかりやすいものを他の人が書いてくださってるので、お任せします。
上の「Project」の逆、unproject と、最初に書いた Raycaster の合わせ技ですね。
##☆キャラを動かした時の「キャラの向きの前方」の位置を知りたい(ローカル座標のワールド座標化:非ボーン)
これはもう、非常にわかりやすいものを他の人が書いてくださってるので、お任せします。
リンク先のVector4型「forward」が、いわゆるローカル座標(キャラを中心とした座標)になります。
##☆オススメ!Canvas重ねのススメ
もうThree.js関係ないんじゃね?と言われそうな勢いですが。
ゲームを作る時、体力ゲージとか得点とか、敵のマーカーとか、いわゆる「2D要素」を表示したいことは多々あります。
このとき、一昔前であればDirectDrawとか、2Dを弄るための命令はありました(何年前の話だ)。
んじゃあ、Three.jsではどうしたら良いのでしょう?
Three.jsが乗っかっているHTMLには、ご存知「Canvas」の「2dContext」という、2D要素をガリッガリに書きまくれる優れたコントロールがあります。そうです。こいつに書いてしまえばいいのです。
どうするかと言うと、「Three.jsの3D表示を行っている要素に対し、全く同じ位置&全く同じサイズのCanvas要素を用意して重ね、重ねたヤツに対して描画する」のです!
コレ、あっさり気づく人は気づくでしょうけど、気づかない人はずっと気づかないくらいの盲点的要素な気が。
Canvasは透明&透過が許されるので、下にあるものを表示したまま、円とか四角とか自由に書けます!
コレは便利ですし、管理も楽です。いやーイイですよコレ。
重なりはZ-indexで指定します。z-indexが大きいほど、手前に来ます。
<!-- 3D描画領域のためのdiv要素を配置 -->
<div id="canvas3d" style="position:fixed; top:0px; left:0px; width:100%; height:100%; z-index: 1; background-color: #000000;"></div>
<!-- その真上に、2D描画領域のためのdiv要素を配置 -->
<canvas id="canvas2d" style="position:fixed; top:0px; left:0px; width:100%; height:100%; z-index: 100; background-color:rgba(0,0,0,0);"></canvas>
なお、コレをやると、多くのThree.jsのサンプルで適用されている「【ページ】にThree.jsレンダー用のdivエレメントを追加(container) して、それに対してレンダーを適用(renderer.domElement)」が、動かなくなります。そのために、
【特定されたdivを、Three.jsの表示結果出力用に指定する】という処理が必要になります。まぁごく簡単な変更ですが
var canvas3D = document.getElementById('canvas3d');
canvas3D.appendChild(renderer.domElement);
そして、上に重なっている[ canvas2d ]のcontext2Dに対して、好き放題書きます。ですが、良くCanvasのサンプルなどでおいてある、「まず黒く塗りつぶします」的なことはやったらダメです(当然、Three.jsの3D結果が見えなくなります)
//conteText2Dは、canvas2dのcontextです。
conteText2D.clearRect(0, 0, conteText2D.canvas.width, conteText2D.canvas.height); //2D描画結果のクリア。これはよくあるので、そのまま
//conteText2D.fillRect(0, 0, conteText2D.canvas.width, conteText2D.canvas.height); これはやりません!
あとは、conteText2Dに対して、好き放題にfillやstrokeするだけです。
Lv2:使いみちは多いんだけど、初歩的とは言いづらいもの(初級者以上)
##☆キャラの手に、持たせたい
ここでいう「キャラ」とは、一通りボーンが組まれており、かつアニメーションさせて動くようなキャラクターです。
アニメーションさせるということは、位置が動きまくります。当たり前です。手の位置なんて、上半身とか腕の上げ角度とか、それはもういろんな要素に引きずられて動きまくります。
じゃあその「動きまくる」ものに対して、持たせる=位置を合わせるには、どうすればよいのでしょう?
1.「持たせる場所」を、しっかり特定する。
これは、いわゆる「ボーン」を特定させます。3Dモデルを作った人がキチンとした方&きちんとした人が作ったインポーターなら、【 mesh.skeleton.bones[] 】 にボーンが、そしてbonesの中のBoneオブジェクトのname に、特定できそうな名前で入っています。
たとえば例のMMDミクであれば、mesh.skeleton.bones[54] が、右手の手首のボーンです。
2.そのBone型に対して、 add する
このBone型(Three.Bone型)に対し、なんとSceneと同じように add が出来るのです。
そう、その add で持たせたいオブジェクト(mesh等)を追加するだけで、そのボーン位置にぴったりと表示するようにしてくれます。
んが、ここで少しだけ問題点。
これでそのまま「add」するだけだと、いわゆる「ぴったりすぎる」のですね(角度もアレですが)。手首のボーン位置なので、ぴったりそのまま、手首にくっついてきます。めり込んでます。これじゃダメです。リ○カっぽくてアウトです。そのため、「手首から少しずらし、手の平の位置で持たせる」のが望ましいですね。
//キャラモデルがmesh。 コレは、ボーン組のmesh=SkinnedMesh であることが大前提。
//手に持たせたいものを emonoMesh とします
//なお、コレは毎フレームやる必要はなく、1回だけ行えばいい。
mesh.skeleton.bones[54].add(emonoMesh ) ; //この 54 番ボーンが、手首のボーン。ここに対して add する
var transMX = new THREE.Matrix4();
var position = ( new THREE.Vector3()).set(-0.9,-1.1,0); //これが、手首から見た、手の平の位置。
var scale = ( new THREE.Vector3()).set(1,2,1); //縦長にする
var quaternion = (new THREE.Quaternion()).setFromAxisAngle((new THREE.Vector3().set(1,0,0)), Math.PI / 2); //角度をx軸からみて90度回転
transMX.compose( position, quaternion, scale ); //各要素をMatrixとして確定させます。
emonoMesh.applyMatrix(transMX); //【持たせた】物体に対して、移動を適用させます。
この移動と変形と回転を適用させると、手首にしっくりくるようになります
この「applyMatrix」で、【持たせた】方の物体のMatrixを変更します。Matrixなので、当然角度や大きさも変更自由です。ちょうどよい大きさを見つけましょう。
##☆キャラの「指先」の座標を知りたい(ボーンを使ったローカル座標のワールド化)
ここでいう「キャラ」とは、一通りボーンが組まれており、かつアニメーションさせて動くようなキャラクターです。
アニメーションさせるということは、位置が動きまくります。当たり前です。手の位置なんて、上半身とか腕の上げ角度とか、それはもういろんな要素に引きずられて動きまくります。
じゃあその「動きまくる」ものの、【動いた後の位置】を知るためには、どうすればよいのでしょう?
ん?上の「ボーンに持たせる」と、何が違うのかって?
こちらは、「その時点での位置を知ること」を目的としています。ずっとくっついてくるのではなく、その場所に残るエフェクトなどが設置できるようになり、
こんな感じのことができるようになります。イイでしょ?ってことでやり方です
var tmpV = new THREE.Vector3(); //入れ物を用意
var localAddPos = new THREE.Vector3(); //取得したいボーンからの、ローカルに進んだ位置として作成
localAddPos.set(-0.1,-0,1); //指先になるように、進めてみた
tmpV.copy( mesh.skeleton.bones[60].localToWorld(localAddPos)); //60番は右人差し指の第三関節のボーン
このObject3D.localToWorld というメソッドで、ボーン座標+ボーンのローカル座標に値を加えたものを、ワールド座標にできます。(Object3Dクラスが持っているメソッドなので、別にBoneじゃなくても使えます)
コレは指先とか銃口とか、ゲーム用途には使いまくりなメソッドです。覚えておきましょう。
#Lv3:ゲーム作る気じゃなければ必要なさそうなもの(習熟度ばらばら)
##☆物体と物体が当たっているかどうかを知りたい
いわゆる「アタリ判定」というものを調べるのに良く使われるのは、 BoundingSphereというものです。
コレは、「そのメッシュ(モデル)の全頂点が入りきるように作られた、ちょうどいい大きさの球体」です。BoundingSphereは、何もしなければ普通にメッシュを読んだときに自動的に作られ、移動すればBoundingSphereもくっついて移動します。イメージとしては、ふぉーすふぃーるどです(雑)
一番最初に書いた「メッシュからの厳密なアタリ判定」よりも、実質的には「2点間の距離」の計測で完結するBoundingSphere同士の判定の方が、圧倒的に高速です。キャラクターにも「玉」となるオブジェクトにも、このBoundingSphereは作られますので、BoundingSphere同士で衝突判定を行うのは、てっとり早い方法でしょう。
BoundingSphere同士の衝突判定は、 intersectsSphereで得ることができます。
//ここは、モデル読み込み後、一回だけ行う
if(charaMesh.geometry.BoundingSphere == null){ charaMesh.geometry.computeBoundingSphere();} //なかったら作る。
//少しだけ小さめにする
charaMesh.geometry.BoundingSpher.radius *= 0.8;
//ここは毎フレーム行う
if(charaMesh.geometry.BoundingSphere.intersectsSphere(tamaMesh.geometry.BoundingSphere)
{
console.log('死んだ!');
}
小さくしている理由は、この方法、試してみれば分かるのですが、そのままの大きさではゲームに使うには「予想以上にアタリ判定が大きく思う」ことでしょう。頭の先から足の先まで、それが奥行きも全部すっぽりなので、見えてない所まで当たるように思えます。
そのため、気持ち小さくして、見た目との違和感を減らしています。
ところでこの geometry.BoundingSphere は、「視野判定」にも使われます。視野判定とは、「カメラから見える範囲に、そのキャラクターが入っているかどうか」の判定です。
ここの判定で外れると、そのキャラクターは描画のためにGPUにセットする対象から外されます。カメラに写らないのに、描画するための計算をするのは無駄だよね、ということです。
そのため、小さくしすぎると、いわば「キャラクターがカメラから見切れるか見切れないか」の隅っこぎりぎりにいるときに描画対象から外れ、パっと消えてしまうという感じになります。中心はカメラから外れてるけど、手だけは見える、ということもなくなってしまいます。
コレを避けるために、アタリ判定の球体とgeometry.BoundingSphere は、別で持たせる方がよい場合が多いです。BoundingSphereの元となっている Sphere オブジェクトは、中心点(Vector3)と半径(float)の2つの引数だけで作れますので、さくっと作ってしまうのが良いでしょう。
//初期化&作成(1回だけ行えばよいもの
if(charaMesh.geometry.BoundingSphere == null){ charaMesh.geometry.computeBoundingSphere();} //
//アタリ判定球を別途定義
var HitBall = new Sphere(charaMesh.geometry.BoundingSphere.center, charaMesh.geometry.BoundingSphere.radius);
HitBall.radius *= 0.5; //けっこー小さめに
//判定球をモデルオブジェクトの「子」として追加
charaMesh.add(HitBall);
charaMesh.HitBall = HitBall; //アクセスを容易にしとく
//ここは毎フレーム行う
if(charaMesh.HitBall.intersectsSphere(tamaMesh.geometry.BoundingSphere)
{
console.log('死んだ!');
}
##☆キャラが「本当に」カメラから見えているか見えていないかを知る、良い?方法
上のトピックで「視野判定」なるものが出ましたが、ココはちょっと違う話。
「視野判定」は、【カメラの見える範囲内】にそのキャラクターがいるかどうかの判定でしたが、実際に「見えている」となると、ちょっと話が変わってくるということにお気づきでしょうか。
そう、「壁」や「地面の裏側」の話です。
スクリーン座標のトピックの時に書きましたが、「壁の裏側に隠れているのに、HPとか情報が表示されててバレバレ」なのは、ゲームによってはがっかりしてしまうことでしょう。コレを防ぐにはどうするか、のアプローチです。
アプローチの一例を挙げると、
1.カメラから、敵の中心点までの距離を測定
2.カメラ位置から敵の位置までのベクトルを取得し、正規化
3.カメラ位置から敵の位置まで、地面を対象とした「Raycaster」を飛ばし、衝突点を検出。
4.「3」で取得した検出点までの距離が、「1」で取得した距離より近ければ、「壁」のほうが敵の手前にある!ということ
という形になります。
そのとーり、既に紹介済みの「Raycaster.intersectObject」で、なんとかなってしまいます。
ココではソースの紹介は省略します。挑戦してみましょう。
##☆「空」を表示させる方法(SkyBoxの表現のために)
Three.jsのサンプルには、非常にキレイな空のサンプルがありますが、残念ながら雲一つ無い空しか出来ません。
ゲームでは、「Skybox」や「Skydome」と呼ばれる、めっっちゃくちゃ大きい球体や立方体の【内側】に、空のテクスチャを貼り付けて表現したりします。
さてさて。また「視野判定」の話になりますが、カメラから外れている物体を表示しないのはあたりまえですが、「カメラの正面にいるけど、表示する必要がない」ものがある、ということを、考えたことはあるでしょうか。
・・そう、「遠くにあって、めっちゃ小さいっていうか見えない」ものは、表示する必要=計算をさせる必要ってありませんよね。
コレ実は、 PerspectiveCamera の宣言時の引数時にパラメータの設定があり、「カメラからこの距離以上のものは描画しない」という設定があったのです。 PerspectiveCamera の3番目と4番目の引数です。この設定は、よく「クリップ」と呼ばれます。3番目は「近すぎる場合に表示しない(ニアクリップ)」、4番目は「遠すぎる場合に表示しない(ファークリップ)」となります。
// 1 がニアクリップ、 2500 がファークリップ
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2500);
前項で「BoundingShpereによる視野判定」がありましたが、アレは「オブジェクト全部が」表示されなくなりますが、この「クリップ」に関しては、メッシュのポリゴン1枚単位(というか1頂点毎)で表示する/しないの判定が行われます。
こんな感じで、なんか「遠くの方の山がメリメリ欠けてから成形されていくように見える」のは、そのせいですね。メッシュは地面として1つなのですが、カメラからの遠さ判定が1頂点ずつ行われているため、「高い」場所は、近づかないと見えてこないとかいうことになります
・・さて。
SkyBoxの話に戻りますが。「サイズをめっちゃ大きくすればよい」という話をしましたが、そのカリング設定のおかげで、大きくしすぎると、今度は「あ、こいつ遠すぎるから表示しなくていいな」という判断がされてしまいます。
空は、山より遠くにあります。では、山より遠くにある空を表示するには、どうしたらよいでしょう。
いろいろ調べた結果、「シーンとカメラを2つ用意する」とする方法しか、なさそうです。でも、それで出来るんです。
ポイントは
1.カメラとシーンを2つずつ用意
2.背景用のカメラには、ファークリップはめちゃくちゃ大きい数をセット
3.レンダラーの「自動クリア」を無効にする
の3点です。
//宣言部分
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2500); //これがメイン用
camera_back = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 250000); //これが背景用
//シーンも2つ
scene = new THREE.Scene();
scene_back = new THREE.Scene();
//レンダラーは1つ
renderer = new THREE.WebGLRenderer();
renderer.autoClear = false; //【重要!】自動クリアを無効にする!
//「遠くでも見える」ものは、scene_backにaddしていく
scene_back.add(sky_mesh);
//遠くの場合は表示しなくてもいいもの(ファークリップが効いてほしいもの)は、普通のsceneに
scene_back.add(zimen_mesh);
scene.add(hito_mesh);
scene.add(teki_mesh);
そして毎フレーム走る描画が下のような感じに。
renderer.clear(true, true, true); //クリアは明示的に行う。
renderer.render(scene_back, camera_back); //後ろに来るもの=背景用のモノを先に描画
renderer.render(scene, camera);
「自動クリアを無効」にしないと、renderer.render が実行された時点で自動的にそれまでの描画結果が破棄されます。つまり、2回目のrenderが走った時点で、1回目の結果が消えてしまいます。そうさせないための自動クリア無効です。
当然ながら、視点の移動や回転などは、両方のカメラに対して行う必要があるので、面倒は2倍になります。うまいことメソッドを用意して逃げましょう。
#まとめに
100%独断で、なるべく出番が多そうなモノをピックアップしたつもりですが、いかがだったでしょうか。
バラバラに散らばった知識が、のちのち1つに繋がり、何かが出来上がる。そんな体験の一つの中に、ここでの知識があったら幸いです。
あと、最後にコレだけ伝えて、お別れの言葉とさせていただきます。
3Dゲーム作りたかったら、おとなしくU●ityのほうがいいよ!