Camera :
- player (camera view target) will never be blocked by another spatial (creature/building).
This is done by raycasting. When another spatial blocks the view, the camera zooms in,
until player is visible.
- When the player moves away from the blocking spatial, the camera will try to zoom out until
it reaches its “preferred” zoom distance.
Controls:
- player look direction can be different from camera direction. I tried to make it like “kingdom of amalur” camera.
Performance cost: Without: 750 fps. With: 700 fps.
Zoomed out at distance 50
Behind building Zoom in
Applet For those lazy to test it: http://rpggame.ucoz.com/test/CollisionCamera/index.html
to run : TestCollisionCamera example you need : town.zip from
https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:beginner:hello_collision
[java]
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.input.ChaseCamera;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
public class CollisionChaseCamera extends ChaseCamera
{
protected Node collidableScene;
protected float zoomAtPreferedDistanceSpeed = 20f; //every sec it will try to zoom out until pref dist.
private float preferredDistance = distance;
private float maxDistanceWithoutCollision;
private float previousDistance;
public CollisionChaseCamera(Camera cam, Node target, Node collidableScene)
{
super(cam, target);
this.collidableScene = collidableScene;
}
public boolean checkCameraForCollisions(Vector3f rayStart)
{
CollisionResults results = new CollisionResults();
Ray ray = new Ray(rayStart, cam.getDirection());
collidableScene.collideWith(ray, results);
CollisionResult result = results.getClosestCollision(); //just sort.
for(int i=0; i < results.size(); i++)
{
result = results.getCollisionDirect(i);
if (result.getGeometry().hasAncestor((Node) target))//when you reach targetGeometry
{
if (i==0) //if first in order, no collisions happened. maxDistanceWithoutCollision = result.getDistance();
{
maxDistanceWithoutCollision = result.getDistance();
return false;
}
maxDistanceWithoutCollision = result.getDistance() - results.getCollisionDirect(i - 1).getDistance();
return true;
}
}
return false;
}
@Override protected void updateCamera(float tpf)
{
if (enabled)
{
float zoomAtPreferredDistanceAmount = zoomAtPreferedDistanceSpeed * tpf;
targetLocation.set(target.getWorldTranslation()).addLocal(lookAtOffset);
vRotation = targetVRotation;
rotation = targetRotation;
distance = targetDistance;
Vector3f camPos ;
boolean zoomFailed ;
if (targetDistance < preferredDistance) //attempt to zoom out camera to reach prefered zoom.
{
setDistance(targetDistance + zoomAtPreferredDistanceAmount);
computePosition();
camPos = pos.addLocal(lookAtOffset); //camPos = cam.getLocation zoomed out by 0.01 (zoomAtPreferedDistanceSpeed)
zoomFailed = true;
}
else
{
camPos = cam.getLocation();
zoomFailed = false;
}
boolean collisionHappened = checkCameraForCollisions(camPos);
if (collisionHappened)
{
if (!Float.isInfinite(maxDistanceWithoutCollision) && !Float.isNaN(maxDistanceWithoutCollision))
{
if (Math.abs(maxDistanceWithoutCollision - previousDistance) > 1.0f)
{
setDistance(maxDistanceWithoutCollision);
zoomFailed = false;
}
}
if (zoomFailed) //else failed zoom restore change.
{
setDistance(targetDistance - zoomAtPreferredDistanceAmount);
}
}
//else if (!collisionHappened) leave distance unchanged since it is fine.
previousDistance = distance;
computePosition();
cam.setLocation(pos.addLocal(lookAtOffset));
prevPos.set(targetLocation); //keeping track on the previous position of the target
cam.lookAt(targetLocation, initialUpVec); //the cam looks at the target
}
}
protected void setDistance(float value)
{
if (value < minDistance)
{
value = minDistance;
if (veryCloseRotation && (targetVRotation < minVerticalRotation))
{
targetVRotation = minVerticalRotation;
}
}
distance = value;
targetDistance = value;
}
@Override protected void zoomCamera(float value)
{
super.zoomCamera(value);
preferredDistance = targetDistance;
}
@Override public void setDefaultDistance(float defaultDistance)
{
super.setDefaultDistance(defaultDistance);
preferredDistance = defaultDistance;
}
public Node getCollidableScene()
{
return collidableScene;
}
public void setCollidableScene(Node collidableScene)
{
this.collidableScene = collidableScene;
}
public float getZoomAtPreferedDistanceSpeed()
{
return zoomAtPreferedDistanceSpeed;
}
public void setZoomAtPreferedDistanceSpeed(float zoomAtPreferedDistanceSpeed)
{
this.zoomAtPreferedDistanceSpeed = zoomAtPreferedDistanceSpeed;
}
public void setRotationSpeed(float speed)
{
rotationSpeed = speed;
}
}
[/java]
[java]
import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.ZipLocator;
import com.jme3.bounding.BoundingBox;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.BoxCollisionShape;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.ChaseCamera;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
public class TestCollisionCamera extends SimpleApplication implements ActionListener
{
protected Spatial stageModel;
protected RigidBodyControl stagePhysics;
protected ChaseCamera chaseCam;
protected Node playerModel;
protected CharacterControl playerPhysics;
protected float playerMovementSpeed = 0.125f;
protected Vector3f walkDirection = new Vector3f(0, 0, 1);
protected boolean left = false, right = false, up = false, down = false;
protected BulletAppState bulletAppState = new BulletAppState();
public static final boolean CAPTULE_CHARACTER_SHAPE = true;
public static final boolean USE_COLLISION_CHASE_CAMERA = true;
public static void main(String[] args)
{
Logger.getLogger("").setLevel(Level.SEVERE);
TestCollisionCamera app = new TestCollisionCamera();
app.start();
}
@Override
public void simpleInitApp()
{
setupOptions();
setupKeys();
setupLight();
setupStage();
setupCharacter(new Vector3f(-0.5f, 0, -0.5f));
setupCamera(playerModel);
}
public void setupOptions()
{
bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);
stateManager.attach(bulletAppState);
viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));
flyCam.setMoveSpeed(30);
}
private void setupCamera(Node player)
{
flyCam.setEnabled(false);
if (USE_COLLISION_CHASE_CAMERA)
{
chaseCam = new CollisionChaseCamera(cam, player, rootNode);
chaseCam.setMinVerticalRotation(-0.6f);
chaseCam.setMinDistance(3.0f); //recommented value: based on char size so that camera doesnt get inside it.
}
else
{
chaseCam = new ChaseCamera(cam, player);
}
chaseCam.registerWithInput(inputManager);
chaseCam.setMaxDistance(30f);
chaseCam.setDefaultDistance(12f);
chaseCam.setDragToRotate(false);
chaseCam.setInvertVerticalAxis(true);
chaseCam.setDefaultHorizontalRotation(-FastMath.HALF_PI);
}
private void setupStage()
{
assetManager.registerLocator(“town.zip”, ZipLocator.class.getName());
stageModel = assetManager.loadModel(“main.scene”);
stageModel.setLocalScale(0.4f);
rootNode.attachChild(stageModel);
CollisionShape sceneShape = CollisionShapeFactory.createMeshShape(stageModel);
stagePhysics = new RigidBodyControl(sceneShape, 0);
stageModel.addControl(stagePhysics);
bulletAppState.getPhysicsSpace().add(stagePhysics);
}
public static BoundingBox getBoundingBox(Spatial node)
{
return (BoundingBox)node.getWorldBound();
}
private void setupCharacter(Vector3f position)
{
playerModel = (Node)assetManager.loadModel(“Models/Sinbad/Sinbad.mesh.xml”);
playerModel.scale(0.25f);
rootNode.attachChild(playerModel);
BoundingBox playerBox = getBoundingBox(playerModel);
CollisionShape playerCapsule;
if (CAPTULE_CHARACTER_SHAPE)
{
playerCapsule = new CapsuleCollisionShape(playerBox.getXExtent(), playerBox.getYExtent() / 2f, 1);
}
else
{
playerCapsule = new BoxCollisionShape(playerBox.getExtent(null));
}
playerPhysics = new CharacterControl(playerCapsule, 0.10f);
playerModel.addControl(playerPhysics);
playerPhysics.setPhysicsLocation(new Vector3f(position.x, playerBox.getYExtent() + position.y, position.z));
bulletAppState.getPhysicsSpace().add(playerPhysics);
playerPhysics.setJumpSpeed(20);
playerPhysics.setFallSpeed(30);
playerPhysics.setGravity(30);
}
private void setupLight()
{
AmbientLight al = new AmbientLight();
al.setColor(ColorRGBA.White.mult(1.3f));
rootNode.addLight(al);
DirectionalLight dl = new DirectionalLight();
dl.setColor(ColorRGBA.White);
dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal());
rootNode.addLight(dl);
}
private void setupKeys()
{
inputManager.addMapping(“Left”, new KeyTrigger(KeyInput.KEY_A));
inputManager.addMapping(“Right”, new KeyTrigger(KeyInput.KEY_D));
inputManager.addMapping(“Up”, new KeyTrigger(KeyInput.KEY_W));
inputManager.addMapping(“Down”, new KeyTrigger(KeyInput.KEY_S));
inputManager.addMapping(“Jump”, new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addListener(this, “Left”);
inputManager.addListener(this, “Right”);
inputManager.addListener(this, “Up”);
inputManager.addListener(this, “Down”);
inputManager.addListener(this, “Jump”);
}
//@toDo: non instant transition of rotations. 0.3 sec to do a full rotation.
public void onAction(String binding, boolean value, float tpf)
{
if (binding.equals(“Left”))
{
left = value;
}
else if (binding.equals(“Right”))
{
right = value;
}
else if (binding.equals(“Up”))
{
up = value;
}
else if (binding.equals(“Down”))
{
down = value;
}
else if (binding.equals(“Jump”))
{
playerPhysics.jump();
}
}
//@toDo: rotate player model.
@Override
public void simpleUpdate(float tpf)
{
super.simpleUpdate(tpf);
getInputManager().setCursorVisible(false);//@toDo: put it in simpleInit after jme bug fixed.
boolean moved = left || right || up || down;
if (!moved)
{
playerPhysics.setWalkDirection(Vector3f.ZERO);
return;
}
float rotx = -(chaseCam.getHorizontalRotation() + FastMath.HALF_PI);
float roty = (chaseCam.getVerticalRotation());
walkDirection = localToWorldCoordinatesIgnoreHeight((left ? 1 : 0) + (right ? -1 : 0), 0, (up ? 1 : 0) + (down ? -1 : 0), roty, rotx);
walkDirection.normalizeLocal();
playerPhysics.setWalkDirection(walkDirection.mult(playerMovementSpeed));
playerPhysics.setViewDirection(walkDirection);
}
/** Converts local cordinates “(dx,dy,dz)” to world coordinates based on Vector3D “rotation”.
- <a href="3D and Audio APIs and Softwares - Jérôme JOUVIE - http:/jerome.jouvie.free.fr/"> More info : </a>
*
- This kind of deplacement generaly used for player movement (in shooting game …).
- The x rotation is ‘ignored’ for the calculation of the deplacement.
- This result that if you look upward and you want to move forward,
- the deplacement is calculated like if your were parallely to the ground.
*/
public static Vector3f localToWorldCoordinatesIgnoreHeight(float dx, float dy, float dz, float rotationX, float rotationY)
{
//Don’t calculate for nothing …
if (dx == 0.0f & dy == 0.0f && dz == 0.0f)
return new Vector3f();
double xRot = -rotationX;
double yRot = -rotationY;
//Calculate the formula
float x = (float) (dx * Math.cos(yRot) + 0 - dz * Math.sin(yRot));
float y = (float) (0 + dy * Math.cos(xRot) + 0);
float z = (float) (dx * Math.sin(yRot) + 0 + dz * Math.cos(yRot));
//Return the vector expressed in the global axis system
return new Vector3f(x, y, z);
}//localToWorldCoordinatesIgnoreHeight
}
[/java]