
OSG 鼠标拾取与高亮显示项目
项目概述
本项目基于OpenSceneGraph(OSG) 三维渲染引擎开发,核心实现鼠标交互式拾取三维模型 + 模型高亮边框显示功能。用户通过鼠标左键点击场景中的3D模型,可实时为模型添加高亮线框效果;再次点击已高亮模型,可取消高亮恢复原始状态。
项目适用于OSG初学者入门交互开发、三维场景拾取功能学习,涵盖OSG核心知识点:事件处理器、射线相交检测、场景节点操作、特效节点使用。
核心功能
- 加载并渲染三维模型(牛模型);
- 鼠标左键点击实现模型精准拾取;
- 拾取后模型自动添加高亮线框(Scribe特效);
- 再次点击取消高亮,支持反复切换。
环境与依赖
核心依赖库说明
| 库文件 | 作用 |
|---|---|
| osgViewer | 场景窗口管理、渲染循环 |
| osgGA | GUI事件处理(鼠标/键盘交互) |
| osgUtil | 射线相交检测、场景优化 |
| osgFX | 提供Scribe高亮特效节点 |
| osgDB | 三维模型文件读写 |
类的继承关系详解
本项目核心类为PickHandler,其继承关系是OSG交互系统的标准设计,层级结构如下:
1. 完整继承链
osg::Referenced
↳ osgGA::GUIEventHandler
↳ PickHandler(自定义拾取处理器)
2. 基类作用
osg::Referenced:OSG所有对象的基类,实现智能指针引用计数,自动管理内存,避免内存泄漏;osgGA::GUIEventHandler:OSG图形界面事件处理基类,定义了统一的事件处理接口,所有鼠标、键盘交互都需要继承此类并重写handle()方法。
3. 自定义类职责
PickHandler:封装鼠标拾取、高亮逻辑,是项目的核心交互类。
完整项目代码
1. 主源码:osg_LineSegmentIntersector.cpp
cpp
#include <osgViewer/Viewer>
#include <osgViewer/ViewerEventHandlers>
#include <osg/Node>
#include <osg/Geode>
#include <osg/Group>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
// 高亮特效头文件
#include <osgFX/Scribe>
// 事件处理基类
#include <osgGA/GUIEventHandler>
// 场景优化+线段相交检测
#include <osgUtil/Optimizer>
#include <osgUtil/LineSegmentIntersector>
#include <iostream>
// 自定义拾取处理器:继承OSG事件处理器
class PickHandler : public osgGA::GUIEventHandler
{
public:
// 构造函数:初始化鼠标坐标
PickHandler() : _mouseX(0), _mouseY(0) {}
/**
* @brief 重写事件处理函数:OSG事件循环自动调用
* @param ea 事件适配器(封装鼠标/键盘事件)
* @param aa 动作适配器(封装视图/窗口对象)
* @return 事件是否被消费
*/
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa) override
{
// 将动作适配器转换为OSG视图对象
osgViewer::View* view = dynamic_cast<osgViewer::View*>(&aa);
if (!view) return false;
// 监听鼠标事件:按下记录坐标,释放执行拾取
switch (ea.getEventType())
{
// 鼠标按下:记录当前坐标
case osgGA::GUIEventAdapter::PUSH:
_mouseX = ea.getX();
_mouseY = ea.getY();
break;
// 鼠标释放:判断是否为单击(无拖拽)
case osgGA::GUIEventAdapter::RELEASE:
if (_mouseX == ea.getX() && _mouseY == ea.getY())
{
pick(view, ea.getX(), ea.getY());
}
break;
default:
break;
}
return false;
}
/**
* @brief 核心拾取函数:实现射线检测+模型高亮
* @param view OSG视图对象
* @param x 鼠标屏幕X坐标
* @param y 鼠标屏幕Y坐标
*/
void pick(osgViewer::View* view, float x, float y)
{
// 1. 定义相交结果集合:存储射线与模型的所有交点
osgUtil::LineSegmentIntersector::Intersections intersections;
// 2. 执行射线相交检测:从鼠标点击位置发射射线,检测与场景的交点
if (view->computeIntersections(x, y, intersections))
{
// 获取第一个有效交点(最近的模型)
osgUtil::LineSegmentIntersector::Intersection hitInfo = *intersections.begin();
// 获取交点对应的节点路径(从根节点到当前叶子节点的层级)
osg::NodePath& nodePath = hitInfo.nodePath;
// 安全判断:节点路径长度不足,直接返回
if (nodePath.size() < 2) return;
// 3. 解析节点:叶子节点(模型)+ 父节点
osg::Node* modelNode = nodePath.back(); // 被拾取的模型节点
// 模型的父节点(用于替换节点实现高亮)
osg::Group* parentNode = dynamic_cast<osg::Group*>(nodePath[nodePath.size() - 2]);
if (!parentNode || !modelNode) return;
// 4. 高亮逻辑:判断是否已高亮,切换状态
// 尝试将父节点转换为高亮特效节点
osgFX::Scribe* highlightNode = dynamic_cast<osgFX::Scribe*>(parentNode);
if (highlightNode)
{
// 情况1:已高亮 → 取消高亮:恢复原始模型节点
osg::Node::ParentList parentList = highlightNode->getParents();
for (auto& parent : parentList)
{
parent->replaceChild(highlightNode, modelNode);
}
}
else
{
// 情况2:未高亮 → 添加高亮:创建Scribe节点包裹模型
osg::ref_ptr<osgFX::Scribe> scribe = new osgFX::Scribe();
scribe->addChild(modelNode); // 将模型作为子节点
parentNode->replaceChild(modelNode, scribe.get()); // 替换原节点
}
}
}
private:
float _mouseX; // 鼠标按下X坐标
float _mouseY; // 鼠标按下Y坐标
};
// 主函数:程序入口
int main()
{
// 1. 创建OSG渲染窗口
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer();
// 注册自定义拾取事件处理器
viewer->addEventHandler(new PickHandler());
// 2. 创建场景根节点
osg::ref_ptr<osg::Group> root = new osg::Group();
// 3. 加载三维模型(cow.osg为OSG官方示例模型)
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("cow.osg");
if (!model)
{
std::cerr << "错误:未找到模型文件 cow.osg" << std::endl;
return -1;
}
root->addChild(model);
// 4. 优化场景数据(提升渲染效率)
osgUtil::Optimizer optimizer;
optimizer.optimize(root.get());
// 5. 设置场景数据并启动渲染循环
viewer->setSceneData(root.get());
viewer->realize();
return viewer->run();
}

核心函数:pick()
pick() 是项目的灵魂函数 ,实现了射线拾取+高亮切换的完整逻辑,分5步执行:
步骤1:定义相交结果容器
cpp
osgUtil::LineSegmentIntersector::Intersections intersections;
- 作用:存储射线与三维模型的所有相交信息(交点坐标、节点路径、法向量等)。
步骤2:执行射线相交检测
cpp
view->computeIntersections(x, y, intersections);
- 核心原理:屏幕坐标转射线
- OSG自动将鼠标的屏幕2D坐标 转换为三维空间射线;
- 射线从相机视点出发,穿透场景;
- 检测射线与模型的交点,将结果存入
intersections。
- 返回值:
true表示射线命中模型,false表示未命中。
步骤3:解析节点路径
cpp
osgUtil::LineSegmentIntersector::Intersection hitInfo = *intersections.begin();
osg::NodePath& nodePath = hitInfo.nodePath;
osg::Node* modelNode = nodePath.back();
osg::Group* parentNode = dynamic_cast<osg::Group*>(nodePath[nodePath.size() - 2]);
nodePath:节点路径,是OSG场景的树形结构层级(根节点→组节点→模型节点);nodePath.back():获取路径最后一个节点,即被拾取的模型叶子节点;- 父节点:模型的直接上级节点,用于后续替换节点实现高亮。
步骤4:高亮状态切换逻辑
-
判断是否已高亮
cpposgFX::Scribe* highlightNode = dynamic_cast<osgFX::Scribe*>(parentNode);osgFX::Scribe:OSG内置的线框高亮特效节点,会为子节点渲染高亮边框;- 类型转换:如果父节点是
Scribe,说明模型已高亮。
-
取消高亮
cppparent->replaceChild(highlightNode, modelNode);- 将高亮节点替换为原始模型节点,恢复默认显示。
-
添加高亮
cpposg::ref_ptr<osgFX::Scribe> scribe = new osgFX::Scribe(); scribe->addChild(modelNode); parentNode->replaceChild(modelNode, scribe.get());- 创建高亮节点,包裹模型节点;
- 用高亮节点替换原始模型节点,场景中实时显示高亮效果。
总结
- OSG事件机制 :继承
GUIEventHandler实现自定义交互,是OSG开发的标准范式; - 射线拾取原理:屏幕坐标转空间射线,通过相交检测实现三维物体拾取;
- 场景节点操作 :通过
replaceChild()动态修改场景树,实现特效切换; - 智能指针 :
osg::ref_ptr自动管理内存,是OSG开发的必备技能; - 特效节点 :
osgFX::Scribe快速实现模型高亮,无需手动编写着色器。
