/**
* Copyright (c) 2003-2009, Xith3D Project Group all rights reserved.
*
* Portions based on the Java3D interface, Copyright by Sun Microsystems.
* Many thanks to the developers of Java3D and Sun Microsystems for their
* innovation and design.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the 'Xith3D Project Group' nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.xith3d.input.modules.fpih;
import org.jagatoo.opengl.enums.DrawMode;
import org.openmali.FastMath;
import org.openmali.spatial.AxisIndicator;
import org.openmali.vecmath2.Colorf;
import org.openmali.vecmath2.Tuple3f;
import org.openmali.vecmath2.Vector3f;
import org.xith3d.input.FirstPersonInputHandler;
import org.xith3d.input.modules.ColliderCheckCallback;
import org.xith3d.input.modules.SlidingColliderCheckCallback;
import org.xith3d.physics.PhysicsEngine;
import org.xith3d.physics.collision.Collideable;
import org.xith3d.physics.collision.CollideableGroup;
import org.xith3d.physics.collision.CollisionEngine;
import org.xith3d.physics.simulation.Body;
import org.xith3d.physics.simulation.SimulationEngine;
import org.xith3d.physics.simulation.SimulationWorld;
import org.xith3d.scenegraph.Node;
import org.xith3d.scenegraph.Shape3D;
import org.xith3d.scenegraph.StaticTransform;
import org.xith3d.scenegraph.TransformGroup;
import org.xith3d.scenegraph.Transformable;
/**
* This is the physics code for the {@link FirstPersonInputHandler}.
*
* @author Marvin Froehlich (aka Qudus)
*/
public class FPIHPhysics
{
private final SimulationWorld simWorld;
private final float width;
private final float height;
private final float halfWidthEpsilon;
private final float halfHeightEpsilon;
private final float maxHalfEpsilon;
private final Collideable collider;
private final Body body;
private final Node colliderDebugger;
private final TransformGroup colliderDebuggerTG;
private boolean collisionsEnabled = true;
private boolean simulationEnabled = true;
private Vector3f colliderOffset = null;
private ColliderCheckCallback colliderCallback = null;
private long lastGameMicros = 0L;
private long lastCollisionTime = -999999999L;
private boolean isJumping = false;
private final Tuple3f lastBodyPos = new Tuple3f();
private final Vector3f lastCollisionDirection = new Vector3f();
public void setCollisionsEnabled( boolean enabled )
{
this.collisionsEnabled = enabled;
if ( colliderDebuggerTG != null )
{
colliderDebuggerTG.setRenderable( enabled );
}
}
public final boolean areCollisionsEnabled()
{
return ( collisionsEnabled );
}
public void setSimulationEnabled( boolean enabled )
{
this.simulationEnabled = enabled;
}
public final boolean isSimulationEnabled()
{
return ( simulationEnabled );
}
public final void updateGameTime( long gameMicros )
{
this.lastGameMicros = gameMicros;
}
public final long getLastGameTime()
{
return ( lastGameMicros );
}
public final Collideable getCollider()
{
return ( collider );
}
public final Body getBody()
{
return ( body );
}
public final Node getDebugNode()
{
return ( colliderDebuggerTG );
}
/**
* Sets the collision-avatar-offset in world coordinates.
*
* @param offsetX
* @param offsetY
* @param offsetZ
*/
public void setColliderOffset( float offsetX, float offsetY, float offsetZ )
{
if ( (offsetX == 0f) && (offsetY == 0f) && (offsetZ == 0f) )
{
this.colliderOffset = null;
return;
}
if ( this.colliderOffset == null )
this.colliderOffset = new Vector3f( offsetX, offsetY, offsetZ );
else
this.colliderOffset.set( offsetX, offsetY, offsetZ );
}
/**
* Sets the collision-avatar-offset in world coordinates.
*
* @param offset
*/
public void setColliderOffset( Tuple3f offset )
{
if ( offset == null )
{
this.colliderOffset = null;
//updateViewInverse();
return;
}
setColliderOffset( offset.getX(), offset.getY(), offset.getZ() );
}
/**
* @return the collision-avatar-offset in world coordinates.
*/
public Vector3f getColliderOffset()
{
return ( colliderOffset );
}
/**
* Called, when the player jumped.
* This method will never contain any code and can easily been
* overridden.
*
* @return if the jump was accepted
*/
public boolean startJump( AxisIndicator upAxis )
{
if ( ( lastGameMicros <= lastCollisionTime + 100000L ) && ( lastCollisionDirection.getY() < 0f ) )
{
if ( simulationEnabled && ( body != null ) )
{
final Vector3f vel = body.getLinearVelocity();
switch ( upAxis )
{
case POSITIVE_X_AXIS:
body.setLinearVelocity( +5, vel.getY(), vel.getZ() );
break;
case NEGATIVE_X_AXIS:
body.setLinearVelocity( -5, vel.getY(), vel.getZ() );
break;
case POSITIVE_Y_AXIS:
body.setLinearVelocity( vel.getX(), +5f, vel.getZ() );
break;
case NEGATIVE_Y_AXIS:
body.setLinearVelocity( vel.getX(), -5f, vel.getZ() );
break;
case POSITIVE_Z_AXIS:
body.setLinearVelocity( vel.getX(), vel.getY(), +5 );
break;
case NEGATIVE_Z_AXIS:
body.setLinearVelocity( vel.getX(), vel.getY(), -5 );
break;
}
}
isJumping = true;
return ( true );
}
return ( false );
}
public void updateFromView( Tuple3f viewPosition )
{
if ( body != null )
{
Tuple3f tmp = Tuple3f.fromPool( viewPosition );
if ( colliderOffset != null )
{
tmp.sub( colliderOffset );
}
body.setPosition( tmp );
collider.setPosition( tmp );
lastBodyPos.set( tmp );
body.resetLinearVelocity();
body.resetAngularVelocity();
Tuple3f.toPool( tmp );
}
}
/**
* Updates the view position from the collider.
*
* @param view the {@link Transformable} to update
* @param tpOffset2 the third-person-offset in view-space
*/
private void updateFromCollider( Transformable view, Vector3f tpOffset2 )
{
Tuple3f pos = Tuple3f.fromPool();
collider.getPosition( pos );
if ( simulationEnabled && ( body != null ) )
{
body.setPosition( pos );
lastBodyPos.set( pos );
}
if ( tpOffset2 != null )
{
pos.add( tpOffset2 );
}
if ( colliderOffset != null )
{
pos.sub( colliderOffset );
}
view.getTransform().setTranslation( pos );
if ( colliderDebuggerTG != null )
{
colliderDebuggerTG.getTransform().setTranslation( pos );
colliderDebuggerTG.updateTransform();
}
Tuple3f.toPool( pos );
}
private boolean updateAndCheckCollider( Vector3f colliderPosition )
{
collider.setPosition( colliderPosition );
if ( colliderCallback != null )
{
if ( colliderCallback.checkCollision( collider ) )
{
lastCollisionDirection.sub( colliderPosition, collider.getPosition() );
collider.getPosition( colliderPosition );
return ( true );
}
}
return ( false );
}
/**
*
* @param gameMicros
* @param frameMicros
* @param view
* @param deltaMovement
* @param rotX
* @param rotY
* @param thirdPersonOffset
*/
public void update( long gameMicros, long frameMicros, Transformable view, Vector3f deltaMovement, float rotX, float rotY, Vector3f thirdPersonOffset )
{
if ( !collisionsEnabled )
{
return;
}
Vector3f colliderPosition = Vector3f.fromPool();
Vector3f bodyDelta = Vector3f.fromPool( 0f, 0f, 0f );
Vector3f totalDelta = Vector3f.fromPool();
// get collider position...
{
view.getTransform().getTranslation( colliderPosition );
if ( colliderOffset != null )
{
colliderPosition.add( colliderOffset );
}
colliderPosition.sub( thirdPersonOffset );
}
// get body-position and delta...
if ( simulationEnabled && ( body != null ) )
{
bodyDelta.sub( body.getPosition(), lastBodyPos );
colliderPosition.add( bodyDelta );
}
totalDelta.add( deltaMovement, bodyDelta );
final float totalDeltaLength = totalDelta.length();
boolean hasCollision = false;
if ( ( totalDeltaLength <= maxHalfEpsilon ) && ( !simulationEnabled || ( body == null ) || ( frameMicros <= simWorld.getMaxStepSize() ) ) )
{
hasCollision = updateAndCheckCollider( colliderPosition );
}
else
{
final float distanceStepFraction0 = totalDeltaLength / maxHalfEpsilon;
final float timeStepFraction0 = (float)frameMicros / ( ( simulationEnabled && ( body != null ) ) ? (float)simWorld.getMaxStepSize() : 1f );
final float stepFraction0 = ( distanceStepFraction0 > timeStepFraction0 ) ? distanceStepFraction0 : timeStepFraction0;
Vector3f oldColliderPosition = Vector3f.fromPool();
oldColliderPosition.sub( colliderPosition, deltaMovement );
oldColliderPosition.sub( bodyDelta );
float stepFraction = stepFraction0;
int step = 0;
while ( ( stepFraction > 0f ) && !hasCollision )
{
colliderPosition.set( totalDelta );
colliderPosition.mul( (float)(++step) / stepFraction0 );
colliderPosition.add( oldColliderPosition );
hasCollision = updateAndCheckCollider( colliderPosition );
stepFraction -= 1f;
}
Vector3f.toPool( oldColliderPosition );
}
if ( hasCollision )
{
if ( simulationEnabled && ( body != null ) )
{
if ( !isJumping || ( gameMicros > lastCollisionTime + 100000L ) )
{
body.resetLinearVelocity();
isJumping = false;
}
}
updateFromCollider( view, thirdPersonOffset );
collider.getPosition( colliderPosition );
lastCollisionTime = gameMicros;
}
else if ( simulationEnabled && ( body != null ) )
{
updateFromCollider( view, thirdPersonOffset );
}
if ( colliderDebuggerTG != null )
{
colliderDebuggerTG.getTransform().setEuler( 0f, rotY, 0f );
colliderDebuggerTG.getTransform().setTranslation( colliderPosition );
colliderDebuggerTG.updateTransform();
}
Vector3f.toPool( totalDelta );
Vector3f.toPool( bodyDelta );
Vector3f.toPool( colliderPosition );
}
/**
* Use this to check for avatar-collisions from.
*
* @param ccc
*/
public void setColliderCheckCallback( ColliderCheckCallback ccc )
{
this.colliderCallback = ccc;
}
public SlidingColliderCheckCallback setSlidingColliderCheckCallback( CollisionEngine collEngine, CollideableGroup collGroup )
{
SlidingColliderCheckCallback ccc = new SlidingColliderCheckCallback( collEngine, collGroup );
setColliderCheckCallback( ccc );
return ( ccc );
}
public void init( Transformable view, Vector3f tpOffset )
{
Vector3f viewPosition = Vector3f.fromPool();
view.getPosition( viewPosition );
if ( tpOffset != null )
{
Vector3f tpOffset2 = Vector3f.fromPool();
view.getTransform().getMatrix4f().transform( tpOffset, tpOffset2 );
viewPosition.add( tpOffset2 );
Vector3f.toPool( tpOffset2 );
}
if ( colliderOffset != null )
{
viewPosition.add( colliderOffset );
}
if ( body == null )
collider.setPosition( viewPosition );
else
body.setPosition( viewPosition );
if ( colliderDebuggerTG != null )
{
colliderDebuggerTG.getTransform().setTranslation( viewPosition );
colliderDebuggerTG.setTransform( colliderDebuggerTG.getTransform() );
}
Vector3f.toPool( viewPosition );
}
/**
* Creates the {@link Collideable}, that is used to check for FPIH
* collisions with the environment.
*
* @param collEngine
* @param width
* @param height
*
* @return the Collideable
*/
protected Collideable createCollideable( CollisionEngine collEngine, float width, float height )
{
//return ( collEngine.newCapsule( width / 2.0f, height - width ) );
/*
* As long as JOODE doesn't support trimesh-capsule collision-detection
* we need to simulate it with two spheres and a cylinder.
*/
CollideableGroup group = collEngine.newGroup( "Simple" );
//Collideable cylinder = collEngine.newCylinder( width / 2f, height - width );
//group.addCollideable( cylinder );
Collideable sphereTop = collEngine.newSphere( width / 2f );
sphereTop.setPositionZ( ( height - width ) / 2f );
group.addCollideable( sphereTop );
Collideable sphereBottom = collEngine.newSphere( width / 2f );
sphereBottom.setPositionZ( -( height - width ) / 2f );
group.addCollideable( sphereBottom );
return ( group );
}
/**
* Creates the Node (probably a Shape3D), that visualizes the Collideable.
*
* @param width
* @param height
*
* @return the Collideable-debugger Node
*/
protected Node createCollideableDebugger( AxisIndicator upAxis, float width, float height )
{
Shape3D colliderDebugger = new org.xith3d.scenegraph.primitives.Capsule( width / 2.0f, height - width, 8, 4, Colorf.BLUE );
switch ( upAxis )
{
case POSITIVE_X_AXIS:
StaticTransform.rotateZ( colliderDebugger, FastMath.PI_HALF );
break;
case NEGATIVE_X_AXIS:
StaticTransform.rotateZ( colliderDebugger, FastMath.PI_HALF );
break;
case POSITIVE_Y_AXIS:
case NEGATIVE_Y_AXIS:
break;
case POSITIVE_Z_AXIS:
StaticTransform.rotateX( colliderDebugger, FastMath.PI_HALF );
break;
case NEGATIVE_Z_AXIS:
StaticTransform.rotateX( colliderDebugger, FastMath.PI_HALF );
break;
}
colliderDebugger.getAppearance( true ).getPolygonAttributes( true ).setDrawMode( DrawMode.LINE );
return ( colliderDebugger );
}
/**
*
* @param upAxis
* @param width
* @param height
* @param offset
* @param collEngine
* @param simEngine
* @param simWorld
* @param integrateWithSimulation
* @param addDebugShape
*/
public FPIHPhysics( AxisIndicator upAxis, float width, float height, Vector3f offset, CollisionEngine collEngine, SimulationEngine simEngine, SimulationWorld simWorld, boolean integrateWithSimulation, boolean addDebugShape )
{
this.simWorld = simWorld;
this.width = width;
this.height = height;
this.halfWidthEpsilon = ( this.width / 2f ) * 0.9f;
this.halfHeightEpsilon = ( this.height / 2f ) * 0.9f;
this.maxHalfEpsilon = Math.max( this.halfWidthEpsilon, this.halfHeightEpsilon );
this.collider = createCollideable( collEngine, width, height );
if ( integrateWithSimulation )
{
this.body = simWorld.newBody();
body.setMass( 85f );
switch ( upAxis )
{
case POSITIVE_X_AXIS:
body.setRotation( 0f, FastMath.PI_HALF, 0f );
break;
case NEGATIVE_X_AXIS:
body.setRotation( 0f, FastMath.PI_HALF, 0f );
break;
case POSITIVE_Y_AXIS:
body.setRotation( FastMath.PI_HALF, 0f, 0f );
break;
case NEGATIVE_Y_AXIS:
body.setRotation( -FastMath.PI_HALF, 0f, 0f );
break;
case POSITIVE_Z_AXIS:
break;
case NEGATIVE_Z_AXIS:
break;
}
body.addCollideable( collider );
if ( simWorld != null )
simWorld.addBody( body );
}
else
{
switch ( upAxis )
{
case POSITIVE_X_AXIS:
collider.setRotation( 0f, FastMath.PI_HALF, 0f );
break;
case NEGATIVE_X_AXIS:
collider.setRotation( 0f, FastMath.PI_HALF, 0f );
break;
case POSITIVE_Y_AXIS:
collider.setRotation( FastMath.PI_HALF, 0f, 0f );
break;
case NEGATIVE_Y_AXIS:
collider.setRotation( -FastMath.PI_HALF, 0f, 0f );
break;
case POSITIVE_Z_AXIS:
break;
case NEGATIVE_Z_AXIS:
break;
}
this.body = null;
}
this.colliderOffset = offset;
if ( addDebugShape )
{
this.colliderDebugger = createCollideableDebugger( upAxis, width, height );
this.colliderDebuggerTG = new TransformGroup();
colliderDebuggerTG.addChild( colliderDebugger );
colliderDebuggerTG.setPickableRecursive( false );
}
else
{
this.colliderDebugger = null;
this.colliderDebuggerTG = null;
}
}
public FPIHPhysics( float width, float height, Vector3f offset, CollisionEngine collEngine, SimulationEngine simEngine, SimulationWorld simWorld, boolean integrateWithSimulation, boolean addDebugShape )
{
this( AxisIndicator.POSITIVE_Y_AXIS, width, height, offset, collEngine, simEngine, simWorld, integrateWithSimulation, addDebugShape );
}
public FPIHPhysics( float width, float height, Vector3f offset, CollisionEngine collEngine, SimulationEngine simEngine, boolean integrateWithSimulation, boolean addDebugShape )
{
this( width, height, offset, collEngine, simEngine, null, integrateWithSimulation, addDebugShape );
}
public FPIHPhysics( float width, float height, Vector3f offset, PhysicsEngine physEngine, SimulationWorld simWorld, boolean integrateWithSimulation, boolean addDebugShape )
{
this( width, height, offset, physEngine.getCollisionEngine(), physEngine.getSimulationEngine(), simWorld, integrateWithSimulation, addDebugShape );
}
public FPIHPhysics( float width, float height, Vector3f offset, PhysicsEngine physEngine, boolean integrateWithSimulation, boolean addDebugShape )
{
this( width, height, offset, physEngine.getCollisionEngine(), physEngine.getSimulationEngine(), integrateWithSimulation, addDebugShape );
}
public FPIHPhysics( float width, float height, Vector3f offset, CollisionEngine collEngine, boolean addDebugShape )
{
this( width, height, offset, collEngine, null, false, addDebugShape );
}
public FPIHPhysics( float width, float height, Vector3f offset, CollisionEngine collEngine )
{
this( width, height, offset, collEngine, false );
}
}