1 //
2 //  SuperTuxKart - a fun racing game with go-kart
3 //  Copyright (C) 2009-2015  Joerg Henrichs
4 //
5 //  This program is free software; you can redistribute it and/or
6 //  modify it under the terms of the GNU General Public License
7 //  as published by the Free Software Foundation; either version 3
8 //  of the License, or (at your option) any later version.
9 //
10 //  This program is distributed in the hope that it will be useful,
11 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 //  GNU General Public License for more details.
14 //
15 //  You should have received a copy of the GNU General Public License
16 //  along with this program; if not, write to the Free Software
17 //  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18 
19 #include "animations/ipo.hpp"
20 
21 #include "io/xml_node.hpp"
22 #include "utils/vs.hpp"
23 #include "utils/log.hpp"
24 
25 #include <string.h>
26 #include <algorithm>
27 #include <cmath>
28 
29 const std::string Ipo::m_all_channel_names[IPO_MAX] =
30                 {"LocX", "LocY", "LocZ", "LocXYZ",
31                  "RotX", "RotY", "RotZ",
32                  "ScaleX", "ScaleY", "ScaleZ" };
33 
34 // ----------------------------------------------------------------------------
35 /** Initialise the Ipo from the specifications in the XML file.
36  *  \param curve The XML node with the IPO data.
37  *  \param fps Frames per second value, necessary to convert frame values
38  *             into time.
39  *  \param reverse If this is set to true, the ipo data will be reverse. This
40  *             is used by the cannon if the track is driven in reverse.
41  */
IpoData(const XMLNode & curve,float fps,bool reverse)42 Ipo::IpoData::IpoData(const XMLNode &curve, float fps, bool reverse)
43 {
44     if(curve.getName()!="curve")
45     {
46         Log::warn("Animations", "Expected 'curve' for animation, got '%s' --> Ignored.",
47                   curve.getName().c_str());
48         return;
49     }
50     std::string channel;
51     curve.get("channel", &channel);
52     m_channel=IPO_MAX;
53     for(unsigned int i=IPO_LOCX; i<IPO_MAX; i++)
54     {
55         if(m_all_channel_names[i]==channel)
56         {
57             m_channel=(IpoChannelType)i;
58             break;
59         }
60     }
61     if(m_channel==IPO_MAX)
62     {
63         Log::error("Animation", "Unknown animation channel: '%s' --> Ignored",
64                 channel.c_str());
65         return;
66     }
67 
68     std::string interp;
69     curve.get("interpolation", &interp);
70     if     (interp=="const" ) m_interpolation = IP_CONST;
71     else if(interp=="linear") m_interpolation = IP_LINEAR;
72     else                      m_interpolation = IP_BEZIER;
73 
74     std::string extend;
75     curve.get("extend", &extend);
76     if      (extend=="cyclic") m_extend = ET_CYCLIC;
77     else if (extend=="const" ) m_extend = ET_CONST;
78     else
79     {
80         // For now extrap and cyclic_extrap do not work
81         Log::warn("Animation", "Unsupported extend '%s' - defaulting to CONST.",
82                   extend.c_str());
83         m_extend = ET_CONST;
84     }
85 
86     if(m_channel==IPO_LOCXYZ)
87         readCurve(curve, reverse);
88     else
89         readIPO(curve, fps, reverse);
90 
91 }   // IpoData
92 
93 // ----------------------------------------------------------------------------
94 /** Reads a blender IPO curve, which constists of a frame number and a control
95  *  point. This only handles a single axis.
96  *  \param node The root node with all curve data points.
97  *  \param fps Frames per second value, necessary to convert the frame based
98  *             data from blender into times.
99  *  \param reverse If this is set, the data are read in reverse. This is used
100  *                 for a cannon in reverse mode.
101  */
readIPO(const XMLNode & curve,float fps,bool reverse)102 void Ipo::IpoData::readIPO(const XMLNode &curve, float fps, bool reverse)
103 {
104     m_start_time =  999999.9f;
105     m_end_time   = -999999.9f;
106     for(unsigned int i=0; i<curve.getNumNodes(); i++)
107     {
108         int node_index = reverse ? curve.getNumNodes()-i-1 : i;
109         const XMLNode *node = curve.getNode(node_index);
110         core::vector2df xy;
111         node->get("c", &xy);
112         // Convert blender's frame number (1 ...) into time (0 ...)
113         float t = (xy.X-1)/fps;
114         Vec3 point(xy.Y, 0, 0, t);
115         m_points.push_back(point);
116         m_start_time = std::min(m_start_time, t);
117         m_end_time   = std::max(m_end_time,   t);
118         if(m_interpolation==IP_BEZIER)
119         {
120             Vec3 handle1, handle2;
121             core::vector2df handle;
122             node->get(reverse ? "h2" : "h1", &handle);
123             handle1.setW((xy.X-1)/fps);
124             handle1.setX(handle.Y);
125             node->get(reverse ? "h1" : "h2", &handle);
126             handle2.setW((xy.X-1)/fps);
127             handle2.setX(handle.Y);
128             m_handle1.push_back(handle1);
129             m_handle2.push_back(handle2);
130         }
131     }   // for i<getNumNodes()
132 }   // IpoData::readIPO
133 
134 // ----------------------------------------------------------------------------
135 /** Reads in 3 dimensional curve data - i.e. the xml file contains xyz, but no
136  *  time. If the curve is using bezier interpolation, the curve is
137  *  approximated by piecewise linear functions. Reason is that bezier curves
138  *  can not (easily) be used for smooth (i.e. constant speed) driving:
139  *  A linear time variation in [0, 1] will result in non-linear distances
140  *  for the bezier function, which is a 3rd degree polynomial (--> the speed
141  *  which is the deviation of this function is a 2nd degree polynomial, and
142  *  therefore not constant!
143  *  \param node The root node with all curve data points.
144  *  \param reverse If this is set, the data are read in reverse. This is used
145  *                 for a cannon in reverse mode.
146  */
readCurve(const XMLNode & curve,bool reverse)147 void Ipo::IpoData::readCurve(const XMLNode &curve, bool reverse)
148 {
149     m_start_time =  0;
150     m_end_time   = -999999.9f;
151     float speed  = 30.0f;
152     curve.get("speed", &speed);
153 
154     for(unsigned int i=0; i<curve.getNumNodes(); i++)
155     {
156         int node_index = reverse ? curve.getNumNodes()-i-1 : i;
157         const XMLNode *node = curve.getNode(node_index);
158         Vec3 point;
159         node->get("c", &point);
160 
161         if(m_interpolation==IP_BEZIER)
162         {
163             Vec3 handle;
164             node->get(reverse ? "h2" : "h1", &handle);
165             m_handle1.push_back(handle);
166             node->get(reverse ? "h1" : "h2", &handle);
167             m_handle2.push_back(handle);
168             if(i>0)
169             {
170                 // We have to take a copy of the end point, since otherwise
171                 // it can happen that as more points are added to m_points
172                 // in the approximateBezier function, the data gets
173                 // reallocated and then the reference to the original point
174                 // is not correct anymore.
175                 Vec3 end_point = m_points[m_points.size()-1];
176                 approximateBezier(0.0f, 1.0f, end_point, point,
177                                               m_handle2[i-1], m_handle1[i]);
178             }
179         }
180         m_points.push_back(point);
181     }   // for i<getNumNodes()
182 
183     // The handles of a bezier curve are not needed anymore and can be
184     // removed now (since the bezier funciton has been replaced with a
185     // piecewise linear
186     if(m_interpolation==IP_BEZIER)
187     {
188         m_handle1.clear();
189         m_handle2.clear();
190         m_interpolation = IP_LINEAR;
191     }
192 
193     if(m_points.size()==0) return;
194 
195     // Compute the time for each segment based on the speed and
196     // store it in the W component.
197     m_points[0].setW(0);
198     for(unsigned int i=1; i<m_points.size(); i++)
199     {
200         m_points[i].setW( (m_points[i]-m_points[i-1]).length()/speed
201                          + m_points[i-1].getW()                   );
202     }
203     m_end_time = m_points.back().getW();
204 }   // IpoData::readCurve
205 
206 // ----------------------------------------------------------------------------
207 /** This function approximates a bezier curve by piecewise linear functions.
208  *  It uses quite primitive approximations: if the estimated distance of
209  *  the bezier curve at between t=t0 and t=t1 is greater than 2, it
210  *  inserts one point at (t0+t1)/2, and recursively splits the two intervals
211  *  further. End condition is either a maximum recursion depth of 6 or
212  *  an estimated curve length of less than 2. It does not add any points
213  *  at t=t0 or t=t1, only between this interval.
214  *  \param t0, t1 The interval which is approximated.
215  *  \param p0, p1, h0, h1: The bezier parameters.
216  *  \param rec_level The recursion level to avoid creating too many points.
217  */
approximateBezier(float t0,float t1,const Vec3 & p0,const Vec3 & p1,const Vec3 & h0,const Vec3 & h1,unsigned int rec_level)218 void Ipo::IpoData::approximateBezier(float t0, float t1,
219                                const Vec3 &p0, const Vec3 &p1,
220                                const Vec3 &h0, const Vec3 &h1,
221                                unsigned int rec_level)
222 {
223     // Limit the granularity by limiting the recursion depth
224     if(rec_level>6)
225         return;
226 
227     float distance = approximateLength(t0, t1, p0, p1, h0, h1);
228     // A more sophisticated estimation might be useful (e.g. taking the
229     // difference between a linear approximation and the actual bezier
230     // curve into accound.
231     if(distance<=0.2f)
232         return;
233 
234     // Insert one point at (t0+t1)/2. First split the left part of
235     // the interval by a recursive call, then insert the point at
236     // (t0+t1)/2, then approximate the right part of the interval.
237     approximateBezier(t0, (t0+t1)*0.5f, p0, p1, h0, h1, rec_level + 1);
238     Vec3 middle;
239     for(unsigned int j=0; j<3; j++)
240         middle[j] = getCubicBezier((t0+t1)*0.5f, p0[j], h0[j], h1[j], p1[j]);
241     m_points.push_back(middle);
242     approximateBezier((t0+t1)*0.5f, t1, p0, p1, h0, h1, rec_level + 1);
243 
244 }   // approximateBezier
245 
246 // ----------------------------------------------------------------------------
247 /** Approximates the length of a bezier curve using a simple Euler
248  *  approximation by dividing the interval [t0, t1] into 10 pieces. Good enough
249  *  for our needs in STK.
250  *  \param t0, t1 Approximate for t in [t0, t1].
251  *  \param p0, p1 The start and end point of the curve.
252  *  \param h0, h1 The control points for the corresponding points.
253  */
approximateLength(float t0,float t1,const Vec3 & p0,const Vec3 & p1,const Vec3 & h0,const Vec3 & h1)254 float Ipo::IpoData::approximateLength(float t0, float t1,
255                                       const Vec3 &p0, const Vec3 &p1,
256                                       const Vec3 &h0, const Vec3 &h1)
257 {
258     assert(m_interpolation == IP_BEZIER);
259 
260     float distance=0;
261     const unsigned int NUM_STEPS=10;
262     float delta = (t1-t0)/NUM_STEPS;
263     Vec3 prev_point;
264     for(unsigned int j=0; j<3; j++)
265         prev_point[j] = getCubicBezier(t0, p0[j], h0[j], h1[j], p1[j]);
266     for(unsigned int i=1; i<=NUM_STEPS; i++)
267     {
268         float t = t0 + i * delta;
269         Vec3 next_point;
270         // Interpolate all three axis
271         for(unsigned j=0; j<3; j++)
272         {
273             next_point[j] = getCubicBezier(t, p0[j], h0[j], h1[j], p1[j]);
274         }
275         distance  += (next_point - prev_point).length();
276         prev_point = next_point;
277     }   // for i< NUM_STEPS
278 
279     return distance;
280 }   // IpoData::approximateLength
281 
282 // ----------------------------------------------------------------------------
283 /** Adjusts the time so that it is between start and end of this Ipo. This
284  *  takes the extend type into account, e.g. cyclic animations will just
285  *  use a modulo operation, while constant extends will return start or
286  *  end time directly.
287  *  \param time The time to adjust.
288  */
adjustTime(float time)289 float Ipo::IpoData::adjustTime(float time)
290 {
291     if(time<m_start_time)
292     {
293         switch(m_extend)
294         {
295         case IpoData::ET_CYCLIC:
296             time = m_start_time + fmodf(time, m_end_time-m_start_time); break;
297         case ET_CONST:
298             time = m_start_time; break;
299         default:
300             // FIXME: ET_CYCLIC_EXTRAP and ET_EXTRAP missing
301             assert(false);
302         }   // switch m_extend
303     }   // if time < m_start_time
304 
305     else if(time > m_end_time)
306     {
307         switch(m_extend)
308         {
309         case ET_CYCLIC:
310             time = m_start_time + fmodf(time, m_end_time-m_start_time); break;
311         case ET_CONST:
312             time = m_end_time; break;
313         default:
314             // FIXME: ET_CYCLIC_EXTRAP and ET_EXTRAP missing
315             assert(false);
316         }   // switch m_extend
317     }   // if time > m_end_time
318     return time;
319 }   // adjustTime
320 
321 // ----------------------------------------------------------------------------
get(float time,unsigned int index,unsigned int n)322 float Ipo::IpoData::get(float time, unsigned int index, unsigned int n)
323 {
324     switch(m_interpolation)
325     {
326     case IP_CONST  : return m_points[n][index];
327     case IP_LINEAR : {
328                         float t = time-m_points[n].getW();
329                         return m_points[n][index]
330                              + t*(m_points[n+1][index]-m_points[n][index]) /
331                                  (m_points[n+1].getW()-m_points[n].getW());
332                      }
333     case IP_BEZIER:  {  if(n==m_points.size()-1)
334                         {
335                             // FIXME: only const implemented atm.
336                             return m_points[n][index];
337                         }
338                         float t = (time-m_points[n].getW())
339                                 / (m_points[n+1].getW()-m_points[n].getW());
340                         return getCubicBezier(t,
341                                               m_points [n  ][index],
342                                               m_handle2[n  ][index],
343                                               m_handle1[n+1][index],
344                                               m_points [n+1][index]);
345                     }
346     }   // switch
347     // Keep the compiler happy:
348     return 0;
349 }   // IpoData::get
350 
351 // ----------------------------------------------------------------------------
352 /** Computes a cubic bezier curve for a given t in [0,1] and four control
353  *  points. The curve will go through p0 (t=0), p3 (t=1).
354  *  \param t The parameter for the bezier curve, must be in [0,1].
355  *  \param p0, p1, p2, p3 The four control points.
356  */
getCubicBezier(float t,float p0,float p1,float p2,float p3) const357 float Ipo::IpoData::getCubicBezier(float t, float p0, float p1,
358                                    float p2, float p3) const
359 {
360     float c = 3.0f*(p1-p0);
361     float b = 3.0f*(p2-p1)-c;
362     float a = p3 - p0 - c - b;
363     return ((a*t+b)*t+c)*t+p0;
364 }   // getCubicBezier
365 
366 // ----------------------------------------------------------------------------
367 /** Determines the derivative of a IPO at a given point.
368  *  \param time At what time value the derivative is to be computed.
369  *  \param index IpoData is based on 3d data. The index specified which
370  *         value to use (0=x, 1=y, 2=z).
371  *  \param n Curve segment to be used for the computation. It must be correct
372  *         for the specified time value.
373  */
getDerivative(float time,unsigned int index,unsigned int n)374 float Ipo::IpoData::getDerivative(float time, unsigned int index,
375                                   unsigned int n)
376 {
377     switch (m_interpolation)
378     {
379     case IP_CONST: return 0;   // Const --> Derivative is 0
380     case IP_LINEAR: {
381         return (m_points[n + 1][index] - m_points[n][index]) /
382                (m_points[n + 1].getW() - m_points[n].getW());
383     }
384     case IP_BEZIER: {
385         if (n == m_points.size() - 1)
386         {
387             // Only const, so derivative is 0
388             return 0;
389         }
390         float t = (time - m_points[n].getW())
391                 / (m_points[n + 1].getW() - m_points[n].getW());
392         return getCubicBezierDerivative(t,
393                                         m_points [n    ][index],
394                                         m_handle2[n    ][index],
395                                         m_handle1[n + 1][index],
396                                         m_points [n + 1][index] );
397     }   // case IPBEZIER
398     default:
399         Log::warn("Ipo::IpoData", "Incorrect interpolation %d",
400                   m_interpolation);
401     }   // switch
402     return 0;
403 }   // IpoData::getDerivative
404 
405 
406 // ----------------------------------------------------------------------------
407 /** Returns the derivative of a cubic bezier curve for a given t in [0,1] and
408  *  four control points. The curve will go through p0 (t=0).
409  *  \param t The parameter for the bezier curve, must be in [0,1].
410  *  \param p0, p1, p2, p3 The four control points.
411  */
getCubicBezierDerivative(float t,float p0,float p1,float p2,float p3) const412 float Ipo::IpoData::getCubicBezierDerivative(float t, float p0, float p1,
413                                              float p2, float p3) const
414 {
415     float c = 3.0f*(p1 - p0);
416     float b = 3.0f*(p2 - p1) - c;
417     float a = p3 - p0 - c - b;
418     // f(t)      = ((a*t + b)*t + c)*t + p0;
419     //           = a*t^3 +b*t^2 + c*t + p0
420     // --> f'(t) = 3*a*t^2 + 2*b*t + c
421     return (3*a * t + 2*b) * t + c;
422 }   // bezier
423 
424 // ============================================================================
425 /** The Ipo constructor. Ipos can share the actual data to interpolate, which
426  *  is stored in a separate IpoData object, see Ipo(const Ipo *ipo)
427  *  constructor. This is used for cannons: the actual check line stores the
428  *  'master' Ipo, and each actual IPO that animate a kart just use a copy
429  *  of this read-only data.
430  *  \param curve The XML data for this curve.
431  *  \param fps Frames per second, used to convert all frame based value
432  *         in the xml file into seconds.
433  *  \param reverse If this is set to true, the ipo data will be reverse. This
434  *             is used by the cannon if the track is driven in reverse.
435  */
Ipo(const XMLNode & curve,float fps,bool reverse)436 Ipo::Ipo(const XMLNode &curve, float fps, bool reverse)
437 {
438     m_ipo_data     = new IpoData(curve, fps, reverse);
439     m_own_ipo_data = true;
440     reset();
441 }   // Ipo
442 
443 // ----------------------------------------------------------------------------
444 /** A copy constructor. It shares the read-only data with the source Ipo
445  *  \param ipo The ipo to copy from.
446  */
Ipo(const Ipo * ipo)447 Ipo::Ipo(const Ipo *ipo)
448 {
449     // Share the read-only data
450     m_ipo_data     = ipo->m_ipo_data;
451     m_own_ipo_data = false;
452     reset();
453 }   // Ipo(Ipo*)
454 
455 // ----------------------------------------------------------------------------
456 /** Creates a copy of this object (the copy constructor is disabled in order
457  *  to avoid implicit copies happening).
458  */
clone()459 Ipo  *Ipo::clone()
460 {
461     return new Ipo(this);
462 }   // clone
463 
464 // ----------------------------------------------------------------------------
465 /** The destructor only frees IpoData if it was created by this instance (and
466  *  not if this instance was copied, therefore sharing the IpoData).
467  */
~Ipo()468 Ipo::~Ipo()
469 {
470     if(m_own_ipo_data)
471         delete m_ipo_data;
472 }   // ~Ipo
473 
474 // ----------------------------------------------------------------------------
475 /** Stores the initial transform. This is necessary for relative IPOs.
476  *  \param xyz Position of the object.
477  *  \param hpr Rotation of the object.
478  */
setInitialTransform(const Vec3 & xyz,const Vec3 & hpr)479 void  Ipo::setInitialTransform(const Vec3 &xyz,
480                                const Vec3 &hpr)
481 {
482     m_ipo_data->m_initial_xyz = xyz;
483     m_ipo_data->m_initial_hpr = hpr;
484 }   // setInitialTransform
485 
486 // ----------------------------------------------------------------------------
487 /** Resets the IPO for (re)starting an animation.
488  */
reset()489 void Ipo::reset()
490 {
491     m_next_n = 1;
492 }   // reset
493 
494 // ----------------------------------------------------------------------------
495 /** Updates the time of this ipo and interpolates the new position and
496  *  rotation (taking the cycle length etc. into account). If a NULL is
497  *  given, the value is not updated.
498  *  \param time Current time for which to determine the interpolation.
499  *  \param xyz The position that needs to be updated (can be NULL).
500  *  \param hpr The rotation that needs to be updated (can be NULL).
501  *  \param scale The scale that needs to be updated (can be NULL)
502  */
update(float time,Vec3 * xyz,Vec3 * hpr,Vec3 * scale)503 void Ipo::update(float time, Vec3 *xyz, Vec3 *hpr,Vec3 *scale)
504 {
505     assert(!std::isnan(time));
506     switch(m_ipo_data->m_channel)
507     {
508     case Ipo::IPO_LOCX   : if(xyz)   xyz  ->setX(get(time, 0)); break;
509     case Ipo::IPO_LOCY   : if(xyz)   xyz  ->setY(get(time, 0)); break;
510     case Ipo::IPO_LOCZ   : if(xyz)   xyz  ->setZ(get(time, 0)); break;
511     case Ipo::IPO_ROTX   : if(hpr)   hpr  ->setX(get(time, 0)); break;
512     case Ipo::IPO_ROTY   : if(hpr)   hpr  ->setY(get(time, 0)); break;
513     case Ipo::IPO_ROTZ   : if(hpr)   hpr  ->setZ(get(time, 0)); break;
514     case Ipo::IPO_SCALEX : if(scale) scale->setX(get(time, 0)); break;
515     case Ipo::IPO_SCALEY : if(scale) scale->setY(get(time, 0)); break;
516     case Ipo::IPO_SCALEZ : if(scale) scale->setZ(get(time, 0)); break;
517     case Ipo::IPO_LOCXYZ :
518         {
519             if(xyz)
520             {
521                 for(unsigned int j=0; j<3; j++)
522                     (*xyz)[j] = get(time, j);
523             }
524             break;
525         }
526 
527     default: assert(false); // shut up compiler warning
528     }    // switch
529 
530 }   // update
531 
532 // ----------------------------------------------------------------------------
533 /** Updates the value of m_next_n to point to the right ipo segment based on
534  *  the time.
535  *  \param t Time for which m_next_n needs to be updated.
536  */
updateNextN(float * time) const537 void Ipo::updateNextN(float *time) const
538 {
539     *time = m_ipo_data->adjustTime(*time);
540 
541     // Time was reset since the last cached value for n,
542     // reset n to start from the beginning again.
543     if (*time < m_ipo_data->m_points[m_next_n - 1].getW())
544         m_next_n = 1;
545     // Search for the first point in the (sorted) array which is greater or equal
546     // to the current time.
547     while (m_next_n < m_ipo_data->m_points.size() - 1 &&
548         *time >= m_ipo_data->m_points[m_next_n].getW())
549     {
550         m_next_n++;
551     }   // while
552 }   // updateNextN
553 
554 // ----------------------------------------------------------------------------
555 /** Returns the interpolated value at the current time (which this objects
556  *  keeps track of).
557  *  \param time The time for which the interpolated value should be computed.
558  */
get(float time,unsigned int index) const559 float Ipo::get(float time, unsigned int index) const
560 {
561     assert(!std::isnan(time));
562 
563     // Avoid crash in case that only one point is given for this IPO.
564     if(m_next_n==0)
565         return m_ipo_data->m_points[0][index];
566 
567     updateNextN(&time);
568 
569     float rval = m_ipo_data->get(time, index, m_next_n-1);
570     assert(!std::isnan(rval));
571     return rval;
572 }   // get
573 
574 // ----------------------------------------------------------------------------
575 /** Returns the derivative for any location based curves.
576  *  \param time Time for which the derivative is being computed.
577  *  \param xyz Pointer where the results should be stored.
578  */
getDerivative(float time,Vec3 * xyz)579 void Ipo::getDerivative(float time, Vec3 *xyz)
580 {
581     // Avoid crash in case that only one point is given for this IPO.
582     if (m_next_n == 0)
583     {
584         // Derivative has no real meaning in case of a single point.
585         // So just return a dummy value.
586         xyz->setValue(1, 0, 0);
587         return;
588     }
589 
590     updateNextN(&time);
591     switch (m_ipo_data->m_channel)
592     {
593     case Ipo::IPO_LOCX: xyz->setX(m_ipo_data->getDerivative(time, m_next_n, 0)); break;
594     case Ipo::IPO_LOCY: xyz->setY(m_ipo_data->getDerivative(time, m_next_n, 0)); break;
595     case Ipo::IPO_LOCZ: xyz->setZ(m_ipo_data->getDerivative(time, m_next_n, 0)); break;
596     case Ipo::IPO_LOCXYZ:
597     {
598         if (xyz)
599         {
600             for (unsigned int j = 0; j < 3; j++)
601                 (*xyz)[j] = m_ipo_data->getDerivative(time, j, m_next_n-1);
602         }
603         break;
604     }
605     default: Log::warn("IPO", "Unexpected channel %d for derivate.",
606                        m_ipo_data->m_channel                         );
607         xyz->setValue(1, 0, 0);
608         break;
609     }   // switch
610 
611 
612 }   // getDerivative
613 
614