three.js でgltf を読み込むと暗く見える
ただのメモです。
three.jsに限った話ではないそうですが、ほかのファイル形式と共存させたい場合は、materialのcolorを1より大きく作り直すのが今のところ手っ取り早くできるかと思います。
もともとシーンに光源、カメラが一緒に入っているかと思いますが例えばここでは削除してあるとして、scene の0番目の子要素に物が入ってるとします。
var gloader = new THREE.GLTFLoader(); gloader.load( loadobj.filePath, function(data){ var scn = data.scene; var obj = scn.children[0]; for (let i = 0; i < obj.children.length; i++){ var m = new THREE.MeshLambertMaterial(); //必要に応じて変える m.copy( obj.children[i].material ); m.side = THREE.DoubleSide; m.opacity = 1; m.color.r= 10; //もともと(r,g,b)=(1,1,1)の場合 m.color.g= 10; m.color.b= 10; obj.children[i].material = m; } }
ブラウザ3Dゲームを作る作戦その8: 軽量化メモ
主に軽量化についてメモ書きしておきます。
軽量化
特に既存のmodelデータを使いたいときの場合。
materialの変更
構造によって細かい部分は変わりますが、基本的にmaterialを新たに作ってcopy→再度セットでうまくいくようです。
MeshBasicMaterial (塗りつぶし)
MeshLambertMaterial (視点が動いても明るさが変わらない)
あたりが軽いそうです。
変え方は一例ですが、今使ってるフィールドマップだと、
var floader = new THREE.TDSLoader(); // ロード floader.load('models/field/Cartoon_land.3DS', (object) => { let fldChild = object.children; for (let i = fldChild.length-1; i >= 0; i--){ var m = new THREE.MeshLambertMaterial(); //拡散反射 m.copy( fldChild[i].material ); m.side = THREE.DoubleSide; m.opacity = 1; fldChild[i].material = m; fldChild[i].rotation.x = THREE.Math.degToRad( -90 ); } });
のような感じです。この例だとobjectの子要素がそれぞれmaterialを持っています。
マップを分割する
Rayscaterが判定するオブジェクトを減らすために、あるエリアに居たら見るオブジェクトはこれだけ、というリストをあらかじめ作っておきます。
オブジェクトがどこに属するかという感じにリストを作るとエリアを跨ぐときに無駄が多い気がするので、
- エリアの真ん中からある距離以内のオブジェクトを登録しておく
感じにしました(重複あり)。あとベースになっているでっかいオブジェクトはすべてのエリアに登録しておきます。今回はエリアを16個に分けて121個のオブジェクトからリストを作りました。
オブジェクトをpushで突っ込んでいますが、idだけ取っておいてもいいと思います。
カメラを調整する
デフォルトだと視野も見える距離の範囲もかなり広かったので、ここを変えると確かに軽くなります。
処理時間を監視する
個人的にはperformance.now() が使い勝手が良かったのでこれを採用しました。
単位は1秒/1000 だっと思います。
let t0 = performance.now(); renderer.render( scene, camera ); let t1 = performance.now(); // t1-t0 が処理時間
ブラウザ3Dゲームを作る作戦その7: 衝突回避
障害物を回避する
NPCがプレーヤーを追いかけてくるような状況で、障害物に引っかからないようにする話です。
調べてみるとたくさん方法があるみたいです。メッシュ上をいろんなアルゴリズムで経路探索したり、事前にマップに経路情報を埋め込んでおいたり、アリみたいにプレーヤーが通った経路を利用したりなど。
お手軽にできそうな方法としては、Raycaster が衝突するobjectを返してくれるのを利用する方法があるようです。
今回は衝突したオブジェクトのboundingSphereに沿って移動するようにしてみました。
絵の中で目標が黒い矢印の方向にいるとすると、自分に一番近い壁のboundingSphere (の少し大きめ)の接線(緑)を目標方向にすれば、壁がどんな形でもおよそ引っかからずに目標に到達できると思います。ダメなケースを挙げればきりがないですが、フィールドがある程度パーツに分かれているなら簡単にできる方法として悪くないと思います。
ちなみにRaycaster は自分からターゲットに伸ばせば坂道など考えずに済むので楽だと思います。
一番近いオブジェクトの boundingSphere のとり方ですが、
intersects = Vray.intersectObjects(fieldObjs.children, true);
で取ってきたとすると、
bSphere = intersects[0].object.geometry.boundingSphere;
として取得できます。スケールや親の位置など諸々のことを考えると、例えばこの bSphere の中心が欲しいときは
bsCenter = intersects[0].object.localToWorld(bSphere.center.clone());
のようにobject.localToWorld をつかって変換するのが楽ですね。
これならRaycaster も追加の1本で済むので処理としてもそこまで重くはならないと思います。
ブラウザ3Dゲームを作る作戦その6: 軽量化
Load の軽量化
- フィールド等のMMD以外のモデルデータは、glTF(GL Transmission Format) がおすすめだそうです(.gltf /.glb)。
.gltf はjson形式、.glbはバイナリ形式になっているそうで、どちらも同じようにThree.js 側から読み込めます。
なのですが他の形式と明るさが合わないので現在調査中。
- VMDは長いループを作らなくてもThree.js 側のループ再生で違和感なく繋げられる(走り出し等もfadeInでつなげればいい)ので、走るモーションも最初から1ループ程度を切り出しておくと軽いデータにできます。
vmdについてモーションとセットにして使いそうなのは loopやフェードイン/アウト、再生速度(timeScale)などでしょうか。
var motionFiles = [ {filePath : './vmd/walk/walk_short.vmd', loop : THREE.LoopRepeat, fadeIn: 0.1, fadeOut: 0.1, timeScale: 1 }, …省略 ];
ループについては、はじめに actionを取り出す時点で
action.setLoop( motionFiles[i].loop );
で設定できます。一度だけ再生したいとき、終了時のポーズで止めたいときは clampWhenFinished を trueにすればよいようです(デフォルトはfalse でactionがセットされていないポーズになる)。
if (motionFiles[i].loop == THREE.LoopOnce){ action.clampWhenFinished = true;}
処理 の軽量化
- モデルデータのメッシュ結合について
例えば今回使っているデータでは children にたくさんの家や木などが入っています。
[:480]
木の幹同士、葉同士など同じマテリアルをblender などで結合すると軽くなるか試していたのですが、パーツの数がよほど多くない限り効果がないように思います。これも今は後回しになりそうです。
- 前回の記事https://iwanabot.hatenablog.com/entry/2020/05/03/203526でRaycaster はさほど重くなさそうなどと申しておりましたが、あれは嘘だ。
プレイヤーを増やしたときのネックになっていたのがRaycaster の処理でしたので、頭の水平2軸はもうやめて、足側も移動キーでX軸とZ軸のどちらかだけのRaycaster を出すようにしました。以前はプレイヤー毎5本でしたが、今はこれで常にY軸と足の片方の軸の2本なので比較的軽くなりました。
ブラウザ3Dゲームを作る作戦その5: フィールドマップとの衝突判定
今回はフィールドマップを追加して衝突判定をしたいと思います。
フィールドはここからお借りしました。
https://www.turbosquid.com/ja/3d-model/free/city
今回から斉天大聖ちゃんになります。みどらさんありがとうございます。
斉天大聖配布開始しました / みどら さんのイラスト - ニコニコ静画 (イラスト)
衝突判定
当たり判定は単純に中心同士の距離などで良い気がしますが、3Dゲームなどででこぼこしたフィールドに沿って歩いたりしてるのをやってみたかったので調べていました。
おそらくですがよく使われるのがRaycaster なるもので、長い線を伸ばして交差するものとぶつかる距離をすべて見つけてくれるようです。(公式リファレンス:https://threejs.org/docs/#api/en/core/Raycaster)
衝突判定の概要
衝突検出できたとしてどう使うかですが、今回はWASDとジャンプ操作なので、天井と床、あと自分に対して前後左右の壁にぶつかるように考えたいと思います。処理はそこまで重そうではなかったので、以下のようにしてみました。
追記:嘘です重いです
- 頭、足から自分に対して縦(Z)横(X)方向の2本と上下(Y)1本の計5本
のRaycater を用意します。各Raycaterで探すものとしては、
- Y方向:頭側で最も低いもの(天井)、足側で最も高いもの(床)
- X,Z方向:最も距離が近いもの(壁)
といった具合です。それらが見つかったとして、処理としては
- 足もとに踏み越えられない壁がある場合、近ければ押し戻す
- 頭付近に壁がある場合、近ければ押し戻す
- 天井が頭より下の場合押し戻す
- 床が足より上の場合押し戻す
これだけにします。踏み越えられるかについては、越えられる高さを決めて足側の縦横のRaycater はその高さで伸ばせばよいと思います。
コードについて
下準備として、気を付けないといけないのは各objectのmaterial は両面から見える状態にしておかないと片方すり抜けます。例えば今回お借りしたフィールドマップですが、3DSファイルなのでTDSLoader.js を先に読み込んでおいてから、以下のようにしています。
//ここにフィールドマップになるものを入れる var fieldObjs = new THREE.Group(); var floader = new THREE.TDSLoader(); floader.load('models/field/Cartoon_land.3DS', (object) => { object.rotation.x = THREE.Math.degToRad( -90 ); //構造はあらかじめ調べておく let fldChild = object.children; for (let i = 0; i < fldChild.length; i++){ fldChild[i].material.side = THREE.DoubleSide; } object.scale.x = 15; object.scale.y = 15; object.scale.z = 15; fieldObjs.add(object); }); scene.add(fieldObjs);
sceneとは別にfieldObjs みたいなグループを作っておけば不要な交差判定を除けるので良いと思います。
次にRaycaster の使い方は公式どおりですが、
let ray = new THREE.Raycaster( 開始点 , 方向ベクトル ); var intersects = ray.intersectObjects( fieldObjs.children, true);
コードが長々とスパゲッティみたいになったので一部省略して載せておきます。
function fieldCollision(agent){ // 下準備-------------------------------------------------------------- // boundingSphere から頭と足もとの位置を決める const bSphere = agent.mesh.geometry.boundingSphere; const bsCenterLocal = new THREE.Vector3(bSphere.center.x, bSphere.center.y, bSphere.center.z); const bsCenter = bsCenterLocal.clone().add(agent.position); let bsHead = bsCenter.clone().add(new THREE.Vector3(0, bSphere.radius, 0)); let bsFoot = bsCenter.clone().add(new THREE.Vector3(0, -bSphere.radius, 0)); // Rayの向き let ZRayVect = new THREE.Vector3( agent.viewVect.x, 0, agent.viewVect.z).normalize(); let XRayVect = new THREE.Vector3( agent.viewVect.z, 0, agent.viewVect.x).normalize(); let YRayVect = new THREE.Vector3( 0, -1, 0); //上から下 // Rayの始点(後方、左、上) const ZRayOrigin = bsCenter.clone().addScaledVector(ZRayVect, -200); const XRayOrigin = bsCenter.clone().addScaledVector(XRayVect, -200); const YRayOrigin = bsCenter.clone().addScaledVector(YRayVect, -200); // 水平2軸は頭と足も用意する let ZRayOriginHead = ZRayOrigin.clone().add(new THREE.Vector3(0, bSphere.radius*0.9, 0)); let ZRayOriginFoot = ZRayOrigin.clone().add(new THREE.Vector3(0, -bSphere.radius*0.7, 0)); let XRayOriginHead = XRayOrigin.clone().add(new THREE.Vector3(0, bSphere.radius*0.9, 0)); let XRayOriginFoot = XRayOrigin.clone().add(new THREE.Vector3(0, -bSphere.radius*0.7, 0)); // Ray let ZrayHead = new THREE.Raycaster(ZRayOriginHead, ZRayVect); let XrayHead = new THREE.Raycaster(XRayOriginHead, XRayVect); let ZrayFoot = new THREE.Raycaster(ZRayOriginFoot, ZRayVect); let XrayFoot = new THREE.Raycaster(XRayOriginFoot, XRayVect); let Yray = new THREE.Raycaster(YRayOrigin, YRayVect); //------------------------------------------------------------------- // (1)衝突検出Y----------------------- var intersects = Yray.intersectObjects(fieldObjs.children, true); // 足Y: 足側で最も高いものを探す // 頭Y: 頭側で最も低いものを探す var footHighestYLocal = -200; var headLowestYLocal = 200; // for (let i = 0; i < intersects.length; i++) { // 相対位置 let footYLocal = 200-intersects[i].distance + bSphere.radius; if( footYLocal > footHighestYLocal && footYLocal <= bSphere.radius){ footHighestYLocal = footYLocal; } // 相対位置 let headYLocal = 200-intersects[i].distance - bSphere.radius; if( headYLocal < headLowestYLocal && headYLocal > -bSphere.radius){ headLowestYLocal = headYLocal; } } // (2)衝突検出Z----------------------- intersects = ZrayFoot.intersectObjects(fieldObjs.children, true); // z: 最も距離が近いもの var footNearestZLocal =200; // for (let i = 0; i < intersects.length; i++) { // 相対位置 let footZLocal = intersects[i].distance - 200; if( Math.abs(footZLocal) < Math.abs(footNearestZLocal) ){ footNearestZLocal = footZLocal; } } // 頭 intersects = ZrayHead.intersectObjects(fieldObjs.children, true); // z: 最も距離が近いもの var headNearestZLocal =200; // for (let i = 0; i < intersects.length; i++) { // 相対位置 let headZLocal = intersects[i].distance - 200; if( Math.abs(headZLocal) < Math.abs(headNearestZLocal) ){ headNearestZLocal = headZLocal; } } // (3)衝突検出X----------------------- // 省略、Zと同じ // 処理をかく // 足もとに踏み越えられない壁がある場合、近ければ押し戻す------------------------------------ // radius*30%の高さにZ衝突面がある場合、近ければ押し出す if ( Math.abs(footNearestZLocal) < bSphere.radius*0.2){ if(footNearestZLocal>0){ agent.position.x -= bSphere.radius*0.2 * ZRayVect.x; agent.position.z -= bSphere.radius*0.2 * ZRayVect.z; }else{ agent.position.x += bSphere.radius*0.2 * ZRayVect.x; agent.position.z += bSphere.radius*0.2 * ZRayVect.z; } } // radius*30%の高さにX衝突面がある場合、近ければ押し出す if ( Math.abs(footNearestXLocal) < bSphere.radius*0.2){ if(footNearestXLocal>0){ agent.position.z -= bSphere.radius*0.2 * ZRayVect.x; agent.position.x -= bSphere.radius*0.2 * ZRayVect.z; }else{ agent.position.z += bSphere.radius*0.2 * ZRayVect.x; agent.position.x += bSphere.radius*0.2 * ZRayVect.z; } } // 頭付近に壁がある場合、近ければ押し戻す------------------------------------ // 省略、足と同じ // 天井が頭より下の場合押し戻す if (headLowestYLocal <= 0){ // 頭をぶつける agent.position.y += headLowestYLocal; } // 床が足より上の場合押し戻す if (footHighestYLocal >= -bSphere.radius*0.01){ // ここまで弾かれていないなら乗り越える agent.position.y += footHighestYLocal; agent.isOnGround =1; }else{ agent.isOnGround =0; } if(mouseDrag>0){ console.log(intersectsFX); //console.log(footNearestZLocal); console.log(footNearestXLocal); } }
ブラウザ3Dゲームを作る作戦その4: MMDデータを読み込んで動かす
前回はcubeだったので今回は代わりにMMDデータを読み込んで使いたいと思います。
読み込む
MMDLoaderというものがあるので、今回はそれを使っていきます。
threejs.org
MMDデータまわりを使うために、一通り javascriptファイルを読み込んでおくと良さそうです。
追記:RawGitが終了するようなのでアドレスを変更
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/libs/mmdparser.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r103/examples/js/libs/ammo.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/loaders/TGALoader.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/loaders/MMDLoader.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/animation/MMDAnimationHelper.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/effects/OutlineEffect.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/animation/CCDIKSolver.js"></script> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r106/examples/js/animation/MMDPhysics.js"></script>
読み込み自体は公式で説明されている通りです。今回は
https://github.com/takahirox/MMDLoader-app#readme
ここで説明されているモデルをお借りしています。
// pmd/pmxファイルの読み込み var miku; var mesh_loaded = 0; // Load a MMD model modelFile = 'https://cdn.rawgit.com/mrdoob/three.js/r87/examples/models/mmd/miku/miku_v2.pmd'; // load のためのオブジェクトを作る const loader = new THREE.MMDLoader(); // ロード部分本体 loader.load( modelFile, function (mesh) { // モデルファイルの読み込み miku = mesh; scene.add( miku ); mesh_loaded +=1; console.log(miku); }, onProgress, onError); //MMD loading progress function onProgress( xhr ) { if ( xhr.lengthComputable ) { let percentComplete = xhr.loaded / xhr.total * 100; console.log( Math.round( percentComplete, 2 ) + '% downloaded' ); } }; // Error MMD Load function onError( xhr ) { };
これでとりあえずモデルだけ差し替えることができました。
移動しているときに歩かせる/走らせる
ざっくり調べた感じだと、特に最近のバージョンでは THREE.MMDAnimationHelper というものを使ってモーションを適用していくようです。
こちらが長いコードになるのですが、大まかな流れは
- モデルデータ(.pmd/.pmx)とモーションデータ(.vmd)を読み込む
- モデルデータとモーションデータ(全部)をTHREE.MMDAnimationHelperにセットする
- セットしたモーションをばらばらに切り出して覚えておく
となっています。
詳細は公式のリファレンス&サンプルを見ると良いと思います。1つのMMDAnimationHelperに対しては1回しかhelper.add できませんが、切り替え方法として
- モーションを切り替えるたびにモーションと(特にphysics)をセットしなおすとメモリが爆発した。
- helperを複数用意するとphysicsのつながりが不自然になった。
なので1つのMMDAnimationHelperにモーションを配列として全部セットして、後で切り替える方法が良さそうです。
下準備:モーションファイル情報をまとめておく
このあたりは個別に変数を作ってもよいと思います。モーションデータは
【MMD】移動モーションv1.3 - BowlRoll
からお借りして改変しています。
// モーションファイル (vmd:Vocaloid Motion Data) var motionFiles = [ {filePath : './vmd/walk/walk.vmd', loop : THREE.LoopOnce, action : null, fadeIn: 0.1, fadeOut: 0.1 }, {filePath : './vmd/walk/run.vmd', loop : THREE.LoopOnce, action : null, fadeIn: 0.1, fadeOut: 0.1 }, {filePath : './vmd/Lat式/Jump_resized/1.段差に飛び乗る/2.ジャンプ_(11f_上移動3~10の間_前移動0~10の間).vmd', loop : THREE.LoopOnce, action : null , fadeIn: 0.1, fadeOut: 1.0}, {filePath : './vmd/Lat式/Jump_resized/1.段差に飛び乗る/3.着地_(8f_移動なし).vmd', loop : THREE.LoopOnce, action : null , fadein: 0.1, fadeOut: 0.1} ];
モデルデータ・モーションデータをロードしてhelperにセットする
// ロード部分本体 loader.load( modelFile, function (mesh) { // モデルファイルの読み込み miku = mesh; // モーションファイル(vmd:Vocaloid Motion Data) LoadVmd( 3, miku ); LoadVmd( 2, miku ); LoadVmd( 1, miku ); LoadVmd( 0, miku ); // motionをセットする helper.add(mesh,{ animation: animations, physics: true, warmup : 6, unitStep : 1/120 }); mesh_loaded =1; //console.log(miku); }, onProgress, onError); // モーションファイル読み込み function LoadVmd( motionId, mesh ){ vmdPath = motionFiles[motionId].filePath; // アニメーションファイルを読み込む loader.loadAnimation(vmdPath, mesh, function(vmd){ // vmdを保持しておく animations[motionId] = vmd; }, onProgress, onError); return true; } //MMD loading progress function onProgress( xhr ) { if ( xhr.lengthComputable ) { let percentComplete = xhr.loaded / xhr.total * 100; console.log( Math.round( percentComplete, 2 ) + '% downloaded' ); } }; // Error MMD Load function onError( xhr ) { };
helper.addのwarmupやunitStepは物理演算で骨格以外が伸びて戻ってこなくなった時などに調整すると良さそうです。
セットしたモーションをばらばらに切り出して覚えておく
ロードが完了したら取り出していきます。どこにセットされているかは公式の
https://threejs.org/docs/#examples/en/animations/MMDAnimationHelper.objects
https://threejs.org/docs/#api/en/animation/AnimationMixer.existingAction
のあたりが参考になります。
function initAction(){ scene.add( miku ); // 全てのモーションがセットされているmixer let mixer = helper.objects.get( miku ).mixer; // 全てのモーションの停止 mixer.stopAllAction(); for(let i=0;i<4; i++){ if(animations[i]){ // ばらばらに取り出す let action = mixer.clipAction( animations[i] ); // Loopさせるかどうか action.setLoop( motionFiles[i].loop ); // 保持しておく motionFiles[i].action= action; }else{return false;} } // インバースキネマティクス(IK)の bone連動 ikHelper = helper.objects.get( miku ).ikSolver.createHelper(); ikHelper.visible = true; scene.add( ikHelper ); // 物理演算の helper physicsHelper = helper.objects.get( miku ).physics.createHelper(); physicsHelper.visible = false; scene.add( physicsHelper ); mesh_loaded +=1; }
ロードがすべて完了してからこのように取り出していくと良さそうです。
切り出したモーションの適用
アクションの制御に関しては
https://threejs.org/docs/#api/en/animation/AnimationAction.reset
あたりが参考になります。切り替え時の骨格の破綻を防ぐためにaction のweight やfadeOut、fadeInなどを利用すると
良いと思います。これらでほとんど破綻しなくなります。
if(player.isStop==0 ){ // motionが切り替わった時 if(player.lastMotion != selectmotion){ //切り替え前のモーションを重みを低く適用&フェードアウトさせる motionFiles[player.lastMotion].action.weight =0.3; helper.update( frameTime ); motionFiles[player.lastMotion].action.fadeOut(motionFiles[selectmotion].fadeOut); //切り替え後のモーションを重みを低く適用&フェードインさせる motionFiles[selectmotion].action.reset(); motionFiles[selectmotion].action.play(); motionFiles[selectmotion].action.fadeIn(motionFiles[selectmotion].fadeIn); motionFiles[selectmotion].action.weight =0.3; }else{ motionFiles[selectmotion].action.weight =1; } helper.update( frameTime ); player.lastMotion = selectmotion; }
フレームごとに呼ばれる animationの中でhelper.update( frameTime ); することで勝手にframeTime分アニメーションを進めてくれます。
ブラウザ3Dゲームを作る作戦その3: カメラで追従する
今回は3人称視点を目指して少しずつ変更していきます。
わかりやすいように床を入れる
これはとても簡単にできました。画像はカメラの位置を適当に調整してあります。
// 床グリッドを追加 var gridHelper = new THREE.GridHelper(500, 100); scene.add(gridHelper);
マウスイベントを取得する
マウスが動いたとかクリックされたなどは、今回は描画画面が監視してくれる関数を使いました。
// マウス情報 var mousex, mousey; var mouseDrag=0; // マウスが動いたとき renderer.domElement.addEventListener('mousemove', e => { //描画画面中心からの差をマウスの x, y とする mousex = e.clientX - renderer.domElement.offsetWidth/2 - renderer.domElement.offsetLeft; mousey = e.clientY - renderer.domElement.offsetHeight/2 - renderer.domElement.offsetTop; }); // マウスが押されたとき renderer.domElement.addEventListener('mousedown', e => { mouseDrag = 1; console.log( mousex +','+mousey ); }); // マウスが離されたとき renderer.domElement.addEventListener('mouseup', e => { mouseDrag = 0; });
描画画面の実体は renderer.domElement のようです。試しに console.log で表示して中を覗いてみると、
offsetWidth と offsetHeight というものがあったのでこの半分を画面の真ん中として使いました。
canvasの表示位置が少し下なのでその分を offsetTop で調整しています。
ここまでを実行するとクリックで console に場所が表示されるようになりました。
向いている方向に進む
カメラはプレイヤーについてくるので、先にプレイヤー操作をもう少し改造します。
オブジェクトを作ってみる
今まで前後左右に動くだけでしたが、向きなども入ってくるので変数をいくつかまとめたオブジェクトを作りたいと思います。
// object をつくる var MyAgent = function(){ this.position = new THREE.Vector3(0, 0, 0); // 場所 this.viewVect = new THREE.Vector3(0, 0, 0); // 向いている方向 this.rotationUp = 0; // 向いている上下の角度 this.rotationRight = THREE.Math.degToRad( -90 );// 向いている水平方向の角度 // 向いている角度から向いている方向を計算する this.updateView = function(){ var y_ = Math.sin(this.rotationUp); var x_ = Math.cos(this.rotationUp) * Math.cos(this.rotationRight); var z_ = Math.cos(this.rotationUp) * Math.sin(this.rotationRight); this.viewVect = new THREE.Vector3(x_, y_, z_); } } // player という名前で作ったobject を1つ用意する player = new MyAgent(); console.log( player ); player.position = cube.position;
console.log で表示してみると、
こんな感じで作ることができました。
向いている方向を基準に前後左右させる
今までの時点ではWASDで xと zが増減しましたが、向きを考えてどれぐらい x と z に値を加えるかを変更します。
さらに player と動かす cube を連動させたいので、キーボードの反映の関数をざっくり書き換えて、
- player の位置をキーボードで動かす
- cube の位置と向きをplayerと同じにする
ように変更します。これで playerが変わっても書き換えが楽になるかと思います。
//プレーヤーの移動 function userMove(frameTime, mesh, agent) { // 向きを更新 agent.updateView(); //w (前進) if(key_on[87]>0){ agent.position.z += frameTime * 5 * agent.viewVect.z; agent.position.x += frameTime * 5 * agent.viewVect.x; } //a if(key_on[65]>0){ agent.position.z -= frameTime * 5 * agent.viewVect.x; agent.position.x += frameTime * 5 * agent.viewVect.z; } //s (後退) if(key_on[83]>0){ agent.position.z -= frameTime * 5 * agent.viewVect.z; agent.position.x -= frameTime * 5 * agent.viewVect.x; } //d (右) if(key_on[68]>0){ agent.position.z += frameTime * 5 * agent.viewVect.x; agent.position.x -= frameTime * 5 * agent.viewVect.z; } // 反映する mesh.rotation.y = -agent.rotationRight; mesh.position.x = agent.position.x; mesh.position.y = agent.position.y; mesh.position.z = agent.position.z; }
向きに対して前進・後退か直角のどちらに進むかといった感じですね。
これを animation の部分で userMove(frameTime, cube, player); のように呼び出せばOKです。
今のところプレイヤーの向いている角度が変わらないので、ここはやっつけで勝手に回転させることにします。
// 時計 clock = new THREE.Clock(); function animate() { window.requestAnimationFrame( animate ); // 現在の1フレームの長さを取得 var frameTime = clock.getDelta(); // とりあえず回転させる player.rotationRight += frameTime * THREE.Math.degToRad( 20 ); userMove(frameTime, cube, player); // シーンを描画 renderer.render( scene, camera ); } animate();
カメラを動かす
カメラの動かし方はいろいろあるとは思いますが、基本的にプレイヤーの後ろからシーンを眺めているようにしたいです。概要としては、
- プレイヤーが向いている方向の背後にカメラの位置を設定する
- プレーヤーと同じ方向をカメラも向く
//カメラの再設置 function cameraUpDate(agent) { // カメラの場所をプレイヤーの後ろに camera.position.x = agent.position.x -agent.viewVect.x *5; camera.position.y = agent.position.y -agent.viewVect.y *5+2; camera.position.z = agent.position.z -agent.viewVect.z *5; // プレイヤーの方向を向く camera.lookAt( agent.position); }
これで cameraUpDate(player); と呼べばカメラ位置が更新されるようになります。
マウスで向きが変わる&カメラが追従する
せっかくなので勝手に回転するのはやめて、やっつけでマウスを押すと方向転換するようにします。
function userMove(frameTime, mesh, agent) { // マウス位置で方向転換 canvasWidth = renderer.domElement.offsetWidth; canvasHeight = renderer.domElement.offsetHeight; dest_angleRight = Math.atan2(mousex, canvasWidth); dest_angleUp = Math.atan2(mousey, canvasHeight); // 水平方向(マウスを押しているとき) if( dest_angleRight && mouseDrag>0){ player.rotationRight += frameTime * dest_angleRight *5; } // 上下(マウスを押しているとき) if( Math.abs(dest_angleUp) > THREE.Math.degToRad( 5 ) && Math.abs(dest_angleRight) < Math.abs(dest_angleUp) && mouseDrag>0){ player.rotationUp -= frameTime * dest_angleUp *4; if(player.rotationUp >= THREE.Math.degToRad( 45 )){player.rotationUp= THREE.Math.degToRad( 45 );} if(player.rotationUp <= THREE.Math.degToRad( -60 )){player.rotationUp= THREE.Math.degToRad( -60 );} } // 向きを更新 agent.updateView(); //w (前進) if(key_on[87]>0){ …以下略
サンプル
違和感はありますがやっとそれらしくなってきましたね。
https://iwanaboz.github.io/html/hogehoge6.html
ブラウザ3Dゲームを作る作戦その2: キーボードで物体を動かす
前回は動かないまま終わりましたが、今回は少し動かしてみました。
まずは箱をその場で回転させる
アニメーションですが、前回のコードで
function animate() { requestAnimationFrame( animate ); renderer.render( scene, camera ); } animate();
の window.requestAnimationFrame( animate ); で animate関数が繰り返し呼ばれるようになっています。フレームレートですが環境に応じて自動で変更してくれるらしく(自分の環境では60fpsでした)、遅延でのカクつきを回避してくれるそうです。
なのでfpsをわざわざ設定する必要はない代わりに、1フレームの長さは知っておかないと体感の速さが環境で変わってしまうようですね。そのあたりを踏まえつつ、箱が回転するように以下のように変更してみました。
// cubeの情報を表示 console.log( cube ); // 時計 clock = new THREE.Clock(); function animate() { window.requestAnimationFrame( animate ); // 現在の1フレームの長さを取得 var frameTime = clock.getDelta(); // 毎秒 20度ずつ回転させる cube.rotation.x += frameTime * THREE.Math.degToRad( 20 ); cube.rotation.y += frameTime * THREE.Math.degToRad( 20 ); // シーンを描画 renderer.render( scene, camera ); } animate();
ちなみに console.log( cube ); と書いておくと、console の画面から cubeオブジェクトの細かい説明が出てくるようです(▼ボタンを押して展開)。haってなんだろう…
なので今回はこの中の rotation.x とか rotation.y の値を少しずつ変更して回転しているように見せた感じだと思います。
次にキーボードで回転させる
まずはキー入力を認識させてみる
キーを押したらキーコードを表示してくれる関数は、今回は以下のようなものを使いました。変な場合分けがあるのはブラウザの種類による違いを吸収するためのようです。
function keyCallBack_on(e) { if ( e && e.keyCode ) { keycode = e.keyCode; } else if ( event && event.keyCode ) { keycode = event.keyCode; } console.log( keycode ); } //押したらkeyCallBack_on関数が呼ばれる document.onkeydown = keyCallBack_on;
animation のループの中で、キーボードのWASDが押されている状態なら動く(押されていないなら動かない)感じにしようかと思ったので、とりあえず全部のキーの on off 状態を知っておくために次のような感じにしました。
// w:87, a:65, s:83, d:68 //押されている:1, 押されてない:0 //すべて 0で初期化 key_on = new Array(256); key_on.fill(0); function keyCallBack_on(e) { if ( e && e.keyCode ) { keycode = e.keyCode; } else if ( event && event.keyCode ) { keycode = event.keyCode; } key_on[keycode] = 1; console.log( keycode ); } function keyCallBack_off(e) { if ( e && e.keyCode ) { keycode = e.keyCode; } else if ( event && event.keyCode ) { keycode = event.keyCode; } key_on[keycode] = 0; } //押したらkeyCallBack_on関数が呼ばれる document.onkeydown = keyCallBack_on; //離したらkeyCallBack_off関数が呼ばれる document.onkeyup = keyCallBack_off;
押してみたら w:87, a:65, s:83, d:68 と返ってきたのでこれらの状態を見ればよい感じですね。あと一息です。
このあたりでちょっと長くなってきたので上のキーボードイベントのコードは別のファイルに書いて読み込むようにしました。
- まずは別ファイルに書きたい javascriptコードをそのまま真っ白なメモ帳に書いて名前を適当な名前.js にして保存
- それから本体のhtmlファイルの中でスクリプトタグで追加するだけで、その場所に javascript コードとして書いたのと同じように動きました。
結構適当に変数を作っていますが、読み込まれる順番など気を付けないといけないようです。
... <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script> <script src="keyCallBack.js"></script> <script> ...
- あとは animation のループでキーが押されているかチェックして動かせばよいので、
function userMove(frameTime) { //w if(key_on[87]>0){cube.position.z -= frameTime * 5;} //a if(key_on[65]>0){cube.position.x -= frameTime * 5;} //s if(key_on[83]>0){cube.position.z += frameTime * 5;} //d if(key_on[68]>0){cube.position.x += frameTime * 5;} }
こんな感じの関数を作って animate() の中で呼ぶようにしました。
サンプル
https://iwanaboz.github.io/html/hogehoge3.html
画面の白い部分を右クリックしてページのソースを表示またはデベロッパーツールの Sources の欄からコードを見れると思います。
ゼロからブラウザ3Dゲームを作る作戦その1
知識がないので手探りですが、効率は無視してとにかく調べながら始めてみます。たくさん間違いがあると思いますがご了承ください。
ブラウザで何か文字を表示
- まずメモ帳を開いて、
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>windowの名前</title> </head> <body> <p>かやちゃ</p> <script> console.log("ドッ!"); </script> </body> </html>
と書いて適当な名前.html で保存します。
- 保存出来たらダブルクリックで開くと、
文字が表示できました。console.log の部分は chrome の場合その他のツール→デベロッパーツールのconsole欄から見られるようです。何かいろいろバグったらここが真っ赤になります。<>みたいなタグで囲む書き方は HTML(HyperText Markup Language)という言語形式で、文字とか絵とかをブラウザに表示しているようです。
javascript と three.js ライブラリ
javascript はHTMLタグ<>で書かれた内容をいじくりまわして、表示するものを変えたりできる言語だそうです。scriptタグで囲まれた中はすべてjavascript で、上の例だと console.log("ドッ!"); がjavascript ということですね。
続いて早速3Dの何かを表示したいのですが、調べてみると three.js というjavascript コードを読み込んでおくと、特別何かをダウンロードせずとも楽に書けるようになるようです。
threejs.org
- 公式サイトのサンプルコードほぼそのままですが、
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>windowの名前</title> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <p>かやちゃ</p> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script> <script> // Our Javascript will go here. console.log("ドッ!"); </script> </body> </html>
これでhttps://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.jsから three.js を読み込めました。
まだ見た目はまだ変わりません。
- ここからdocumentation とにらめっこしながらまずシーンを用意して、光源とカメラを追加します。scriptタグ内に
//シーンを用意 var scene = new THREE.Scene(); //光源を用意してシーンに追加 const ambient = new THREE.AmbientLight(0xeeeeee); scene.add(ambient); //カメラを用意 (画角、縦横比、写す手前と後ろの限界) var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); //カメラの場所(右手座標系) camera.position.x = 2; camera.position.y = -2; camera.position.z = 5;
xが右、yが上、zが手前方向みたいですね。
- ブラウザの画面にカメラで見たシーンを描画してくれるレンダラと呼ばれる何かを用意
//描画するシステムを用意 var renderer = new THREE.WebGLRenderer(); //描画サイズをwindowサイズに renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement );
- 次に表示するもの(今回は箱)を用意
// 箱の形を用意 var geometry = new THREE.BoxGeometry(); // 表面の素材を用意 var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); // 形と素材を指定した cubeをつくってシーンに追加 var cube = new THREE.Mesh( geometry, material ); scene.add( cube );
- ここまでこしらえてから、レンダラさんにカメラで撮ったシーンを画面に描画してもらいます。
function animate() { requestAnimationFrame( animate ); renderer.render( scene, camera ); } animate();
実行する
ここまでかけたらダブルクリックして早速開いてみると、それっぽいものが出ていますね。
サンプル
https://iwanaboz.github.io/html/hogehoge1.html
全体の内容は以下の通りです。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>windowの名前</title> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <p>かやちゃ</p> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script> <script> // Our Javascript will go here. console.log("ドッ!"); //シーンを用意 var scene = new THREE.Scene(); //光源を用意してシーンに追加 const ambient = new THREE.AmbientLight(0xeeeeee); scene.add(ambient); //カメラの場所 (画角、縦横比、写す手前と後ろの限界) var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); //カメラの場所(右手座標系) camera.position.x = 2; camera.position.y = -2; camera.position.z = 5; //描画するシステムを用意 var renderer = new THREE.WebGLRenderer(); //描画サイズをwindowサイズに renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); // 箱の形を用意 var geometry = new THREE.BoxGeometry(); // 表面の素材を用意 var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); // 形と素材を指定した cubeをつくってシーンに追加 var cube = new THREE.Mesh( geometry, material ); scene.add( cube ); function animate() { requestAnimationFrame( animate ); renderer.render( scene, camera ); } animate(); </script> </body> </html>
今回はここまでにしようと思います。