source: osgVisual/trunk/src/util/visual_util.cpp @ 344

Last change on this file since 344 was 295, checked in by Torben Dannhauer, 13 years ago
File size: 19.4 KB
Line 
1/* -*-c++-*- osgVisual - Copyright (C) 2009-2011 Torben Dannhauer
2 *
3 * This library is based on OpenSceneGraph, open source and may be redistributed and/or modified under
4 * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or
5 * (at your option) any later version.  The full license is in LICENSE file
6 * included with this distribution, and on the openscenegraph.org website.
7 *
8 * osgVisual requires for some proprietary modules a license from the correspondig manufacturer.
9 * You have to aquire licenses for all used proprietary modules.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * OpenSceneGraph Public License for more details.
15*/
16
17#include <visual_util.h>
18#include <osg/Material>
19
20using namespace osgVisual;
21
22util::util(void)
23{
24}
25
26util::~util(void)
27{
28}
29
30xmlNode* util::getSceneryXMLConfig(std::string configFilename, xmlDoc*& doc)
31{
32        doc = NULL;
33        xmlNode *root_element = NULL;
34
35        // Check for valid parameters
36        if(configFilename == "")
37        {
38                OSG_ALWAYS << "ERROR - util::getModuleXMLConfig() : Invalid Configuration Filename!" << std::endl;
39                return NULL;
40        }
41
42        // It is a valid XML document?
43        doc = xmlReadFile(configFilename.c_str(), NULL, 0);     
44        if (doc == NULL)
45        {
46                OSG_ALWAYS << "ERROR - util::getModuleXMLConfig() : " << configFilename << " is not a valid XML file!" << std::endl;
47                return NULL;
48        }
49
50        //  Get the root element node
51        root_element = xmlDocGetRootElement(doc);
52
53        // If file is a valid osgVisual config file, check all the root xml node and all of it's children of osgvisualconfiguration for the specified module
54        xmlNode* tmpNode = checkXMLNodeChildrenForScenery(root_element);
55
56        if( !tmpNode ) // if no valid node was found: clena up. Otherwise: the caller has to clean up.
57        {
58                xmlFreeDoc(doc);        // free the document
59                xmlCleanupParser();     // Free the global variables that may have been allocated by the parser.
60                return NULL;
61        }
62        else
63                return tmpNode;
64}
65
66xmlNode* util::checkXMLNodeChildrenForScenery(xmlNode* node)
67{
68        for (xmlNode *cur_node = node; cur_node; cur_node = cur_node->next)     // iterate through all elements
69        {
70                // Is the node the one we are searching for?
71                if (cur_node->type == XML_ELEMENT_NODE)
72                {
73                        std::string node_name=reinterpret_cast<const char*>(cur_node->name);
74                        if (node_name == "scenery")
75                        {
76                                OSG_DEBUG << "XML node scenery found" << std::endl;
77                                return cur_node;
78                        }
79                        else    // Otherwise: check its children..
80                        {
81                                xmlNode* tmp_XmlNode = checkXMLNodeChildrenForScenery(cur_node->children);
82                                if(tmp_XmlNode)
83                                        return tmp_XmlNode;
84                        }
85                }       // IF NODE TYPE = ELEMENT END
86
87                // Proceed with next node in this loop.
88        }
89        return NULL;
90}
91
92xmlNode* util::getModuleXMLConfig(std::string configFilename, std::string moduleName, xmlDoc*& doc, bool& disabled)
93{
94        doc = NULL;
95        disabled = false;
96        xmlNode *root_element = NULL;
97
98        // Check for valid parameters
99        if(configFilename == "")
100        {
101                OSG_ALWAYS << "ERROR - util::getModuleXMLConfig() : Invalid Configuration Filename!" << std::endl;
102                return NULL;
103        }
104        if(moduleName == "")
105        {
106                OSG_ALWAYS << "ERROR - util::getModuleXMLConfig() : Invalid Module Filename!" << std::endl;
107                return NULL;
108        }
109
110        // It is a valid XML document?
111        doc = xmlReadFile(configFilename.c_str(), NULL, 0);     
112        if (doc == NULL)
113        {
114                OSG_ALWAYS << "ERROR - util::getModuleXMLConfig() : " << configFilename << " is not a valid XML file!" << std::endl;
115                return NULL;
116        }
117
118        //  Get the root element node
119        root_element = xmlDocGetRootElement(doc);
120
121        // Check if it is an osgVisual configuration file
122        std::string node_name=reinterpret_cast<const char*>(root_element->name);
123        if(!(root_element->type == XML_ELEMENT_NODE && node_name == "osgvisualconfiguration"))
124        {
125                OSG_ALWAYS << "ERROR - util::getModuleXMLConfig() : " << configFilename << " is not an osgVisual configuration file!" << std::endl;
126                return NULL;
127        }
128
129        // If file is a valid osgVisual config file, check all the root xml node and all of it's children of osgvisualconfiguration for the specified module
130        xmlNode* tmpNode = checkXMLNodeChildrenForModule(root_element, moduleName, disabled);
131
132        if( !tmpNode ) // if no valid node was found or the module is disabled: clean up. Otherwise: the caller has to clean up.
133        {
134                xmlFreeDoc(doc);        // free the document
135                xmlCleanupParser();     // Free the global variables that may have been allocated by the parser.
136                return NULL;
137        }
138        else
139                return tmpNode;
140
141}
142
143xmlNode* util::checkXMLNodeChildrenForModule(xmlNode* node, std::string moduleName, bool& disabled)
144{
145        for (xmlNode *cur_node = node; cur_node; cur_node = cur_node->next)     // iterate through all XML elements
146        {
147                // Is the node the one we are searching for?
148                if (cur_node->type == XML_ELEMENT_NODE)
149                {
150                        // Extract Node Name and the first attribute: "module name"
151                        std::string node_name = reinterpret_cast<const char*>(cur_node->name);
152                        std::string modName = "";
153                        xmlAttr  *attr = cur_node->properties;
154                        while ( attr ) 
155                        { 
156                                std::string attr_name=reinterpret_cast<const char*>(attr->name);
157                                std::string attr_value=reinterpret_cast<const char*>(attr->children->content);
158                                if( attr_name == "name" )
159                                        modName = attr_value;
160                                attr = attr->next;
161                        }
162                       
163                        // Check each node for the searched module
164                        if (node_name == "module" && modName == moduleName)
165                        {
166                                // Check if the module is active
167                                xmlAttr  *attr = cur_node->properties;
168                                while ( attr ) 
169                                { 
170                                        std::string attr_name=reinterpret_cast<const char*>(attr->name);
171                                        std::string attr_value=reinterpret_cast<const char*>(attr->children->content);
172                                        if( attr_name == "enabled" && attr_value == "yes" )
173                                        {
174                                                OSG_DEBUG << "Found XML module configuration for " << moduleName << std::endl;
175                                                return cur_node;
176                                        }
177                                        if( attr_name == "enabled" && attr_value == "no" )
178                                        {
179                                                disabled = true;
180                                                OSG_DEBUG << "Found XML module configuration for " << moduleName << ", but it is DISABLED." << std::endl;
181                                                return NULL;
182                                        }
183                                        attr = attr->next;
184                                }
185                        }
186                        else    // Otherwise: check its children..
187                        {
188                                xmlNode* tmp_XmlNode = checkXMLNodeChildrenForModule(cur_node->children, moduleName, disabled);
189                                if(tmp_XmlNode)
190                                        return tmp_XmlNode;
191                        }
192                }       // IF NODE TYPE = ELEMENT END
193
194                // Proceed with next node in this loop.
195        }
196        return NULL;
197}
198
199osg::Node* util::findNamedNode(const std::string& searchName_, osg::Node* currNode_)
200{
201   osg::Group* currGroup;
202   osg::Node* foundNode;
203
204   // check to see if we have a valid (non-NULL) node.
205   // if we do have a null node, return NULL.
206   if ( !currNode_)
207   {
208      return NULL;
209   }
210
211   // We have a valid node, check to see if this is the node we
212   // are looking for. If so, return the current node.
213   if (currNode_->getName() == searchName_)
214   {
215      return currNode_;
216   }
217
218   // We have a valid node, but not the one we are looking for.
219   // Check to see if it has children (non-leaf node). If the node
220   // has children, check each of the child nodes by recursive call.
221   // If one of the recursive calls returns a non-null value we have
222   // found the correct node, so return this node.
223   // If we check all of the children and have not found the node,
224   // return NULL
225   currGroup = currNode_->asGroup(); // returns NULL if not a group.
226   if ( currGroup ) 
227   {
228      for (unsigned int i = 0 ; i < currGroup->getNumChildren(); i ++)
229      { 
230         foundNode = findNamedNode(searchName_, currGroup->getChild(i));
231         if (foundNode)
232                 {
233                         std::cout << "Node gefunden in Ebene: " << i << std::endl;
234            return foundNode; // found a match!
235                }
236      }
237      return NULL; // We have checked each child node - no match found.
238   }
239   else 
240           return NULL; // leaf node, no match
241}
242
243osg::ref_ptr<osg::Geode> util::getDemoCylinder(double length_, double width_, osg::Vec3 offset_ )
244{
245        osg::ref_ptr<osg::Geode> cyl = new osg::Geode();
246        osg::ref_ptr<osg::ShapeDrawable> shape = new osg::ShapeDrawable(new osg::Cylinder( offset_, width_, length_ ));
247        osg::Vec4 color = osg::Vec4(255.0, 0.0, 0.0, 1.0);
248        shape->setColor( color );
249        cyl->addDrawable( shape );
250
251        return cyl;
252}
253
254osg::ref_ptr<osg::Geode> util::getDemoSphere(double radius_, osg::Vec3 offset_ )
255{
256        osg::ref_ptr<osg::Geode> sphere = new osg::Geode();
257        osg::ref_ptr<osg::ShapeDrawable> shape = new osg::ShapeDrawable(new osg::Sphere( offset_, radius_ ) );
258        osg::Vec4 color = osg::Vec4(255.0, 0.0, 0.0, 1.0);
259        shape->setColor( color );
260        sphere->addDrawable( shape );
261
262        return sphere;
263}
264
265bool util::intersect(const osg::Vec3d& start_, const osg::Vec3d& end_, osg::Vec3d& intersection_, osg::Node* node_, osg::Node::NodeMask intersectTraversalMask_ )
266{
267        osg::ref_ptr<osgUtil::LineSegmentIntersector> lsi = new osgUtil::LineSegmentIntersector(start_,end_);
268
269        osgUtil::IntersectionVisitor iv(lsi.get());
270        iv.setTraversalMask(intersectTraversalMask_);
271   
272        node_->accept(iv);
273   
274        if (lsi->containsIntersections())
275        {
276                intersection_ = lsi->getIntersections().begin()->getWorldIntersectPoint();
277                return true;    // Intersect found
278        }
279        return false;   // No intersect found
280}
281
282bool util::queryHeightOfTerrain(double& hot_, osg::Node* rootNode_, double lat_, double lon_, osg::Node::NodeMask traversalMask_)
283{
284        // Get ellipsoid model
285        osg::CoordinateSystemNode* csn = dynamic_cast<osg::CoordinateSystemNode*>(rootNode_);
286        if ( !csn )
287        {
288                OSG_NOTIFY( osg::FATAL ) << "util::queryHeightOfTerrain() :: Invalid CSN!" << std::endl;
289                return false;
290        }
291        osg::EllipsoidModel* ellipsoid = csn->getEllipsoidModel();
292        if ( !ellipsoid )
293        {
294                OSG_NOTIFY( osg::FATAL ) << "util::queryHeightOfTerrain() :: Invalid ellipsoid!" << std::endl;
295                return false;
296        }
297
298        // Setup both endpoints of intersect line
299        double X,Y,Z;
300        ellipsoid->convertLatLongHeightToXYZ(lat_, lon_, 30000, X, Y, Z);
301        osg::Vec3d s = osg::Vec3d(X, Y, Z);
302        ellipsoid->convertLatLongHeightToXYZ(lat_, lon_, -30000, X, Y, Z);
303        osg::Vec3d e = osg::Vec3d(X, Y, Z);
304
305        // Query intersection point
306        osg::Vec3d ip;
307        if ( util::intersect(s, e, ip, rootNode_, traversalMask_) )
308        {
309                double lat2_, lon2_;
310                ellipsoid->convertXYZToLatLongHeight( ip.x(), ip.y(), ip.z(), lat2_, lon2_, hot_ );     // Convert Intersection Point back to Lat Lon, HOT.
311                //OSG_NOTIFY(osg::ALWAYS) << "lat: "<< osg::RadiansToDegrees(lat2_) <<", Lon: " << osg::RadiansToDegrees(lon2_) << ", Hot: " << hot_ << std::endl;
312                return true;
313        }
314
315        // If no intersection point found: set HOT to zero and return false.
316        hot_ = 0;
317        return false;
318}
319
320bool util::queryHeightAboveTerrainInWGS84(double& hat_, osg::Node* rootNode_, double lat_, double lon_, double height_, osg::Node::NodeMask traversalMask_)
321{
322        // Get HOT by asking util::queryHeightOfTerrain() :)
323        double HOT;
324        if ( !util::queryHeightOfTerrain(HOT, rootNode_, lat_, lon_, traversalMask_) )
325        {
326                OSG_NOTIFY( osg::INFO ) << "util::queryHeightAboveTerrainInWGS84() :: Unable to get HOT, will use 0 for HOT!" << std::endl;
327        }
328
329        // Calculate HAT
330        hat_ = height_ - HOT;
331        return true;
332}
333
334bool util::queryHeightAboveTerrainInWorld(double& hat_, osg::Node* rootNode_, double x_, double y_, double z_, osg::Node::NodeMask traversalMask_)
335{
336        // Get ellipsoid model
337        osg::CoordinateSystemNode* csn = dynamic_cast<osg::CoordinateSystemNode*>(rootNode_);
338        if ( !csn )
339        {
340                OSG_NOTIFY( osg::FATAL ) << "util::queryHeightAboveTerrainInWorld() :: Invalid CSN!" << std::endl;
341                return false;
342        }
343        osg::EllipsoidModel* ellipsoid = csn->getEllipsoidModel();
344        if ( !ellipsoid )
345        {
346                OSG_NOTIFY( osg::FATAL ) << "util::queryHeightAboveTerrainInWorld() :: Invalid ellipsoid!" << std::endl;
347                return false;
348        }
349
350        // Transform XYZ into LatLonHeight
351        double lat_, lon_, height_;
352        ellipsoid->convertXYZToLatLongHeight(x_, y_, z_, lat_, lon_, height_);
353
354        // ask util::queryHeightAboveTerrainInWGS84() to calc HAT :)
355        if( !util::queryHeightAboveTerrainInWGS84(hat_, rootNode_, lat_, lon_, height_, traversalMask_ ) )
356        {
357                OSG_NOTIFY( osg::FATAL ) << "util::queryHeightAboveTerrainInWorld() :: Unable to get HAT!" << std::endl;
358                return false;
359        }
360
361        return true;
362}
363
364bool util::calculateEarthRadiusAtWGS84Coordinate(double lat_, double lon_, osg::Node* rootNode_, double& radius_)
365{
366        // Calculate radius:
367        double x, y, z;
368       
369        if ( util::calculateXYZAtWGS84Coordinate(lat_, lon_, 0.0, rootNode_, x, y, z) )
370        {
371                radius_ = sqrt( pow(x, 2) + pow(y, 2) + pow(z, 2) );
372                return true;
373        }
374        else
375        {
376                OSG_NOTIFY( osg::FATAL ) << "util::calculateEarthRadiusAtWGS84Coordinate() :: Unable to calculate Earth Radius!" << std::endl;
377                return false;
378        }
379}
380
381bool util::calculateXYZAtWGS84Coordinate(double lat_, double lon_, double height_, osg::Node* rootNode_, double& x_, double& y_, double& z_)
382{
383        // Get ellipsoid model
384        osg::CoordinateSystemNode* csn = dynamic_cast<osg::CoordinateSystemNode*>(rootNode_);
385        if ( !csn )
386                return false;
387        osg::EllipsoidModel* ellipsoid = csn->getEllipsoidModel();
388        if ( !ellipsoid )
389                return false;
390
391        // Calculate xyz:
392        ellipsoid->convertLatLongHeightToXYZ( lat_, lon_, height_, x_, y_, z_);
393        return true;
394}
395
396bool util::getWGS84ofCamera( osg::Camera* camera_, osg::Node* rootNode_, double& lat_, double& lon_, double& height_ )
397{
398        // Get ellipsoid model
399        osg::CoordinateSystemNode* csn = dynamic_cast<osg::CoordinateSystemNode*>(rootNode_);
400        if ( !csn )
401                return false;
402        osg::EllipsoidModel* ellipsoid = csn->getEllipsoidModel();
403        if ( !ellipsoid )
404                return false;
405
406        osg::Vec3d eye, dir, up;
407        camera_->getViewMatrixAsLookAt(eye,dir,up); // Get XYZ from camera
408        ellipsoid->convertXYZToLatLongHeight(eye.x(), eye.y(), eye.z(), lat_, lon_, height_);
409        return true;
410}
411
412void util::getXYZofCamera( osg::Camera* camera_, double& x_, double& y_, double& z_ )
413{
414        osg::Vec3d eye, dir, up;
415        camera_->getViewMatrixAsLookAt(eye,dir,up); // Get XYZ from camera
416        x_ = eye.x();
417        y_ = eye.y();
418        z_ = eye.z();
419}
420
421bool util::removeClosebuttonOnGLWindow(osgViewer::Viewer* viewer_)
422{
423#ifdef FUNFUNCTIONS_ENABLED
424#ifdef WIN32
425        osgViewer::ViewerBase::Windows wins;
426        viewer_->getWindows(wins);
427        osgViewer::GraphicsHandleWin32* hwnd = dynamic_cast<osgViewer::GraphicsHandleWin32*>(wins[0]);
428       
429        HMENU hMenu = GetSystemMenu(hwnd->getHWND(), FALSE);
430        ::EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | (MF_DISABLED | MF_GRAYED)); 
431#endif
432#endif
433        return true;
434}
435
436bool util::setTransparentWindowBackground(osgViewer::Viewer* viewer_)
437{
438#ifdef FUNFUNCTIONS_ENABLED
439#ifdef WIN32
440        osgViewer::ViewerBase::Windows wins;
441        viewer_->getWindows(wins);
442        osgViewer::GraphicsHandleWin32* hwnd = dynamic_cast<osgViewer::GraphicsHandleWin32*>(wins[0]);
443        HWND _hwnd = hwnd->getHWND();
444        viewer_->getDisplaySettings()->setMinimumNumAlphaBits(8);
445
446   // Create and populate the Blur Behind structure
447   DWM_BLURBEHIND bb = {0};
448   // Disable Blur Behind and Blur Region;
449   bb.dwFlags = DWM_BB_ENABLE;
450   bb.fEnable = true;
451   bb.hRgnBlur = NULL;
452
453   // Ensable Blur Behind
454   HRESULT hr = DwmEnableBlurBehindWindow(_hwnd, &bb);
455   if (SUCCEEDED(hr))
456      return true;
457   else
458           return false;
459
460#endif
461#endif
462        return true;
463}
464
465std::vector<std::string> util::getTerrainFromXMLConfig(std::string configFilename)
466{
467        xmlDoc* tmpDoc;
468        xmlNode* sceneryNode = util::getSceneryXMLConfig(configFilename, tmpDoc);
469        std::vector<std::string> filenames;
470
471        // Iterate through nodes and search for terrian entry
472        for (xmlNode *cur_node = sceneryNode->children; cur_node; cur_node = cur_node->next)
473        {
474                std::string node_name=reinterpret_cast<const char*>(cur_node->name);
475
476                if(cur_node->type == XML_ELEMENT_NODE && node_name == "terrain")
477                {
478                        xmlAttr  *attr = cur_node->properties;
479                        while ( attr ) 
480                        { 
481                                std::string attr_name=reinterpret_cast<const char*>(attr->name);
482                                std::string attr_value=reinterpret_cast<const char*>(attr->children->content);
483                                if( attr_name.find("filename") != std::string::npos )
484                                {
485                                        filenames.push_back(attr_value);
486                                }
487                                attr = attr->next; 
488                        }
489                }
490        }// FOR all nodes END
491
492        // Cleanup
493        if(sceneryNode)
494        {
495                xmlFreeDoc(tmpDoc); xmlCleanupParser();
496        }
497
498        return filenames;
499}
500
501std::string util::getAnimationPathFromXMLConfig(std::string configFilename)
502{
503        xmlDoc* tmpDoc;
504        xmlNode* sceneryNode = util::getSceneryXMLConfig(configFilename, tmpDoc);
505        std::string animationpath = "";
506
507        // Iterate through nodes and search for terrian entry
508        for (xmlNode *cur_node = sceneryNode->children; cur_node; cur_node = cur_node->next)
509        {
510                std::string node_name=reinterpret_cast<const char*>(cur_node->name);
511
512                if(cur_node->type == XML_ELEMENT_NODE && node_name == "animationpath")
513                {
514                        xmlAttr  *attr = cur_node->properties;
515                        while ( attr ) 
516                        { 
517                                std::string attr_name=reinterpret_cast<const char*>(attr->name);
518                                std::string attr_value=reinterpret_cast<const char*>(attr->children->content);
519                                if( attr_name == "filename" )
520                                {
521                                        animationpath = attr_value;
522                                }
523                                attr = attr->next; 
524                        }
525                }
526        }// FOR all nodes END
527
528        // Cleanup
529        if(sceneryNode)
530        {
531                xmlFreeDoc(tmpDoc); xmlCleanupParser();
532        }
533
534        return animationpath;
535}
536
537double util::strToDouble(std::string s)
538{
539        double tmp;
540        std::stringstream sstr(s);
541    if (!(sstr >> tmp))
542        {
543                OSG_ALWAYS << __FUNCTION__ << "Warning:Unable to convert "<< s <<" to double, using 0.0 as default!" << std::endl;
544                return 0.0;
545        }
546        else
547                return tmp;
548}
549
550int util::strToInt(std::string s)
551{
552        int tmp;
553        std::stringstream sstr(s);
554        if (!(sstr >> tmp))
555        {
556                OSG_ALWAYS << __FUNCTION__ << "Warning:Unable to convert "<< s <<" to int, using 0 as default!" << std::endl;
557                return 0;
558        }
559        else
560                return tmp;
561}
562
563bool util::strToBool(std::string s)
564{
565        if(s=="yes")
566                return(true);
567        if(s=="no")
568                return(false);
569        OSG_ALWAYS << __FUNCTION__ << "Warning:Unable to convert "<< s <<" to bool, using false as default!" << std::endl;
570        return(false);
571}
572
573void util::AddCylinderBetweenPoints(osg::Vec3d StartPoint, osg::Vec3d EndPoint, float radius, osg::Vec4d CylinderColor, osg::Group *pAddToThisGroup)
574{
575        osg::ref_ptr<osg::Geode> geode = new osg::Geode;
576        osg::Vec3d center;
577        float height;
578        osg::ref_ptr<osg::Cylinder> cylinder;
579        osg::ref_ptr<osg::Drawable> cylinderDrawable;
580        osg::ref_ptr<osg::Material> pMaterial;
581
582        height = (StartPoint-EndPoint).length();
583        center = osg::Vec3( (StartPoint.x() + EndPoint.x()) / 2, (StartPoint.y() + EndPoint.y()) / 2, (StartPoint.z() + EndPoint.z()) / 2);
584
585        // This is the default direction for the cylinders to face in OpenGL
586        osg::Vec3d z = osg::Vec3d(0,0,1);
587
588        // Get diff between two points you want cylinder along
589        osg::Vec3d p = StartPoint - EndPoint;
590
591        // Get CROSS product (the axis of rotation)
592        osg::Vec3d t = z ^ p;
593
594        // Get angle. length is magnitude of the vector
595        double angle = acos( (z * p) / p.length());
596
597        // Create a cylinder between the two points with the given radius
598        cylinder = new osg::Cylinder(center,radius,height);
599        cylinder->setRotation(osg::Quat(angle, osg::Vec3(t.x(), t.y(), t.z())));
600
601        cylinderDrawable = new osg::ShapeDrawable(cylinder );
602        geode->addDrawable(cylinderDrawable);
603
604        // Set the color of the cylinder that extends between the two points.
605        pMaterial = new osg::Material;
606        pMaterial->setDiffuse( osg::Material::FRONT, CylinderColor);
607        geode->getOrCreateStateSet()->setAttribute( pMaterial, osg::StateAttribute::OVERRIDE );
608
609        // Add the cylinder between the two points to an existing group
610        pAddToThisGroup->addChild(geode);
611}
Note: See TracBrowser for help on using the repository browser.