资讯详情

资讯详情

OSG海面场景搭建:从网格动画、粒子特效到模型加载与倒影实现

简介基于OSG的海洋场景项目包是一份面向三维仿真开发者和OSG学习者的实用案例用于演示在动态海面上添加小船、模拟月光、风力吹拂以及实时水面倒影的完整实现思路。压缩包共包含一百七十三个文件整体大小约四十二点九四兆字节其中既有PNG纹理、frag与vert着色器、obj、3ds、ive模型等资源文件也提供了C源文件、头文件、Visual Studio工程配置以及编译好的exe程序与动态库方便直接运行和对照学习。已有二百四十九人浏览学习。项目以osgOcean插件为渲染核心涵盖了海浪网格生成、风向风力参数调节、天空盒构建与月光源设置、水面反射与折射等关键模块运行后可看到月光下的海面小船随波浪轻微起伏倒影清晰整体极具沉浸感。对于想研究OSG场景图、着色器编程或海洋仿真的开发者这套资料能提供从工程结构到渲染效果的直观参考省去从零搭建环境的成本。1. osg 海面场景小船、月亮、风、倒影要怎么搭一个标题里塞了四个视觉元素实际考验的是你在 osg 里把场景图拆开再拼回去的能力海面是带动态顶点坐标的网格小船是外部模型月亮是带光源的公告板风和倒影分别是粒子和镜像处理。不要一开始就找“海面插件”或者“月亮特效”OSG 里这些效果基本都是手写节点组合。这篇文章会从一张可运行的海面网格出发加船、加月亮、加风粒子最后用一个低成本倒影让场景完整。你会看到每个元素如何挂到根节点上也会看到帧率跑不动时第一个该缩的是哪个参数。2. 用 osgParticle 给海面加风浪网格与粒子分开做海面的第一版最容易写错以为“风”等于把整个海平面吹歪于是把所有顶点随机抬一下结果画面完全不像水。正确做法是把海面和风拆成两层海面网格负责大面积波动粒子和动画负责视觉上的浪花与风的方向。两层共用一张大纹理时量级和频率都不同视觉才不违和。2.1 海面网格为什么不能只做平面一个最朴素的海面是 256×256 的矩形网格每个顶点是 one positionuv 按比例分布。要让它有水感必须让顶点高度随时间变化并且变化不能是均匀上下的而要以正弦波叠加的方式沿着某个风向传播。这里我倾向于用 osg::Geometry 加一个 NodeCallback 更新顶点数组而不是把整个海面做成刚体位移。2.1.1 几何节点和海面 UpdateCallback 的最小结构#include osg/Geometry #include osg/Geode #include osg/NodeCallback #include osg/Vec3 #include cmath class SeaUpdate : public osg::NodeCallback { public: SeaUpdate(osg::Geometry* geom, int gridSize) : _geom(geom), _size(gridSize), _time(0.0f) {} virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) override { _time 0.016f; // 约 60FPS 的步进实际应该用 nv-getFrameStamp()-getSimulationTime() osg::Vec3Array* verts static_castosg::Vec3Array*(_geom-getVertexArray()); for (unsigned int i 0; i verts-size(); i) { osg::Vec3 v (*verts)[i]; float x v.x(); float z v.z(); // 两个不同频率的正弦波叠加模拟风的动量传递 float y1 std::sin(x * 0.5f _time * 1.3f) * 0.8f; float y2 std::sin(z * 0.3f - _time * 0.9f) * 0.4f; v.y() y1 y2; } _geom-dirtyDisplayList(); _geom-dirtyBound(); traverse(node, nv); } private: osg::ref_ptrosg::Geometry _geom; int _size; float _time; };这段代码的关键不是波形本身而是dirtyDisplayList()和dirtyBound()。OSG 在默认情况下会把几何体缓存成显示列表不改这两个标志顶点数组改了也不会重画。dirtyBound()是为了让包围盒跟随海面高度变化否则视锥裁剪可能把最高的浪尖裁掉。正式项目里不要把时间写在 0.016f要用FrameStamp里的真实时间否则视口渲染速度变化波浪速度会跟着变。海面网格本身用DrawArrays(GL_TRIANGLES)绘制绘制顺序要按行列生成推荐用 strip 而不是 triangles因为相同网格尺寸下 strip 能少传大量顶点索引。2.2 用 osgParticle 做可见的风与水雾风是不可见的东西想让用户“看到风”常见做法是让粒子沿着风向飘。OSG 自带的 osgParticle 库足够做一组从小船旁边飞过的白雾粒子不需要引入新的依赖。2.2.1 一个发射到船侧的风浪粒子发射器#include osgParticle/ParticleSystem #include osgParticle/ModularEmitter #include osgParticle/RadialShooter #include osgParticle/ParticleSystemUpdater #include osg/Texture2D #include osg/BlendFunc osg::ref_ptrosgParticle::ParticleSystem createWindParticles(osg::Texture2D* tex) { osg::ref_ptrosgParticle::ParticleSystem ps new osgParticle::ParticleSystem; ps-setMaxParticleCount(800); osgParticle::Particle* p ps-getDefaultParticleTemplate(); p-setShape(osgParticle::Particle::QUAD); p-setLifeTime(2.5f, 4.0f); p-setSizeRange(osgParticle::rangev2(0.4f, 1.0f)); p-setAlphaRange(osgParticle::rangef(0.05f, 0.45f)); p-setColorRange(osgParticle::rangev4( osg::Vec4(0.9f, 0.95f, 1.0f, 0.4f), osg::Vec4(0.6f, 0.7f, 0.8f, 0.0f))); osg::ref_ptrosgParticle::ModularEmitter emitter new osgParticle::ModularEmitter; emitter-setParticleSystem(ps.get()); emitter-setParticleDuration(6.0f); emitter-setCounter(new osgParticle::RandomRateCounter(20.0f, 40.0f)); osgParticle::PointPlacer* placer new osgParticle::PointPlacer; placer-setCenter(osg::Vec3(2.0f, 1.0f, -1.0f)); emitter-setPlacer(placer); osgParticle::RadialShooter* shooter new osgParticle::RadialShooter; shooter-setThetaRange(-0.5f, 0.5f); shooter-setPhiRange(0.6f, 1.2f); shooter-setInitialSpeedRange(1.2f, 2.8f); emitter-setShooter(shooter); osg::ref_ptrosgParticle::ParticleSystemUpdater updater new osgParticle::ParticleSystemUpdater; updater-addParticleSystem(ps.get()); ps-setDefaultAttributes(tex, true, false); return ps.get(); }这段代码里最容易被忽视的是setDefaultAttributes(tex, true, false)第二个参数表示粒子纹理需要光照第三个表示不使用深度测试。水面雾气一般开着深度测试反而会让粒子被海面遮挡到看不见所以我关掉深度测试并开启混合再用 Alpha 值做淡出。PointPlacer的 center 是粒子出生位置它在海面上方 1 米左右能保证粒子不会从海底冒出来。2.2.2 风的方向调整 Theta 和速度范围参数取值范围示例效果setThetaRange-0.3f, 0.3f粒子在水平方向上的散射角值越小风越集中setPhiRange0.5f, 1.2f粒子相对垂直方向的散射角模拟浪花飞溅setInitialSpeedRange1.2f, 2.8f初速下限和上限风速越大粒子越远setParticleDuration4.0f ~ 8.0f发射器存活时间可以配合回调关闭持续发射setAlphaRange0.05f ~ 0.45fAlpha 最大值越高雾越浓郁小心遮挡小船到这一步你能看到一个一直在起伏的海面和一小股水雾从船侧飘过。如果把ParticleSystemUpdater加到根节点不要忘了给它addChild一个空 Geode 来挂粒子系统否则 updater 不会进入场景图的更新遍历。3. assimp 转换后加载小船模型顶点、材质和原点对齐小船模型通常来自建模软件格式可能是.fbx、.dae或.gltf。OSG 自带读不了那么多种格式所以常走 assimp 这条路。关于“osg 用 assimp”的最稳做法不是让 assimp 直接输出.osg文件而是用 assimp 把模型数据导入内存再把顶点、材质、骨骼逐项复制到 OSG 节点上。这个过程也就一百多行代码。3.1 assimp 转换 osg 的三条路哪条更可靠先区分三种做法让 assimp 导出成中间格式再用 osgDB 读例如导出成.obj再转.osgb。这个方式依赖导出器对纹理路径的处理经常丢材质。直接编译osgdb_assimp插件让 osgDB 读.fbx或.dae。这是最快的但插件版本和 assimp 库版本要精确匹配否则运行时报找不到插件的错误。用 assimp C API 遍历aiScene手动构建osg::Geometry。可控性最高能顺手做单位换算和坐标轴修改。我一般选第三种尤其当模型里带着旋转动画或骨骼时手写遍历反而更好处理。下面给一个只处理静态网格的转换框架能覆盖船体这类不带骨骼的模型。3.2 用 assimp 把 boat model 转成 osg 节点#include assimp/Importer.hpp #include assimp/scene.h #include assimp/postprocess.h #include osg/Geometry #include osg/Geode #include osg/MatrixTransform osg::Node* loadBoatWithAssimp(const std::string path, float scale, osg::Vec3 offset) { Assimp::Importer importer; const aiScene* scene importer.ReadFile( path, aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_GenNormals | aiProcess_FlipUVs); if (!scene || !scene-mRootNode) { return nullptr; } osg::ref_ptrosg::Group group new osg::Group; for (unsigned int m 0; m scene-mNumMeshes; m) { aiMesh* mesh scene-mMeshes[m]; osg::ref_ptrosg::Vec3Array verts new osg::Vec3Array; osg::ref_ptrosg::Vec3Array normals new osg::Vec3Array; osg::ref_ptrosg::Vec2Array uvs new osg::Vec2Array; for (unsigned int v 0; v mesh-mNumVertices; v) { aiVector3D pos mesh-mVertices[v]; verts-push_back(osg::Vec3(pos.x, pos.y, pos.z) * scale); if (mesh-mNormals) { aiVector3D n mesh-mNormals[v]; normals-push_back(osg::Vec3(n.x, n.y, n.z)); } if (mesh-mTextureCoords[0]) { aiVector3D tc mesh-mTextureCoords[0][v]; uvs-push_back(osg::Vec2(tc.x, tc.y)); } } osg::ref_ptrosg::Geometry geom new osg::Geometry; geom-setVertexArray(verts.get()); if (!normals-empty()) { geom-setNormalArray(normals.get(), osg::Array::BIND_PER_VERTEX); } if (!uvs-empty()) { geom-setTexCoordArray(0, uvs.get()); } for (unsigned int f 0; f mesh-mNumFaces; f) { aiFace* face mesh-mFaces[f]; osg::ref_ptrosg::UIntArray indices new osg::UIntArray; for (unsigned int i 0; i face-mNumIndices; i) { indices-push_back(face-mIndices[i]); } geom-addPrimitiveSet(new osg::DrawElementsUInt( GL_TRIANGLES, indices-size(), (GLuint*)indices-getDataPointer())); } osg::ref_ptrosg::Geode geode new osg::Geode; geode-addDrawable(geom.get()); group-addChild(geode.get()); } osg::ref_ptrosg::MatrixTransform mt new osg::MatrixTransform; mt-setMatrix(osg::Matrix::translate(offset)); mt-addChild(group.get()); return mt.release(); }这段代码里aiProcess_FlipUVs很关键大部分建模软件用的 UV 坐标和 OpenGL 期望的是上下相反的不翻转纹理贴图会上下颠倒。scale是单位换算很多船模是厘米建模放进以米为单位的场景里会大一百倍所以单独缩一次比在 OSG 里反复调MatrixTransform更直观。面索引读取时注意从aiMesh的mFaces中取不能直接拿整个 mesh 的 index bufferassimp 会为相邻 mesh 复用不同的索引偏移。顺带回答经常被问到的“osg 可以加载 las 吗”LAS 是点云格式OSG 默认不含 LAS 解算器需要找专门的 las 插件把点云读成顶点数组。如果只拿它当一组点看待原理和上面遍历 mesh 没有区别只是少了面片索引。3.3 模型加载后的位置和朝向处理加载完小船后通常需要把船底放在海平面附近否则船会悬空或陷进水面。假设海面在 y0船体包围盒底部是-0.8那么偏移量就是osg::Vec3 center boatNode-getBound().center(); float bottom center.y() - boatNode-getBound().radius(); boatNode-asTransform()-asMatrixTransform()-setMatrix( osg::Matrix::translate(0.0f, -bottom, 0.0f));这里用包围盒半径估算底部是个粗糙办法只适合对称船模。精度要求高时应该通过 assimp 遍历每个 mesh 的最小顶点 Y再和包围盒合并得到真正的几何下边界。这个值用于后续倒影节点的高度参数错的话倒影会错开。4. 月亮与倒影一个 billboard 加一份翻转节点月亮在场景里其实是一个很小的部分但它的位置决定了光照和倒影的拍摄方向。做月亮的最好办法是把它放在远离场景的坐标上再用方向光指向场景这样倒影和光照方向保持一致。倒影则有两种主流实现翻转模型和反射纹理按帧率和显卡能力取舍。4.1 用 Billboard 做一个月亮同时照亮小船OSG 的osg::Billboard可以保证一个多边形始终正对相机适合贴圆形月亮纹理。4.1.1 月亮贴纸和光源的组合#include osg/Billboard #include osg/Texture2D #include osg/LightSource #include osg/StateSet #include osg/Material osg::ref_ptrosg::Geode createMoon(osg::Texture2D* moonTex, osg::Vec3 position) { osg::ref_ptrosg::Billboard billboard new osg::Billboard; billboard-setMode(osg::Billboard::POINT_ROT_EYE); osg::ref_ptrosg::Geometry quad osg::createTexturedQuadGeometry( osg::Vec3(-15.0f, -15.0f, 0.0f), osg::Vec3(30.0f, 0.0f, 0.0f), osg::Vec3(0.0f, 30.0f, 0.0f)); quad-getOrCreateStateSet()-setTextureAttributeAndModes(0, moonTex); billboard-addDrawable(quad.get(), osg::Vec3(position.x(), position.y(), position.z())); billboard-getOrCreateStateSet()-setMode(GL_BLEND, osg::StateAttribute::ON); billboard-getOrCreateStateSet()-setRenderingHint(osg::StateSet::TRANSPARENT_BIN); osg::ref_ptrosg::Geode geode new osg::Geode; geode-addDrawable(billboard.get()); return geode; }POINT_ROT_EYE模式让 billboard 在视线方向旋转看上去月亮始终是圆的。这里给quad加一个单独的纹理状态不要直接往StateSet里丢一个圆图还开着光照否则纹理会被 scene lighting 染色月亮变成灰色。接着加一个osg::LightSource把灯的位置放在月亮附近方向指向小船中心强度不要太高否则海面反光会过曝。光照参数推荐值注意点Light::setAmbient(0.2, 0.2, 0.25, 1)蓝色调环境光模仿夜里天空散射Light::setDiffuse(0.8, 0.85, 1.0, 1)月光偏冷色别用纯白Light::setPosition(月亮.x, 月亮.y, 月亮.z, 1)w1 表示点光源w0 是方向光Light::setConstantAttenuation0.2调大可以看到明显的距离衰减4.2 低成本倒影翻转小船模型并半透明化真反射的做法是再渲染一遍场景到纹理但成本高、代码长。一个很实用的替代方案是直接创建一个船模型的镜像节点沿水平面缩放(1, -1, 1)然后把透明度调到 0.4 左右。这个方案在截图和相机不太动的场景里观感很好也是全屏反射纹理掉帧时的退路。#include osg/MatrixTransform #include osg/Material #include osg/BlendFunc #include osg/Depth osg::Node* createBoatReflection(osg::Node* boat, float seaLevel, float alpha) { osg::ref_ptrosg::MatrixTransform mirror new osg::MatrixTransform; mirror-setMatrix(osg::Matrix::scale(1.0f, -1.0f, 1.0f) * osg::Matrix::translate(0.0f, 2.0f * seaLevel, 0.0f)); osg::ref_ptrosg::Node cloned dynamic_castosg::Node*(boat-clone(osg::CopyOp::DEEP_COPY_ALL)); mirror-addChild(cloned.get()); osg::ref_ptrosg::Material mat new osg::Material; mat-setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4(0.6f, 0.7f, 0.8f, alpha)); mat-setTransparent(osg::Material::FRONT_AND_BACK, true); cloned-getOrCreateStateSet()-setAttributeAndModes(mat.get()); cloned-getOrCreateStateSet()-setMode(GL_BLEND, osg::StateAttribute::ON); cloned-getOrCreateStateSet()-setAttributeAndModes(new osg::Depth(osg::Depth::LEQUAL)); return mirror.release(); }scale(1, -1, 1)之后模型沿 Y 轴翻转原本 y2 的船顶变成了 y-2所以需要再平移到海平面两侧对称。公式是翻转后的位置 2 * seaLevel - 原位置上面的矩阵组合已经把这个关系写进去了。这里有一个坑clone(DEEP_COPY_ALL)会连贴图都深拷贝一份内存开销变大。如果只是做倒影可以只 copy 几何和状态不复制纹理对象但那样代码要多一层自定义节点转换普通场景直接用 deep copy 更省心。4.3 反射纹理方案什么时候才值得用翻转节点有一个明显缺陷相机角度改变时镜像内容不会像真镜子那样随视角变化海面上的月亮倒影也不会被扭曲。如果要做出更真实的倒影用osg::Camera做 RTT 反射纹理创建一个osg::Camera把它放在海平面以下俯视海面渲染出一个反射纹理。把该纹理贴到海面平面上用 shader 采样。反射相机必须设置setRenderOrder在场景主相机之前。这个方案性能开销大约等于多渲染半场场景对水面网格细分高的情况会更难受。所以我的建议是如果目标帧率是 60船和月亮用翻转节点如果要发布到 PC 且显卡有余量再用 RTT 升级海面倒影。无论哪种方案海面材质必须带一定透明度否则倒影贴在水面上会被完全不透明的海水盖住。5. 把全部元素挂到场景树验证帧数与细分密度组装场景树时我会严格按层级来组织避免后续调试时找不到节点。5.1 场景树节点拼装顺序osg::ref_ptrosg::Group root new osg::Group; // 海面几何节点 osg::ref_ptrosg::Geometry seaGeom createSeaGrid(128, 128); osg::ref_ptrosg::Geode seaGeode new osg::Geode; seaGeode-addDrawable(seaGeom.get()); seaGeode-setUpdateCallback(new SeaUpdate(seaGeom.get(), 128)); // 粒子系统 osg::ref_ptrosgParticle::ParticleSystem windPs createWindParticles(fogTex.get()); osg::ref_ptrosg::Geode particleGeode new osg::Geode; particleGeode-addDrawable(windPs.get()); osg::ref_ptrosgParticle::ParticleSystemUpdater updater new osgParticle::ParticleSystemUpdater; updater-addParticleSystem(windPs.get()); // 小船和倒影 osg::ref_ptrosg::Node boat loadBoatWithAssimp(boat.fbx, 0.01f, osg::Vec3(0, 0, 0)); osg::ref_ptrosg::Node boatReflection createBoatReflection(boat.get(), 0.0f, 0.35f); // 月亮和光照 osg::ref_ptrosg::Geode moonGeode createMoon(moonTex.get(), osg::Vec3(80, 40, 120)); osg::ref_ptrosg::LightSource moonLight createMoonLight(osg::Vec3(80, 40, 120)); root-addChild(seaGeode.get()); root-addChild(particleGeode.get()); root-addChild(updater.get()); root-addChild(boat.get()); root-addChild(boatReflection.get()); root-addChild(moonGeode.get()); root-addChild(moonLight.get());这里有几个顺序上的细节粒子系统更新器必须先于其他节点加入场景否则粒子位置更新会晚一帧倒影节点必须加在船节点之后因为createBoatReflection使用了船模型的包围盒信息晚加不会有问题但调试时后加更容易确认它没有遮挡原始船。5.2 用 ViewerStats 验证帧数和细分跑起来后用viewer.getViewerStats()-collectStats(frame_rate, true)观察帧率或者直接开启环境变量OSG_STATS1。当帧率低于 30最优先调整的是海面网格细分数比如从 256×256 减到 96×96。不要先动粒子数因为粒子是 CPU 端更新几百个粒子远没有几万顶点影响大。也可以用setDataVariance(osg::Object::DYNAMIC)标记海面几何体OSG 会跳过 VBO 的 static 优化路径减少更新时的属性重传。最后检查osg::Viewer::setRunMaxFrameRate如果你只是看静态图设一个 60 的上限省下的 GPU 资源可以都分给反射纹理。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →