]> git.mxchange.org Git - simgear.git/blob - simgear/scene/model/particles.cxx
Replace rotation animation update callbacks with cull callbacks
[simgear.git] / simgear / scene / model / particles.cxx
1 // particles.cxx - classes to manage particles
2 // started in 2008 by Tiago Gusmão, using animation.hxx as reference
3 // Copyright (C) 2008 Tiago Gusmão
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 as
7 // published by the Free Software Foundation; either version 2 of the
8 // License, or (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful, but
11 // WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 // 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 //
19
20 #ifdef HAVE_CONFIG_H
21 #  include <simgear_config.h>
22 #endif
23
24 #include <simgear/misc/sg_path.hxx>
25 #include <simgear/props/props.hxx>
26 #include <simgear/props/props_io.hxx>
27 #include <simgear/scene/util/OsgMath.hxx>
28 #include <simgear/structure/OSGVersion.hxx>
29
30 #include <osgParticle/SmokeTrailEffect>
31 #include <osgParticle/FireEffect>
32 #include <osgParticle/ConnectedParticleSystem>
33 #include <osgParticle/MultiSegmentPlacer>
34 #include <osgParticle/SectorPlacer>
35 #include <osgParticle/ConstantRateCounter>
36 #include <osgParticle/ParticleSystemUpdater>
37 #include <osgParticle/ParticleSystem>
38 #include <osgParticle/FluidProgram>
39
40 #include <osg/Geode>
41 #include <osg/Group>
42 #include <osg/MatrixTransform>
43 #include <osg/Node>
44
45 #include "particles.hxx"
46
47 #if SG_OSG_VERSION >= 27004
48 #define OSG_PARTICLE_FIX 1
49 #endif
50
51 namespace simgear
52 {
53 void GlobalParticleCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
54 {
55     enabled = !enabledNode || enabledNode->getBoolValue();
56     if (!enabled)
57         return;
58     SGQuatd q
59         = SGQuatd::fromLonLatDeg(modelRoot->getFloatValue("/position/longitude-deg",0),
60                                  modelRoot->getFloatValue("/position/latitude-deg",0));
61     osg::Matrix om(toOsg(q));
62     osg::Vec3 v(0,0,9.81);
63     gravity = om.preMult(v);
64     // NOTE: THIS WIND COMPUTATION DOESN'T SEEM TO AFFECT PARTICLES
65     const osg::Vec3& zUpWind = Particles::getWindVector();
66     osg::Vec3 w(zUpWind.y(), zUpWind.x(), -zUpWind.z());
67     wind = om.preMult(w);
68
69     // SG_LOG(SG_GENERAL, SG_ALERT,
70     //        "wind vector:" << w[0] << "," <<w[1] << "," << w[2]);
71 }
72
73
74 //static members
75 osg::Vec3 GlobalParticleCallback::gravity;
76 osg::Vec3 GlobalParticleCallback::wind;
77 bool GlobalParticleCallback::enabled = true;
78 SGConstPropertyNode_ptr GlobalParticleCallback::enabledNode = 0;
79
80 osg::ref_ptr<osg::Group> Particles::commonRoot;
81 osg::ref_ptr<osgParticle::ParticleSystemUpdater> Particles::psu = new osgParticle::ParticleSystemUpdater;
82 osg::ref_ptr<osg::Geode> Particles::commonGeode = new osg::Geode;;
83 osg::Vec3 Particles::_wind;
84 bool Particles::_frozen = false;
85
86 Particles::Particles() : 
87     useGravity(false),
88     useWind(false)
89 {
90 }
91
92 template <typename Object>
93 class PointerGuard{
94 public:
95     PointerGuard() : _ptr(0) {}
96     Object* get() { return _ptr; }
97     Object* operator () ()
98     {
99         if (!_ptr)
100             _ptr = new Object;
101         return _ptr;
102     }
103 private:
104     Object* _ptr;
105 };
106
107 osg::Group* Particles::getCommonRoot()
108 {
109     if(!commonRoot.valid())
110     {
111         SG_LOG(SG_GENERAL, SG_DEBUG, "Particle common root called!\n");
112         commonRoot = new osg::Group;
113         commonRoot.get()->setName("common particle system root");
114         commonGeode.get()->setName("common particle system geode");
115         commonRoot.get()->addChild(commonGeode.get());
116         commonRoot.get()->addChild(psu.get());
117     }
118     return commonRoot.get();
119 }
120
121 void transformParticles(osgParticle::ParticleSystem* particleSys,
122                         const osg::Matrix& mat)
123 {
124     const int numParticles = particleSys->numParticles();
125     if (particleSys->areAllParticlesDead())
126         return;
127     for (int i = 0; i < numParticles; ++i) {
128         osgParticle::Particle* P = particleSys->getParticle(i);
129         if (!P->isAlive())
130             continue;
131         P->transformPositionVelocity(mat);
132     }
133 }
134
135 osg::Group * Particles::appendParticles(const SGPropertyNode* configNode,
136                                           SGPropertyNode* modelRoot,
137                                           const osgDB::Options*
138                                           options)
139 {
140     SG_LOG(SG_GENERAL, SG_DEBUG, "Setting up a particle system!\n");
141
142     osgParticle::ParticleSystem *particleSys;
143
144     //create a generic particle system
145     std::string type = configNode->getStringValue("type", "normal");
146     if (type == "normal")
147         particleSys = new osgParticle::ParticleSystem;
148     else
149         particleSys = new osgParticle::ConnectedParticleSystem;
150     //may not be used depending on the configuration
151     PointerGuard<Particles> callback;
152
153     getPSU()->addParticleSystem(particleSys); 
154     getPSU()->setUpdateCallback(new GlobalParticleCallback(modelRoot));
155     //contains counter, placer and shooter by default
156     osgParticle::ModularEmitter* emitter = new osgParticle::ModularEmitter;
157
158     emitter->setParticleSystem(particleSys);
159
160     // Set up the alignment node ("stolen" from animation.cxx)
161     // XXX Order of rotations is probably not correct.
162     osg::MatrixTransform *align = new osg::MatrixTransform;
163     osg::Matrix res_matrix;
164     res_matrix.makeRotate(
165         configNode->getFloatValue("offsets/pitch-deg", 0.0)*SG_DEGREES_TO_RADIANS,
166         osg::Vec3(0, 1, 0),
167         configNode->getFloatValue("offsets/roll-deg", 0.0)*SG_DEGREES_TO_RADIANS,
168         osg::Vec3(1, 0, 0),
169         configNode->getFloatValue("offsets/heading-deg", 0.0)*SG_DEGREES_TO_RADIANS,
170         osg::Vec3(0, 0, 1));
171
172     osg::Matrix tmat;
173     tmat.makeTranslate(configNode->getFloatValue("offsets/x-m", 0.0),
174                        configNode->getFloatValue("offsets/y-m", 0.0),
175                        configNode->getFloatValue("offsets/z-m", 0.0));
176     align->setMatrix(res_matrix * tmat);
177
178     align->setName("particle align");
179
180     //if (dynamic_cast<CustomModularEmitter*>(emitter)==0) SG_LOG(SG_GENERAL, SG_ALERT, "observer error\n");
181     //align->addObserver(dynamic_cast<CustomModularEmitter*>(emitter));
182
183     align->addChild(emitter);
184
185     //this name can be used in the XML animation as if it was a submodel
186     std::string name = configNode->getStringValue("name", "");
187     if (!name.empty())
188         align->setName(name);
189     std::string attach = configNode->getStringValue("attach", "world");
190     if (attach == "local") { //local means attached to the model and not the world
191         osg::Geode* g = new osg::Geode;
192         align->addChild(g);
193         g->addDrawable(particleSys);
194 #ifndef OSG_PARTICLE_FIX
195         emitter->setReferenceFrame(osgParticle::Emitter::ABSOLUTE_RF);
196 #endif
197     } else {
198 #ifdef OSG_PARTICLE_FIX
199         callback()->particleFrame = new osg::MatrixTransform();
200         osg::Geode* g = new osg::Geode;
201         g->addDrawable(particleSys);
202         callback()->particleFrame->addChild(g);
203         getCommonRoot()->addChild(callback()->particleFrame.get());
204 #else
205         getCommonGeode()->addDrawable(particleSys);
206 #endif
207     }
208     std::string textureFile;
209     if (configNode->hasValue("texture")) {
210         //SG_LOG(SG_GENERAL, SG_ALERT,
211         //       "requested:"<<configNode->getStringValue("texture","")<<"\n");
212         textureFile= osgDB::findFileInPath(configNode->getStringValue("texture",
213                                                                       ""),
214                                            options->getDatabasePathList());
215         //SG_LOG(SG_GENERAL, SG_ALERT, "found:"<<textureFile<<"\n");
216
217         //for(unsigned i = 0; i < options->getDatabasePathList().size(); ++i)
218         //    SG_LOG(SG_GENERAL, SG_ALERT,
219         //           "opts:"<<options->getDatabasePathList()[i]<<"\n");
220     }
221
222     particleSys->setDefaultAttributes(textureFile,
223                                       configNode->getBoolValue("emissive",
224                                                                true),
225                                       configNode->getBoolValue("lighting",
226                                                                false));
227
228     std::string alignstr = configNode->getStringValue("align", "billboard");
229
230     if (alignstr == "fixed")
231         particleSys->setParticleAlignment(osgParticle::ParticleSystem::FIXED);
232
233     const SGPropertyNode* placernode = configNode->getChild("placer");
234
235     if (placernode) {
236         std::string emitterType = placernode->getStringValue("type", "point");
237
238         if (emitterType == "sector") {
239             osgParticle::SectorPlacer *splacer = new  osgParticle::SectorPlacer;
240             float minRadius, maxRadius, minPhi, maxPhi;
241
242             minRadius = placernode->getFloatValue("radius-min-m",0);
243             maxRadius = placernode->getFloatValue("radius-max-m",1);
244             minPhi = (placernode->getFloatValue("phi-min-deg",0)
245                       * SG_DEGREES_TO_RADIANS);
246             maxPhi = (placernode->getFloatValue("phi-max-deg",360.0f)
247                       * SG_DEGREES_TO_RADIANS);
248
249             splacer->setRadiusRange(minRadius, maxRadius);
250             splacer->setPhiRange(minPhi, maxPhi);
251             emitter->setPlacer(splacer);
252         } else if (emitterType == "segments") {
253             std::vector<SGPropertyNode_ptr> segments
254                 = placernode->getChildren("vertex");
255             if (segments.size()>1) {
256                 osgParticle::MultiSegmentPlacer *msplacer
257                     = new osgParticle::MultiSegmentPlacer();
258                 float x,y,z;
259
260                 for (unsigned i = 0; i < segments.size(); ++i) {
261                     x = segments[i]->getFloatValue("x-m",0);
262                     y = segments[i]->getFloatValue("y-m",0);
263                     z = segments[i]->getFloatValue("z-m",0);
264                     msplacer->addVertex(x, y, z);
265                 }
266                 emitter->setPlacer(msplacer);
267             } else {
268                 SG_LOG(SG_GENERAL, SG_ALERT,
269                        "Detected particle system using segment(s) with less than 2 vertices\n");
270             }
271         } //else the default placer in ModularEmitter is used (PointPlacer)
272     }
273
274     const SGPropertyNode* shnode = configNode->getChild("shooter");
275
276     if (shnode) {
277         float minTheta, maxTheta, minPhi, maxPhi, speed, spread;
278
279         minTheta = (shnode->getFloatValue("theta-min-deg",0)
280                     * SG_DEGREES_TO_RADIANS);
281         maxTheta = (shnode->getFloatValue("theta-max-deg",360.0f)
282                     * SG_DEGREES_TO_RADIANS);
283         minPhi = shnode->getFloatValue("phi-min-deg",0)* SG_DEGREES_TO_RADIANS;
284         maxPhi = (shnode->getFloatValue("phi-max-deg",360.0f)
285                   * SG_DEGREES_TO_RADIANS); 
286
287         osgParticle::RadialShooter *shooter = new osgParticle::RadialShooter;
288         emitter->setShooter(shooter);
289
290         shooter->setThetaRange(minTheta, maxTheta);
291         shooter->setPhiRange(minPhi, maxPhi);
292
293         const SGPropertyNode* speednode = shnode->getChild("speed-mps");
294
295         if (speednode) {
296             if (speednode->hasValue("value")) {
297                 speed = speednode->getFloatValue("value",0);
298                 spread = speednode->getFloatValue("spread",0);
299                 shooter->setInitialSpeedRange(speed-spread, speed+spread);
300             } else {
301                 callback()->setupShooterSpeedData(speednode, modelRoot);
302             }
303         }
304
305         const SGPropertyNode* rotspeednode = shnode->getChild("rotation-speed");
306
307         if (rotspeednode) {
308             float x1,y1,z1,x2,y2,z2;
309             x1 = rotspeednode->getFloatValue("x-min-deg-sec",0) * SG_DEGREES_TO_RADIANS;
310             y1 = rotspeednode->getFloatValue("y-min-deg-sec",0) * SG_DEGREES_TO_RADIANS;
311             z1 = rotspeednode->getFloatValue("z-min-deg-sec",0) * SG_DEGREES_TO_RADIANS;
312             x2 = rotspeednode->getFloatValue("x-max-deg-sec",0) * SG_DEGREES_TO_RADIANS;
313             y2 = rotspeednode->getFloatValue("y-max-deg-sec",0) * SG_DEGREES_TO_RADIANS;
314             z2 = rotspeednode->getFloatValue("z-max-deg-sec",0) * SG_DEGREES_TO_RADIANS;
315             shooter->setInitialRotationalSpeedRange(osg::Vec3f(x1,y1,z1), osg::Vec3f(x2,y2,z2));
316         }
317     } //else ModularEmitter uses the default RadialShooter
318
319
320     const SGPropertyNode* conditionNode = configNode->getChild("condition");
321     const SGPropertyNode* counternode = configNode->getChild("counter");
322
323     if (conditionNode || counternode) {
324         osgParticle::RandomRateCounter* counter
325             = new osgParticle::RandomRateCounter;
326         emitter->setCounter(counter);
327         float pps = 0.0f, spread = 0.0f;
328
329         if (counternode) {
330             const SGPropertyNode* ppsnode = counternode->getChild("particles-per-sec");
331             if (ppsnode) {
332                 if (ppsnode->hasValue("value")) {
333                     pps = ppsnode->getFloatValue("value",0);
334                     spread = ppsnode->getFloatValue("spread",0);
335                     counter->setRateRange(pps-spread, pps+spread);
336                 } else {
337                     callback()->setupCounterData(ppsnode, modelRoot);
338                 }
339             }
340         }
341
342         if (conditionNode) {
343             callback()->setupCounterCondition(conditionNode, modelRoot);
344             callback()->setupCounterCondition(pps, spread);
345         }
346     } //TODO: else perhaps set higher values than default? 
347
348     const SGPropertyNode* particlenode = configNode->getChild("particle");
349     if (particlenode) {
350         osgParticle::Particle &particle
351             = particleSys->getDefaultParticleTemplate();
352         float r1=0, g1=0, b1=0, a1=1, r2=0, g2=0, b2=0, a2=1;
353         const SGPropertyNode* startcolornode
354             = particlenode->getNode("start/color");
355         if (startcolornode) {
356             const SGPropertyNode* componentnode
357                 = startcolornode->getChild("red");
358             if (componentnode) {
359                 if (componentnode->hasValue("value"))
360                     r1 = componentnode->getFloatValue("value",0);
361                 else 
362                     callback()->setupColorComponent(componentnode, modelRoot,
363                                                     0, 0);
364             }
365             componentnode = startcolornode->getChild("green");
366             if (componentnode) {
367                 if (componentnode->hasValue("value"))
368                     g1 = componentnode->getFloatValue("value", 0);
369                 else
370                     callback()->setupColorComponent(componentnode, modelRoot,
371                                                     0, 1);
372             }
373             componentnode = startcolornode->getChild("blue");
374             if (componentnode) {
375                 if (componentnode->hasValue("value"))
376                     b1 = componentnode->getFloatValue("value",0);
377                 else
378                     callback()->setupColorComponent(componentnode, modelRoot,
379                                                     0, 2);
380             }
381             componentnode = startcolornode->getChild("alpha");
382             if (componentnode) {
383                 if (componentnode->hasValue("value"))
384                     a1 = componentnode->getFloatValue("value",0);
385                 else
386                     callback()->setupColorComponent(componentnode, modelRoot,
387                                                     0, 3);
388             }
389         }
390         const SGPropertyNode* endcolornode = particlenode->getNode("end/color");
391         if (endcolornode) {
392             const SGPropertyNode* componentnode = endcolornode->getChild("red");
393
394             if (componentnode) {
395                 if (componentnode->hasValue("value"))
396                     r2 = componentnode->getFloatValue("value",0);
397                 else
398                     callback()->setupColorComponent(componentnode, modelRoot,
399                                                     1, 0);
400             }
401             componentnode = endcolornode->getChild("green");
402             if (componentnode) {
403                 if (componentnode->hasValue("value"))
404                     g2 = componentnode->getFloatValue("value",0);
405                 else
406                     callback()->setupColorComponent(componentnode, modelRoot,
407                                                     1, 1);
408             }
409             componentnode = endcolornode->getChild("blue");
410             if (componentnode) {
411                 if (componentnode->hasValue("value"))
412                     b2 = componentnode->getFloatValue("value",0);
413                 else
414                     callback()->setupColorComponent(componentnode, modelRoot,
415                                                     1, 2);
416             }
417             componentnode = endcolornode->getChild("alpha");
418             if (componentnode) {
419                 if (componentnode->hasValue("value"))
420                     a2 = componentnode->getFloatValue("value",0);
421                 else
422                     callback()->setupColorComponent(componentnode, modelRoot,
423                                                     1, 3);
424             }
425         }
426         particle.setColorRange(osgParticle::rangev4(osg::Vec4(r1,g1,b1,a1),
427                                                     osg::Vec4(r2,g2,b2,a2)));
428
429         float startsize=1, endsize=0.1f;
430         const SGPropertyNode* startsizenode = particlenode->getNode("start/size");
431         if (startsizenode) {
432             if (startsizenode->hasValue("value"))
433                 startsize = startsizenode->getFloatValue("value",0);
434             else
435                 callback()->setupStartSizeData(startsizenode, modelRoot);
436         }
437         const SGPropertyNode* endsizenode = particlenode->getNode("end/size");
438         if (endsizenode) {
439             if (endsizenode->hasValue("value"))
440                 endsize = endsizenode->getFloatValue("value",0);
441             else
442                 callback()->setupEndSizeData(endsizenode, modelRoot);
443         }
444         particle.setSizeRange(osgParticle::rangef(startsize, endsize));
445         float life=5;
446         const SGPropertyNode* lifenode = particlenode->getChild("life-sec");
447         if (lifenode) {
448             if (lifenode->hasValue("value"))
449                 life =  lifenode->getFloatValue("value",0);
450             else
451                 callback()->setupLifeData(lifenode, modelRoot);
452         }
453
454         particle.setLifeTime(life);
455         if (particlenode->hasValue("radius-m"))
456             particle.setRadius(particlenode->getFloatValue("radius-m",0));
457         if (particlenode->hasValue("mass-kg"))
458             particle.setMass(particlenode->getFloatValue("mass-kg",0));
459         if (callback.get()) {
460             callback.get()->setupStaticColorComponent(r1, g1, b1, a1,
461                                                       r2, g2, b2, a2);
462             callback.get()->setupStaticSizeData(startsize, endsize);
463         }
464         //particle.setColorRange(osgParticle::rangev4( osg::Vec4(r1, g1, b1, a1), osg::Vec4(r2, g2, b2, a2)));
465     }
466
467     const SGPropertyNode* programnode = configNode->getChild("program");
468     osgParticle::FluidProgram *program = new osgParticle::FluidProgram();
469
470     if (programnode) {
471         std::string fluid = programnode->getStringValue("fluid", "air");
472
473         if (fluid=="air")
474             program->setFluidToAir();
475         else
476             program->setFluidToWater();
477
478         if (programnode->getBoolValue("gravity", true)) {
479 #ifdef OSG_PARTICLE_FIX
480             program->setToGravity();
481 #else
482             if (attach == "world")
483                 callback()->setupProgramGravity(true);
484             else
485                 program->setToGravity();
486 #endif
487         } else
488             program->setAcceleration(osg::Vec3(0,0,0));
489
490         if (programnode->getBoolValue("wind", true))
491             callback()->setupProgramWind(true);
492         else
493             program->setWind(osg::Vec3(0,0,0));
494
495         align->addChild(program);
496
497         program->setParticleSystem(particleSys);
498     }
499
500     if (callback.get()) {  //this means we want property-driven changes
501         SG_LOG(SG_GENERAL, SG_DEBUG, "setting up particle system user data and callback\n");
502         //setup data and callback
503         callback.get()->setGeneralData(dynamic_cast<osgParticle::RadialShooter*>(emitter->getShooter()),
504                                        dynamic_cast<osgParticle::RandomRateCounter*>(emitter->getCounter()),
505                                        particleSys, program);
506         emitter->setUpdateCallback(callback.get());
507     }
508
509     return align;
510 }
511
512 void Particles::operator()(osg::Node* node, osg::NodeVisitor* nv)
513 {
514     //SG_LOG(SG_GENERAL, SG_ALERT, "callback!\n");
515     this->particleSys->setFrozen(_frozen);
516
517     using namespace osg;
518     if (shooterValue)
519         shooter->setInitialSpeedRange(shooterValue->getValue(),
520                                       (shooterValue->getValue()
521                                        + shooterExtraRange));
522     if (counterValue)
523         counter->setRateRange(counterValue->getValue(),
524                               counterValue->getValue() + counterExtraRange);
525     else if (counterCond)
526         counter->setRateRange(counterStaticValue,
527                               counterStaticValue + counterStaticExtraRange);
528     if (!GlobalParticleCallback::getEnabled() || (counterCond && !counterCond->test()))
529         counter->setRateRange(0, 0);
530     bool colorchange=false;
531     for (int i = 0; i < 8; ++i) {
532         if (colorComponents[i]) {
533             staticColorComponents[i] = colorComponents[i]->getValue();
534             colorchange=true;
535         }
536     }
537     if (colorchange)
538         particleSys->getDefaultParticleTemplate().setColorRange(osgParticle::rangev4( Vec4(staticColorComponents[0], staticColorComponents[1], staticColorComponents[2], staticColorComponents[3]), Vec4(staticColorComponents[4], staticColorComponents[5], staticColorComponents[6], staticColorComponents[7])));
539     if (startSizeValue)
540         startSize = startSizeValue->getValue();
541     if (endSizeValue)
542         endSize = endSizeValue->getValue();
543     if (startSizeValue || endSizeValue)
544         particleSys->getDefaultParticleTemplate().setSizeRange(osgParticle::rangef(startSize, endSize));
545     if (lifeValue)
546         particleSys->getDefaultParticleTemplate().setLifeTime(lifeValue->getValue());
547 #ifdef OSG_PARTICLE_FIX
548     if (particleFrame.valid()) {
549         MatrixList mlist = node->getWorldMatrices();
550         if (!mlist.empty()) {
551             const Matrix& particleMat = particleFrame->getMatrix();
552             Vec3d emitOrigin(mlist[0](3, 0), mlist[0](3, 1), mlist[0](3, 2));
553             Vec3d displace
554                 = emitOrigin - Vec3d(particleMat(3, 0), particleMat(3, 1),
555                                      particleMat(3, 2));
556             if (displace * displace > 10000.0 * 10000.0) {
557                 // Make new frame for particle system, coincident with
558                 // the emitter frame, but oriented with local Z.
559                 SGGeod geod = SGGeod::fromCart(toSG(emitOrigin));
560                 Matrix newParticleMat = makeZUpFrame(geod);
561                 Matrix changeParticleFrame
562                     = particleMat * Matrix::inverse(newParticleMat);
563                 particleFrame->setMatrix(newParticleMat);
564                 transformParticles(particleSys.get(), changeParticleFrame);
565             }
566         }
567     }
568     if (program.valid() && useWind)
569         program->setWind(_wind);
570 #else
571     if (program.valid()) {
572         if (useGravity)
573             program->setAcceleration(GlobalParticleCallback::getGravityVector());
574         if (useWind)
575             program->setWind(GlobalParticleCallback::getWindVector());
576     }
577 #endif
578 }
579 } // namespace simgear