API Docs for:
Show:

File: ../src/octree.js

/**
* @namespace GL
*/

/**
*   Octree generator for fast ray triangle collision with meshes
*	Dependencies: glmatrix.js (for vector and matrix operations)
* @class Octree
* @constructor
* @param {Mesh} mesh object containing vertices buffer (indices buffer optional)
*/

global.Octree = GL.Octree = function Octree( mesh )
{
	this.root = null;
	this.total_depth = 0;
	this.total_nodes = 0;
	if(mesh)
	{
		this.buildFromMesh(mesh);
		this.total_nodes = this.trim();
	}
}

Octree.MAX_NODE_TRIANGLES_RATIO = 0.1;
Octree.MAX_OCTREE_DEPTH = 8;
Octree.OCTREE_MARGIN_RATIO = 0.01;
Octree.OCTREE_MIN_MARGIN = 0.1;

var octree_tested_boxes = 0;
var octree_tested_triangles = 0;

Octree.prototype.buildFromMesh = function(mesh)
{
	this.total_depth = 0;
	this.total_nodes = 0;

	var vertices = mesh.getBuffer("vertices").data;
	var triangles = mesh.getIndexBuffer("triangles");
	if(triangles) 
		triangles = triangles.data; //get the internal data

	var root = this.computeAABB(vertices);
	this.root = root;
	this.total_nodes = 1;
	this.total_triangles = triangles ? triangles.length / 3 : vertices.length / 9;
	this.max_node_triangles = this.total_triangles * Octree.MAX_NODE_TRIANGLES_RATIO;

	var margin = vec3.create();
	vec3.scale( margin, root.size, Octree.OCTREE_MARGIN_RATIO );
	if(margin[0] < Octree.OCTREE_MIN_MARGIN) margin[0] = Octree.OCTREE_MIN_MARGIN;
	if(margin[1] < Octree.OCTREE_MIN_MARGIN) margin[1] = Octree.OCTREE_MIN_MARGIN;
	if(margin[2] < Octree.OCTREE_MIN_MARGIN) margin[2] = Octree.OCTREE_MIN_MARGIN;

	vec3.sub(root.min, root.min, margin);
	vec3.add(root.max, root.max, margin);

	root.faces = [];
	root.inside = 0;


	//indexed
	if(triangles)
	{
		for(var i = 0; i < triangles.length; i+=3)
		{
			var face = new Float32Array([vertices[triangles[i]*3], vertices[triangles[i]*3+1],vertices[triangles[i]*3+2],
						vertices[triangles[i+1]*3], vertices[triangles[i+1]*3+1],vertices[triangles[i+1]*3+2],
						vertices[triangles[i+2]*3], vertices[triangles[i+2]*3+1],vertices[triangles[i+2]*3+2]]);
			this.addToNode(face,root,0);
		}
	}
	else
	{
		for(var i = 0; i < vertices.length; i+=9)
		{
			var face = new Float32Array( vertices.subarray(i,i+9) );
			this.addToNode(face,root,0);
		}
	}

	return root;
}

Octree.prototype.addToNode = function(face,node, depth)
{
	node.inside += 1;

	//has children
	if(node.c)
	{
		var aabb = this.computeAABB(face);
		var added = false;
		for(var i in node.c)
		{
			var child = node.c[i];
			if (Octree.isInsideAABB(aabb,child))
			{
				this.addToNode(face,child, depth+1);
				added = true;
				break;
			}
		}
		if(!added)
		{
			if(node.faces == null)
				node.faces = [];
			node.faces.push(face);
		}
	}
	else //add till full, then split
	{
		if(node.faces == null) node.faces = [];
		node.faces.push(face);

		//split
		if(node.faces.length > this.max_node_triangles && depth < Octree.MAX_OCTREE_DEPTH)
		{
			this.splitNode(node);
			if(this.total_depth < depth + 1)
				this.total_depth = depth + 1;

			var faces = node.faces.concat();
			node.faces = null;

			//redistribute all nodes
			for(var i in faces)
			{
				var face = faces[i];
				var aabb = this.computeAABB(face);
				var added = false;
				for(var j in node.c)
				{
					var child = node.c[j];
					if (Octree.isInsideAABB(aabb,child))
					{
						this.addToNode(face,child, depth+1);
						added = true;
						break;
					}
				}
				if (!added)
				{
					if(node.faces == null)
						node.faces = [];
					node.faces.push(face);
				}
			}
		}
	}
};

Octree.prototype.octree_pos_ref = [[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]];

Octree.prototype.splitNode = function(node)
{
	node.c = [];
	var half = [(node.max[0] - node.min[0]) * 0.5, (node.max[1] - node.min[1]) * 0.5, (node.max[2] - node.min[2]) * 0.5];

	for(var i in this.octree_pos_ref)
	{
		var ref = this.octree_pos_ref[i];

		var newnode = {};
		this.total_nodes += 1;

		newnode.min = [ node.min[0] + half[0] * ref[0],  node.min[1] + half[1] * ref[1],  node.min[2] + half[2] * ref[2]];
		newnode.max = [newnode.min[0] + half[0], newnode.min[1] + half[1], newnode.min[2] + half[2]];
		newnode.faces = null;
		newnode.inside = 0;
		node.c.push(newnode);
	}
}

Octree.prototype.computeAABB = function(vertices)
{
	var min = new Float32Array([ vertices[0], vertices[1], vertices[2] ]);
	var max = new Float32Array([ vertices[0], vertices[1], vertices[2] ]);

	for(var i = 0; i < vertices.length; i+=3)
	{
		for(var j = 0; j < 3; j++)
		{
			if(min[j] > vertices[i+j]) 
				min[j] = vertices[i+j];
			if(max[j] < vertices[i+j]) 
				max[j] = vertices[i+j];
		}
	}

	return {min: min, max: max, size: vec3.sub( vec3.create(), max, min) };
}

//remove empty nodes
Octree.prototype.trim = function(node)
{
	node = node || this.root;
	if(!node.c)
		return 1;

	var num = 1;
	var valid = [];
	var c = node.c;
	for(var i = 0; i < c.length; ++i)
	{
		if(c[i].inside)
		{
			valid.push(c[i]);
			num += this.trim(c[i]);
		}
	}
	node.c = valid;
	return num;
}

/**
* Test collision between ray and triangles in the octree
* @method testRay
* @param {vec3} origin ray origin position
* @param {vec3} direction ray direction position
* @param {number} dist_min
* @param {number} dist_max
* @return {HitTest} object containing pos and normal
*/
Octree.prototype.testRay = (function(){ 
	var origin_temp = vec3.create();
	var direction_temp = vec3.create();
	var min_temp = vec3.create();
	var max_temp = vec3.create();

	return function(origin, direction, dist_min, dist_max, test_backfaces )
	{
		octree_tested_boxes = 0;
		octree_tested_triangles = 0;

		if(!this.root)
		{
			throw("Error: octree not build");
		}

		origin_temp.set( origin );
		direction_temp.set( direction );
		min_temp.set( this.root.min );
		max_temp.set( this.root.max );

		var test = Octree.hitTestBox( origin_temp, direction_temp, min_temp, max_temp );
		if(!test) //no collision with mesh bounding box
			return null;

		var test = Octree.testRayInNode( this.root, origin_temp, direction_temp, test_backfaces );
		if(test != null)
		{
			var pos = vec3.scale( vec3.create(), direction, test.t );
			vec3.add( pos, pos, origin );
			test.pos = pos;
			return test;
		}

		return null;
	}
})();

/**
* test collision between sphere and the triangles in the octree (only test if there is any vertex inside the sphere)
* @method testSphere
* @param {vec3} origin sphere center
* @param {number} radius
* @return {Boolean} true if the sphere collided with the mesh
*/
Octree.prototype.testSphere = function( origin, radius )
{
	origin = vec3.clone(origin);
	octree_tested_boxes = 0;
	octree_tested_triangles = 0;

	if(!this.root)
		throw("Error: octree not build");

	//better to use always the radius squared, because all the calculations are going to do that
	var rr = radius * radius;

	if( !Octree.testSphereBox( origin, rr, vec3.clone(this.root.min), vec3.clone(this.root.max) ) )
		return false; //out of the box

	return Octree.testSphereInNode( this.root, origin, rr );
}

//WARNING: cannot use static here, it uses recursion
Octree.testRayInNode = function( node, origin, direction, test_backfaces )
{
	var test = null;
	var prev_test = null;
	octree_tested_boxes += 1;

	//test faces
	if(node.faces)
		for(var i = 0, l = node.faces.length; i < l; ++i)
		{
			var face = node.faces[i];
			octree_tested_triangles += 1;
			test = Octree.hitTestTriangle( origin, direction, face.subarray(0,3) , face.subarray(3,6), face.subarray(6,9), test_backfaces );
			if (test==null)
				continue;
			test.face = face;
			if(prev_test)
				prev_test.mergeWith( test );
			else
				prev_test = test;
		}

	//WARNING: cannot use statics here, this function uses recursion
	var child_min = vec3.create();
	var child_max = vec3.create();

	//test children nodes faces
	var child;
	if(node.c)
		for(var i = 0; i < node.c.length; ++i)
		{
			child = node.c[i];
			child_min.set( child.min );
			child_max.set( child.max );

			//test with node box
			test = Octree.hitTestBox( origin, direction, child_min, child_max );
			if( test == null )
				continue;

			//nodebox behind current collision, then ignore node
			if(prev_test && test.t > prev_test.t)
				continue;

			//test collision with node
			test = Octree.testRayInNode( child, origin, direction, test_backfaces );
			if(test == null)
				continue;

			if(prev_test)
				prev_test.mergeWith( test );
			else
				prev_test = test;
		}

	return prev_test;
}

//WARNING: cannot use static here, it uses recursion
Octree.testSphereInNode = function( node, origin, radius2 )
{
	var test = null;
	var prev_test = null;
	octree_tested_boxes += 1;

	//test faces
	if(node.faces)
		for(var i = 0, l = node.faces.length; i < l; ++i)
		{
			var face = node.faces[i];
			octree_tested_triangles += 1;
			if( Octree.testSphereTriangle( origin, radius2, face.subarray(0,3) , face.subarray(3,6), face.subarray(6,9) ) )
				return true;
		}

	//WARNING: cannot use statics here, this function uses recursion
	var child_min = vec3.create();
	var child_max = vec3.create();

	//test children nodes faces
	var child;
	if(node.c)
		for(var i = 0; i < node.c.length; ++i)
		{
			child = node.c[i];
			child_min.set( child.min );
			child_max.set( child.max );

			//test with node box
			if( !Octree.testSphereBox( origin, radius2, child_min, child_max ) )
				continue;

			//test collision with node content
			if( Octree.testSphereInNode( child, origin, radius2 ) )
				return true;
		}

	return false;
}

//test if one bounding is inside or overlapping another bounding
Octree.isInsideAABB = function(a,b)
{
	if(a.min[0] < b.min[0] || a.min[1] < b.min[1] || a.min[2] < b.min[2] ||
		a.max[0] > b.max[0] || a.max[1] > b.max[1] || a.max[2] > b.max[2])
		return false;
	return true;
}


Octree.hitTestBox = (function(){ 
	var tMin = vec3.create();
	var tMax = vec3.create();
	var inv = vec3.create();
	var t1 = vec3.create();
	var t2 = vec3.create();
	var tmp = vec3.create();
	var epsilon = 1.0e-6;
	var eps = vec3.fromValues( epsilon,epsilon,epsilon );
	
	return function( origin, ray, box_min, box_max ) {
		vec3.subtract( tMin, box_min, origin );
		vec3.subtract( tMax, box_max, origin );
		
		if(	vec3.maxValue(tMin) < 0 && vec3.minValue(tMax) > 0)
			return new HitTest(0,origin,ray);

		inv[0] = 1/ray[0];	inv[1] = 1/ray[1];	inv[2] = 1/ray[2];
		vec3.multiply(tMin, tMin, inv);
		vec3.multiply(tMax, tMax, inv);
		vec3.min(t1, tMin, tMax);
		vec3.max(t2, tMin, tMax);
		var tNear = vec3.maxValue(t1);
		var tFar = vec3.minValue(t2);

		if (tNear > 0 && tNear < tFar) {
			var hit = vec3.add( vec3.create(), vec3.scale(tmp, ray, tNear ), origin);
			vec3.add( box_min, box_min, eps);
			vec3.subtract(box_min, box_min, eps);
			return new HitTest(tNear, hit, vec3.fromValues(
			  (hit[0] > box_max[0]) - (hit[0] < box_min[0]),
			  (hit[1] > box_max[1]) - (hit[1] < box_min[1]),
			  (hit[2] > box_max[2]) - (hit[2] < box_min[2]) ));
		}

		return null;
	}
})();

Octree.hitTestTriangle = (function(){ 
	
	var AB = vec3.create();
	var AC = vec3.create();
	var toHit = vec3.create();
	var tmp = vec3.create();
	
	return function( origin, ray, A, B, C, test_backfaces ) {
		vec3.subtract( AB, B, A );
		vec3.subtract( AC, C, A );
		var normal = vec3.cross( vec3.create(), AB, AC ); //returned
		vec3.normalize( normal, normal );
		if( !test_backfaces && vec3.dot(normal,ray) > 0)
			return null; //ignore backface

		var t = vec3.dot(normal, vec3.subtract( tmp, A, origin )) / vec3.dot(normal,ray);

	    if (t > 0)
		{
			var hit = vec3.scale(vec3.create(), ray, t); //returned
			vec3.add(hit, hit, origin);
			vec3.subtract( toHit, hit, A );
			var dot00 = vec3.dot(AC,AC);
			var dot01 = vec3.dot(AC,AB);
			var dot02 = vec3.dot(AC,toHit);
			var dot11 = vec3.dot(AB,AB);
			var dot12 = vec3.dot(AB,toHit);
			var divide = dot00 * dot11 - dot01 * dot01;
			var u = (dot11 * dot02 - dot01 * dot12) / divide;
			var v = (dot00 * dot12 - dot01 * dot02) / divide;
			if (u >= 0 && v >= 0 && u + v <= 1)
				return new HitTest(t, hit, normal);
		}
	    return null;
	};
})();

//from http://realtimecollisiondetection.net/blog/?p=103
//radius must be squared
Octree.testSphereTriangle = (function(){ 
	
	var A = vec3.create();
	var B = vec3.create();
	var C = vec3.create();
	var AB = vec3.create();
	var AC = vec3.create();
	var BC = vec3.create();
	var CA = vec3.create();
	var V = vec3.create();
	
	return function( P, rr, A_, B_, C_ ) {
		vec3.sub( A, A_, P );
		vec3.sub( B, B_, P );
		vec3.sub( C, C_, P );

		vec3.sub( AB, B, A );
		vec3.sub( AC, C, A );

		vec3.cross( V, AB, AC );
		var d = vec3.dot( A, V );
		var e = vec3.dot( V, V );
		var sep1 = d * d > rr * e;
		var aa = vec3.dot(A, A);
		var ab = vec3.dot(A, B);
		var ac = vec3.dot(A, C);
		var bb = vec3.dot(B, B);
		var bc = vec3.dot(B, C);
		var cc = vec3.dot(C, C);
		var sep2 = (aa > rr) & (ab > aa) & (ac > aa);
		var sep3 = (bb > rr) & (ab > bb) & (bc > bb);
		var sep4 = (cc > rr) & (ac > cc) & (bc > cc);

		var d1 = ab - aa;
		var d2 = bc - bb;
		var d3 = ac - cc;

		vec3.sub( BC, C, B );
		vec3.sub( CA, A, C );

		var e1 = vec3.dot(AB, AB);
		var e2 = vec3.dot(BC, BC);
		var e3 = vec3.dot(CA, CA);

		var Q1 = vec3.scale(vec3.create(), A, e1); vec3.sub( Q1, Q1, vec3.scale(vec3.create(), AB, d1) );
		var Q2 = vec3.scale(vec3.create(), B, e2); vec3.sub( Q2, Q2, vec3.scale(vec3.create(), BC, d2) );
		var Q3 = vec3.scale(vec3.create(), C, e3); vec3.sub( Q3, Q3, vec3.scale(vec3.create(), CA, d3) );

		var QC = vec3.scale( vec3.create(), C, e1 ); QC = vec3.sub( QC, QC, Q1 );
		var QA = vec3.scale( vec3.create(), A, e2 ); QA = vec3.sub( QA, QA, Q2 );
		var QB = vec3.scale( vec3.create(), B, e3 ); QB = vec3.sub( QB, QB, Q3 );

		var sep5 = ( vec3.dot(Q1, Q1) > rr * e1 * e1) & (vec3.dot(Q1, QC) > 0 );
		var sep6 = ( vec3.dot(Q2, Q2) > rr * e2 * e2) & (vec3.dot(Q2, QA) > 0 );
		var sep7 = ( vec3.dot(Q3, Q3) > rr * e3 * e3) & (vec3.dot(Q3, QB) > 0 );

		var separated = sep1 | sep2 | sep3 | sep4 | sep5 | sep6 | sep7
		return !separated;
	};
})();

Octree.testSphereBox = function( center, radius2, box_min, box_max ) {

	// arvo's algorithm from gamasutra
	// http://www.gamasutra.com/features/19991018/Gomez_4.htm
	var s, d = 0.0;
	//find the square of the distance
	//from the sphere to the box
	for(var i = 0; i < 3; ++i) 
	{ 
		if( center[i] < box_min[i] )
		{
			s = center[i] - box_min[i];
			d += s*s; 
		}
		else if( center[i] > box_max[i] )
		{ 
			s = center[i] - box_max[i];
			d += s*s; 
		}
	}
	//return d <= r*r

	if (d <= radius2)
	{
		return true;
		/*
		// this is used just to know if it overlaps or is just inside, but I dont care
		// make an aabb aabb test with the sphere aabb to test inside state
		var halfsize = vec3.fromValues( radius, radius, radius );
		var sphere_bbox = BBox.fromCenterHalfsize( center, halfsize );
		if ( geo.testBBoxBBox(bbox, sphere_bbox) )
			return INSIDE;
		return OVERLAP;	
		*/
	}

	return false; //OUTSIDE;
};