dimanche 12 juin 2016

three.js improve collision speed

I'm using raycasting to handle the collisions between mouse position and grid cells. However, it's a little bit slow and inaccurate at times, so I would like to know:

  • Can I somehow offload the raycasting / collision handling to a webworker?
  • Sometimes the raycaster doesn't return anything even though the mouse pointer is right over a cell. Is my mouse position caculated correctly? (see below example)
  • Am I handling the collisions correctly in my example below? Is there any room to improve the speed?

This is the main function that handles the grid cell collisions:

( It only checks cells that are within the AOS (area of sight) circle )

function checkCollisions(){
    raycaster.setFromCamera( mouse, camera );
    intersects = raycaster.intersectObjects( n_grid.cells_in_AOS );
    if ( intersects.length > 0 ) {
        if ( intersected !=  intersects[ 0 ].object ) {
            if ( intersected ) {
                if( intersects[ 0 ].object.attr.id != prev_cell_id ){ //// make sure it's not the same cell ///
                    intersected.mouseOut();
                    prev_cell_id = intersects[ 0 ].object.attr.id;
                }
            }
            intersected = intersects[ 0 ].object;
            intersects[ 0 ].object.mouseOver();;
        }
    }
}

var renderer, camera, scene, controls, n_grid;

var intersects = null;
var intersected = null;
var raycaster = new THREE.Raycaster();
var prev_cell_id = -1;
var mouse = new THREE.Vector2();

///////// [ Square ] ////////////
var Square = function(options){
	if(typeof options != "undefined"){
		this.attr = {
			coords: options.coords || [],
			size : options.size,
			margin: options.margin || 0,
			pos: options.b_pos,
			opacity: options.opacity || 1,
			id: typeof(options.coords) == "undefined" ? -1 : options.coords[0]+""+options.coords[1],
		};
		
		var cell_size =  (this.attr.size + ( this.attr.margin * 2 ));
		
		this.colors = {
			selected : 0x40ff00,
			hover: 0x00ff00,
			path: 0x3399ff,
			ship: {
				selected: 0xff8000,
				hover: 0xff5c33
			},
			base : parseInt(options.color)
		};
		
		this.states = {
			selected : false,
			hovered: false,
			isPath: false,
			isPlayer: false
		};
		
		var rectGeom = this.generate_lines();
		var material = new THREE.LineBasicMaterial({ side: THREE.DoubleSide, color: options.color ,  transparent: true, opacity: this.attr.opacity }); ///// color not being changed!!!???/   ///
		
		THREE.Line.call( this,  rectGeom,  material );
		
		this.scale.set( 1, 1, 1 );
		if( this.attr.coords.length == 0 ){
			this.position.set( this.attr.pos.x  , this.attr.pos.y, this.attr.pos.z );
		}else{
			this.position.set( this.attr.pos.x + (this.attr.coords[0] * cell_size) + this.attr.margin + (cell_size/2), this.attr.pos.y, this.attr.pos.z + ( this.attr.coords[1] * cell_size) + this.attr.margin + (cell_size/2) );
		}
		
		
		this.rotation.set( ( Math.PI / 2 ) , 0 , 0 ); 
		
	}else{
		console.error("[Square] no parameters defined. ]");
	}
}

Square.prototype =  Object.create( THREE.Line.prototype );
Square.prototype.constructor = Square;

///// State handlers ////
Square.prototype.toggleState = function(state){
	this.states[ state ] = !this.states[ state ];
};

Square.prototype.checkState = function( state ){
	return this.states[ state ];
};
/////// Mouse events ////
Square.prototype.mouseOver = function(){
	if( ! this.checkState( "hovered" ) ){
		if( ! this.checkState( "selected" ) ){
			this.toggleState( "hovered" );
			this.change_color( this.colors.hover, 0.8 );
		}
	}
};
Square.prototype.mouseOut = function(){
	if( this.checkState( "hovered" ) ){
		if( ! this.checkState( "selected" ) ){
			this.change_color( this.colors.base );
			this.toggleState( "hovered" );
		}
	}
};
Square.prototype.select = function(){
	if( ! this.checkState( "selected" ) ){
		this.toggleState( "selected" );
		this.change_color( this.colors.selected , 1 );
	}
};
Square.prototype.unselect = function(){
	if( this.checkState( "selected" ) ){
		this.toggleState( "selected" );
		this.change_color( this.colors.base );
	}
};
/////// Mouse events ////

Square.prototype.change_color = function(color, opacity){
		this.material.color.setHex( color );
		this.material.opacity = opacity || this.attr.opacity;
};

Square.prototype.generate_lines = function(){
	var rectShape = new THREE.Shape( );
	var half_size = (this.attr.size/2);
	rectShape.autoClose = true;
	rectShape.moveTo( -half_size,-half_size );
	rectShape.lineTo( half_size,-half_size );
	rectShape.lineTo( half_size, half_size );
	rectShape.lineTo( -half_size, half_size );
	return rectShape.createPointsGeometry();
};
///////// [ Square ] ////////////


/////// [ Circle ] ////////////////
var Circle = function(options){
	if(typeof options != "undefined"){
		//console.log("[Circle] Initializing");
		this.attr = {
			segments: options.segments,
			radius : options.radius,
			color: options.color,
			pos: options.pos
		}
		
		var segments = this.generate_segments();
		var material = new THREE.LineBasicMaterial({ color:  options.color , transparent: true, opacity: options.opacity});   
		THREE.Line.call( this , segments , material);
			
		this.scale.set( 1, 1, 1 );
		this.position.set( options.pos.x, options.pos.y, options.pos.z ); //// x,y,z //// 
		this.rotation.set( ( Math.PI / 2 ) , 0 , 0 ); 
	}else{
		console.error("[Circle] no parameters defined. ]");
	}
};

Circle.prototype =  Object.create( THREE.Line.prototype );
Circle.prototype.constructor = Circle;

Circle.prototype.generate_segments = function(){
	var geometry = new THREE.Geometry();
	for(var s= 0; s <= this.attr.segments; s++) {
		var theta =  (s / this.attr.segments) * Math.PI * 2;
		geometry.vertices.push( new THREE.Vector3( Math.cos( theta ) * this.attr.radius, Math.sin( theta ) * this.attr.radius, 0 ) );         
	}
	return geometry;
};
/////// [ Circle ] ////////////////


///////////// [ Grid ] ////////////////
var Grid = function( options ){
	if(typeof options !== "undefined"){
		//console.log("[Grid] Initializing");
		this.cell_settings = options.cell;
		this.grid_size = options.size;
		
		THREE.Object3D.call( this );
		
		this.pos = {
			x: options.pos.x - this.cell_settings.margin,
			y: options.pos.y,
			z: options.pos.z - this.cell_settings.margin
		};
		
		var sq_margin = (this.cell_settings.margin * 2);
		this.sq_size = this.cell_settings.size + sq_margin;
		this.size = { 
			w: this.grid_size[0] * this.sq_size,
			h: this.grid_size[1] * this.sq_size
		};
		//console.log( this.grid_size )

		this.cells = new THREE.Object3D();
		
		this.cells_in_AOS = []; //// cells in Area of Sight ////
		
		this.galaxyCircle = {
			pos: {
				x: this.pos.x + (this.size.w / 2),
				y: this.pos.y,
				z: this.pos.z + (this.size.w / 2),
			},
			r: (this.size.w / 2),
		};
		
		this.AOS_circle = { //// Area of Sight ////
			pos: {
				x: this.galaxyCircle.pos.x,
				y: this.galaxyCircle.pos.y,
				z: this.galaxyCircle.pos.z 
			},
			r: options.AOS.r, //// radius ////
			color: 0x33cccc
		};
		
		this.generate();
	}else{
		console.error("[Grid] no parameters defined. ]");
	}
};

Grid.prototype = Object.create( THREE.Object3D.prototype );
Grid.prototype.constructor = Grid;

Grid.prototype.generate = function(){
	//////////////// Galaxy center point //////
	this.add_point( this.AOS_circle.pos, 0x033d6ff );
	////// AOS circle border ////
	this.add_circle_border( this.AOS_circle );

	////// fill the grid with square cells ////////
	for ( var row = 0; row < this.grid_size[0]; row++){
		for ( var col = 0; col < this.grid_size[1]; col++){
			this.add_cell2grid( row, col );
		}
	}

	this.position.set( this.pos.x , this.pos.y, this.pos.z );
	///// center the grid /////
	this.cells.position.set( this.pos.x - ((this.grid_size[0] * this.sq_size) / 2), this.pos.y, this.pos.z - ((this.grid_size[1]* this.sq_size) / 2) );
	this.add( this.cells );
	
	//// map borders  /////
	this.add_circle_border({ 
		r: (this.size.w / 2),
		color: 0x00ff00
	});
	this.add_square_border( this.size.w , 0xffff00);
	this.add_point();
	//// map borders  /////
};
	
Grid.prototype.add_point = function( pos, color ){
	var geometry =new THREE.BoxGeometry( 0.5, 5.5, 0.5 )
	if( typeof pos === "object" ){ 
		geometry = new THREE.BoxGeometry( 0.5, 10.5, 0.5 );
	}
	var material = new THREE.MeshBasicMaterial( {color: color || 0xcc33ff} );
	var cube = new THREE.Mesh( geometry, material );
	if( typeof pos !== "object" ){ 
		cube.position.set( 0,0,0 );
	}else{
		cube.position.set( pos.x, pos.y, pos.z );
	}
	this.add(cube)
};


Grid.prototype.add_cell2grid = function(row, col){
	var cell = new Square({
		coords: [row, col],
		size: this.cell_settings.size,
		margin: this.cell_settings.margin,
		opacity: this.cell_settings.opacity,
		color: 0xFFFFFF,
		b_pos: {
			x: this.pos.x ,
			y: this.pos.y,
			z: this.pos.z
		} //// base position ///
	});
	
	
	if( this.isColidingWith( this.galaxyCircle, cell ) ){ 
		if( ! this.is_in_AOS( cell ) ){
			cell.visible = false;
		}else{
			this.cells_in_AOS.push(cell );
		}
		
		if( this instanceof THREE.Object3D ){
			this.cells.add(cell);
		}else{
			console.error("[Grid] is not THREE.Object3D type. ");
		}
	}
};

Grid.prototype.is_in_AOS = function( cell ){ //// Circle colision ////
	return this.isColidingWith( this.AOS_circle, cell )
};
	
Grid.prototype.isColidingWith = function( cA, cell ){ //// Circle colision ////
	var d_x = cA.pos.x - cell.position.x;
	var d_y = cA.pos.z - cell.position.z;
	var dist = Math.sqrt( d_x * d_x + d_y * d_y );
	
	if( dist < ( cA.r - (( cell.attr.size / 2) + (cell.attr.margin * 2))  ) ){
		return true;
	}
	return false;
};

Grid.prototype.getCellAt = function( p ){
	this.wColiisions.postMessage( [ p.x , p.y ] );
};

Grid.prototype.add_circle_border = function( options ){
	var circle = new Circle({
		segments: 50,
		radius: options.r,
		color: options.color,
		opacity: 0.5,
		pos: {
			x: this.pos.x,
			y: 0.5,
			z: this.pos.z
		}
	});
	
	this.add( circle );
};

	
Grid.prototype.add_square_border = function( square_size, color ){
	var square = new Square({
		size: square_size,
		color: color,
		b_pos: { //// base position ///
			x: this.pos.x,
			y: this.pos.y,
			z: this.pos.z
		} 
	});

	this.add(square);
};

///////////// [ Grid ] ////////////////


////// Initializers ////////////////////////
function initEvents(){
	//console.log("- Events");
	//renderer.domElement.addEventListener( 'mousedown', onMouseDown, false );
	renderer.domElement.addEventListener( 'mousemove', onMouseMove, false );
}

function initRenderer(){
	//console.log("- Renderer");
	renderer = new THREE.WebGLRenderer({antialias:true});
	renderer.setSize( window.innerWidth, window.innerHeight);
	document.body.appendChild(renderer.domElement);
	renderer.setClearColor(0x264d73, 1);
}

function initScene(){
	//console.log("- Scene")
	scene = new THREE.Scene();
}

function initCamera(){
	//console.log("- Camera");
	camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 1, 10000);
	camera.position.set(0, 40, 40);
	camera.lookAt(scene.position);
	scene.add(camera);
	
	//controls = new THREE.OrbitControls( camera , renderer.domElement );
}

function initLights(){
	//console.log("- Lights");
	var aLight = new THREE.AmbientLight(0xD0D0D0, 0.5);
	scene.add(aLight);
}

function initGrid(){
	n_grid = new Grid({
		size: [20, 20],
		pos: { x: 0, y: 0, z: 0 },
		cell: {
			size: 7,
			margin: 0.1,
			opacity: 0.4
		},
		AOS: {
			r: 30
		}
	});
	
	scene.add( n_grid );
}

////// Initializers ////////////////////////


///// Mouse events ////////
function onMouseMove(e){
	e.preventDefault();
	mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1;
	mouse.y = -( e.clientY / window.innerHeight ) * 2 + 1;
}

function checkCollisions(){
	raycaster.setFromCamera( mouse, camera );
	intersects = raycaster.intersectObjects( n_grid.cells_in_AOS );
	if ( intersects.length > 0 ) {
		if ( intersected !=  intersects[ 0 ].object ) {
			if ( intersected ) {
				if( intersects[ 0 ].object.attr.id != prev_cell_id ){ //// make sure it's not the same cell ///
					intersected.mouseOut();
					prev_cell_id = intersects[ 0 ].object.attr.id;
				}
			}
			intersected = intersects[ 0 ].object;
			intersects[ 0 ].object.mouseOver();;
		}
	}
}

function debounce( func, wait, immediate ) {
    var timeout;
    return function(){
        var context = this, args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        }, wait);
        if (immediate && !timeout) func.apply(context, args);
    };
}

///// Mouse events ////////

///// Main /////////
function main(){
	//console.log(" Initializing: ");
	initRenderer(window.innerWidth, window.innerHeight );
	initScene();
	initCamera(window.innerWidth, window.innerHeight );
	initLights();
	initGrid();
	initEvents();
	animate();
}

function animate(){
	window.requestAnimationFrame( animate );
	checkCollisions();
	render_all();
}

function render_all(){
	//controls.update();
	renderer.render(scene, camera);
}

main();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r77/three.min.js"></script>
<header>
	<style>
		body canvas{
			width: 100%,
			height: 100%;
			margin:0;
			padding:0;
		}
	</style>
</header>

<body>
</body>

Aucun commentaire:

Enregistrer un commentaire