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 
BillboardControl()82     public BillboardControl() {
83         super();
84         orient = new Matrix3f();
85         look = new Vector3f();
86         left = new Vector3f();
87         alignment = Alignment.Screen;
88     }
89 
cloneForSpatial(Spatial spatial)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
controlUpdate(float tpf)98     protected void controlUpdate(float tpf) {
99     }
100 
101     @Override
controlRender(RenderManager rm, ViewPort vp)102     protected void controlRender(RenderManager rm, ViewPort vp) {
103         Camera cam = vp.getCamera();
104         rotateBillboard(cam);
105     }
106 
fixRefreshFlags()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      */
rotateBillboard(Camera cam)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      */
rotateCameraAligned(Camera camera)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      */
rotateScreenAligned(Camera camera)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      */
rotateAxial(Camera camera, Vector3f axis)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      */
getAlignment()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      */
setAlignment(Alignment alignment)284     public void setAlignment(Alignment alignment) {
285         this.alignment = alignment;
286     }
287 
288     @Override
write(JmeExporter e)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
read(JmeImporter e)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