js/SceneKit/SCNPhysicsWorld.js
'use strict'
import NSObject from '../ObjectiveC/NSObject'
import SCNBox from './SCNBox'
import SCNCapsule from './SCNCapsule'
import SCNGeometryPrimitiveType from './SCNGeometryPrimitiveType'
import SCNGeometrySource from './SCNGeometrySource'
import SCNHitTestResult from './SCNHitTestResult'
import SCNMatrix4 from './SCNMatrix4'
//import SCNPhysicsBody from './SCNPhysicsBody'
import SCNPhysicsBodyType from './SCNPhysicsBodyType'
//import SCNPhysicsBehavior from './SCNPhysicsBehavior'
import SCNPhysicsContact from './SCNPhysicsContact'
//import SCNPhysicsContactDelegate from './SCNPhysicsContactDelegate'
import SCNPhysicsShape from './SCNPhysicsShape'
import SCNSphere from './SCNSphere'
import SCNVector3 from './SCNVector3'
//import _Ammo from '../third_party/ammo'
const _TestOption = {
backfaceCulling: 'backfaceCulling',
collisionBitMask: 'collisionBitMask',
searchMode: 'results'
}
const _TestSearchMode = {
all: 'all',
any: 'any',
closest: 'closest'
}
/**
* The global simulation of collisions, gravity, joints, and other physics effects in a scene.
* @access public
* @extends {NSObject}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld
*/
export default class SCNPhysicsWorld extends NSObject {
static get _propTypes() {
return {
gravity: 'SCNVector3',
speed: 'double',
timeStep: 'double',
scale: ['double', '_scale'],
// _allBehaviors
// contactDelegate
scene: ['SCNScene', '_scene']
}
}
/**
* constructor
* @access public
* @constructor
*/
constructor() {
super()
// Managing the Physics Simulation
/**
* A vector that specifies the gravitational acceleration applied to physics bodies in the physics world.
* @type {SCNVector3}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512855-gravity
*/
this.gravity = new SCNVector3(0, 0, 0)
/**
* The rate at which the simulation executes.
* @type {number}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512851-speed
*/
this.speed = 0
/**
* The time interval between updates to the physics simulation.
* @type {number}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512881-timestep
*/
this.timeStep = 0
/**
* @access private
* @type {number}
*/
this._scale = 1.0
// Registering Physics Behaviors
this._allBehaviors = []
// Detecting Contacts Between Physics Bodies
/**
* A delegate that is called when two physics bodies come in contact with each other.
* @type {?SCNPhysicsContactDelegate}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512843-contactdelegate
*/
this.contactDelegate = null
//const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration()
//const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration)
//const overlappingPairCache = new Ammo.btDbvtBroadphase()
//const solver = new Ammo.btSequentialImpulseConstraintSolver()
//this._world = new Ammo.btDiscreteDynamicsWorld(
// dispatcher, overlappingPairCache, solver, collisionConfiguration
//)
this._prevTime = null
/**
* @access private
* @type {SCNScene}
*/
this._scene = null
// for rayTest
this._renderer = null
}
// Managing the Physics Simulation
/**
* Forces the physics engine to reevaluate possible collisions between physics bodies.
* @access public
* @returns {void}
* @desc By default, SceneKit checks for collisions between physics bodies only once per simulation step. If you directly change the positions of any physics bodies outside of a SCNPhysicsContactDelegate method, call the updateCollisionPairs() method before using any of the methods listed in Searching for Physics Bodies Detecting Contacts Between Physics Bodies.
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512877-updatecollisionpairs
*/
updateCollisionPairs() {
}
// Registering Physics Behaviors
/**
* Adds a behavior to the physics world.
* @access public
* @param {SCNPhysicsBehavior} behavior - The behavior to be added.
* @returns {void}
* @desc Physics behaviors constrain or modify the effects of the physics simulation on sets of physics bodies. For example, the SCNPhysicsHingeJoint behavior causes two bodies to move as if connected by a hinge that pivots around a specific axis, and the SCNPhysicsVehicle behavior causes a body to roll like a car or other wheeled vehicle.To use a behavior in your scene, follow these steps:Create SCNPhysicsBody objects and attach them to each node that participates in the behavior.Create and configure a behavior object joining the physics bodies. See SCNPhysicsBehavior for a list of behavior classes.Call addBehavior(_:) on your scene’s physics world object to add the behavior to the physics simulation.
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512839-addbehavior
*/
addBehavior(behavior) {
if(this._allBehaviors.indexOf(behavior) >= 0){
return
}
this._allBehaviors.push(behavior)
}
/**
* Removes a behavior from the physics world.
* @access public
* @param {SCNPhysicsBehavior} behavior - The behavior to be removed.
* @returns {void}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512870-removebehavior
*/
removeBehavior(behavior) {
const index = this._allBehaviors.indexOf(behavior)
if(index < 0){
return
}
this._allBehaviors.splice(index, 1)
}
/**
* Removes all behaviors affecting bodies in the physics world.
* @access public
* @returns {void}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512849-removeallbehaviors
*/
removeAllBehaviors() {
this._allBehaviors = []
}
/**
* The list of behaviors affecting bodies in the physics world.
* @type {SCNPhysicsBehavior[]}
* @desc
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512853-allbehaviors
*/
get allBehaviors() {
return this._allBehaviors.slice(0)
}
// Detecting Contacts Between Physics Bodies
/**
* Checks for contacts between two physics bodies.
* @access public
* @param {SCNPhysicsBody} bodyA - The first body (to test for contact with the second).
* @param {SCNPhysicsBody} bodyB - The second body (to test for contact with the first).
* @param {?Map<SCNPhysicsWorld.TestOption, Object>} [options = null] - A dictionary of options affecting the test, or nil to use default options. For applicable keys and the possible values, see Physics Test Options Keys.
* @returns {SCNPhysicsContact[]} -
* @desc SceneKit sends messages to the physics world’s contactDelegate object only when collisions occur between bodies whose collisionBitMask and categoryBitMask properties overlap, and only for collisions between certain types of bodies. (For details, see SCNPhysicsBodyType.) Use this method to directly test for contacts between any two bodies at a time of your choosing. For example, to implement a game where the player character can pick up an item, you might call this method when the player presses the “pick up” button to see if the player character is in contact with the item to be picked up.
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512875-contacttestbetween
*/
contactTestBetween(bodyA, bodyB, options = null) {
// FIXME: use physics library
if((bodyA.categoryBitMask & bodyB.contactTestBitMask) === 0){
return []
}
if(!bodyA.physicsShape || !bodyB.physicsShape){
return []
}
if(bodyA.type === SCNPhysicsBodyType.static && bodyB.type === SCNPhysicsBodyType.static){
return []
}
if(bodyA._position.sub(bodyB._position).length() > bodyA._radius + bodyB._radius){
return []
}
const shapeA = bodyA.physicsShape._shape
const shapeB = bodyB.physicsShape._shape
if((shapeA instanceof SCNBox) && (shapeB instanceof SCNBox)){
return SCNPhysicsWorld._contactTestBetweenBoxes(bodyA, bodyB, options)
}else if((shapeA instanceof SCNBox) && (shapeB instanceof SCNSphere)){
return SCNPhysicsWorld._contactTestBetweenBoxAndSphere(bodyA, bodyB, options)
}else if((shapeB instanceof SCNBox) && (shapeA instanceof SCNSphere)){
return SCNPhysicsWorld._contactTestBetweenBoxAndSphere(bodyB, bodyA, options, true)
}else if((shapeA instanceof SCNSphere) && (shapeB instanceof SCNSphere)){
return SCNPhysicsWorld._contactTestBetweenSpheres(bodyA, bodyB, options)
}
return []
}
/**
* @access private
* @param {SCNPhysicsBody} boxA -
* @param {SCNPhysicsBody} boxB -
* @param {Object} options -
* @returns {SCNPhysicsContact[]} -
*/
static _contactTestBetweenBoxes(boxA, boxB, options) {
const shapeA = boxA.physicsShape._shape
const shapeB = boxB.physicsShape._shape
const tb = boxB._transform.mult(boxA._invTransform)
const nb1 = (new SCNVector3(tb.m11, tb.m12, tb.m13)).normalize()
const nb2 = (new SCNVector3(tb.m21, tb.m22, tb.m23)).normalize()
const nb3 = (new SCNVector3(tb.m31, tb.m32, tb.m33)).normalize()
const b1 = nb1.mul(shapeB.width * 0.5)
const b2 = nb2.mul(shapeB.height * 0.5)
const b3 = nb3.mul(shapeB.length * 0.5)
const d = tb.getTranslation()
const lax = shapeA.width * 0.5
const lay = shapeA.height * 0.5
const laz = shapeA.length * 0.5
// Ae1
let rA = lax
let rB = Math.abs(b1.x) + Math.abs(b2.x) + Math.abs(b3.x)
let L = Math.abs(d.x)
if(L > rA + rB){
return []
}
// Ae2
rA = lay
rB = Math.abs(b1.y) + Math.abs(b2.y) + Math.abs(b3.y)
L = Math.abs(d.y)
if(L > rA + rB){
return []
}
// Ae3
rA = laz
rB = Math.abs(b1.z) + Math.abs(b2.z) + Math.abs(b3.z)
L = Math.abs(d.z)
if(L > rA + rB){
return []
}
// Be1
rA = Math.abs(nb1.x * lax) + Math.abs(nb1.y * lay) + Math.abs(nb1.z * laz)
rB = b1.length()
L = Math.abs(d.dot(nb1))
if(L > rA + rB){
return []
}
// Be2
rA = Math.abs(nb2.x * lax) + Math.abs(nb2.y * lay) + Math.abs(nb2.z * laz)
rB = b2.length()
L = Math.abs(d.dot(nb2))
if(L > rA + rB){
return []
}
// Be3
rA = Math.abs(nb3.x * lax) + Math.abs(nb3.y * lay) + Math.abs(nb3.z * laz)
rB = b3.length()
L = Math.abs(d.dot(nb3))
if(L > rA + rB){
return []
}
// C11
let axis = new SCNVector3(0, -nb1.z, nb1.y)
rA = Math.abs(axis.y * lay) + Math.abs(axis.z * laz)
rB = Math.abs(axis.dot(b2)) + Math.abs(axis.dot(b3))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C12
axis = new SCNVector3(0, -nb2.z, nb2.y)
rA = Math.abs(axis.y * lay) + Math.abs(axis.z * laz)
rB = Math.abs(axis.dot(b3)) + Math.abs(axis.dot(b1))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C13
axis = new SCNVector3(0, -nb3.z, nb3.y)
rA = Math.abs(axis.y * lay) + Math.abs(axis.z * laz)
rB = Math.abs(axis.dot(b1)) + Math.abs(axis.dot(b2))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C21
axis = new SCNVector3(nb1.z, 0, -nb1.x)
rA = Math.abs(axis.x * lax) + Math.abs(axis.z * laz)
rB = Math.abs(axis.dot(b2)) + Math.abs(axis.dot(b3))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C22
axis = new SCNVector3(nb2.z, 0, -nb2.x)
rA = Math.abs(axis.x * lax) + Math.abs(axis.z * laz)
rB = Math.abs(axis.dot(b3)) + Math.abs(axis.dot(b1))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C23
axis = new SCNVector3(nb3.z, 0, -nb3.x)
rA = Math.abs(axis.x * lax) + Math.abs(axis.z * laz)
rB = Math.abs(axis.dot(b1)) + Math.abs(axis.dot(b2))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C31
axis = new SCNVector3(-nb1.y, nb1.x, 0)
rA = Math.abs(axis.x * lax) + Math.abs(axis.y * lay)
rB = Math.abs(axis.dot(b2)) + Math.abs(axis.dot(b3))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C32
axis = new SCNVector3(-nb2.y, nb2.x, 0)
rA = Math.abs(axis.x * lax) + Math.abs(axis.y * lay)
rB = Math.abs(axis.dot(b3)) + Math.abs(axis.dot(b1))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
// C33
axis = new SCNVector3(-nb3.y, nb3.x, 0)
rA = Math.abs(axis.x * lax) + Math.abs(axis.y * lay)
rB = Math.abs(axis.dot(b1)) + Math.abs(axis.dot(b2))
L = Math.abs(d.dot(axis))
if(L > rA + rB){
return []
}
const contact = new SCNPhysicsContact()
contact._nodeA = boxA._node
contact._nodeB = boxB._node
contact._contactPoint = boxA._position.add(d.mul(0.5)) // TODO: implement
contact._contactNormal = d.normalize() // TODO: implement
contact._penetrationDistance = 0 // TODO: implement
return [contact]
}
/**
* @access private
* @param {SCNPhysicsBody} sphereA -
* @param {SCNPhysicsBody} sphereB -
* @param {Object} options -
* @returns {SCNPhysicsContact[]} -
*/
static _contactTestBetweenSpheres(sphereA, sphereB, options) {
const shapeA = sphereA.physicsShape._shape
const shapeB = sphereB.physicsShape._shape
const posA = sphereA._position
const posB = sphereB._position
const radA = shapeA.radius
const radB = shapeB.radius
const vec = posA.sub(posB)
const l = vec.length()
if(l > radA + radB){
return []
}
const contact = new SCNPhysicsContact()
contact._nodeA = sphereA._node
contact._nodeB = sphereB._node
contact._contactPoint = posA.add(vec.mul((radA - radB + l) * 0.5))
contact._contactNormal = vec.mul(-1).normalize()
contact._penetrationDistance = radA + radB - l
return [contact]
}
/**
* @access private
* @param {SCNPhysicsBody} box -
* @param {SCNPhysicsBody} sphere -
* @param {Object} options -
* @returns {SCNPhysicsContact[]} -
*/
static _contactTestBetweenBoxAndSphere(box, sphere, reverse = false) {
const boxShape = box.physicsShape._shape
const sphereShape = sphere.physicsShape._shape
const size = new SCNVector3()
let transform = null
const spherePos = sphere._position.transform(box._invTransform)
const v = new SCNVector3()
const w = boxShape.width * 0.5
const h = boxShape.height * 0.5
const l = boxShape.length * 0.5
if(Math.abs(spherePos.x) - w <= 0){
v.x = 0
}else{
v.x = spherePos.x - w
}
if(Math.abs(spherePos.y) - h <= 0){
v.y = 0
}else{
v.y = spherePos.y - h
}
if(Math.abs(spherePos.z) - l <= 0){
v.z = 0
}else{
v.z = spherePos.z - l
}
const d = v.length()
if(d > sphereShape.radius){
return []
}
const contact = new SCNPhysicsContact()
if(reverse){
contact._nodeA = sphere._node
contact._nodeB = box._node
}else{
contact._nodeA = box._node
contact._nodeB = sphere._node
}
contact._contactPoint = v.transform(box._transform)
contact._contactNormal = v.rotate(box._transform).normalize()
if(reverse){
contact._contactNormal = contact._contactNormal.mul(-1)
}
contact._penetrationDistance = d - sphereShape.radius
return [contact]
}
/**
* Checks for contacts between one physics body and any other bodies in the physics world.
* @access public
* @param {SCNPhysicsBody} body - The body to test for contact.
* @param {?Map<SCNPhysicsWorld.TestOption, Object>} [options = null] - A dictionary of options affecting the test, or nil to use default options. For applicable keys and the possible values, see Physics Test Options Keys.
* @returns {SCNPhysicsContact[]} -
* @desc SceneKit sends messages to the physics world’s contactdelegate object only when collisions occur between bodies whose collisionBitMask and categoryBitMask properties overlap, and only for collisions between certain types of bodies. (For details, see SCNPhysicsBodyType.) Use this method to directly test for all contacts between one body and any other bodies at a time of your choosing. For example, to implement a game with a “wall jump” effect, you could call this method when the player presses the jump button to see if the player character is in contact with any walls.
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512841-contacttest
*/
contactTestWith(body, options = null) {
return []
}
// Searching for Physics Bodies
/**
* Searches for physics bodies along a line segment between two points in the physics world.
* @access public
* @param {SCNVector3} origin - An endpoint of the line segment to search, specified in the scene’s world coordinate system.
* @param {SCNVector3} dest - The other endpoint of the line segment to search, specified in the scene’s world coordinate system.
* @param {?Map<SCNPhysicsWorld.TestOption, Object>} [options = null] - A dictionary of options affecting the test, or nil to use default options. For applicable keys and the possible values, see Physics Test Options Keys.
* @returns {SCNHitTestResult[]} -
* @desc Use this method to implement concepts such as line of sight in your app. For example, in a game you might implement behavior for an enemy character by searching for physics bodies along a line between the enemy character’s position and the player character’s position, as illustrated below:// Options: Look only for the closest object along line of sight,
// and use the collision bitmask to avoid finding the enemy itself.
NSDictionary *options = @{ SCNPhysicsTestSearchModeKey : SCNPhysicsTestSearchModeClosest,
SCNPhysicsTestCollisionBitMaskKey : @(kMyCategoryPlayer) };
NSArray *results = [physicsWorld rayTestWithSegmentFromPoint:enemy.position
toPoint:player.position
options:options];
if (results.firstObject.node == player) {
// Enemy can see player: begin pursuit.
} else {
// Enemy cannot see player: remain idle.
}
// Options: Look only for the closest object along line of sight,
// and use the collision bitmask to avoid finding the enemy itself.
NSDictionary *options = @{ SCNPhysicsTestSearchModeKey : SCNPhysicsTestSearchModeClosest,
SCNPhysicsTestCollisionBitMaskKey : @(kMyCategoryPlayer) };
NSArray *results = [physicsWorld rayTestWithSegmentFromPoint:enemy.position
toPoint:player.position
options:options];
if (results.firstObject.node == player) {
// Enemy can see player: begin pursuit.
} else {
// Enemy cannot see player: remain idle.
}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512857-raytestwithsegment
*/
rayTestWithSegmentFromTo(origin, dest, options = null) {
let opt = options
if(Array.isArray(options)){
opt = new Map(options)
}else if(options === null){
opt = new Map()
}
const results = []
let backfaceCulling = true
let collisionBitMask = -1
let searchMode = _TestSearchMode.any
if(opt.has(_TestOption.backfaceCulling)){
backfaceCulling = opt.get(_TestOption.backfaceCulling)
}
if(opt.has(_TestOption.collisionBitMask)){
collisionBitMask = opt.get(_TestOption.collisionBitMask)
}
if(opt.has(_TestOption.searchMode)){
searchMode = opt.get(_TestOption.searchMode)
}
//return this._renderer._physicsHitTestByGPU(origin, dest, opt)
this._scene.rootNode.enumerateChildNodes((child) => {
if(child.presentation
&& child.presentation.physicsBody
&& (child.presentation.physicsBody.categoryBitMask & collisionBitMask)){
const hits = SCNPhysicsWorld._hitTestWithSegmentPhysicsNode(origin, dest, child.presentation)
if(hits.length > 0){
// convert from child's coordinate to this node's coordinate
for(const h of hits){
h._node = child
h._worldCoordinates = child.convertPositionTo(h._localCoordinates, null)
h._worldNormal = child.convertPositionTo(h._localNormal, null)
h._localCoordinates = child.convertPositionFrom(h._localCoordinates, child)
h._localNormal = child.convertPositionFrom(h._localNormal, child)
}
results.push(...hits)
if(searchMode === _TestSearchMode.any){
// stop searching
return true
}
}
}
return false
})
if(results.length === 0){
return results
}
let sortedResults = results.sort((a, b) => a._distance - b._distance)
if(searchMode === _TestSearchMode.closest){
sortedResults = [sortedResults[0]]
}
return sortedResults
}
/**
* Searches for physics bodies in the space formed by moving a convex shape through the physics world.
* @access public
* @param {SCNPhysicsShape} shape - A physics shape. This shape must enclose a convex volume. For details on creating shapes that satisfy this requirement, see SCNPhysicsShape.
* @param {SCNMatrix4} from - A transform matrix representing the initial position and orientation of the shape.
* @param {SCNMatrix4} to - A transform matrix representing the final position and orientation of the shape.
* @param {?Map<SCNPhysicsWorld.TestOption, Object>} [options = null] - A dictionary of options affecting the test, or nil to use default options. For applicable keys and the possible values, see Physics Test Options Keys.
* @returns {SCNPhysicsContact[]} -
* @desc Use this method when it’s important to plan for (or avoid) collisions ahead of the physics simulation. For example, in a game you might plan maneuvers for a flying character to fit through the gaps between static bodies in the physics world, as illustrated below:// Look for potential collisions along the spaceship's current path.
SCNMatrix4 current = spaceship.transform;
SCNMatrix4 upAhead = SCNMatrix4Translate(current, 0, 0, LOOK_AHEAD_DISTANCE);
NSArray *contacts = [physicsWorld convexSweepTestWithShape:spaceship.physicsBody.physicsShape
fromTransform:current
toTransform:upAhead
options:nil];
if (contacts.count == 0) {
// Flight path looks okay.
} else {
// Flight path will cause a collision: look for another way around.
}
// Look for potential collisions along the spaceship's current path.
SCNMatrix4 current = spaceship.transform;
SCNMatrix4 upAhead = SCNMatrix4Translate(current, 0, 0, LOOK_AHEAD_DISTANCE);
NSArray *contacts = [physicsWorld convexSweepTestWithShape:spaceship.physicsBody.physicsShape
fromTransform:current
toTransform:upAhead
options:nil];
if (contacts.count == 0) {
// Flight path looks okay.
} else {
// Flight path will cause a collision: look for another way around.
}
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld/1512859-convexsweeptest
*/
convexSweepTestWith(shape, from, to, options = null) {
// TODO: implement
return []
}
// Structures
/**
* @type {Object} TestOption
* @property {string} backfaceCulling The key for choosing whether to ignore back-facing polygons in physics shapes when searching for contacts.
* @property {string} collisionBitMask The key for selecting which categories of physics bodies that SceneKit should test for contacts.
* @property {string} searchMode The key for selecting the number and order of contacts to be tested.
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld.testoption
*/
static get TestOption() {
return _TestOption
}
/**
* @type {Object} TestSearchMode
* @property {string} all Searches should return all contacts matching the search parameters.
* @property {string} any Searches should return only the first contact found regardless of its position relative to the search parameters.
* @property {string} closest Searches should return only the closest contact to the beginning of the search.
* @see https://developer.apple.com/documentation/scenekit/scnphysicsworld.testsearchmode
*/
static get TestSearchMode() {
return _TestSearchMode
}
_simulate(time) {
// FIXME: use physics library
//this._world.stepSimulation(1.0/60.0, 0)
if(!this._renderer){
return
}
const objects = this._renderer._createRenderingPhysicsNodeArray()
const contacts = []
for(const obj of objects){
const body = obj.physicsBody
body._prevPosition = body._position
if(body.type === SCNPhysicsBodyType.kinematic){
body._resetTransform()
}else if(body.type === SCNPhysicsBodyType.dynamic){
// TODO: move physics bodies
}
body._positionDiff = body._position.sub(body._prevPosition)
}
const staticType = SCNPhysicsBodyType.static
for(let i=0; i<objects.length; i++){
const bodyA = objects[i].presentation.physicsBody
if(bodyA.physicsShape._sourceGeometry instanceof SCNCapsule){
contacts.push(...SCNPhysicsWorld._capsuleTestWithObjects(bodyA, objects))
}
for(let j=0; j<objects.length; j++){
if(i === j){
continue
}
const bodyB = objects[j].presentation.physicsBody
contacts.push(...this.contactTestBetween(bodyA, bodyB))
}
}
if(this.contactDelegate){
for(const contact of contacts){
if(this.contactDelegate.physicsWorldDidBegin){
this.contactDelegate.physicsWorldDidBegin(this, contact)
}
}
// TODO: callback
// this.contactDelegate.physicsWorldDidUpdate
// this.contactDelegate.physicsWorldDidEnd
}
for(const obj of objects){
const body = obj.physicsBody
body._prevPosition = body._position
}
}
/**
* @access private
* @param {SCNPhysicsBody} body -
* @param {SCNNode[]} objects -
* @returns {SCNPhysicsContact[]} -
*/
static _capsuleTestWithObjects(body, objects) {
const result = []
const objs = objects.filter((obj) => {
const bodyB = obj.presentation.physicsBody
if(bodyB === body){
return false
}
if(bodyB.physicsShape._type !== SCNPhysicsShape.ShapeType.concavePolyhedron){
return false
}
if((body.categoryBitMask & bodyB.contactTestBitMask) !== 0){
return true
}
if((bodyB.categoryBitMask & body.contactTestBitMask) !== 0){
return true
}
return false
})
if(objs.length === 0){
return result
}
const bodyTransform = body._node._worldTransform
const capsule = body.physicsShape._sourceGeometry
for(const obj of objs){
if(!this._intersectsBoundingBox(body._node, obj)){
continue
}
const contacts = this._contactTestCapsuleAndConcave(body._node, obj)
result.push(...contacts)
}
return result
}
/**
*
* @access private
* @param {SCNNode} node1 -
* @param {SCNNode} node2 -
* @returns {boolean} -
*/
static _intersectsBoundingBox(node1, node2) {
const pos1 = node1._worldTranslation
const pos2 = node2._worldTranslation
const geo1 = node1.physicsBody.physicsShape._sourceGeometry
const geo2 = node2.physicsBody.physicsShape._sourceGeometry
if(!geo1 || !geo2){
return false
}
const box1 = geo1.boundingBox
const box2 = geo2.boundingBox
if((box1.min.x + pos1.x > box2.max.x + pos2.x) || (box1.max.x + pos1.x < box2.min.x + pos2.x)){
return false
}
if((box1.min.y + pos1.y > box2.max.y + pos2.y) || (box1.max.y + pos1.y < box2.min.y + pos2.y)){
return false
}
if((box1.min.z + pos1.z > box2.max.z + pos2.z) || (box1.max.z + pos1.z < box2.min.z + pos2.z)){
return false
}
return true
}
/**
* @access private
* @param {SCNNode} capNode - the node which has a capsule physicsShape
* @param {SCNNode} conNode - the node which has a concave physicsShape
* @returns {SCNPhysicsContact[]} -
*/
static _contactTestCapsuleAndConcave(capNode, conNode) {
const result = []
const capBody = capNode.physicsBody
const conBody = conNode.physicsBody
const capsule = capBody.physicsShape._sourceGeometry
const concave = conBody.physicsShape._sourceGeometry
const transform = capBody._transform.mult(conBody._invTransform)
const capSize = capsule.capRadius
const capHeight = capsule.height * 0.5 - capSize
const capV = capBody._positionDiff.rotate(transform).normalize()
const p0 = (new SCNVector3(0, capHeight, 0)).transform(transform)
const p1 = (new SCNVector3(0, -capHeight, 0)).transform(transform)
const elems = concave.geometryElements
const vert = concave.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
for(const elem of elems){
if(elem._primitiveType !== SCNGeometryPrimitiveType.triangles){
// TODO: support other primitive types.
continue
}
const edata = elem._data
const elen = elem._primitiveCount
let ind = 0
//console.warn(` elen = ${elen}`)
for(let i=0; i<elen; i++){
const v0 = vert._scnVectorAt(edata[ind])
const v1 = vert._scnVectorAt(edata[ind + 1])
const v2 = vert._scnVectorAt(edata[ind + 2])
ind += 3
//const n = this._normalOfTriangle(v0, v1, v2)
//if(n.dot(capV) >= 0){
// continue
//}
const contactInfo = this._capsuleTriangleContact(p0, p1, capSize, v0, v1, v2)
if(contactInfo){
const contact = new SCNPhysicsContact()
contact._nodeA = capNode
contact._nodeB = conNode
contact._contactPoint = contactInfo.point
contact._contactNormal = contactInfo.normal
contact._penetrationDistance = contactInfo.penetration
result.push(contact)
}
}
}
//console.warn(` result length = ${result.length}`)
return result
}
// http://marupeke296.com/COL_3D_No27_CapsuleCapsule.html
/**
*
* @access private
* @param {SCNVector3} p0 - position of an edge of the capsule (in the triangle's coordinate)
* @param {SCNVector3} p1 - position of another edge of the capsule (in the triangle's coordinate)
* @param {number} capSize - capsule radius
* @param {SCNVector3} v0 - vertex position (in the triangle's coordinate)
* @param {SCNVector3} v1 - vertex position (in the triangle's coordinate)
* @param {SCNVector3} v2 - vertex position (in the triangle's coordinate)
* @returns {?Object} -
* {SCNVector3} point -
* {SCNVector3} normal -
* {number} distance -
*/
static _capsuleTriangleContact(p0, p1, capSize, v0, v1, v2) {
const seg = p1.sub(p0)
const segTri = this._segmentTriangleIntersection(p0, p1, v0, v1, v2)
if(segTri.intersection){
let penetration = 0
if(segTri.d0 < 0){
penetration = capSize - segTri.d0
}else{
penetration = capSize - segTri.d1
}
return {
point: segTri.intersection,
normal: segTri.normal,
distance: 0,
penetration: penetration
}
}
const d0 = this._segmentSegmentDist(p0, p1, v0, v1)
let min = d0
const d1 = this._segmentSegmentDist(p0, p1, v1, v2)
if(d1.distance < min.distance){
min = d1
}
const d2 = this._segmentSegmentDist(p0, p1, v2, v0)
if(d2.distance < min.distance){
min = d2
}
const h0 = p0.add(segTri.normal.mul(-segTri.d0))
if(this._pointIsInsideTriangle(h0, v0, v1, v2)){
if(Math.abs(segTri.d0) < min.distance){
min.distance = Math.abs(segTri.d0)
min.nearestPos1 = h0
}
}
const h1 = p1.add(segTri.normal.mul(-segTri.d1))
if(this._pointIsInsideTriangle(h1, v0, v1, v2)){
if(Math.abs(segTri.d1) < min.distance){
min.distance = Math.abs(segTri.d1)
min.nearestPos1 = h1
}
}
if(min.distance < capSize){
return {
point: min.nearestPos1,
normal: segTri.normal,
distance: 0,
penetration: (capSize - min.distance)
}
}
return null
}
/**
*
* @access private
* @param {SCNVector3} p0 - an edge of the segment
* @param {SCNVector3} p1 - another edge of the segment
* @param {SCNVector3} v0 - the first point of the vertex
* @param {SCNVector3} v1 - the second point of the vertex
* @param {SCNVector3} v2 - the third point of the vertex
* @returns {Object} -
* {SCNVector3} normal - normal vector of the vertex
* {number} d0 - distance between p0 and the plane which contains the vertex
* {number} d1 - distance between p1 and the plane which contains the vertex
* {?SCNVector3} intersection - intersection point of the segment and the vertex
*/
static _segmentTriangleIntersection(p0, p1, v0, v1, v2) {
const v0p0 = p0.sub(v0)
const v0p1 = p1.sub(v0)
const n = this._normalOfTriangle(v0, v1, v2)
const d0 = v0p0.dot(n)
const d1 = v0p1.dot(n)
const result = { normal: n, d0: d0, d1: d1, intersection: null }
if(d0 * d1 > 0){
return result
}
const t = d0 / (d0 - d1)
const h = v0p0.mul(1-t).add(v0p1.mul(t)).add(v0)
if(!this._pointIsInsideTriangle(h, v0, v1, v2)){
return result
}
result.intersection = h
return result
}
/**
*
* @access private
* @param {SCNVector3} p0 - the first point of the triangle
* @param {SCNVector3} p1 - the second point of the triangle
* @param {SCNVector3} p2 - the third point of the triangle
* @returns {SCNVector3} - normal vector (normalized)
*/
static _normalOfTriangle(p0, p1, p2) {
const v1 = p1.sub(p0)
const v2 = p2.sub(p0)
return v1.cross(v2).normalize()
}
/**
*
* @access private
* @param {SCNVector3} p - point
* @param {SCNVector3} p0 - the first point of the triangle
* @param {SCNVector3} p1 - the second point of the triangle
* @param {SCNVector3} p2 - the third point of the triangle
* @returns {boolean} - true if the point is in the triangle.
*/
static _pointIsInsideTriangle(p, p0, p1, p2) {
const n = this._normalOfTriangle(p0, p1, p2)
const v0 = p1.sub(p0).cross(n).dot(p.sub(p0))
const v1 = p2.sub(p1).cross(n).dot(p.sub(p1))
const v2 = p0.sub(p2).cross(n).dot(p.sub(p2))
if(v0 < 0 && v1 < 0 && v2 < 0){
return true
}
if(v0 > 0 && v1 > 0 && v2 > 0){
return true
}
return false
}
/**
*
* @access private
* @param {SCNVector3} p - point
* @param {SCNVector3} lp - a point on the line
* @param {SCNVector3} lv - line vector
* @returns {Object} -
* {number} coeff -
* {SCNVector3} nearestPos -
* {number} distance -
*/
static _pointLineDist(p, lp, lv) {
const len2 = lv.length2()
let t = 0
if(len2 > 0){
t = lv.dot(p.sub(lp)) / len2
}
const h = lp.add(lv.mul(t))
const d = h.sub(p).length()
return {
coeff: t,
nearestPos: h,
distance: d
}
}
/**
*
* @access private
* @param {SCNVector3} p - point
* @param {SCNVector3} s0 - an edge of the segment
* @param {SCNVector3} s1 - another edge of the segment
* @returns {Object} -
* {number} coeff -
* {SCNVector3} nearestPos -
* {number} distance -
*/
static _pointSegmentDist(p, s0, s1) {
const lv = s1.sub(s0)
const plDist = this._pointLineDist(p, s0, lv)
if(plDist.coeff < 0){
const d = s0.sub(p).length()
return {
coeff: plDist.coeff,
nearestPos: s0,
distance: d
}
}else if(plDist.coeff > 1){
const d = s1.sub(p).length()
return {
coeff: plDist.coeff,
nearestPos: s1,
distance: d
}
}
return plDist
}
/**
*
* @access private
* @param {SCNVector3} p0 - a point on the first line
* @param {SCNVector3} v0 - a line vector
* @param {SCNVector3} p1 - a point on the second line
* @param {SCNVector3} v1 - a line vector
* @returns {Object} -
* {number} coeff0 -
* {SCNVector3} nearestPos0 -
* {number} coeff1 -
* {SCNVector3} nearestPos1 -
* {number} distance -
*/
static _lineLineDist(p0, v0, p1, v1) {
if(this._isParallel(v0, v1)){
const plDist = this._pointLineDist(p0, p1, v1)
return {
coeff0: 0,
nearestPos0: p0,
coeff1: plDist.coeff,
nearestPos1: plDist.nearestPos,
distance: plDist.distance
}
}
const v01 = v0.dot(v1)
const v00 = v0.dot(v0)
const v11 = v1.dot(v1)
const p10 = p0.sub(p1)
const coeff0 = (v01 * v1.dot(p10) - v11 * v0.dot(p10)) / (v00 * v11 - v01 * v01)
const np0 = p0.add(v0.mul(coeff0))
const coeff1 = v1.dot(np0.sub(p1)) / v11
const np1 = p1.add(v1.mul(coeff1))
const d = np1.sub(np0).length()
return {
coeff0: coeff0,
nearestPos0: np0,
coeff1: coeff1,
nearestPos1: np1,
distance: d
}
}
/**
*
* @access private
* @param {SCNVector3} v0 - line vector
* @param {SCNVector3} v1 - line vector
* @returns {boolean} - true if the lines are parallel
*/
static _isParallel(v0, v1) {
const l = v0.cross(v1).length2()
return (l < 0.0000000000001)
}
/**
*
* @access private
* @param {number} -
* @returns {number} -
*/
static _clamp(val) {
if(val < 0){
return 0
}
if(val > 1){
return 1
}
return val
}
/**
*
* @access private
* @param {SCNVector3} s00 - an edge of the first segment
* @param {SCNVector3} s01 - another edge of the first segment
* @param {SCNVector3} s10 - an edge of the second segment
* @param {SCNVector3} s11 - another edge of the second segment
* @returns {Object} -
* {number} coeff0 -
* {SCNVector3} nearestPos0 -
* {number} coeff1 -
* {SCNVector3} nearestPos1 -
* {number} distance -
*/
static _segmentSegmentDist(s00, s01, s10, s11) {
const v0 = s01.sub(s00)
const v1 = s11.sub(s10)
let dist = null
if(this._isParallel(v0, v1)){
dist = this._pointSegmentDist(s00, s10, s11)
if(0.0 <= dist.coeff && dist.coeff <= 1.0){
return {
coeff0: 0.0,
nearestPos0: s00,
coeff1: dist.coeff,
nearestPos1: dist.nearestPos,
distance: dist.distance
}
}
dist.coeff0 = 0.0
dist.coeff1 = dist.coeff
}else{
dist = this._lineLineDist(s00, v0, s10, v1)
if(0.0 <= dist.coeff0 && dist.coeff0 <= 1.0
&& 0.0 <= dist.coeff1 && dist.coeff1 <= 1.0){
return dist
}
}
let dist2 = dist
const t0 = this._clamp(dist.coeff0)
if(t0 !== dist.coeff0){
const p0 = s00.add(v0.mul(t0))
dist2 = this._pointSegmentDist(p0, s10, s11)
if(0.0 <= dist2.coeff && dist2.coeff <= 1.0){
return {
coeff0: t0,
nearestPos0: p0,
coeff1: dist2.coeff,
nearestPos1: dist2.nearestPos,
distance: dist2.distance
}
}
dist2.coeff1 = dist2.coeff
}
const t1 = this._clamp(dist2.coeff1)
const p1 = s10.add(v1.mul(t1))
const dist3 = this._pointSegmentDist(p1, s00, s01)
if(0.0 <= dist3.coeff && dist3.coeff <= 1.0){
return {
coeff0: dist3.coeff,
nearestPos0: dist3.nearestPos,
coeff1: t1,
nearestPos1: p1,
distance: dist3.distance
}
}
const t = this._clamp(dist3.coeff)
const p = s00.add(v0.mul(t))
const d = p1.sub(p).length()
return {
coeff0: t,
nearestPos0: p,
coeff1: t1,
nearestPos1: p1,
distance: d
}
}
/**
* @access private
* @param {SCNVector3} p0 - An endpoint of the line segment to test, specified in the world coordinate system.
* @param {SCNVector3} p1 - The other endpoint of the line segment to test, specified in the world coordinate system.
* @param {SCNNode} node -
* @returns {SCNHitTestResult[]} -
*/
static _hitTestWithSegmentNode(pointA, pointB, node) {
let n = node
if(node.presentation && node.presentation.geometry){
n = node.presentation
}
const geo = n.geometry
if(!geo){
return []
}
const pA = n.convertPositionFrom(pointA, null)
const pB = n.convertPositionFrom(pointB, null)
//if(this._segmentBoundingBoxIntersects(pA, pB, geo.boundingBox) !== null){
const r = this._segmentBoundingBoxIntersects(pA, pB, geo.boundingBox)
if(r !== null){
console.error('segmentBoundingBoxIntersects: ' + r.near + ', ' + r.far)
return this._hitTestWithSegmentGeometry(pA, pB, geo)
}
return []
}
/**
* @access private
* @param {SCNVector3} p0 - An endpoint of the line segment to test, specified in the world coordinate system.
* @param {SCNVector3} p1 - The other endpoint of the line segment to test, specified in the world coordinate system.
* @param {SCNNode} node -
* @returns {SCNHitTestResult[]} -
*/
static _hitTestWithSegmentPhysicsNode(pointA, pointB, node) {
let n = node
if(node.presentation && node.presentation.physicsBody){
n = node.presentation
}
const body = n.physicsBody
if(body === null){
return []
}
const shape = body.physicsShape
if(shape === null){
return []
}
const geo = shape._sourceGeometry
if(geo === null){
return []
}
const pA = pointA.transform(body._invTransform)
const pB = pointB.transform(body._invTransform)
const r = this._segmentBoundingBoxIntersects(pA, pB, geo.boundingBox)
if(r !== null){
return this._hitTestWithSegmentGeometry(pA, pB, geo)
}
return []
}
/**
* @access private
* @param {SCNVector3} pointA -
* @param {SCNVector3} pointB -
* @param {Object} boundingBox -
* @results {?Object} -
*/
static _segmentBoundingBoxIntersects(pointA, pointB, boundingBox) {
const v = pointB.sub(pointA)
const r = this._lineBoundingBoxIntersects(pointA, v, boundingBox)
if(r === null){
return null
}
if(r.near > 1 || r.far < 0){
return null
}
return r
}
/**
* @access private
* @param {SCNVector3} p - a point on the line
* @param {SCNVector3} v - line vector
* @param {Object} boundingBox -
* @returns {?Object} -
*/
static _lineBoundingBoxIntersects(p, v, boundingBox) {
const epsilon = 0.000001
const odd = new SCNVector3(1.0/v.x, 1.0/v.y, 1.0/v.z)
const bmin = boundingBox.min
const bmax = boundingBox.max
const t1 = bmin.sub(p).mulv(odd)
const t2 = bmax.sub(p).mulv(odd)
let near = -Infinity
let far = Infinity
if(Math.abs(v.x) < epsilon){
if(p.x < bmin.x || bmax.x < p.x){
return null
}
}else if(t1.x < t2.x){
near = Math.max(near, t1.x)
far = Math.min(far, t2.x)
}else{
near = Math.max(near, t2.x)
far = Math.min(far, t1.x)
}
if(Math.abs(v.y) < epsilon){
if(p.y < bmin.y || bmax.y < p.y){
return null
}
}else if(t1.y < t2.y){
near = Math.max(near, t1.y)
far = Math.min(far, t2.y)
}else{
near = Math.max(near, t2.y)
far = Math.min(far, t1.y)
}
if(Math.abs(v.z) < epsilon){
if(p.z < bmin.z || bmax.z < p.z){
return null
}
}else if(t1.z < t2.z){
near = Math.max(near, t1.z)
far = Math.min(far, t2.z)
}else{
near = Math.max(near, t2.z)
far = Math.min(far, t1.z)
}
if(near > far){
return null
}
return { near: near, far: far }
}
/**
* @access private
* @param {SCNVector3} p0 - An endpoint of the line segment to test, specified in the geometry's local coordinate system.
* @param {SCNVector3} p1 - The other endpoint of the line segment to test, specified in the geometry's local coordinate system.
* @param {SCNGeometry} geometry -
* @returns {SCNHitTestResult[]} -
*/
static _hitTestWithSegmentGeometry(pointA, pointB, geometry) {
const results = []
const elems = geometry.geometryElements
const elemCount = elems.length
const vert = geometry.getGeometrySourcesForSemantic(SCNGeometrySource.Semantic.vertex)[0]
for(let i=0; i<elemCount; i++){
const elem = elems[i]
if(elem._primitiveType !== SCNGeometryPrimitiveType.triangles){
// TODO: support other primitive types.
continue
}
const edata = elem._data
const elen = elem._primitiveCount
let ind = 0
for(let j=0; j<elen; j++){
const v0 = vert._scnVectorAt(edata[ind])
const v1 = vert._scnVectorAt(edata[ind + 1])
const v2 = vert._scnVectorAt(edata[ind + 2])
ind += 3
const r = this._segmentTriangleIntersection(pointA, pointB, v0, v1, v2)
if(r.intersection){
const result = new SCNHitTestResult()
result._geometryIndex = i
result._faceIndex = j
result._localCoordinates = r.intersection
result._localNormal = r.normal
result._distance = r.intersection.sub(pointA).length()
results.push(result)
}
}
}
return results
}
}