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