1. OsgEarth三维可视化开发入门第一次接触OsgEarth时我被它强大的地理信息可视化能力震撼到了。作为一个基于OpenSceneGraph(OSG)的开源三维地球引擎OsgEarth能够将真实世界的地理数据以三维形式呈现出来这对于GIS开发者和三维可视化爱好者来说简直是神器。在实际项目中我经常需要实现飞机模型的动态轨迹展示。比如在航空管制系统中需要实时显示飞机的飞行路径在军事仿真中要模拟战机的作战轨迹在无人机监控平台则要追踪无人机的飞行状态。这些场景都离不开三个关键技术点模型动态移动、轨迹实时绘制和雷达扫描效果。OsgEarth的优势在于它完美结合了地理坐标系统和三维渲染引擎。通过EllipsoidModel椭球体模型可以精确地将经纬度坐标转换为三维场景中的位置。比如下面这段代码就展示了如何将地理坐标转换为场景矩阵osg::Matrixd mtTemp; m_rpCoordSystem-getEllipsoidModel()-computeLocalToWorldTransformFromLatLongHeight( osg::DegreesToRadians(34.3762), // 纬度 osg::DegreesToRadians(109.1263), // 经度 460, // 高度 mtTemp); // 输出矩阵2. 飞机模型加载与路径规划2.1 模型加载与初始定位加载飞机模型是第一步我通常会使用.ive或.osgb格式的三维模型文件。这里有个坑要注意模型尺寸和坐标系。很多飞机模型在制作时使用的单位可能是米或者厘米而OsgEarth场景的单位通常是米所以需要适当缩放m_rpnodeAirFly osgDB::readNodeFile(B737.ive); m_rpmtFlyself new osg::MatrixTransform; m_rpmtFlyself-setMatrix(osg::Matrixd::scale(10, 10, 10)* // 缩放10倍 osg::Matrixd::rotate(3*osg::PI_4,osg::Vec3(0, 0, 1))); // 初始旋转模型的光照设置也很重要特别是法线处理。我发现如果不开启GL_RESCALE_NORMAL模型在特定角度下会出现奇怪的明暗变化m_rpmtFlyself-getOrCreateStateSet()-setMode(GL_RESCALE_NORMAL, osg::StateAttribute::ON);2.2 飞行路径设置技巧飞行路径通常由一系列关键点组成每个点包含经度、纬度、高度和速度信息。在实际项目中我从GPS设备获取这些数据但在演示时可以手动设置osg::ref_ptrosg::Vec4Array rpvaTemp new osg::Vec4Array; rpvaTemp-push_back(osg::Vec4(109.1347, 34.3834, 537, 50)); // 起点 rpvaTemp-push_back(osg::Vec4(109.1174, 34.3686, 567, 500)); // 中间点 rpvaTemp-push_back(osg::Vec4(108.8794, 34.1944, 3000, 800));// 爬升段路径平滑处理是个技术活。我试过多种插值算法最后发现对于航空应用Catmull-Rom样条曲线效果最好既能保证路径平滑又不会让飞机转弯时显得不自然。3. 动态轨迹与飞行姿态实现3.1 实时轨迹绘制原理飞机飞过的地方要留下轨迹线这个功能看似简单实现时却有不少门道。我的做法是使用UpdateCallback机制在每一帧更新时记录飞机当前位置并绘制线段void CreateTrackCallback::operator()(osg::Node* node,osg::NodeVisitor* nv) { osg::MatrixTransform* pmtTrans dynamic_castosg::MatrixTransform*(node); if(pmtTrans) { osg::Matrix mtx pmtTrans-getMatrix(); m_Vec3CurPosition mtx.getTrans(); m_proot-addChild(BuildTrack(m_Vec3LastPosition,m_Vec3CurPosition)); } traverse(node,nv); m_Vec3LastPosition m_Vec3CurPosition; }这里有个性能优化点如果轨迹点太多会导致场景图过于复杂。我的解决方案是设置最大轨迹点数超过时就移除最早的线段。3.2 飞行姿态计算详解飞机在转弯时需要调整机头方向这个计算最让我头疼。需要同时考虑水平转向角偏航和垂直转向角俯仰// 水平转向角计算 if(iter-x() iter2-x()) { dshuiPingAngle osg::PI_2; // 经度相同直角转弯 } else { dshuiPingAngle atan((iter2-y() - iter-y())/(iter2-x() - iter-x())); if(iter2-x() iter-x()) { dshuiPingAngle osg::PI; } } // 垂直转向角计算 if(iter-z() iter2-z()) { dchuiZhiAngle 0; // 高度相同水平飞行 } else { dchuiZhiAngle atan((iter2-z() - iter-z()) / sqrt(pow(dGetDis(Vec3positionCur, Vec3positionNext), 2)) - pow((iter2-z() - iter-z()), 2))); }实际测试中发现当两点经纬度完全相同但高度不同时垂直起降需要特殊处理否则会出现计算异常。4. 雷达扫描与飞行彩带效果4.1 雷达扫描实现方案雷达效果是项目的亮点之一。我采用圆锥体来表示雷达扫描范围通过不断旋转这个圆锥体来模拟扫描效果osg::ref_ptrosg::Geode CBuildRader::BuildRader(float fRadius, float fHeight) { buildRaderCallback new CBuildRaderCallback(2,fRadius,fHeight); osg::ref_ptrosg::Geometry rpGeom new osg::Geometry; // 创建三角形表示雷达波束 rpVec3Array-push_back(osg::Vec3f(0,0,0)); // 顶点 rpVec3Array-push_back(osg::Vec3f(0,0,-fHeight)); // 底部中心 rpVec3Array-push_back(osg::Vec3f(fRadius,0,-fHeight)); // 底部边缘 rpGeom-addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES,0,3)); }旋转是通过UpdateCallback实现的每帧更新三角形的位置void CBuildRaderCallback::operator()(osg::Node* node,osg::NodeVisitor* nv) { double dRotateTime nv-getFrameStamp()-getReferenceTime(); rpVertexArray-push_back(osg::Vec3( m_fRadius * cosf(dRotateTime * m_fSpeed), m_fRadius * sinf(dRotateTime * m_fSpeed), -m_fHeight)); // 移除旧的顶点 rpVertexArray-erase(rpVertexArray-begin(), rpVertexArray-begin()3); }4.2 飞行彩带制作技巧飞行彩带Trailer是表现飞机运动轨迹的另一种方式。与实线轨迹不同彩带是半透明的带状物会随着时间慢慢消失void CViewerWidget::BuildRibbon(int size, osg::MatrixTransform* scaler,int ribbonWidth) { // 初始化顶点和颜色 for(unsigned int i0; isize-1; i2) { (*rpvec3Vertex)[i] osg::Vec3(0,0,0); (*rpvec3Vertex)[i1] osg::Vec3(0,0,0); float falpha sinf(osg::PI * (float)i / (float)size); (*rpvec4Color)[i] osg::Vec4(m_vec3RibbonColor,falpha); (*rpvec4Color)[i1] osg::Vec4(m_vec3RibbonColor,falpha); } // 重要启用透明混合 rpgeom-getOrCreateStateSet()-setMode(GL_BLEND,osg::StateAttribute::ON); rpgeom-getOrCreateStateSet()-setRenderingHint(osg::StateSet::TRANSPARENT_BIN); }彩带的更新策略与轨迹线不同它采用队列方式新的位置会推入队列旧的位置会被挤出形成流动效果void CTrailerCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) { for(unsigned int i0; im_nsize-3; i2) { (*pvec3Vertex)[i] (*pvec3Vertex)[i2]; // 向前移动顶点 (*pvec3Vertex)[i1] (*pvec3Vertex)[i3]; } // 添加新顶点 (*pvec3Vertex)[m_nsize-2] osg::Vec3(0.0f,-m_nwidth,0.0f)* mtx; (*pvec3Vertex)[m_nsize-1] osg::Vec3(0.0f,m_nwidth,0.0f)* mtx; }5. 性能优化与常见问题在实现上述效果时我遇到过不少性能问题。当场景中有多架飞机同时运动时帧率会明显下降。经过分析发现问题主要出在以下几个方面首先是轨迹线的绘制。最初我每帧都创建新的Geometry对象这会导致内存快速增长。优化方案是复用Geometry对象只更新顶点数据rpGeom-setDataVariance(osg::Object::DYNAMIC); // 声明为动态对象 rpGeom-setUseDisplayList(false); // 禁用显示列表 rpGeom-setUseVertexBufferObjects(true); // 启用VBO其次是透明物体的渲染顺序问题。OsgEarth场景中有大量半透明物体如彩带、雷达波束如果渲染顺序不对会出现奇怪的混合效果。解决方案是// 设置渲染bin和排序模式 stateSet-setRenderBinDetails(10, DepthSortedBin); stateSet-setRenderingHint(osg::StateSet::TRANSPARENT_BIN);另一个常见问题是坐标转换的精度。当飞机飞得离原点很远时会出现抖动现象。这是因为单精度浮点数精度不足导致的。解决方法是在每个飞机本地坐标系下进行计算最后再转换到世界坐标。