式姫で遊ぶ日記

ブラウザ3Dゲームを作る作戦その5: フィールドマップとの衝突判定

今回はフィールドマップを追加して衝突判定をしたいと思います。
フィールドはここからお借りしました。
https://www.turbosquid.com/ja/3d-model/free/city

今回から斉天大聖ちゃんになります。みどらさんありがとうございます。
斉天大聖配布開始しました / みどら さんのイラスト - ニコニコ静画 (イラスト)

f:id:iwanabot:20200503193355p:plain:w480

衝突判定

当たり判定は単純に中心同士の距離などで良い気がしますが、3Dゲームなどででこぼこしたフィールドに沿って歩いたりしてるのをやってみたかったので調べていました。
おそらくですがよく使われるのがRaycaster なるもので、長い線を伸ばして交差するものとぶつかる距離をすべて見つけてくれるようです。(公式リファレンス:https://threejs.org/docs/#api/en/core/Raycaster

衝突判定の概要

 衝突検出できたとしてどう使うかですが、今回はWASDとジャンプ操作なので、天井と床、あと自分に対して前後左右の壁にぶつかるように考えたいと思います。処理はそこまで重そうではなかったので、以下のようにしてみました。
追記:嘘です重いです

  • 頭、足から自分に対して縦(Z)横(X)方向の2本と上下(Y)1本の計5本

のRaycater を用意します。各Raycaterで探すものとしては、

  • Y方向:頭側で最も低いもの(天井)、足側で最も高いもの(床)
  • X,Z方向:最も距離が近いもの(壁)

といった具合です。それらが見つかったとして、処理としては

  1. 足もとに踏み越えられない壁がある場合、近ければ押し戻す
  2. 頭付近に壁がある場合、近ければ押し戻す
  3. 天井が頭より下の場合押し戻す
  4. 床が足より上の場合押し戻す

これだけにします。踏み越えられるかについては、越えられる高さを決めて足側の縦横のRaycater はその高さで伸ばせばよいと思います。
f:id:iwanabot:20200504103835p:plain:w480

コードについて

下準備として、気を付けないといけないのは各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);
	}
}