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 */
32package com.jme3.animation;
33
34import com.jme3.export.*;
35import com.jme3.renderer.RenderManager;
36import com.jme3.renderer.ViewPort;
37import com.jme3.scene.Mesh;
38import com.jme3.scene.Spatial;
39import com.jme3.scene.control.AbstractControl;
40import com.jme3.scene.control.Control;
41import com.jme3.util.TempVars;
42import java.io.IOException;
43import java.util.ArrayList;
44import java.util.Collection;
45import java.util.HashMap;
46
47/**
48 * <code>AnimControl</code> is a Spatial control that allows manipulation
49 * of skeletal animation.
50 *
51 * The control currently supports:
52 * 1) Animation blending/transitions
53 * 2) Multiple animation channels
54 * 3) Multiple skins
55 * 4) Animation event listeners
56 * 5) Animated model cloning
57 * 6) Animated model binary import/export
58 *
59 * Planned:
60 * 1) Hardware skinning
61 * 2) Morph/Pose animation
62 * 3) Attachments
63 * 4) Add/remove skins
64 *
65 * @author Kirill Vainer
66 */
67public final class AnimControl extends AbstractControl implements Cloneable {
68
69    /**
70     * Skeleton object must contain corresponding data for the targets' weight buffers.
71     */
72    Skeleton skeleton;
73    /** only used for backward compatibility */
74    @Deprecated
75    private SkeletonControl skeletonControl;
76    /**
77     * List of animations
78     */
79    HashMap<String, Animation> animationMap;
80    /**
81     * Animation channels
82     */
83    private transient ArrayList<AnimChannel> channels = new ArrayList<AnimChannel>();
84    /**
85     * Animation event listeners
86     */
87    private transient ArrayList<AnimEventListener> listeners = new ArrayList<AnimEventListener>();
88
89    /**
90     * Creates a new animation control for the given skeleton.
91     * The method {@link AnimControl#setAnimations(java.util.HashMap) }
92     * must be called after initialization in order for this class to be useful.
93     *
94     * @param skeleton The skeleton to animate
95     */
96    public AnimControl(Skeleton skeleton) {
97        this.skeleton = skeleton;
98        reset();
99    }
100
101    /**
102     * Serialization only. Do not use.
103     */
104    public AnimControl() {
105    }
106
107    /**
108     * Internal use only.
109     */
110    public Control cloneForSpatial(Spatial spatial) {
111        try {
112            AnimControl clone = (AnimControl) super.clone();
113            clone.spatial = spatial;
114            clone.channels = new ArrayList<AnimChannel>();
115            clone.listeners = new ArrayList<AnimEventListener>();
116
117            if (skeleton != null) {
118                clone.skeleton = new Skeleton(skeleton);
119            }
120
121            // animationMap is reference-copied, animation data should be shared
122            // to reduce memory usage.
123
124            return clone;
125        } catch (CloneNotSupportedException ex) {
126            throw new AssertionError();
127        }
128    }
129
130    /**
131     * @param animations Set the animations that this <code>AnimControl</code>
132     * will be capable of playing. The animations should be compatible
133     * with the skeleton given in the constructor.
134     */
135    public void setAnimations(HashMap<String, Animation> animations) {
136        animationMap = animations;
137    }
138
139    /**
140     * Retrieve an animation from the list of animations.
141     * @param name The name of the animation to retrieve.
142     * @return The animation corresponding to the given name, or null, if no
143     * such named animation exists.
144     */
145    public Animation getAnim(String name) {
146        if (animationMap == null) {
147            animationMap = new HashMap<String, Animation>();
148        }
149        return animationMap.get(name);
150    }
151
152    /**
153     * Adds an animation to be available for playing to this
154     * <code>AnimControl</code>.
155     * @param anim The animation to add.
156     */
157    public void addAnim(Animation anim) {
158        if (animationMap == null) {
159            animationMap = new HashMap<String, Animation>();
160        }
161        animationMap.put(anim.getName(), anim);
162    }
163
164    /**
165     * Remove an animation so that it is no longer available for playing.
166     * @param anim The animation to remove.
167     */
168    public void removeAnim(Animation anim) {
169        if (!animationMap.containsKey(anim.getName())) {
170            throw new IllegalArgumentException("Given animation does not exist "
171                    + "in this AnimControl");
172        }
173
174        animationMap.remove(anim.getName());
175    }
176
177    /**
178     * Create a new animation channel, by default assigned to all bones
179     * in the skeleton.
180     *
181     * @return A new animation channel for this <code>AnimControl</code>.
182     */
183    public AnimChannel createChannel() {
184        AnimChannel channel = new AnimChannel(this);
185        channels.add(channel);
186        return channel;
187    }
188
189    /**
190     * Return the animation channel at the given index.
191     * @param index The index, starting at 0, to retrieve the <code>AnimChannel</code>.
192     * @return The animation channel at the given index, or throws an exception
193     * if the index is out of bounds.
194     *
195     * @throws IndexOutOfBoundsException If no channel exists at the given index.
196     */
197    public AnimChannel getChannel(int index) {
198        return channels.get(index);
199    }
200
201    /**
202     * @return The number of channels that are controlled by this
203     * <code>AnimControl</code>.
204     *
205     * @see AnimControl#createChannel()
206     */
207    public int getNumChannels() {
208        return channels.size();
209    }
210
211    /**
212     * Clears all the channels that were created.
213     *
214     * @see AnimControl#createChannel()
215     */
216    public void clearChannels() {
217        channels.clear();
218    }
219
220    /**
221     * @return The skeleton of this <code>AnimControl</code>.
222     */
223    public Skeleton getSkeleton() {
224        return skeleton;
225    }
226
227    /**
228     * Adds a new listener to receive animation related events.
229     * @param listener The listener to add.
230     */
231    public void addListener(AnimEventListener listener) {
232        if (listeners.contains(listener)) {
233            throw new IllegalArgumentException("The given listener is already "
234                    + "registed at this AnimControl");
235        }
236
237        listeners.add(listener);
238    }
239
240    /**
241     * Removes the given listener from listening to events.
242     * @param listener
243     * @see AnimControl#addListener(com.jme3.animation.AnimEventListener)
244     */
245    public void removeListener(AnimEventListener listener) {
246        if (!listeners.remove(listener)) {
247            throw new IllegalArgumentException("The given listener is not "
248                    + "registed at this AnimControl");
249        }
250    }
251
252    /**
253     * Clears all the listeners added to this <code>AnimControl</code>
254     *
255     * @see AnimControl#addListener(com.jme3.animation.AnimEventListener)
256     */
257    public void clearListeners() {
258        listeners.clear();
259    }
260
261    void notifyAnimChange(AnimChannel channel, String name) {
262        for (int i = 0; i < listeners.size(); i++) {
263            listeners.get(i).onAnimChange(this, channel, name);
264        }
265    }
266
267    void notifyAnimCycleDone(AnimChannel channel, String name) {
268        for (int i = 0; i < listeners.size(); i++) {
269            listeners.get(i).onAnimCycleDone(this, channel, name);
270        }
271    }
272
273    /**
274     * Internal use only.
275     */
276    @Override
277    public void setSpatial(Spatial spatial) {
278        if (spatial == null && skeletonControl != null) {
279            this.spatial.removeControl(skeletonControl);
280        }
281
282        super.setSpatial(spatial);
283
284        //Backward compatibility.
285        if (spatial != null && skeletonControl != null) {
286            spatial.addControl(skeletonControl);
287        }
288    }
289
290    final void reset() {
291        if (skeleton != null) {
292            skeleton.resetAndUpdate();
293        }
294    }
295
296    /**
297     * @return The names of all animations that this <code>AnimControl</code>
298     * can play.
299     */
300    public Collection<String> getAnimationNames() {
301        return animationMap.keySet();
302    }
303
304    /**
305     * Returns the length of the given named animation.
306     * @param name The name of the animation
307     * @return The length of time, in seconds, of the named animation.
308     */
309    public float getAnimationLength(String name) {
310        Animation a = animationMap.get(name);
311        if (a == null) {
312            throw new IllegalArgumentException("The animation " + name
313                    + " does not exist in this AnimControl");
314        }
315
316        return a.getLength();
317    }
318
319    /**
320     * Internal use only.
321     */
322    @Override
323    protected void controlUpdate(float tpf) {
324        if (skeleton != null) {
325            skeleton.reset(); // reset skeleton to bind pose
326        }
327
328        TempVars vars = TempVars.get();
329        for (int i = 0; i < channels.size(); i++) {
330            channels.get(i).update(tpf, vars);
331        }
332        vars.release();
333
334        if (skeleton != null) {
335            skeleton.updateWorldVectors();
336        }
337    }
338
339    /**
340     * Internal use only.
341     */
342    @Override
343    protected void controlRender(RenderManager rm, ViewPort vp) {
344    }
345
346    @Override
347    public void write(JmeExporter ex) throws IOException {
348        super.write(ex);
349        OutputCapsule oc = ex.getCapsule(this);
350        oc.write(skeleton, "skeleton", null);
351        oc.writeStringSavableMap(animationMap, "animations", null);
352    }
353
354    @Override
355    public void read(JmeImporter im) throws IOException {
356        super.read(im);
357        InputCapsule in = im.getCapsule(this);
358        skeleton = (Skeleton) in.readSavable("skeleton", null);
359        animationMap = (HashMap<String, Animation>) in.readStringSavableMap("animations", null);
360
361        if (im.getFormatVersion() == 0) {
362            // Changed for backward compatibility with j3o files generated
363            // before the AnimControl/SkeletonControl split.
364
365            // If we find a target mesh array the AnimControl creates the
366            // SkeletonControl for old files and add it to the spatial.
367            // When backward compatibility won't be needed anymore this can deleted
368            Savable[] sav = in.readSavableArray("targets", null);
369            if (sav != null) {
370                Mesh[] targets = new Mesh[sav.length];
371                System.arraycopy(sav, 0, targets, 0, sav.length);
372                skeletonControl = new SkeletonControl(targets, skeleton);
373                spatial.addControl(skeletonControl);
374            }
375        }
376    }
377}
378