UNPKG

cannon

Version:

A lightweight 3D physics engine written in JavaScript.

830 lines (688 loc) 23.8 kB
module.exports = Ray; var Vec3 = require('../math/Vec3'); var Quaternion = require('../math/Quaternion'); var Transform = require('../math/Transform'); var ConvexPolyhedron = require('../shapes/ConvexPolyhedron'); var Box = require('../shapes/Box'); var RaycastResult = require('../collision/RaycastResult'); var Shape = require('../shapes/Shape'); var AABB = require('../collision/AABB'); /** * A line in 3D space that intersects bodies and return points. * @class Ray * @constructor * @param {Vec3} from * @param {Vec3} to */ function Ray(from, to){ /** * @property {Vec3} from */ this.from = from ? from.clone() : new Vec3(); /** * @property {Vec3} to */ this.to = to ? to.clone() : new Vec3(); /** * @private * @property {Vec3} _direction */ this._direction = new Vec3(); /** * The precision of the ray. Used when checking parallelity etc. * @property {Number} precision */ this.precision = 0.0001; /** * Set to true if you want the Ray to take .collisionResponse flags into account on bodies and shapes. * @property {Boolean} checkCollisionResponse */ this.checkCollisionResponse = true; /** * If set to true, the ray skips any hits with normal.dot(rayDirection) < 0. * @property {Boolean} skipBackfaces */ this.skipBackfaces = false; /** * @property {number} collisionFilterMask * @default -1 */ this.collisionFilterMask = -1; /** * @property {number} collisionFilterGroup * @default -1 */ this.collisionFilterGroup = -1; /** * The intersection mode. Should be Ray.ANY, Ray.ALL or Ray.CLOSEST. * @property {number} mode */ this.mode = Ray.ANY; /** * Current result object. * @property {RaycastResult} result */ this.result = new RaycastResult(); /** * Will be set to true during intersectWorld() if the ray hit anything. * @property {Boolean} hasHit */ this.hasHit = false; /** * Current, user-provided result callback. Will be used if mode is Ray.ALL. * @property {Function} callback */ this.callback = function(result){}; } Ray.prototype.constructor = Ray; Ray.CLOSEST = 1; Ray.ANY = 2; Ray.ALL = 4; var tmpAABB = new AABB(); var tmpArray = []; /** * Do itersection against all bodies in the given World. * @method intersectWorld * @param {World} world * @param {object} options * @return {Boolean} True if the ray hit anything, otherwise false. */ Ray.prototype.intersectWorld = function (world, options) { this.mode = options.mode || Ray.ANY; this.result = options.result || new RaycastResult(); this.skipBackfaces = !!options.skipBackfaces; this.collisionFilterMask = typeof(options.collisionFilterMask) !== 'undefined' ? options.collisionFilterMask : -1; this.collisionFilterGroup = typeof(options.collisionFilterGroup) !== 'undefined' ? options.collisionFilterGroup : -1; if(options.from){ this.from.copy(options.from); } if(options.to){ this.to.copy(options.to); } this.callback = options.callback || function(){}; this.hasHit = false; this.result.reset(); this._updateDirection(); this.getAABB(tmpAABB); tmpArray.length = 0; world.broadphase.aabbQuery(world, tmpAABB, tmpArray); this.intersectBodies(tmpArray); return this.hasHit; }; var v1 = new Vec3(), v2 = new Vec3(); /* * As per "Barycentric Technique" as named here http://www.blackpawn.com/texts/pointinpoly/default.html But without the division */ Ray.pointInTriangle = pointInTriangle; function pointInTriangle(p, a, b, c) { c.vsub(a,v0); b.vsub(a,v1); p.vsub(a,v2); var dot00 = v0.dot( v0 ); var dot01 = v0.dot( v1 ); var dot02 = v0.dot( v2 ); var dot11 = v1.dot( v1 ); var dot12 = v1.dot( v2 ); var u,v; return ( (u = dot11 * dot02 - dot01 * dot12) >= 0 ) && ( (v = dot00 * dot12 - dot01 * dot02) >= 0 ) && ( u + v < ( dot00 * dot11 - dot01 * dot01 ) ); } /** * Shoot a ray at a body, get back information about the hit. * @method intersectBody * @private * @param {Body} body * @param {RaycastResult} [result] Deprecated - set the result property of the Ray instead. */ var intersectBody_xi = new Vec3(); var intersectBody_qi = new Quaternion(); Ray.prototype.intersectBody = function (body, result) { if(result){ this.result = result; this._updateDirection(); } var checkCollisionResponse = this.checkCollisionResponse; if(checkCollisionResponse && !body.collisionResponse){ return; } if((this.collisionFilterGroup & body.collisionFilterMask)===0 || (body.collisionFilterGroup & this.collisionFilterMask)===0){ return; } var xi = intersectBody_xi; var qi = intersectBody_qi; for (var i = 0, N = body.shapes.length; i < N; i++) { var shape = body.shapes[i]; if(checkCollisionResponse && !shape.collisionResponse){ continue; // Skip } body.quaternion.mult(body.shapeOrientations[i], qi); body.quaternion.vmult(body.shapeOffsets[i], xi); xi.vadd(body.position, xi); this.intersectShape( shape, qi, xi, body ); if(this.result._shouldStop){ break; } } }; /** * @method intersectBodies * @param {Array} bodies An array of Body objects. * @param {RaycastResult} [result] Deprecated */ Ray.prototype.intersectBodies = function (bodies, result) { if(result){ this.result = result; this._updateDirection(); } for ( var i = 0, l = bodies.length; !this.result._shouldStop && i < l; i ++ ) { this.intersectBody(bodies[i]); } }; /** * Updates the _direction vector. * @private * @method _updateDirection */ Ray.prototype._updateDirection = function(){ this.to.vsub(this.from, this._direction); this._direction.normalize(); }; /** * @method intersectShape * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body */ Ray.prototype.intersectShape = function(shape, quat, position, body){ var from = this.from; // Checking boundingSphere var distance = distanceFromIntersection(from, this._direction, position); if ( distance > shape.boundingSphereRadius ) { return; } var intersectMethod = this[shape.type]; if(intersectMethod){ intersectMethod.call(this, shape, quat, position, body); } }; var vector = new Vec3(); var normal = new Vec3(); var intersectPoint = new Vec3(); var a = new Vec3(); var b = new Vec3(); var c = new Vec3(); var d = new Vec3(); var tmpRaycastResult = new RaycastResult(); /** * @method intersectBox * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body */ Ray.prototype.intersectBox = function(shape, quat, position, body){ return this.intersectConvex(shape.convexPolyhedronRepresentation, quat, position, body); }; Ray.prototype[Shape.types.BOX] = Ray.prototype.intersectBox; /** * @method intersectPlane * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body */ Ray.prototype.intersectPlane = function(shape, quat, position, body){ var from = this.from; var to = this.to; var direction = this._direction; // Get plane normal var worldNormal = new Vec3(0, 0, 1); quat.vmult(worldNormal, worldNormal); var len = new Vec3(); from.vsub(position, len); var planeToFrom = len.dot(worldNormal); to.vsub(position, len); var planeToTo = len.dot(worldNormal); if(planeToFrom * planeToTo > 0){ // "from" and "to" are on the same side of the plane... bail out return; } if(from.distanceTo(to) < planeToFrom){ return; } var n_dot_dir = worldNormal.dot(direction); if (Math.abs(n_dot_dir) < this.precision) { // No intersection return; } var planePointToFrom = new Vec3(); var dir_scaled_with_t = new Vec3(); var hitPointWorld = new Vec3(); from.vsub(position, planePointToFrom); var t = -worldNormal.dot(planePointToFrom) / n_dot_dir; direction.scale(t, dir_scaled_with_t); from.vadd(dir_scaled_with_t, hitPointWorld); this.reportIntersection(worldNormal, hitPointWorld, shape, body, -1); }; Ray.prototype[Shape.types.PLANE] = Ray.prototype.intersectPlane; /** * Get the world AABB of the ray. * @method getAABB * @param {AABB} aabb */ Ray.prototype.getAABB = function(result){ var to = this.to; var from = this.from; result.lowerBound.x = Math.min(to.x, from.x); result.lowerBound.y = Math.min(to.y, from.y); result.lowerBound.z = Math.min(to.z, from.z); result.upperBound.x = Math.max(to.x, from.x); result.upperBound.y = Math.max(to.y, from.y); result.upperBound.z = Math.max(to.z, from.z); }; var intersectConvexOptions = { faceList: [0] }; /** * @method intersectHeightfield * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body */ Ray.prototype.intersectHeightfield = function(shape, quat, position, body){ var data = shape.data, w = shape.elementSize, worldPillarOffset = new Vec3(); // Convert the ray to local heightfield coordinates var localRay = new Ray(this.from, this.to); Transform.pointToLocalFrame(position, quat, localRay.from, localRay.from); Transform.pointToLocalFrame(position, quat, localRay.to, localRay.to); // Get the index of the data points to test against var index = []; var iMinX = null; var iMinY = null; var iMaxX = null; var iMaxY = null; var inside = shape.getIndexOfPosition(localRay.from.x, localRay.from.y, index, false); if(inside){ iMinX = index[0]; iMinY = index[1]; iMaxX = index[0]; iMaxY = index[1]; } inside = shape.getIndexOfPosition(localRay.to.x, localRay.to.y, index, false); if(inside){ if (iMinX === null || index[0] < iMinX) { iMinX = index[0]; } if (iMaxX === null || index[0] > iMaxX) { iMaxX = index[0]; } if (iMinY === null || index[1] < iMinY) { iMinY = index[1]; } if (iMaxY === null || index[1] > iMaxY) { iMaxY = index[1]; } } if(iMinX === null){ return; } var minMax = []; shape.getRectMinMax(iMinX, iMinY, iMaxX, iMaxY, minMax); var min = minMax[0]; var max = minMax[1]; // // Bail out if the ray can't touch the bounding box // // TODO // var aabb = new AABB(); // this.getAABB(aabb); // if(aabb.intersects()){ // return; // } for(var i = iMinX; i <= iMaxX; i++){ for(var j = iMinY; j <= iMaxY; j++){ if(this.result._shouldStop){ return; } // Lower triangle shape.getConvexTrianglePillar(i, j, false); Transform.pointToWorldFrame(position, quat, shape.pillarOffset, worldPillarOffset); this.intersectConvex(shape.pillarConvex, quat, worldPillarOffset, body, intersectConvexOptions); if(this.result._shouldStop){ return; } // Upper triangle shape.getConvexTrianglePillar(i, j, true); Transform.pointToWorldFrame(position, quat, shape.pillarOffset, worldPillarOffset); this.intersectConvex(shape.pillarConvex, quat, worldPillarOffset, body, intersectConvexOptions); } } }; Ray.prototype[Shape.types.HEIGHTFIELD] = Ray.prototype.intersectHeightfield; var Ray_intersectSphere_intersectionPoint = new Vec3(); var Ray_intersectSphere_normal = new Vec3(); /** * @method intersectSphere * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body */ Ray.prototype.intersectSphere = function(shape, quat, position, body){ var from = this.from, to = this.to, r = shape.radius; var a = Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2) + Math.pow(to.z - from.z, 2); var b = 2 * ((to.x - from.x) * (from.x - position.x) + (to.y - from.y) * (from.y - position.y) + (to.z - from.z) * (from.z - position.z)); var c = Math.pow(from.x - position.x, 2) + Math.pow(from.y - position.y, 2) + Math.pow(from.z - position.z, 2) - Math.pow(r, 2); var delta = Math.pow(b, 2) - 4 * a * c; var intersectionPoint = Ray_intersectSphere_intersectionPoint; var normal = Ray_intersectSphere_normal; if(delta < 0){ // No intersection return; } else if(delta === 0){ // single intersection point from.lerp(to, delta, intersectionPoint); intersectionPoint.vsub(position, normal); normal.normalize(); this.reportIntersection(normal, intersectionPoint, shape, body, -1); } else { var d1 = (- b - Math.sqrt(delta)) / (2 * a); var d2 = (- b + Math.sqrt(delta)) / (2 * a); if(d1 >= 0 && d1 <= 1){ from.lerp(to, d1, intersectionPoint); intersectionPoint.vsub(position, normal); normal.normalize(); this.reportIntersection(normal, intersectionPoint, shape, body, -1); } if(this.result._shouldStop){ return; } if(d2 >= 0 && d2 <= 1){ from.lerp(to, d2, intersectionPoint); intersectionPoint.vsub(position, normal); normal.normalize(); this.reportIntersection(normal, intersectionPoint, shape, body, -1); } } }; Ray.prototype[Shape.types.SPHERE] = Ray.prototype.intersectSphere; var intersectConvex_normal = new Vec3(); var intersectConvex_minDistNormal = new Vec3(); var intersectConvex_minDistIntersect = new Vec3(); var intersectConvex_vector = new Vec3(); /** * @method intersectConvex * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body * @param {object} [options] * @param {array} [options.faceList] */ Ray.prototype.intersectConvex = function intersectConvex( shape, quat, position, body, options ){ var minDistNormal = intersectConvex_minDistNormal; var normal = intersectConvex_normal; var vector = intersectConvex_vector; var minDistIntersect = intersectConvex_minDistIntersect; var faceList = (options && options.faceList) || null; // Checking faces var faces = shape.faces, vertices = shape.vertices, normals = shape.faceNormals; var direction = this._direction; var from = this.from; var to = this.to; var fromToDistance = from.distanceTo(to); var minDist = -1; var Nfaces = faceList ? faceList.length : faces.length; var result = this.result; for (var j = 0; !result._shouldStop && j < Nfaces; j++) { var fi = faceList ? faceList[j] : j; var face = faces[fi]; var faceNormal = normals[fi]; var q = quat; var x = position; // determine if ray intersects the plane of the face // note: this works regardless of the direction of the face normal // Get plane point in world coordinates... vector.copy(vertices[face[0]]); q.vmult(vector,vector); vector.vadd(x,vector); // ...but make it relative to the ray from. We'll fix this later. vector.vsub(from,vector); // Get plane normal q.vmult(faceNormal,normal); // If this dot product is negative, we have something interesting var dot = direction.dot(normal); // Bail out if ray and plane are parallel if ( Math.abs( dot ) < this.precision ){ continue; } // calc distance to plane var scalar = normal.dot(vector) / dot; // if negative distance, then plane is behind ray if (scalar < 0){ continue; } // if (dot < 0) { // Intersection point is from + direction * scalar direction.mult(scalar,intersectPoint); intersectPoint.vadd(from,intersectPoint); // a is the point we compare points b and c with. a.copy(vertices[face[0]]); q.vmult(a,a); x.vadd(a,a); for(var i = 1; !result._shouldStop && i < face.length - 1; i++){ // Transform 3 vertices to world coords b.copy(vertices[face[i]]); c.copy(vertices[face[i+1]]); q.vmult(b,b); q.vmult(c,c); x.vadd(b,b); x.vadd(c,c); var distance = intersectPoint.distanceTo(from); if(!(pointInTriangle(intersectPoint, a, b, c) || pointInTriangle(intersectPoint, b, a, c)) || distance > fromToDistance){ continue; } this.reportIntersection(normal, intersectPoint, shape, body, fi); } // } } }; Ray.prototype[Shape.types.CONVEXPOLYHEDRON] = Ray.prototype.intersectConvex; var intersectTrimesh_normal = new Vec3(); var intersectTrimesh_localDirection = new Vec3(); var intersectTrimesh_localFrom = new Vec3(); var intersectTrimesh_localTo = new Vec3(); var intersectTrimesh_worldNormal = new Vec3(); var intersectTrimesh_worldIntersectPoint = new Vec3(); var intersectTrimesh_localAABB = new AABB(); var intersectTrimesh_triangles = []; var intersectTrimesh_treeTransform = new Transform(); /** * @method intersectTrimesh * @private * @param {Shape} shape * @param {Quaternion} quat * @param {Vec3} position * @param {Body} body * @param {object} [options] * @todo Optimize by transforming the world to local space first. * @todo Use Octree lookup */ Ray.prototype.intersectTrimesh = function intersectTrimesh( mesh, quat, position, body, options ){ var normal = intersectTrimesh_normal; var triangles = intersectTrimesh_triangles; var treeTransform = intersectTrimesh_treeTransform; var minDistNormal = intersectConvex_minDistNormal; var vector = intersectConvex_vector; var minDistIntersect = intersectConvex_minDistIntersect; var localAABB = intersectTrimesh_localAABB; var localDirection = intersectTrimesh_localDirection; var localFrom = intersectTrimesh_localFrom; var localTo = intersectTrimesh_localTo; var worldIntersectPoint = intersectTrimesh_worldIntersectPoint; var worldNormal = intersectTrimesh_worldNormal; var faceList = (options && options.faceList) || null; // Checking faces var indices = mesh.indices, vertices = mesh.vertices, normals = mesh.faceNormals; var from = this.from; var to = this.to; var direction = this._direction; var minDist = -1; treeTransform.position.copy(position); treeTransform.quaternion.copy(quat); // Transform ray to local space! Transform.vectorToLocalFrame(position, quat, direction, localDirection); //body.vectorToLocalFrame(direction, localDirection); Transform.pointToLocalFrame(position, quat, from, localFrom); //body.pointToLocalFrame(from, localFrom); Transform.pointToLocalFrame(position, quat, to, localTo); //body.pointToLocalFrame(to, localTo); var fromToDistanceSquared = localFrom.distanceSquared(localTo); mesh.tree.rayQuery(this, treeTransform, triangles); for (var i = 0, N = triangles.length; !this.result._shouldStop && i !== N; i++) { var trianglesIndex = triangles[i]; mesh.getNormal(trianglesIndex, normal); // determine if ray intersects the plane of the face // note: this works regardless of the direction of the face normal // Get plane point in world coordinates... mesh.getVertex(indices[trianglesIndex * 3], a); // ...but make it relative to the ray from. We'll fix this later. a.vsub(localFrom,vector); // Get plane normal // quat.vmult(normal, normal); // If this dot product is negative, we have something interesting var dot = localDirection.dot(normal); // Bail out if ray and plane are parallel // if (Math.abs( dot ) < this.precision){ // continue; // } // calc distance to plane var scalar = normal.dot(vector) / dot; // if negative distance, then plane is behind ray if (scalar < 0){ continue; } // Intersection point is from + direction * scalar localDirection.scale(scalar,intersectPoint); intersectPoint.vadd(localFrom,intersectPoint); // Get triangle vertices mesh.getVertex(indices[trianglesIndex * 3 + 1], b); mesh.getVertex(indices[trianglesIndex * 3 + 2], c); var squaredDistance = intersectPoint.distanceSquared(localFrom); if(!(pointInTriangle(intersectPoint, b, a, c) || pointInTriangle(intersectPoint, a, b, c)) || squaredDistance > fromToDistanceSquared){ continue; } // transform intersectpoint and normal to world Transform.vectorToWorldFrame(quat, normal, worldNormal); //body.vectorToWorldFrame(normal, worldNormal); Transform.pointToWorldFrame(position, quat, intersectPoint, worldIntersectPoint); //body.pointToWorldFrame(intersectPoint, worldIntersectPoint); this.reportIntersection(worldNormal, worldIntersectPoint, mesh, body, trianglesIndex); } triangles.length = 0; }; Ray.prototype[Shape.types.TRIMESH] = Ray.prototype.intersectTrimesh; /** * @method reportIntersection * @private * @param {Vec3} normal * @param {Vec3} hitPointWorld * @param {Shape} shape * @param {Body} body * @return {boolean} True if the intersections should continue */ Ray.prototype.reportIntersection = function(normal, hitPointWorld, shape, body, hitFaceIndex){ var from = this.from; var to = this.to; var distance = from.distanceTo(hitPointWorld); var result = this.result; // Skip back faces? if(this.skipBackfaces && normal.dot(this._direction) > 0){ return; } result.hitFaceIndex = typeof(hitFaceIndex) !== 'undefined' ? hitFaceIndex : -1; switch(this.mode){ case Ray.ALL: this.hasHit = true; result.set( from, to, normal, hitPointWorld, shape, body, distance ); result.hasHit = true; this.callback(result); break; case Ray.CLOSEST: // Store if closer than current closest if(distance < result.distance || !result.hasHit){ this.hasHit = true; result.hasHit = true; result.set( from, to, normal, hitPointWorld, shape, body, distance ); } break; case Ray.ANY: // Report and stop. this.hasHit = true; result.hasHit = true; result.set( from, to, normal, hitPointWorld, shape, body, distance ); result._shouldStop = true; break; } }; var v0 = new Vec3(), intersect = new Vec3(); function distanceFromIntersection(from, direction, position) { // v0 is vector from from to position position.vsub(from,v0); var dot = v0.dot(direction); // intersect = direction*dot + from direction.mult(dot,intersect); intersect.vadd(from,intersect); var distance = position.distanceTo(intersect); return distance; }