blob: 40c2fea517eb6e568fc8b67ed311cf110c867ad6 [file] [log] [blame]
/*
* Copyright (c) 2009-2010 jMonkeyEngine
* All rights reserved.
*
* 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 'jMonkeyEngine' 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) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.control;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix3f;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.io.IOException;
public class BillboardControl extends AbstractControl {
private Matrix3f orient;
private Vector3f look;
private Vector3f left;
private Alignment alignment;
/**
* Determines how the billboard is aligned to the screen/camera.
*/
public enum Alignment {
/**
* Aligns this Billboard to the screen.
*/
Screen,
/**
* Aligns this Billboard to the camera position.
*/
Camera,
/**
* Aligns this Billboard to the screen, but keeps the Y axis fixed.
*/
AxialY,
/**
* Aligns this Billboard to the screen, but keeps the Z axis fixed.
*/
AxialZ;
}
public BillboardControl() {
super();
orient = new Matrix3f();
look = new Vector3f();
left = new Vector3f();
alignment = Alignment.Screen;
}
public Control cloneForSpatial(Spatial spatial) {
BillboardControl control = new BillboardControl();
control.alignment = this.alignment;
control.setSpatial(spatial);
return control;
}
@Override
protected void controlUpdate(float tpf) {
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
Camera cam = vp.getCamera();
rotateBillboard(cam);
}
private void fixRefreshFlags(){
// force transforms to update below this node
spatial.updateGeometricState();
// force world bound to update
Spatial rootNode = spatial;
while (rootNode.getParent() != null){
rootNode = rootNode.getParent();
}
rootNode.getWorldBound();
}
/**
* rotate the billboard based on the type set
*
* @param cam
* Camera
*/
private void rotateBillboard(Camera cam) {
switch (alignment) {
case AxialY:
rotateAxial(cam, Vector3f.UNIT_Y);
break;
case AxialZ:
rotateAxial(cam, Vector3f.UNIT_Z);
break;
case Screen:
rotateScreenAligned(cam);
break;
case Camera:
rotateCameraAligned(cam);
break;
}
}
/**
* Aligns this Billboard so that it points to the camera position.
*
* @param camera
* Camera
*/
private void rotateCameraAligned(Camera camera) {
look.set(camera.getLocation()).subtractLocal(
spatial.getWorldTranslation());
// coopt left for our own purposes.
Vector3f xzp = left;
// The xzp vector is the projection of the look vector on the xz plane
xzp.set(look.x, 0, look.z);
// check for undefined rotation...
if (xzp.equals(Vector3f.ZERO)) {
return;
}
look.normalizeLocal();
xzp.normalizeLocal();
float cosp = look.dot(xzp);
// compute the local orientation matrix for the billboard
orient.set(0, 0, xzp.z);
orient.set(0, 1, xzp.x * -look.y);
orient.set(0, 2, xzp.x * cosp);
orient.set(1, 0, 0);
orient.set(1, 1, cosp);
orient.set(1, 2, look.y);
orient.set(2, 0, -xzp.x);
orient.set(2, 1, xzp.z * -look.y);
orient.set(2, 2, xzp.z * cosp);
// The billboard must be oriented to face the camera before it is
// transformed into the world.
spatial.setLocalRotation(orient);
fixRefreshFlags();
}
/**
* Rotate the billboard so it points directly opposite the direction the
* camera's facing
*
* @param camera
* Camera
*/
private void rotateScreenAligned(Camera camera) {
// coopt diff for our in direction:
look.set(camera.getDirection()).negateLocal();
// coopt loc for our left direction:
left.set(camera.getLeft()).negateLocal();
orient.fromAxes(left, camera.getUp(), look);
Node parent = spatial.getParent();
Quaternion rot=new Quaternion().fromRotationMatrix(orient);
if ( parent != null ) {
rot = parent.getWorldRotation().inverse().multLocal(rot);
rot.normalizeLocal();
}
spatial.setLocalRotation(rot);
fixRefreshFlags();
}
/**
* Rotate the billboard towards the camera, but keeping a given axis fixed.
*
* @param camera
* Camera
*/
private void rotateAxial(Camera camera, Vector3f axis) {
// Compute the additional rotation required for the billboard to face
// the camera. To do this, the camera must be inverse-transformed into
// the model space of the billboard.
look.set(camera.getLocation()).subtractLocal(
spatial.getWorldTranslation());
spatial.getParent().getWorldRotation().mult(look, left); // coopt left for our own
// purposes.
left.x *= 1.0f / spatial.getWorldScale().x;
left.y *= 1.0f / spatial.getWorldScale().y;
left.z *= 1.0f / spatial.getWorldScale().z;
// squared length of the camera projection in the xz-plane
float lengthSquared = left.x * left.x + left.z * left.z;
if (lengthSquared < FastMath.FLT_EPSILON) {
// camera on the billboard axis, rotation not defined
return;
}
// unitize the projection
float invLength = FastMath.invSqrt(lengthSquared);
if (axis.y == 1) {
left.x *= invLength;
left.y = 0.0f;
left.z *= invLength;
// compute the local orientation matrix for the billboard
orient.set(0, 0, left.z);
orient.set(0, 1, 0);
orient.set(0, 2, left.x);
orient.set(1, 0, 0);
orient.set(1, 1, 1);
orient.set(1, 2, 0);
orient.set(2, 0, -left.x);
orient.set(2, 1, 0);
orient.set(2, 2, left.z);
} else if (axis.z == 1) {
left.x *= invLength;
left.y *= invLength;
left.z = 0.0f;
// compute the local orientation matrix for the billboard
orient.set(0, 0, left.y);
orient.set(0, 1, left.x);
orient.set(0, 2, 0);
orient.set(1, 0, -left.y);
orient.set(1, 1, left.x);
orient.set(1, 2, 0);
orient.set(2, 0, 0);
orient.set(2, 1, 0);
orient.set(2, 2, 1);
}
// The billboard must be oriented to face the camera before it is
// transformed into the world.
spatial.setLocalRotation(orient);
fixRefreshFlags();
}
/**
* Returns the alignment this Billboard is set too.
*
* @return The alignment of rotation, AxialY, AxialZ, Camera or Screen.
*/
public Alignment getAlignment() {
return alignment;
}
/**
* Sets the type of rotation this Billboard will have. The alignment can
* be Camera, Screen, AxialY, or AxialZ. Invalid alignments will
* assume no billboard rotation.
*/
public void setAlignment(Alignment alignment) {
this.alignment = alignment;
}
@Override
public void write(JmeExporter e) throws IOException {
super.write(e);
OutputCapsule capsule = e.getCapsule(this);
capsule.write(orient, "orient", null);
capsule.write(look, "look", null);
capsule.write(left, "left", null);
capsule.write(alignment, "alignment", Alignment.Screen);
}
@Override
public void read(JmeImporter e) throws IOException {
super.read(e);
InputCapsule capsule = e.getCapsule(this);
orient = (Matrix3f) capsule.readSavable("orient", null);
look = (Vector3f) capsule.readSavable("look", null);
left = (Vector3f) capsule.readSavable("left", null);
alignment = capsule.readEnum("alignment", Alignment.class, Alignment.Screen);
}
}