uphysics
Version:
Unrealistic physic engine.
1,332 lines (1,021 loc) • 40.9 kB
JavaScript
/*
Uphysics
Copyright (c) 2017 Cédric Ronvel
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/* jshint -W072, -W073 */
"use strict" ;
var arrayKit = require( 'array-kit' ) ;
var Logfella = require( 'logfella' ) ;
var log = Logfella.global.use( 'physic' ) ;
/*
TODO:
- continuous collision should be able to retrieve data from dynamic vertices creation, since it already raytrace things
- dynamic vertices should be tested against all surfaces, and moved inbounds when necessary
- each edge should shoot a trace (from one vertex to the other) against all foreign surfaces, because in 3D,
checking vertices is not enough even for volumes made of planes
- edge backface culling
- manage dynamic vertices for edges
- vertices/edges reduction for volumes that has vertices connected to more than 3 faces
*/
/*
Minkowski GJK algorithm resources, for the day I would want to make the switch:
* http://www.dyn4j.org/2010/04/gjk-gilbert-johnson-keerthi/
*/
/*
Shapes are like “Brushes”: they are convex CSG intersection of basic primitives:
* plane: separate the space in two side: the positive side (outside) and the negative side (inside)
* cylinder: infinite tube, separate into the inner (inside) and the outer (outside)
* sphere: separate into the inner (inside) and the outer (outside) of the sphere
*/
function Shape( surfaces , boundingBox , noBackFaceCulling ) { return Shape.create( surfaces , boundingBox , noBackFaceCulling ) ; }
module.exports = Shape ;
var physic = require( './physic.js' ) ;
/*
* primitives: array of object, where:
* type: the type of the primitive
* ... parameters for this type
* boundingBox: object having properties: xMin, xMax, yMin, yMax, zMin, zMax
For intance only 'sphere' and 'plane' primitives exists.
*/
Shape.create = function create( surfaces , boundingBox , noBackFaceCulling )
{
var self = Object.create( Shape.prototype , {
surfaces: { value: surfaces , writable: true , enumerable: true } ,
vertices: { value: [] , writable: true , enumerable: true } ,
// edges are segments, a list of BoundVectors
edges: { value: [] , writable: true , enumerable: true } ,
noBackFaceCulling: { value: !! noBackFaceCulling , writable: true , enumerable: true } ,
boundingBox: { value: boundingBox , writable: true , enumerable: true } ,
surfaceFlags: { value: 0 , writable: true , enumerable: true } ,
// surfaces that have dynamic vertices
dynamicSurfaces: { value: [] , writable: true , enumerable: true } ,
} ) ;
self.init() ;
return self ;
} ;
/*
* create vertices from faces
* create edges from faces
*/
Shape.prototype.init = function init()
{
var i , j , k ,
slen = this.surfaces.length ,
edgeVertices ,
sharedEdgeVertices = new Array( slen ) ,
outSurfaces = [] ,
s1 , s2 , s3 , vertex ;
// Reset vertices
this.vertices.length = 0 ;
for ( i = 0 ; i < slen ; i ++ )
{
s1 = this.surfaces[ i ] ;
this.surfaceFlags &= s1.type ;
if ( s1.type === physic.DOT )
{
// Filter them out of surfaces, move them to vertices
this.vertices.push( s1.surface.dup() ) ;
continue ;
}
outSurfaces.push( s1 ) ;
if ( s1.type !== physic.PLANE )
{
// For instance, we do not create special edges
this.dynamicSurfaces.push( s1 ) ;
continue ;
}
if ( ! sharedEdgeVertices[ i ] ) { sharedEdgeVertices[ i ] = [] ; }
for ( j = i + 1 ; j < slen ; j ++ )
{
s2 = this.surfaces[ j ] ;
// For instance, nothing to for non-plane
if ( s2.type !== physic.PLANE ) { continue ; }
// Parallel planes? skip them!
if ( s1.surface.isParallelToPlane( s2.surface ) ) { continue ; }
// Reset the edgeVertices
edgeVertices = sharedEdgeVertices[ i ][ j ] ? sharedEdgeVertices[ i ][ j ].slice() : [] ;
//log.verbose( "%d %d edgeVertices count: %d" , i , j , edgeVertices.length ) ;
if ( ! sharedEdgeVertices[ j ] ) { sharedEdgeVertices[ j ] = [] ; }
for ( k = j + 1 ; k < slen ; k ++ )
{
s3 = this.surfaces[ k ] ;
// At this stage, we do not care about non-plane
if ( s3.type !== physic.PLANE ) { continue ; }
vertex = s1.surface.threePlanesIntersection( s2.surface , s3.surface ) ;
if ( vertex && Shape.isVertexTouchingVolume( vertex , this.surfaces ) )
{
if ( ! Shape.hasVertex( this.vertices , vertex ) ) { this.vertices.push( vertex ) ; }
if ( ! Shape.hasVertex( edgeVertices , vertex ) ) { edgeVertices.push( vertex ) ; }
if ( ! sharedEdgeVertices[ i ][ k ] ) { sharedEdgeVertices[ i ][ k ] = [ vertex ] ; }
else if ( ! Shape.hasVertex( sharedEdgeVertices[ i ][ k ] , vertex ) ) { sharedEdgeVertices[ i ][ k ].push( vertex ) ; }
if ( ! sharedEdgeVertices[ j ][ k ] ) { sharedEdgeVertices[ j ][ k ] = [ vertex ] ; }
else if ( ! Shape.hasVertex( sharedEdgeVertices[ j ][ k ] , vertex ) ) { sharedEdgeVertices[ j ][ k ].push( vertex ) ; }
}
}
switch ( edgeVertices.length )
{
case 0 :
// infinite edge (line)
console.log( "Infinite edge (line) or edge out of volume" ) ;
break ;
case 1 :
// infinite edge (ray)
console.log( "Infinite edge (ray) or edge out of volume" ) ;
break ;
case 2 :
// regular finite edges (line segment)
console.log( "Regular finite edges (line segment)" ) ;
this.edges.push( physic.BoundVector3D.fromTo( edgeVertices[ 0 ] , edgeVertices[ 1 ] ) ) ;
break ;
default :
console.log( "Error: edge with more than 2 vertices (" + edgeVertices.length + ")" ) ;
break ;
}
}
}
s1 = this.surfaces = outSurfaces ;
log.info( "Created a shape with %d surfaces, %d edges and %d vertices" , this.surfaces.length , this.edges.length , this.vertices.length ) ;
} ;
// Return true if the vertex is already stored, using epsilon comparison
Shape.hasVertex = function hasVertex( arrayOfVertices , vertex )
{
var i , vlen = arrayOfVertices.length ;
for ( i = 0 ; i < vlen ; i ++ )
{
if ( vertex.isEqualTo( arrayOfVertices[ i ] ) ) { return true ; }
}
return false ;
} ;
// Check if a position is inside
Shape.isVertexInsideVolume = function isVertexInsideVolume( vertex , surfaces )
{
var i , slen = surfaces.length ;
for ( i = 0 ; i < slen ; i ++ )
{
if ( ! physic.epsilonLt( surfaces[ i ].surface.testVector( vertex ) , 0 ) )
{
return false ;
}
}
return !! slen ;
} ;
// Check if a position is inside or touching (on the surface)
Shape.isVertexInsideOrTouchingVolume = function isVertexInsideOrTouchingVolume( vertex , surfaces )
{
var i , slen = surfaces.length ;
for ( i = 0 ; i < slen ; i ++ )
{
if ( ! physic.epsilonLte( surfaces[ i ].surface.testVector( vertex ) , 0 ) )
{
return false ;
}
}
return !! slen ;
} ;
// Check if a position is touching (on the surface)
Shape.isVertexTouchingVolume = function isVertexTouchingVolume( vertex , surfaces )
{
var i , slen = surfaces.length , test , touching = false ;
// A position is touching the shape if it has at least one test that is
// epsilon-equal to zero and no test that is epsilon-greater than zero.
for ( i = 0 ; i < slen ; i ++ )
{
test = physic.epsilonZero( surfaces[ i ].surface.testVector( vertex ) ) ;
if ( test > 0 ) { return false ; }
if ( test === 0 ) { touching = true ; }
}
return !! slen ;
} ;
// Prepare interaction between 2 shapes
Shape.prototype.prepareInteraction = function prepareInteraction( position , foreignShape , foreignPosition , trace )
{
// Shorthand to the static object
var interaction = prepareInteraction.interaction ;
interaction.diffPosition.setFromTo( position , foreignPosition ) ;
// This is problematic when the diff is null: many time we wouldnt' know in which direction to push things.
// Make it slightly non-null.
if ( interaction.diffPosition.isNull() ) { interaction.diffPosition.x += physic.getEpsilon() ; }
// Inverse diff is used a lot, so better cache it now!
interaction.invDiffPosition.setInvVector( interaction.diffPosition ) ;
if ( ! this.noBackFaceCulling || this.dynamicSurfaces.length )
{
// Reset arrays
interaction.surfaces = interaction.cachedSurfaces ;
interaction.vertices = interaction.cachedVertices ;
interaction.cachedSurfaces.length = 0 ;
interaction.cachedVertices.length = 0 ;
}
else
{
interaction.surfaces = this.surfaces ;
interaction.vertices = this.vertices ;
}
if ( ! foreignShape.noBackFaceCulling || foreignShape.dynamicSurfaces.length )
{
// Reset arrays
interaction.foreignSurfaces = interaction.cachedForeignSurfaces ;
interaction.foreignVertices = interaction.cachedForeignVertices ;
interaction.cachedForeignSurfaces.length = 0 ;
interaction.cachedForeignVertices.length = 0 ;
}
else
{
interaction.foreignSurfaces = foreignShape.surfaces ;
interaction.foreignVertices = foreignShape.vertices ;
}
if ( ! this.noBackFaceCulling )
{
this.addFrontFaces( interaction.surfaces , interaction.vertices , interaction.diffPosition ) ;
}
if ( ! foreignShape.noBackFaceCulling )
{
foreignShape.addFrontFaces( interaction.foreignSurfaces , interaction.foreignVertices , interaction.invDiffPosition ) ;
}
if ( this.dynamicSurfaces.length )
{
this.addDynamicVertices( interaction.vertices , interaction.foreignSurfaces , interaction.diffPosition , trace ) ;
}
if ( foreignShape.dynamicSurfaces.length )
{
if ( trace )
{
// Revert the trace vector
trace.vector.inv() ;
foreignShape.addDynamicVertices( interaction.foreignVertices , interaction.surfaces , interaction.invDiffPosition , trace ) ;
trace.vector.inv() ;
}
else
{
foreignShape.addDynamicVertices( interaction.foreignVertices , interaction.surfaces , interaction.invDiffPosition ) ;
}
}
return prepareInteraction.interaction ;
} ;
// Static cached object.
// Avoid massive object creation that would cause GC to kick in often
Shape.prototype.prepareInteraction.interaction = {
diffPosition: physic.Vector3D( 0 , 0 , 0 ) ,
invDiffPosition: physic.Vector3D( 0 , 0 , 0 ) ,
surfaces: null ,
vertices: null ,
cachedSurfaces: [] ,
cachedVertices: [] ,
foreignSurfaces: null ,
foreignVertices: null ,
cachedForeignSurfaces: [] ,
cachedForeignVertices: []
} ;
// Add to interaction surfaces all surfaces that are not back-face culled
Shape.prototype.addFrontFaces = function addFrontFaces( surfaces , vertices , vector )
{
var i , len ;
// Add vertices
for ( i = 0 , len = this.vertices.length ; i < len ; i ++ )
{
if ( physic.epsilonGte( this.vertices[ i ].dot( vector ) , 0 ) )
{
vertices.push( this.vertices[ i ] ) ;
}
}
// Add surfaces
for ( i = 0 , len = this.surfaces.length ; i < len ; i ++ )
{
// Non-plane are always added
if ( this.surfaces[ i ].type !== physic.PLANE || physic.epsilonGte( this.surfaces[ i ].surface.normal.dot( vector ) , 0 ) )
{
surfaces.push( this.surfaces[ i ] ) ;
}
}
} ;
Shape.prototype.addDynamicVertices = function addDynamicVertices( vertices , foreignSurfaces , diffPosition , trace )
{
var i , j ,
dlen = this.dynamicSurfaces.length ,
flen = foreignSurfaces.length ;
for ( i = 0 ; i < dlen ; i ++ )
{
if ( this.dynamicSurfaces[ i ].type === physic.SPHERE )
{
for ( j = 0 ; j < flen ; j ++ )
{
if ( foreignSurfaces[ j ].type === physic.PLANE )
{
this.spherePlaneVertices( vertices , this.dynamicSurfaces[ i ] , foreignSurfaces[ j ] , diffPosition , trace ) ;
}
else if ( foreignSurfaces[ j ].type === physic.SPHERE )
{
this.sphereSphereVertices( vertices , this.dynamicSurfaces[ i ] , foreignSurfaces[ j ] , diffPosition , trace ) ;
}
else if ( foreignSurfaces[ j ].type === physic.CYLINDER )
{
this.sphereCylinderVertices( vertices , this.dynamicSurfaces[ i ] , foreignSurfaces[ j ] , diffPosition , trace ) ;
}
}
}
else if ( this.dynamicSurfaces[ i ].type === physic.CYLINDER )
{
for ( j = 0 ; j < flen ; j ++ )
{
if ( foreignSurfaces[ j ].type === physic.PLANE )
{
this.cylinderPlaneVertices( vertices , this.dynamicSurfaces[ i ] , foreignSurfaces[ j ] , diffPosition , trace ) ;
}
else if ( foreignSurfaces[ j ].type === physic.SPHERE )
{
this.cylinderSphereVertices( vertices , this.dynamicSurfaces[ i ] , foreignSurfaces[ j ] , diffPosition , trace ) ;
}
else if ( foreignSurfaces[ j ].type === physic.CYLINDER )
{
this.cylinderCylinderVertices( vertices , this.dynamicSurfaces[ i ] , foreignSurfaces[ j ] , diffPosition , trace ) ;
}
}
}
}
} ;
Shape.prototype.spherePlaneVertices = function spherePlaneVertices( vertices , dsurf , fsurf , diffPosition , trace )
{
var vertex = dsurf.surface.center.dup().moveAlong( fsurf.surface.normal , - dsurf.surface.r ) ;
vertex = this.fixVertexAlongSphere( vertex , dsurf.surface ) ;
if ( vertex )
{
//console.log( "+Dynamic Vertex:" , vertex ) ;
vertices.push( vertex ) ;
}
} ;
Shape.prototype.sphereSphereVertices = function sphereSphereVertices( vertices , dsurf , fsurf , diffPosition , trace )
{
var vertex , intersection ;
// Cached objects
var localPosition = sphereSphereVertices.localPosition ;
// Only one vertex per sphere pair.
// If a small sphere is entirely inside a big one, the big one wouldn't have any
// of its vertex inside the small.
// /!\ Except if the sphere is not the only one surface of the shape /!\
//if ( dsurf.surface.r > fsurf.surface.r ) { continue ; }
if ( trace )
{
// Use to foreign space
//log.error( trace ) ;
trace.position.setVector( dsurf.surface.center ).sub( diffPosition ) ;
intersection = fsurf.surface.traceIntersection( trace , dsurf.surface.r ) ;
// There will not be any collision, skip it
if ( ! intersection ) { return ; }
// Diff vector
vertex = physic.Vector3D.fromTo( intersection , fsurf.surface.center ).setLength( dsurf.surface.r ) ;
// Real vertex position
vertex.add( dsurf.surface.center ) ;
}
else
{
vertex = dsurf.surface.center.dup() ;
localPosition.setVector( fsurf.surface.center ).add( diffPosition ) ;
// If both position are equal, we use the center of the sphere, if not,
// we project on the sphere surface, toward the foreign sphere center
if ( ! vertex.isEqualTo( localPosition ) ) { vertex.moveToward( localPosition , dsurf.surface.r ) ; }
}
vertex = this.fixVertexAlongSphere( vertex , dsurf.surface ) ;
if ( vertex )
{
//console.log( "+Dynamic Vertex:" , vertex ) ;
vertices.push( vertex ) ;
}
} ;
Shape.prototype.sphereSphereVertices.localPosition = physic.Vector3D() ;
Shape.prototype.sphereCylinderVertices = function sphereCylinderVertices( vertices , dsurf , fsurf , diffPosition , trace )
{
var vertex , intersection ;
// Cached objects
var localAxis = sphereCylinderVertices.localAxis ;
if ( trace )
{
// Use to foreign space
//log.error( trace ) ;
trace.position.setVector( dsurf.surface.center ).sub( diffPosition ) ;
intersection = fsurf.surface.traceIntersection( trace , dsurf.surface.r ) ;
// There will not be any collision, skip it
if ( ! intersection ) { return ; }
// Diff vector
vertex = fsurf.surface.axis.pointProjection( intersection )
.sub( intersection )
.setLength( dsurf.surface.r ) ;
// Real vertex position
vertex.add( dsurf.surface.center ) ;
}
else
{
// Create the cylinder axis boundVector
localAxis.setBoundVector( fsurf.surface.axis ).position.add( diffPosition ) ;
// Get the closest point of the axis to the sphere center
vertex = localAxis.pointProjection( dsurf.surface.center ) ;
// Move away from the axis, toward to the sphere center, stop at the sphere surface
vertex.moveAtDistanceOf( dsurf.surface.center , dsurf.surface.r ) ;
}
vertex = this.fixVertexAlongSphere( vertex , dsurf.surface ) ;
if ( vertex )
{
//console.log( "+Dynamic Vertex:" , vertex ) ;
vertices.push( vertex ) ;
}
} ;
Shape.prototype.sphereCylinderVertices.localAxis = physic.BoundVector3D() ;
Shape.prototype.cylinderPlaneVertices = function cylinderPlaneVertices( vertices , dsurf , fsurf , diffPosition , trace )
{
var vertex , dot ;
// Cached objects
var localTrace = cylinderPlaneVertices.localTrace ;
dot = physic.epsilonZero( dsurf.surface.axis.vector.dot( fsurf.surface.normal ) ) ;
localTrace.setVectors(
dsurf.surface.axis.position ,
physic.epsilonNotEq( Math.abs( dot ) , 1 ) ?
fsurf.surface.normal : dsurf.surface.axis.vector.getAnyOrthogonal()
) ;
vertex = dsurf.surface.traceIntersection( localTrace , undefined , Infinity ) ;
// Cylinder are infinite, so move its dynamic vertex far far away,
// it will be clipped back by other surfaces if it is needed.
if ( dot > 0 )
{
vertex.moveAlong( dsurf.surface.axis.vector , - physic.FAR_AWAY ) ;
}
else if ( dot < 0 )
{
vertex.moveAlong( dsurf.surface.axis.vector , physic.FAR_AWAY ) ;
}
vertex = this.fixVertexAlongAxis( vertex , dsurf.surface.axis.vector ) ;
if ( vertex )
{
//console.log( "+Dynamic Vertex:" , vertex ) ;
vertices.push( vertex ) ;
}
} ;
Shape.prototype.cylinderPlaneVertices.localTrace = physic.BoundVector3D() ;
Shape.prototype.cylinderSphereVertices = function cylinderSphereVertices( vertices , dsurf , fsurf , diffPosition , trace )
{
var vertex ;
// Cached objects
var localPosition = cylinderSphereVertices.localPosition ;
if ( trace )
{
// Local space
//log.error( trace ) ;
trace.position.setVector( fsurf.surface.center ).add( diffPosition ) ;
vertex = dsurf.surface.traceIntersection( trace , fsurf.surface.r ) ;
// There will not be any collision, skip it
if ( ! vertex ) { return ; }
}
else
{
// Foreign surface position
localPosition.setVector( fsurf.surface.center ).add( diffPosition ) ;
// Get the closest point of the axis to the sphere center
vertex = dsurf.surface.axis.pointProjection( localPosition ) ;
// Move away from the axis, toward to the sphere center, by the cylinder radius
vertex.moveToward( localPosition , dsurf.surface.r ) ;
}
vertex = this.fixVertexAlongAxis( vertex , dsurf.surface.axis.vector ) ;
if ( vertex )
{
//console.log( "+Dynamic Vertex:" , vertex ) ;
vertices.push( vertex ) ;
}
} ;
Shape.prototype.cylinderSphereVertices.localPosition = physic.Vector3D() ;
Shape.prototype.cylinderCylinderVertices = function cylinderCylinderVertices( vertices , dsurf , fsurf , diffPosition , trace )
{
var vertex , intersection , normal , segment ;
// Cached objects
var localAxis = cylinderCylinderVertices.localAxis ;
if ( trace )
{
// Foreign space
if ( dsurf.surface.axis.isCollinearTo( fsurf.surface.axis ) )
{
// Both axis are parallel, so the Minkowski shape is the original cylinder
// with its radius being the sum of both cylinder
trace.position.setVector( dsurf.surface.axis.position ).sub( diffPosition ) ;
intersection = fsurf.surface.traceIntersection( trace , dsurf.surface.r ) ;
// There will not be any collision, skip it
if ( ! intersection ) { return ; }
// Diff vector
vertex = physic.Vector3D.fromTo( intersection , fsurf.surface.axis.position )
.diffProjectOn( fsurf.surface.axis.vector )
.setLength( dsurf.surface.r ) ;
// Real vertex position
vertex.add( dsurf.surface.axis.position ) ;
// /!\ Should add a vertex on both end of the Cylinder (cutted by clipping plane) /!\
}
else
{
// Axis are not parallel, so the Minkowski shape is a plane
// whose normal is the cross-product of both axis, or its inverse
trace.position.setVector( dsurf.surface.axis.position ).sub( diffPosition ) ;
normal = dsurf.surface.axis.vector.crossProduct( fsurf.surface.axis.vector ) ;
if ( normal.dot( physic.Vector3D.fromTo( fsurf.surface.axis.position , trace.position ) ) < 0 )
{
normal.inv() ;
}
intersection = physic.Plane3D.fromVectors( fsurf.surface.axis.position , normal )
.traceIntersection( trace , dsurf.surface.r + fsurf.surface.r ) ;
// There will not be any collision, skip it
if ( ! intersection ) { return ; }
// Diff vector
normal.setLength( dsurf.surface.r ) ;
localAxis.setVectors( intersection , dsurf.surface.axis.vector ) ;
vertex = localAxis.shortestSegmentToLine( fsurf.surface.axis ).position.sub( intersection ).sub( normal ) ;
// Real vertex position
vertex.add( dsurf.surface.axis.position ) ;
}
}
else
{
// Create the cylinder local axis
localAxis.setBoundVector( fsurf.surface.axis ).position.add( diffPosition ) ;
segment = dsurf.surface.axis.shortestSegmentToLine( localAxis ) ;
vertex = segment.position.moveAlong( segment.vector , dsurf.surface.r ) ;
}
vertex = this.fixVertexAlongAxis( vertex , dsurf.surface.axis.vector ) ;
if ( vertex )
{
//console.log( "+Dynamic Vertex:" , vertex ) ;
vertices.push( vertex ) ;
}
} ;
Shape.prototype.cylinderCylinderVertices.localAxis = physic.BoundVector3D() ;
// Move a dynamic vertex back on the volume's surface
Shape.prototype.fixVertexAlongAxis = function fixVertexAlongAxis( vertex , axis )
{
var i , slen = this.surfaces.length ;
var line = fixVertexAlongAxis.line.setVectors( vertex , axis ) ;
for ( i = 0 ; i < slen ; i ++ )
{
if ( ! physic.epsilonLte( this.surfaces[ i ].surface.testVector( vertex ) , 0 ) )
{
// Drop this vertex if it is moved by a non-plane primitive
if ( this.surfaces[ i ].type !== physic.PLANE )
{
log.warning( "Dropping a dynamic vertex: out of a non-plane primitive" ) ;
return null ;
}
//console.log( "moving a vertex from" , vertex ) ;
vertex = this.surfaces[ i ].surface.lineIntersection( line ) ;
//console.log( "to" , vertex ) ;
}
}
return vertex ;
} ;
Shape.prototype.fixVertexAlongAxis.line = physic.BoundVector3D() ;
// Move a dynamic vertex back on the volume's surface
Shape.prototype.fixVertexAlongSphere = function fixVertexAlongSphere( vertex , sphere )
{
//log.warning( ".fixVertexAlongSphere() was not tested" ) ;
var i , slen = this.surfaces.length ;
var circle ;
for ( i = 0 ; i < slen ; i ++ )
{
if ( ! physic.epsilonLte( this.surfaces[ i ].surface.testVector( vertex ) , 0 ) )
{
// Drop this vertex if it is moved by a non-plane primitive
if ( this.surfaces[ i ].type !== physic.PLANE )
{
log.warning( "Dropping a dynamic vertex: out of a non-plane primitive" ) ;
return null ;
}
//console.log( "moving a vertex from" , vertex ) ;
circle = sphere.planeIntersection( this.surfaces[ i ].surface ) ;
vertex = circle.pointProjection( vertex ) ;
//console.log( "to" , vertex ) ;
}
}
return vertex ;
} ;
// Check if a shape's bounding box is overlapping another
Shape.prototype.isBboxOverlapping = function isBboxOverlapping( position , foreignShape , foreignPosition )
{
return this.boundingBox.translatedBoundingBoxesIntersection( position , foreignShape.boundingBox , foreignPosition ) ;
} ;
// Check if a shape's bounding box is overlapping another, for continuous collisions
Shape.prototype.isSweepingBboxOverlapping = function isSweepingBboxOverlapping(
oldPosition , position ,
foreignShape , foreignOldPosition , foreignPosition )
{
return this.boundingBox.sweepingBoundingBoxesFromToIntersection(
oldPosition , position ,
foreignShape.boundingBox , foreignOldPosition , foreignPosition ) ;
} ;
// Check if a shape is overlapping another
Shape.prototype.isOverlapping = function isOverlapping( position , foreignShape , foreignPosition )
{
var i , vlen ,
vertex = Object.create( physic.Vector3D.prototype ) ;
var interaction = this.prepareInteraction( position , foreignShape , foreignPosition ) ;
// Check the foreign vertices against the shape
for ( i = 0 , vlen = interaction.surfaces.length && interaction.foreignVertices.length ; i < vlen ; i ++ )
{
vertex.set3Vectors( interaction.foreignVertices[ i ] , foreignPosition , position ) ;
//console.log( '#' + i + 'a:' , interaction.foreignVertices[ i ] , '->' , vertex ) ;
if ( Shape.isVertexInsideVolume( vertex , interaction.surfaces ) ) { return true ; }
}
// Check the vertices against the foreign shape
for ( i = 0 , vlen = interaction.foreignSurfaces.length && interaction.vertices.length ; i < vlen ; i ++ )
{
vertex.set3Vectors( interaction.vertices[ i ] , position , foreignPosition ) ;
//console.log( '#' + i + 'b:' , interaction.vertices[ i ] , '->' , vertex ) ;
if ( Shape.isVertexInsideVolume( vertex , interaction.foreignSurfaces ) ) { return true ; }
}
return false ;
} ;
// /!\ Where the bounding box test should be done? here? upstream? /!\
// Check if a shape is overlapping another
Shape.prototype.getCollision = function getCollision( position , foreignShape , foreignPosition )
{
var i , vlen , collision , offset ,
maxOffset = 0 ,
vertex = Object.create( physic.Vector3D.prototype ) ;
var interaction = this.prepareInteraction( position , foreignShape , foreignPosition ) ;
var collisions = getCollision.collisions ;
collisions.length = 0 ;
// Check the vertices against the foreign shape
for ( i = 0 , vlen = interaction.foreignSurfaces.length && interaction.vertices.length ; i < vlen ; i ++ )
{
vertex.set3Vectors( interaction.vertices[ i ] , position , foreignPosition ) ;
//console.log( '#' + i + 'a:' , interaction.vertices[ i ] , '->' , vertex ) ;
collision = Shape.getAllVertexCollisions( vertex , interaction.foreignSurfaces , interaction.invDiffPosition , false ) ;
//console.log( '??' , collision ) ;
if ( collision )
{
if ( ( offset = collision[ collision.minIndex ].offset ) > maxOffset )
{
maxOffset = offset ;
collisions.maxIndex = collisions.length ;
}
collisions.push( collision ) ;
}
}
// Check the foreign vertices against the shape
for ( i = 0 , vlen = interaction.surfaces.length && interaction.foreignVertices.length ; i < vlen ; i ++ )
{
vertex.set3Vectors( interaction.foreignVertices[ i ] , foreignPosition , position ) ;
//console.log( '#' + i + 'b:' , interaction.foreignVertices[ i ] , '->' , vertex ) ;
collision = Shape.getAllVertexCollisions( vertex , interaction.surfaces , interaction.diffPosition , true ) ;
//console.log( '??' , collision ) ;
if ( collision )
{
if ( ( offset = collision[ collision.minIndex ].offset ) > maxOffset )
{
maxOffset = offset ;
collisions.maxIndex = collisions.length ;
}
collisions.push( collision ) ;
}
}
if ( ! collisions.length )
{
// No collision!
return null ;
}
else if ( ! maxOffset )
{
// Just touching
return { t: 1 , displacement: physic.nullVector , normal: physic.nullVector } ;
}
return this.solveVerticesCollisions( collisions ) ;
} ;
Shape.prototype.getCollision.collisions = [] ;
/*
vertex: relative coordinate to the surface space
diffPosition: diff position between the surface and the foreign surface of the vertex
*/
Shape.getAllVertexCollisions = function getAllVertexCollisions( vertex , surfaces , diffPosition , foreign )
{
var i , slen = surfaces.length , normal , offset , contact , intersection , point , minOffset = Infinity ;
if ( ! slen ) { return null ; }
var tests = getAllVertexCollisions.tests ;
tests.length = 0 ;
var localTrace = getAllVertexCollisions.localTrace ;
// First run all tests, early out if one does not pass
for ( i = 0 ; i < slen ; i ++ )
{
tests[ i ] = physic.epsilonZero( surfaces[ i ].surface.testVector( vertex ) ) ;
if ( tests[ i ] > 0 ) { return null ; }
}
var collisions = [] ;
for ( i = 0 ; i < slen ; i ++ )
{
switch ( surfaces[ i ].type )
{
case physic.PLANE :
// The normal is the surface normal
normal = surfaces[ i ].surface.normal.dup() ;
// For plane, the 'test' value IS the distance from the surface
offset = - tests[ i ] ;
contact = offset === 0 ? vertex : vertex.dup().apply( normal , offset ) ;
break ;
case physic.SPHERE :
normal = physic.Vector3D.fromTo( surfaces[ i ].surface.center , vertex ) ;
if ( normal.dot( diffPosition ) > 0 )
{
normal.normalize() ; // MUST BE normalized
offset = - surfaces[ i ].surface.normalizeTest( tests[ i ] ) ;
contact = offset === 0 ? vertex : vertex.dup().apply( normal , offset ) ;
}
else
{
// The object has penetrated too much inside the surface, we can't reject by the vertex,
// but we should use diffPosition to reject it.
localTrace.setVectors( vertex , diffPosition ) ;
intersection = surfaces[ i ].surface.traceIntersection( localTrace , undefined , Infinity ) ;
if ( ! intersection )
{
// Fallback: this should not happen, however if we are really unlucky,
// the vertex may be just touching and may not intersect due to epsilon.
log.error( ".getAllVertexCollisions() -- SPHERE: Fallback!" ) ;
normal.inv().normalize() ;
offset = 0 ;
contact = vertex ;
}
else
{
normal = physic.Vector3D.fromTo( vertex , intersection ).normalize() ; // MUST BE normalized
offset = vertex.pointDistance( intersection ) ;
contact = intersection ;
}
}
//console.log( "offset:" , offset , "normal:" , normal ) ;
break ;
case physic.CYLINDER :
// Get the closest point of the axis to the vertex
point = surfaces[ i ].surface.axis.pointProjection( vertex ) ;
normal = physic.Vector3D.fromTo( point , vertex ) ;
if ( normal.dot( diffPosition ) > 0 )
{
normal.normalize() ; // MUST BE normalized
offset = - surfaces[ i ].surface.normalizeTest( tests[ i ] ) ;
contact = offset === 0 ? vertex : vertex.dup().apply( normal , offset ) ;
}
else
{
// The object has penetrated too much inside the surface, we can't reject by the vertex,
// but we should use diffPosition to reject it.
localTrace.setVectors( vertex , diffPosition ) ;
intersection = surfaces[ i ].surface.traceIntersection( localTrace , undefined , Infinity ) ;
if ( ! intersection )
{
// Fallback: this should not happen, however if we are really unlucky,
// the vertex may be just touching and may not intersect due to epsilon.
log.error( ".getAllVertexCollisions() -- CYLINDER: Fallback!" ) ;
normal.inv().normalize() ;
offset = 0 ;
contact = vertex ;
}
else
{
normal = physic.Vector3D.fromTo( vertex , intersection ).normalize() ; // MUST BE normalized
offset = vertex.pointDistance( intersection ) ;
contact = intersection ;
}
}
//console.log( "offset:" , offset , "normal:" , normal ) ;
break ;
}
// If foreign, reverse the normal: act as if it's the self shape that is moving, not the foreign shape
if ( foreign ) { normal.inv() ; }
if ( offset < minOffset ) { minOffset = offset ; collisions.minIndex = i ; }
collisions[ i ] = {
//vertex: vertex.dup() ,
normal: normal ,
offset: offset ,
currentOffset: offset ,
contact: contact ,
foreign: foreign
} ;
}
//collisions.sort( ( a , b ) => a.offset - b.offset ) ;
return collisions ;
} ;
Shape.getAllVertexCollisions.tests = [] ;
Shape.getAllVertexCollisions.localTrace = physic.BoundVector3D() ;
// Produce a displacement vector from the array of array of collisions
Shape.prototype.solveVerticesCollisions = function solveVerticesCollisions( collisions )
{
var i , iMax , j , jMax , adjustement ,
vertCollisions , vertSurfCollision ,
maxVertOffset , minVertSurfOffset ,
maxVertCollisions , minVertSurfCollision ;
/*
1\ apply the new global maximum of minimum and remove it
2\ for each remaining vertex, find if one collision is solved by the displacement
3\ if so, remove the whole vertex collisions
4\ if not, compute the remaining offset for each vertex collision and the new minimum
5\ find the new global maximum of minimum
6\ repeat until no vertex remains
This works for plane, but it's not so great for sphere, because unlike plane,
the normal depends on the vertex position.
Anyway, this will be “good enough” for instance.
*/
var displacement = solveVerticesCollisions.displacement.set( 0 , 0 , 0 ) ;
//log.warning( "solveVerticesCollisions() -- collisions: %[4]Y" , collisions ) ;
while ( collisions.length )
{
// Apply
maxVertCollisions = collisions[ collisions.maxIndex ] ;
minVertSurfCollision = maxVertCollisions[ maxVertCollisions.minIndex ] ;
displacement.apply( minVertSurfCollision.normal , minVertSurfCollision.currentOffset ) ;
//console.log( "New displacement:" , minVertSurfCollision.currentOffset , "->" , displacement ) ;
// Remove the collision
//collisions.splice( collisions.maxIndex , 1 ) ;
arrayKit.delete( collisions , collisions.maxIndex ) ;
// Now find the next vertex needing to move
maxVertOffset = 0 ;
for ( i = 0 , iMax = collisions.length ; i < iMax ; i ++ )
{
vertCollisions = collisions[ i ] ;
minVertSurfOffset = Infinity ;
for ( j = 0 , jMax = vertCollisions.length ; j < jMax ; j ++ )
{
vertSurfCollision = vertCollisions[ j ] ;
adjustement = physic.epsilonZero( vertSurfCollision.normal.dot( displacement ) ) ;
if ( adjustement < 0 )
{
// Too bad, the displacement is wrong for this vertex collision!
// There isn't much to do, just hope that things would go better for another surface
console.log( '>>> Bad adjustement' , adjustement , vertSurfCollision.normal , displacement ) ;
continue ;
}
vertSurfCollision.currentOffset = vertSurfCollision.offset - adjustement ;
if ( vertSurfCollision.currentOffset < minVertSurfOffset )
{
minVertSurfOffset = vertSurfCollision.currentOffset ;
// If the previous displacement totally solved our constraint, we should break immediately
if ( minVertSurfOffset <= 0 ) { break ; }
vertCollisions.minIndex = j ;
}
}
if ( minVertSurfOffset === Infinity || minVertSurfOffset <= 0 )
{
// This vertex has no adjustement possible OR has already been adjusted by another displacement,
// remove it and update 'i' for the next iteration.
//collisions.splice( i , 1 ) ;
arrayKit.delete( collisions , i ) ;
i -- ;
iMax -- ;
}
else if ( minVertSurfOffset > maxVertOffset )
{
maxVertOffset = minVertSurfOffset ;
collisions.maxIndex = i ;
}
}
}
return {
t: 1 , // t is always 1 for discrete collisions
displacement: displacement ,
normal: displacement.dup().normalize() ,
} ;
} ;
Shape.prototype.solveVerticesCollisions.displacement = physic.Vector3D( 0 , 0 , 0 ) ;
// Check if a shape is overlapping another
Shape.prototype.getContinuousCollision = function getContinuousCollision(
oldPosition , position ,
foreignShape , foreignOldPosition , foreignPosition )
{
var i , vlen , collision , bestCollision = null ,
minT = 2 ; // minT should be anything greater than 1
// Relative movement
var trace = physic.BoundVector3D( 0 , 0 , 0 , 0 , 0 , 0 ) ;
trace.vector.setVector( position ).sub( oldPosition ).add( foreignOldPosition ).sub( foreignPosition ) ;
// Maybe we should fallback too if old positions are already overlapping?
if ( trace.vector.isNull() )
{
//console.log( "\n!!! No movement! fallback to the discrete collision! !!!\n" ) ;
return this.getCollision( position , foreignShape , foreignPosition ) ;
}
var interaction = this.prepareInteraction( oldPosition , foreignShape , foreignOldPosition , trace ) ;
var collisions = getContinuousCollision.collisions ;
collisions.length = 0 ;
// Check the vertices against the foreign shape
for ( i = 0 , vlen = interaction.foreignSurfaces.length && interaction.vertices.length ; i < vlen ; i ++ )
{
trace.position.set3Vectors( interaction.vertices[ i ] , oldPosition , foreignOldPosition ) ;
//console.log( '#' + i + 'a:' , interaction.vertices[ i ] , '->' , trace ) ;
collision = Shape.getTraceCollision( trace , interaction.foreignSurfaces , false ) ;
//console.log( '??' , collision ) ;
if ( collision && collision.t < minT )
{
minT = collision.t ;
bestCollision = collision ;
}
}
// Inverse the movement
trace.vector.inv() ;
// Check the foreign vertices against the shape
for ( i = 0 , vlen = interaction.surfaces.length && interaction.foreignVertices.length ; i < vlen ; i ++ )
{
trace.position.set3Vectors( interaction.foreignVertices[ i ] , foreignOldPosition , oldPosition ) ;
//console.log( '#' + i + 'b:' , interaction.foreignVertices[ i ] , '->' , trace ) ;
collision = Shape.getTraceCollision( trace , interaction.surfaces , true ) ;
//console.log( '??' , collision ) ;
if ( collision && collision.t < minT )
{
minT = collision.t ;
bestCollision = collision ;
}
}
return bestCollision ;
} ;
Shape.prototype.getContinuousCollision.collisions = [] ;
/*
vertex: relative coordinate to the surface space
*/
Shape.getTraceCollision = function getTraceCollision( trace , surfaces , foreign )
{
var i , slen = surfaces.length , contact , intersection ,
minT = 2 , // anything greater than 1
normal ;
if ( ! slen ) { return null ; }
for ( i = 0 ; i < slen ; i ++ )
{
// Get the contact point
intersection = surfaces[ i ].surface.traceIntersection( trace ) ;
//console.log( "Trace collision:" , intersection ) ;
// Quit now if no intersection for this trace, or if the intersection would happen after
// the current best candidate, or if is not inside/touching the volume
if (
! intersection ||
intersection.t >= minT ||
! Shape.isVertexInsideOrTouchingVolume( intersection , surfaces )
)
{
//console.log( "Outside shape" ) ;
continue ;
}
//console.log( "Inside shape" ) ;
// We've got a new best candidate!
switch ( surfaces[ i ].type )
{
case physic.PLANE :
// The normal is *ALWAYS* the surface normal, wherever the contact happend
normal = surfaces[ i ].surface.normal.dup() ;
break ;
case physic.SPHERE :
// The normal is the vector from the sphere center toward the contact
normal = physic.Vector3D.fromTo( surfaces[ i ].surface.center , intersection ).normalize() ; // MUST BE normalized
break ;
case physic.CYLINDER :
// The normal is the vector from the axis of the cylinder toward the contact
normal = surfaces[ i ].surface.axis.lineToPointVector( intersection ).normalize() ;
break ;
}
if ( physic.epsilonLte( intersection.t , 0 ) && normal.dotProduct( trace.vector ) > 0 )
{
// The movement is already directed outward and happens immediately.
// We are facing epsilon issue, most probably after a collision in the previous frame.
// Forget about this collision.
continue ;
}
// Replace the former candidate
contact = intersection ;
minT = contact.t ;
}
if ( ! contact ) { return null ; }
var displacement = contact.dup().sub( trace.position ).sub( trace.vector ) ;
//displacement = contact.dup().sub( trace.getEndPoint() ) ;
// If foreign, reverse the normal and the displacement: act as if
// it's the self shape that is moving, not the foreign shape
if ( foreign )
{
normal.inv() ;
displacement.inv() ;
}
return {
t: contact.t ,
displacement: displacement ,
normal: normal ,
// Useful???
//vertex: vertex.dup() ,
//currentT: contactT ,
contact: contact ,
foreign: foreign
} ;
} ;
Shape.createDot = require( './shapes/dot.js' ) ;
Shape.createPlane = require( './shapes/plane.js' ) ;
Shape.createSphere = require( './shapes/sphere.js' ) ;
Shape.createInfiniteCylinder = require( './shapes/infiniteCylinder.js' ) ;
Shape.createBox = require( './shapes/box.js' ) ;
Shape.createCylinder = require( './shapes/cylinder.js' ) ;
Shape.createOctahedron = require( './shapes/octahedron.js' ) ;