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