【OSG学习笔记】Day 49: 实战鼠标拾取与高亮显示

OSG 鼠标拾取与高亮显示项目

项目概述

本项目基于OpenSceneGraph(OSG) 三维渲染引擎开发,核心实现鼠标交互式拾取三维模型 + 模型高亮边框显示功能。用户通过鼠标左键点击场景中的3D模型,可实时为模型添加高亮线框效果;再次点击已高亮模型,可取消高亮恢复原始状态。

项目适用于OSG初学者入门交互开发、三维场景拾取功能学习,涵盖OSG核心知识点:事件处理器、射线相交检测、场景节点操作、特效节点使用。

核心功能

  1. 加载并渲染三维模型(牛模型);
  2. 鼠标左键点击实现模型精准拾取;
  3. 拾取后模型自动添加高亮线框(Scribe特效);
  4. 再次点击取消高亮,支持反复切换。

环境与依赖

核心依赖库说明

库文件 作用
osgViewer 场景窗口管理、渲染循环
osgGA GUI事件处理(鼠标/键盘交互)
osgUtil 射线相交检测、场景优化
osgFX 提供Scribe高亮特效节点
osgDB 三维模型文件读写

类的继承关系详解

本项目核心类为PickHandler,其继承关系是OSG交互系统的标准设计,层级结构如下:

1. 完整继承链

复制代码
osg::Referenced 
  ↳ osgGA::GUIEventHandler 
       ↳ PickHandler(自定义拾取处理器)

2. 基类作用

  1. osg::Referenced :OSG所有对象的基类,实现智能指针引用计数,自动管理内存,避免内存泄漏;
  2. 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);
  • 核心原理:屏幕坐标转射线
    1. OSG自动将鼠标的屏幕2D坐标 转换为三维空间射线
    2. 射线从相机视点出发,穿透场景;
    3. 检测射线与模型的交点,将结果存入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:高亮状态切换逻辑

  1. 判断是否已高亮

    cpp 复制代码
    osgFX::Scribe* highlightNode = dynamic_cast<osgFX::Scribe*>(parentNode);
    • osgFX::Scribe:OSG内置的线框高亮特效节点,会为子节点渲染高亮边框;
    • 类型转换:如果父节点是Scribe,说明模型已高亮。
  2. 取消高亮

    cpp 复制代码
    parent->replaceChild(highlightNode, modelNode);
    • 将高亮节点替换为原始模型节点,恢复默认显示。
  3. 添加高亮

    cpp 复制代码
    osg::ref_ptr<osgFX::Scribe> scribe = new osgFX::Scribe();
    scribe->addChild(modelNode);
    parentNode->replaceChild(modelNode, scribe.get());
    • 创建高亮节点,包裹模型节点;
    • 用高亮节点替换原始模型节点,场景中实时显示高亮效果。

总结

  1. OSG事件机制 :继承GUIEventHandler实现自定义交互,是OSG开发的标准范式;
  2. 射线拾取原理:屏幕坐标转空间射线,通过相交检测实现三维物体拾取;
  3. 场景节点操作 :通过replaceChild()动态修改场景树,实现特效切换;
  4. 智能指针osg::ref_ptr自动管理内存,是OSG开发的必备技能;
  5. 特效节点osgFX::Scribe快速实现模型高亮,无需手动编写着色器。
相关推荐
Wyawsl2 小时前
keepalived高可用与负载均衡笔记
运维·笔记·负载均衡
chushiyunen2 小时前
obs studio软件、直播、视频录制笔记
笔记·音视频
何如呢2 小时前
FIFO的IP核学习
学习·fpga开发
折锦烟3 小时前
AI Agent 开发 0-1 学习路线(学习目标)
学习
艾莉丝努力练剑3 小时前
【Linux线程】Linux系统多线程(六):<线程同步与互斥>线程同步(上)
java·linux·运维·服务器·c++·学习·线程
brave_zhao3 小时前
什么是增值税
学习
sheeta19983 小时前
LeetCode 每日一题笔记 日期:2026.04.14 题目:2463.最小移动距离
笔记·算法·leetcode
herinspace3 小时前
管家婆实用帖-如何使用ping命令检测网络环境
网络·数据库·人工智能·学习·excel·语音识别
阳光宅男@李光熠3 小时前
【电子通识】为什么PCB能短接还要用0Ω电阻?0欧电阻怎么做降额?
笔记·学习