自己实现一个osgEarth的RTTPicker

前一章中自认为对OE的RTTPicker有足够的了解,但是还有一些理论中的步骤没有找到源码出处。为了证实自己的理解,决定自己做一个RTTPicker。

代码构成

一共由两个部分组成,一个是封装的manager管理器RTTPickerMgr,一个是自己的RTTPicker实现RTTPicker。其中管理器类仅涉及对外接口和id管理,这里不做展示;而RTTPicker实现文件里还有附带的自定义图片纹理PickImage和调整拾取位的相机回调CameraInitDrawCallBack,实现如下所示:

代码实现

SpiralIterator

在对图片进行采像素操作时,OE内部采用了一种螺旋迭代器的实现,会遍历循环指定位置及周围的像素直到找到合法的颜色值。这里采用同款的迭代器实现,如下所示。


    //RTTPicker.cpp

    struct SpiralIterator
    {
        unsigned _ring;
        unsigned _maxRing;
        unsigned _leg;
        int      _x, _y;
        int      _w, _h;
        int      _offsetX, _offsetY;
        unsigned _count;

        SpiralIterator(int w, int h, int maxDist, float x, float y) :
                w(w), _h(h), _maxRing(maxDist), _count(0), _ring(1), _leg(0), _x(0), _y(0)
        {
            _offsetX = x;
            _offsetY = y;
        }

        bool next()
        {
            // first time, just use the start point
            if (_count++ == 0)
                return true;

            // spiral until we get to the next valid in-bounds pixel:
            do {
                switch (_leg) {
                case 0: ++_x; if (_x == _ring) ++_leg; break;
                case 1: ++_y; if (_y == _ring) ++_leg; break;
                case 2: --_x; if (-_x == _ring) ++_leg; break;
                case 3: --_y; if (-_y == _ring) { _leg = 0; ++_ring; } break;
                }
            } while (_ring <= _maxRing && (_x + _offsetX < 0 || _x + _offsetX >= _w || _y + _offsetY < 0 || _y + _offsetY >= _h));

            return _ring <= _maxRing;
        }

        int s() const { return _x + _offsetX; }

        int t() const { return _y + _offsetY; }
    };

顾名思义,给迭代器传入图片宽高、最大距离和起始位置,不停的调用next()接口,迭代器就会螺旋式的遍历目标位置及周围的像素值。

PickImage

在OE内置的RTTPicker方案中,没有对离屏渲染的数据进行特殊处理,当前视口里渲染的东西统统都存进了纹理里面,但是实际上我们要拿来识别的仅有鼠标点击那一块儿的像素颜色值,所以这边重写了一个图片对象,在写入像素数据时只存入点击位置的像素。


    //RTTPicker.h
    class PickImage : public osg::Image
    {
    public:
        PickImage(CRTTPickerHandler* picker)
            : m_rttPicker(picker) {};

        virtual ~PickImage() {};

        virtual void readPixels(int x, int y, int width, int height,
            GLenum pixelFormat, GLenum type, int packing = 1);

    private:
        CRTTPickerHandler* m_rttPicker;
    };

osg::Image类写像素数据到内存调用的接口即为上文提到的readPixles接口。其实现如下所示:


void PickImage::readPixels(int x, int y, int width, int height, GLenum pixelFormat, GLenum type, int packing /*= 1*/)
{
    if (m_rttPicker->isReadId())
    {
        unsigned char bytes[10] = {};

        float x = m_rttPicker->getX();
        float y = m_rttPicker->getY();
    
        SpiralIterator iter(width, height, 2, x, y);
    
        bool isCorrect = false;
        std::vector<osg::Vec4> colors;
        while (iter.next())
        {
            auto s = iter.s();
            auto t = iter.t();
            glReadPixels(s, t, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, bytes);
    
            osg::Vec4 aa(bytes[0], bytes[1], bytes[2], bytes[3]);
            int id = (int)(
                ((unsigned)(bytes[0]) << 24) +
                ((unsigned)(bytes[1]) << 16) +
                ((unsigned)(bytes[2]) << 8) +
                ((unsigned)(bytes[3])));
    
            if (id > 0)
            {
                isCorrect = true;
                m_rttPicker->setObjectId(id);
                break;
                //colors.push_back(aa);
            }
        }
    
        if (!isCorrect)
        {
            m_rttPicker->setObjectId(-1);
        }
        m_rttPicker->setReadedId(true);
        m_rttPicker->setReadId(false);
    }
    m_rttPicker->setUniformValue(0);
}

在每一帧渲染时会调用readPixels读取缓存中的像素写入到纹理里。在上部分的实现中使用螺旋迭代器检查鼠标检测点附近的像素,然后将像素颜色转为objectid,如果id合法就将其输出。

CameraInitDrawCallback和显示切换

RTT拾取识别的时候缓存里特定目标的像素是经过objectID编码后的颜色。为了不影响正常渲染效果的显示,需要有一个标志位用来切换特定绘制对象正常颜色和编码颜色的切换,还要有一个回调在每帧渲染前重置这个标志位。这个回调依附在场景相机里,实现如下:


class CameraInitDrawCallback : public osg::Camera::DrawCallback
{
public:
    CameraInitDrawCallback(CRTTPickerHandler* pHandler)
        : m_pHandler(pHandler) {};

    virtual ~CameraInitDrawCallback() {};

    virtual void operator () (osg::RenderInfo& renderInfo) const
    {
        if (m_pHandler)
        {
            for (auto& pObj : m_pHandler->m_vObjs)
            {
                if (pObj.valid())
                {
                    pObj->getPickUniform()->set(1);
                }
            }
        }
    }

private:
    CRTTPickerHandler* m_pHandler;
};

可以看到这个回调的作用是从handler中获取所有可拾取对象,从每个对象获取显示切换的标志位并重置它。我这边实现里标志位为1代表显示编码后的颜色,那么在可拾取对象的片段着色器里需要加入类似如下的代码:


    if(u_RttIsPick > 0.0)
    {
        gl_FragColor = u_RttColor;
    }
    else
    {
        gl_FragColor = originColor;
    }


其中u_RttIsPick就是拾取标志位,这个uniform需要暴露出来给外面访问修改;与此同时u_RttColor代表编码后的颜色,这个颜色值也需要可拾取对象自己管理。

CRTTPickerHandler

最后将以上部分串联起来,也是rtt拾取最核心的实现就是事件处理类CRTTPickerHandler了。事件处理器类还有个附属的处理回调,用于实现不同点击结果执行不同的处理。
处理器类的定义如下所示:


#ifndef RTT_PICK_MASK
#define RTT_PICK_MASK 8848
#endif

struct CRTTPickCallback;
class CPickableObj;

class CRTTPickerHandler : public osgGA::GUIEventHandler
{
public:
    CRTTPickerHandler();

    //添加处理回调
    void setCallBack(CRTTPickCallback* callback);

    //添加可拾取对象
    void addPickableNode(CPickableObj* pObj);

    //移除可拾取对象
    void removePickableNode(CPickableObj* pObj);

    //设置点击后需要高亮的那个objectID
    void setHightLightOID(const unsigned int& id);

    //是否读取到id
    bool isReadId();

    float getX();
    float getY();
    void setObjectId(int objectId);
    void setReadedId(bool flag);
    void setReadId(bool flag);

    void setUniformValue(int value);

public:
    virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa);

private:
    void init();

protected:
    friend class CameraInitDrawCallback;
    std::vector<osg::observer_ptr<CPickableObj>> m_vObjs;

    osg::observer_ptr<osg::Camera> m_opMainCamera;
    osg::ref_ptr<osg::Camera>    m_rpPickCamera;
    osg::ref_ptr<osg::Image> m_pickImage;
    osg::ref_ptr<CRTTPickCallback> m_rpPickCallback;
    float m_x;
    float m_y;
    int  m_objectId;
    bool m_isReadId; // 是否读取到了Id
    bool m_isReadedId; // 是否需要进行Id读取
    int m_objectID;
    unsigned int m_curFrame;
};

实现如下所示:


//拾取到合法objectid时的处理回调
struct CRTTPickCallback : public osg::Referenced
{
    CRTTPickCallback(CRTTPickerHandler* pHandler)
        :m_pHandler(pHandler)
    {
    }


    virtual void onHit(unsigned int id)
    {
        //LinkLineHandlerProvider是外部管理类,这里仅作为提供id->颜色的转换并将点击事件发送给外界,不做展示。
        auto handleProvider = LinkLineHandlerProvider::getInstance();

        std::string uniqueStrID = handleProvider->getUniqueStrIDFromObjectID(id);
        if (uniqueStrID.empty())
        {
            return onMiss();
        }
        handleProvider->sendLinePickEvent(id);
        m_pHandler->setHightLightOID(id);
    }

    virtual void onMiss()
    {
        m_pHandler->setHightLightOID(unsigned int(0));
    }


    virtual bool accept(const osgGA::GUIEventAdapter& ea, const osgGA::GUIActionAdapter& aa)
    { 
        return ea.getEventType() == ea.RELEASE && ea.getButton() == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
    }
private:
    CRTTPickerHandler* m_pHandler;
};


CRTTPickerHandler::CRTTPickerHandler()
    :m_opMainCamera(nullptr)
{
    init();
}

bool CRTTPickerHandler::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{

    if (ea.getEventType() == ea.FRAME)
    {
        unsigned int frame = m_opMainCamera->getView()->getFrameStamp() ? m_opMainCamera->getView()->getFrameStamp()->getFrameNumber() : 0u;

        m_rpPickCamera->setViewMatrix(m_opMainCamera->getViewMatrix());
        m_rpPickCamera->setProjectionMatrix(m_opMainCamera->getProjectionMatrix());
        //camera会自动根据viewport改变大小,不需要重新设置
        if (m_isReadedId && frame > m_curFrame)
        {
            m_curFrame = frame;
            
            if (m_objectId > 0)
            {
                
                m_rpPickCallback->onHit(m_objectId);
            }
            else
            {
                m_rpPickCallback->onMiss();
            }

            m_isReadedId = false;
            m_rpPickCamera->setNodeMask(0);
        }
    }

    if (m_rpPickCallback->accept(ea, aa))
    {
        if (!FxUtil::MathUtil::equal(m_x, ea.getX()) || !FxUtil::MathUtil::equal(m_y, ea.getY()))
        {
            m_x = ea.getX();
            m_y = ea.getY();
            //m_rpPickCamera->setNodeMask(~0);
            m_rpPickCamera->setNodeMask(RTT_PICK_MASK);

            m_isReadId = true;
            m_isReadedId = false;
            m_curFrame = m_opMainCamera->getView()->getFrameStamp() ? m_opMainCamera->getView()->getFrameStamp()->getFrameNumber() : 0u;
        }
    }
    return false;
}

void CRTTPickerHandler::init()
{
    //这里是获取场景主相机的函数,不作实现展示
    m_opMainCamera = getCamera();

    m_x = -1;
    m_y = -1;
    m_objectId = 0;
    m_isReadedId = false;
    m_isReadId = false;

    //图片初始化时应该设置为可利用的最大值,不然缩放会丢失部分数据
    auto screen = QGuiApplication::primaryScreen();
    auto screenSize = screen->geometry().size();
    int imageWidth = screenSize.width();
    int imageHeight = screenSize.height();


    m_pickImage = new PickImage(this);
    m_pickImage->allocateImage(imageWidth, imageHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE);

    m_rpPickCamera = new osg::Camera();
    m_rpPickCamera->setClearColor(osg::Vec4(0, 0, 0, 0));
    m_rpPickCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    m_rpPickCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF_INHERIT_VIEWPOINT);


    m_rpPickCamera->setViewport(m_opMainCamera->getViewport());
    m_rpPickCamera->setRenderOrder(osg::Camera::PRE_RENDER);
    m_rpPickCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
    m_rpPickCamera->attach(osg::Camera::COLOR_BUFFER, m_pickImage.get());

    m_rpPickCamera->setInitialDrawCallback(new CameraInitDrawCallback(this));

    //关闭拾取相机的颜色混合,不然拾取到的颜色不会等于当初设的颜色
    auto rttSS = m_rpPickCamera->getOrCreateStateSet();
    osg::StateAttribute::GLModeValue disable = osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED;

    rttSS->setMode(GL_BLEND, disable);
    rttSS->setMode(GL_LIGHTING, disable);
    rttSS->setMode(GL_CULL_FACE, disable);

    // Disabling GL_BLEND is not enough, because osg::Text re-enables it
    // without regard for the OVERRIDE.
    rttSS->setAttributeAndModes(new osg::BlendFunc(GL_ONE, GL_ZERO), osg::StateAttribute::OVERRIDE);

    //相机只跟踪场景实体根节点,防止大气层等其他半透明节点挡住地球影响rtt拾取到的颜色
    osg::Node* objRoot = getObjectsRootNode();
    m_rpPickCamera->addChild(objRoot);
    
    //这里是将相机加入到场景结构树里
    addNode(m_rpPickCamera);


    setCallBack(new CRTTPickCallback(this));
}

float CRTTPickerHandler::getX()
{
    return m_x;
}

bool CRTTPickerHandler::isReadId()
{
    return m_isReadId;
}

float CRTTPickerHandler::getY()
{
    return m_y;
}

void CRTTPickerHandler::setObjectId(int objectId)
{
    m_objectId = objectId;
}

void CRTTPickerHandler::setReadedId(bool flag)
{
    m_isReadedId = flag;
}

void CRTTPickerHandler::setReadId(bool flag)
{
    m_isReadId = flag;
}

void CRTTPickerHandler::setUniformValue(int value)
{
    for (auto pObj : m_vObjs)
    {
        if (pObj.valid())
        {
            pObj->getPickUniform()->set(value);
        }
    }
}

void CRTTPickerHandler::setCallBack(CRTTPickCallback* callback)
{
    m_rpPickCallback = callback;
}

void CRTTPickerHandler::addHandleLineNode(CPickableObj* pObj)
{
    for (auto iter = m_vObjs.begin(); iter != m_vObjs.end(); iter++)
    {
        if (iter->get() == pObj)
        {
            return;
        }
    }

    osg::observer_ptr<CPickableObj> opObj = pObj;
    m_vObjs.push_back(opObj);
}

void CRTTPickerHandler::removeHandleLineNode(CPickableObj* pObj)
{
    for (auto iter = m_vObjs.begin(); iter != m_vObjs.end(); iter++)
    {
        if (iter->get() == pObj)
        {
            m_vObjs.erase(iter);
            break;
        }
    }
}

void CRTTPickerHandler::setHightLightOID(const unsigned int & id)
{
    //根据被拾取的id获取对应颜色,并设置到所有可拾取对象。让每个可拾取对象比对自身rttColor,比对成功就进行高亮显示
    osg::Vec4 color = LinkLineHandlerProvider::getInstance()->getRTTColorIDFromObjectID(id);
    for (auto iter = m_vObjs.begin(); iter != m_vObjs.end(); iter++)
    {
        if (iter->get())
        {
            iter->get()->getHightLightUniform()->set(color);
        }
    }
}

下面进行部分实现设计的说明:

  • 识别专用的相机: 处理器类的成员函数m_rpPickCamera是实现rtt颜色变换的重要媒介。
  1. 该拾取相机下面是所有实体的根节点,以便后续可以观察到所有可拾取对象。
  2. 拾取相机使用setInitialDrawCallback绑定标志位重置回调,这个回调会在每一帧开始渲染前被调用,保证每次刷新时可拾取对象都是正常显示的。
  3. 每次帧刷新时,拾取相机都会和主相机同步视图和投影矩阵,保证从拾取相机中获取的缓存是和主相机的缓存是一样的,这样所有关于rtt相关的操作都能在该相机上实现。
  4. 鼠标事件发生时,拾取相机设置了RTT_PICK_MASK的蒙版,那么所有没设置这个蒙版值的实体都不会被该相机拍摄到;将所有可拾取对象设置为此蒙版,就能在鼠标事件发生时,拾取相机只生成视野里可拾取对象的缓存。
  5. 拾取结果处理完成后,直到下一次拾取事件发生,这段时间本质上拾取相机都不需要工作,所以直接将其蒙版设为0,不让他拍摄任何东西。
  • 识别后延迟处理: 处理器的成员函数有个叫m_curFrame用于记录鼠标事件发生时的帧数,m_isReadedId标志位用于确认readPixels步骤时读取到了合法的id。当当前帧数大于鼠标事件发生的帧数,且m_isReadedId标志位为真时,才会进行识别结果处理流程。通过将识别和处理错开,保证有充足的时间提供给readPixels的迭代器循环读取周围像素进行模糊匹配。

简要流程总结

简单总结一下rtt拾取的工作流程:

  1. 帧刷新前调用回调,把所有可拾取对象的拾取标志位重置。
  2. 正常帧刷新时,拾取相机参数和主相机同步。
  3. 拾取时间发生时,记录当前鼠标位置,并将拾取相机的蒙版设为可拾取对象专用蒙版值。
  4. 拾取相机因为蒙版的缘故会只拍摄到可拾取对象
  5. 相机把缓存写入图片时,会把存在颜色转换的像素转换为objectID对应的颜色编码;后续调用readPixels在拾取区域附近匹配对应的objectid,得到拾取结果
  6. 后续帧绘制时得到拾取结果,再将结果传入专门的拾取处理回调进行后续处理:比如将该拾取对象高亮。
posted @ 2025-02-12 13:49  Joyfulmika  阅读(114)  评论(0)    收藏  举报