Collision detection
Contents
Collision detection
jPCT has basically three different methods for collision detection. Depending on the application and its requirements, one may be more suitable than the other. This section provides a brief overview. To use any kind of collision detection, the collision mode has to be set correctly. An Object3D can either receive collisions from other objects/entities, cause collisions or both (or nothing). Collision detection on complex models largely benefits from using octrees.
Ray-Polygon
Methods using this approach usually are named checkXXXCollision(...). The idea of this collision test is simple: A ray will be casted into the scene and the closest polygon in its way will block it. There another method that works very similar to this in World: calcMinDistance. Its basically the same stuff, but it returns the actual distance to the hit polygon which the other checkXXXCollision-methods won't. This method is fine for detecting bullet hits, for placing a car's wheel on the ground (the car-example is using it for example) and similar.
Sphere-Polygon
Methods using this approach usually are named checkXXXCollisionSpherical(...). Here, a sphere is queried if some other object intersects with it. If this is the case, the translation vector will be adjusted, so that the collision won't happen (if you actually apply the corrected vector, of course you don't have to). This isn't a swept approach, i.e. for a given translation vector, two tests happen: One at the start of the translation and one at the end. If the translation is too large for and/or the sphere's radius too small to make these tests cover the whole translation, any collision in between won't be recognized. This method is great for resolving collisions, i.e. if some object already is in a collision, this method can help to pull it out of it. Personally, i'm using this method to compensate for accuracy problems of the other methods, i.e. if they cause an intersection where they should actually avoid it, i'm using method to resolve it. I don't recommend to use this method for a FPS game or similar.
Ellipsoid-Polygon
This is the most powerful collision detection approach in jPCT. Methods using it are usually named checkXXXCollisionEllipsoid(...). It's similar to the spherical approach, but it has some advantages. At first, it's using an ellipsoid, not a sphere. That means, that it's easier to approximate human-like objects for example with it than it would be with sphere. However, you can always use a sphere as your ellipsoid. Second, it's a swept algorithm, i.e. all collisions along the translation will be detected and the translation will be adjusted while this happens. Compared to the spherical approach, the corrected translation isn't really capable of resolving an existing intersection, but it's very good in avoiding it. With this collision detection method, it's pretty easy to let your character climb stairs, rocks etc... One drawback: This is the slowest approach of all three.
Trouble shooting
Some common problems explained in short:
- Collision detection fails: Try to adjust the collideOffset. If your scene uses larger polygons than i had in mind when setting this value to a default of 40, collision detection will fails on such polygons. Keep in mind that lower values are usually faster, so don't increase it to 1000000something with no need.
- Source and/or target collisions are not working: Make sure that the collision mode has been set correctly ([1]/[2]). If an object should be both, don't call the setCollisionMode-method twice, but combine the constants with |.
- Collision detection works somehow, but the object in question doesn't move smooth. It looks like as if it collides with something but nothing can be seen: If an object is both, target and source of a collision, it may collide with itself. Adjust the collision mode before checking for collisions of that object and reset it afterwards. Another problem can be caused by the collision threshold being set to a pretty low value. Play around with this value.
Other methods
There are no other methods build into jPCT, but you can always use a Physics engine of your choice instead of the build in methods.
A simple example
This is a (pretty hacky) example code that uses the software renderer of desktop jPCT to render some primitives plus a kind of player (a simple cube) that you can move around with the arrow keys. Watch out for the borders of the plane, nothing prevents you from falling into the abyss. This code also shows the use of multi-threading for the software renderer.
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.threed.jpct.*; import com.threed.jpct.util.*; public class CollisionDemoSoftware extends JFrame { private static final long serialVersionUID = 1L; private static final float DAMPING = 0.1f; private static final float SPEED = 1f; private static final float MAXSPEED = 1f; private Graphics g = null; private KeyMapper keyMapper = null; private FrameBuffer fb = null; private World world = null; private Object3D plane = null; private Object3D ramp = null; private Object3D cube = null; private Object3D cube2 = null; private Object3D sphere = null; private boolean up = false; private boolean down = false; private boolean left = false; private boolean right = false; private boolean doloop = true; private SimpleVector moveRes = new SimpleVector(0, 0, 0); private SimpleVector ellipsoid = new SimpleVector(2, 2, 2); public CollisionDemoSoftware() { int numberOfProcs = Runtime.getRuntime().availableProcessors(); Config.useMultipleThreads = numberOfProcs > 1; Config.useMultiThreadedBlitting = numberOfProcs > 1; Config.loadBalancingStrategy = 1; Config.maxNumberOfCores = numberOfProcs; Config.lightMul = 1; Config.mtDebug = true; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); pack(); setSize(1024, 768); setResizable(false); setLocationRelativeTo(null); setVisible(true); g = getGraphics(); } private void initStuff() { fb = new FrameBuffer(1024, 768, FrameBuffer.SAMPLINGMODE_NORMAL); world = new World(); fb.enableRenderer(IRenderer.RENDERER_SOFTWARE); keyMapper = new KeyMapper(this); plane = Primitives.getPlane(20, 10); plane.rotateX((float) Math.PI / 2f); ramp = Primitives.getCube(20); ramp.rotateX((float) Math.PI / 2f); sphere = Primitives.getSphere(30); sphere.translate(-50, 10, 50); cube2 = Primitives.getCube(20); cube2.translate(60, -20, 60); cube = Primitives.getCube(2); cube.translate(-50, -10, -50); plane.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS); ramp.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS); sphere.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS); cube2.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS); cube.setCollisionMode(Object3D.COLLISION_CHECK_SELF); world.addObject(plane); world.addObject(ramp); world.addObject(cube); world.addObject(sphere); world.addObject(cube2); Light light = new Light(world); light.setPosition(new SimpleVector(0, -80, 0)); light.setIntensity(140, 120, 120); light.setAttenuation(-1); world.setAmbientLight(20, 20, 20); world.buildAllObjects(); } private void move() { KeyState ks = null; while ((ks = keyMapper.poll()) != KeyState.NONE) { if (ks.getKeyCode() == KeyEvent.VK_UP) { up = ks.getState(); } if (ks.getKeyCode() == KeyEvent.VK_DOWN) { down = ks.getState(); } if (ks.getKeyCode() == KeyEvent.VK_LEFT) { left = ks.getState(); } if (ks.getKeyCode() == KeyEvent.VK_RIGHT) { right = ks.getState(); } if (ks.getKeyCode() == KeyEvent.VK_ESCAPE) { doloop = false; } } // move the cube if (up) { SimpleVector t = cube.getZAxis(); t.scalarMul(SPEED); moveRes.add(t); } if (down) { SimpleVector t = cube.getZAxis(); t.scalarMul(-SPEED); moveRes.add(t); } if (left) { cube.rotateY((float) Math.toRadians(-1)); } if (right) { cube.rotateY((float) Math.toRadians(1)); } // avoid high speeds if (moveRes.length() > MAXSPEED) { moveRes.makeEqualLength(new SimpleVector(0, 0, MAXSPEED)); } cube.translate(0, -0.02f, 0); moveRes = cube.checkForCollisionEllipsoid(moveRes, ellipsoid, 8); cube.translate(moveRes); // finally apply the gravity: SimpleVector t = new SimpleVector(0, 1, 0); t = cube.checkForCollisionEllipsoid(t, ellipsoid, 1); cube.translate(t); // damping if (moveRes.length() > DAMPING) { moveRes.makeEqualLength(new SimpleVector(0, 0, DAMPING)); } else { moveRes = new SimpleVector(0, 0, 0); } } private void doIt() throws Exception { Camera cam = world.getCamera(); cam.moveCamera(Camera.CAMERA_MOVEOUT, 100); cam.moveCamera(Camera.CAMERA_MOVEUP, 100); cam.lookAt(ramp.getTransformedCenter()); long start = System.currentTimeMillis(); long fps = 0; while (doloop) { move(); cam.setPositionToCenter(cube); cam.align(cube); cam.rotateCameraX((float) Math.toRadians(30)); cam.moveCamera(Camera.CAMERA_MOVEOUT, 100); fb.clear(Color.RED); world.renderScene(fb); world.draw(fb); fb.update(); fb.display(g); fps++; if (System.currentTimeMillis() - start >= 1000) { start = System.currentTimeMillis(); System.out.println(fps); fps = 0; } } fb.disableRenderer(IRenderer.RENDERER_SOFTWARE); System.exit(0); } public static void main(String[] args) throws Exception { CollisionDemoSoftware cd = new CollisionDemoSoftware(); cd.initStuff(); cd.doIt(); } }