1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.jme3.scene.control; 34 35 import com.jme3.export.InputCapsule; 36 import com.jme3.export.JmeExporter; 37 import com.jme3.export.JmeImporter; 38 import com.jme3.export.OutputCapsule; 39 import com.jme3.math.FastMath; 40 import com.jme3.math.Matrix3f; 41 import com.jme3.math.Quaternion; 42 import com.jme3.math.Vector3f; 43 import com.jme3.renderer.Camera; 44 import com.jme3.renderer.RenderManager; 45 import com.jme3.renderer.ViewPort; 46 import com.jme3.scene.Node; 47 import com.jme3.scene.Spatial; 48 import java.io.IOException; 49 50 public class BillboardControl extends AbstractControl { 51 52 private Matrix3f orient; 53 private Vector3f look; 54 private Vector3f left; 55 private Alignment alignment; 56 57 /** 58 * Determines how the billboard is aligned to the screen/camera. 59 */ 60 public enum Alignment { 61 /** 62 * Aligns this Billboard to the screen. 63 */ 64 Screen, 65 66 /** 67 * Aligns this Billboard to the camera position. 68 */ 69 Camera, 70 71 /** 72 * Aligns this Billboard to the screen, but keeps the Y axis fixed. 73 */ 74 AxialY, 75 76 /** 77 * Aligns this Billboard to the screen, but keeps the Z axis fixed. 78 */ 79 AxialZ; 80 } 81 82 public BillboardControl() { 83 super(); 84 orient = new Matrix3f(); 85 look = new Vector3f(); 86 left = new Vector3f(); 87 alignment = Alignment.Screen; 88 } 89 90 public Control cloneForSpatial(Spatial spatial) { 91 BillboardControl control = new BillboardControl(); 92 control.alignment = this.alignment; 93 control.setSpatial(spatial); 94 return control; 95 } 96 97 @Override 98 protected void controlUpdate(float tpf) { 99 } 100 101 @Override 102 protected void controlRender(RenderManager rm, ViewPort vp) { 103 Camera cam = vp.getCamera(); 104 rotateBillboard(cam); 105 } 106 107 private void fixRefreshFlags(){ 108 // force transforms to update below this node 109 spatial.updateGeometricState(); 110 111 // force world bound to update 112 Spatial rootNode = spatial; 113 while (rootNode.getParent() != null){ 114 rootNode = rootNode.getParent(); 115 } 116 rootNode.getWorldBound(); 117 } 118 119 /** 120 * rotate the billboard based on the type set 121 * 122 * @param cam 123 * Camera 124 */ 125 private void rotateBillboard(Camera cam) { 126 switch (alignment) { 127 case AxialY: 128 rotateAxial(cam, Vector3f.UNIT_Y); 129 break; 130 case AxialZ: 131 rotateAxial(cam, Vector3f.UNIT_Z); 132 break; 133 case Screen: 134 rotateScreenAligned(cam); 135 break; 136 case Camera: 137 rotateCameraAligned(cam); 138 break; 139 } 140 } 141 142 /** 143 * Aligns this Billboard so that it points to the camera position. 144 * 145 * @param camera 146 * Camera 147 */ 148 private void rotateCameraAligned(Camera camera) { 149 look.set(camera.getLocation()).subtractLocal( 150 spatial.getWorldTranslation()); 151 // coopt left for our own purposes. 152 Vector3f xzp = left; 153 // The xzp vector is the projection of the look vector on the xz plane 154 xzp.set(look.x, 0, look.z); 155 156 // check for undefined rotation... 157 if (xzp.equals(Vector3f.ZERO)) { 158 return; 159 } 160 161 look.normalizeLocal(); 162 xzp.normalizeLocal(); 163 float cosp = look.dot(xzp); 164 165 // compute the local orientation matrix for the billboard 166 orient.set(0, 0, xzp.z); 167 orient.set(0, 1, xzp.x * -look.y); 168 orient.set(0, 2, xzp.x * cosp); 169 orient.set(1, 0, 0); 170 orient.set(1, 1, cosp); 171 orient.set(1, 2, look.y); 172 orient.set(2, 0, -xzp.x); 173 orient.set(2, 1, xzp.z * -look.y); 174 orient.set(2, 2, xzp.z * cosp); 175 176 // The billboard must be oriented to face the camera before it is 177 // transformed into the world. 178 spatial.setLocalRotation(orient); 179 fixRefreshFlags(); 180 } 181 182 /** 183 * Rotate the billboard so it points directly opposite the direction the 184 * camera's facing 185 * 186 * @param camera 187 * Camera 188 */ 189 private void rotateScreenAligned(Camera camera) { 190 // coopt diff for our in direction: 191 look.set(camera.getDirection()).negateLocal(); 192 // coopt loc for our left direction: 193 left.set(camera.getLeft()).negateLocal(); 194 orient.fromAxes(left, camera.getUp(), look); 195 Node parent = spatial.getParent(); 196 Quaternion rot=new Quaternion().fromRotationMatrix(orient); 197 if ( parent != null ) { 198 rot = parent.getWorldRotation().inverse().multLocal(rot); 199 rot.normalizeLocal(); 200 } 201 spatial.setLocalRotation(rot); 202 fixRefreshFlags(); 203 } 204 205 /** 206 * Rotate the billboard towards the camera, but keeping a given axis fixed. 207 * 208 * @param camera 209 * Camera 210 */ 211 private void rotateAxial(Camera camera, Vector3f axis) { 212 // Compute the additional rotation required for the billboard to face 213 // the camera. To do this, the camera must be inverse-transformed into 214 // the model space of the billboard. 215 look.set(camera.getLocation()).subtractLocal( 216 spatial.getWorldTranslation()); 217 spatial.getParent().getWorldRotation().mult(look, left); // coopt left for our own 218 // purposes. 219 left.x *= 1.0f / spatial.getWorldScale().x; 220 left.y *= 1.0f / spatial.getWorldScale().y; 221 left.z *= 1.0f / spatial.getWorldScale().z; 222 223 // squared length of the camera projection in the xz-plane 224 float lengthSquared = left.x * left.x + left.z * left.z; 225 if (lengthSquared < FastMath.FLT_EPSILON) { 226 // camera on the billboard axis, rotation not defined 227 return; 228 } 229 230 // unitize the projection 231 float invLength = FastMath.invSqrt(lengthSquared); 232 if (axis.y == 1) { 233 left.x *= invLength; 234 left.y = 0.0f; 235 left.z *= invLength; 236 237 // compute the local orientation matrix for the billboard 238 orient.set(0, 0, left.z); 239 orient.set(0, 1, 0); 240 orient.set(0, 2, left.x); 241 orient.set(1, 0, 0); 242 orient.set(1, 1, 1); 243 orient.set(1, 2, 0); 244 orient.set(2, 0, -left.x); 245 orient.set(2, 1, 0); 246 orient.set(2, 2, left.z); 247 } else if (axis.z == 1) { 248 left.x *= invLength; 249 left.y *= invLength; 250 left.z = 0.0f; 251 252 // compute the local orientation matrix for the billboard 253 orient.set(0, 0, left.y); 254 orient.set(0, 1, left.x); 255 orient.set(0, 2, 0); 256 orient.set(1, 0, -left.y); 257 orient.set(1, 1, left.x); 258 orient.set(1, 2, 0); 259 orient.set(2, 0, 0); 260 orient.set(2, 1, 0); 261 orient.set(2, 2, 1); 262 } 263 264 // The billboard must be oriented to face the camera before it is 265 // transformed into the world. 266 spatial.setLocalRotation(orient); 267 fixRefreshFlags(); 268 } 269 270 /** 271 * Returns the alignment this Billboard is set too. 272 * 273 * @return The alignment of rotation, AxialY, AxialZ, Camera or Screen. 274 */ 275 public Alignment getAlignment() { 276 return alignment; 277 } 278 279 /** 280 * Sets the type of rotation this Billboard will have. The alignment can 281 * be Camera, Screen, AxialY, or AxialZ. Invalid alignments will 282 * assume no billboard rotation. 283 */ 284 public void setAlignment(Alignment alignment) { 285 this.alignment = alignment; 286 } 287 288 @Override 289 public void write(JmeExporter e) throws IOException { 290 super.write(e); 291 OutputCapsule capsule = e.getCapsule(this); 292 capsule.write(orient, "orient", null); 293 capsule.write(look, "look", null); 294 capsule.write(left, "left", null); 295 capsule.write(alignment, "alignment", Alignment.Screen); 296 } 297 298 @Override 299 public void read(JmeImporter e) throws IOException { 300 super.read(e); 301 InputCapsule capsule = e.getCapsule(this); 302 orient = (Matrix3f) capsule.readSavable("orient", null); 303 look = (Vector3f) capsule.readSavable("look", null); 304 left = (Vector3f) capsule.readSavable("left", null); 305 alignment = capsule.readEnum("alignment", Alignment.class, Alignment.Screen); 306 } 307 } 308