osg中NDC坐标转换为世界坐标

目录

1.前言

2.原理

3.实现


1.前言

使用射线投掷法(Ray-casting)在三维场景中拾取物体时,我们会从观察点,即眼睛所在位置向场景中发射一条射线,射线的方向通常由"鼠标"位置确定。在此类应用中需要解决的一个主要问题是:如何将二维窗口坐标转为三维场景坐标?我们接下来将对此问题进行探讨。

2.原理

世界坐标系中的点通过视图矩阵和投影矩阵变换到裁剪空间(Clip space),再通过透视除法变换为标准化设备坐标(NDC, normalized device coordinate)。NDC是一个长宽高取值都为[-1, 1]的空间,也就是说世界坐标变换为NDC坐标后,x,y,z都在[-1,1]的顶点才是可见的。

本位以下公式中的坐标向量使用齐次坐标,即v=(x,y,z,w)^{T},坐标分量分别用v.x,\ v.y,\ v.z\ ,v.w表示。

假设世界坐标系中有一点P,齐次坐标记为\large v_{world},其中v_{world}.w=1.0,则有

\large v_{clip}=M_{proj}*M_{view} * v_{world}                                      (1)

\large M_{vp} = M_{proj} * M_{view}                                                             (2)

在公式(1)中,\large v_{clip} 表示P点在裁剪空间坐标系中的坐标,\large M_{view}表示视图矩阵,\large M_{proj}表示投影矩阵。在公式(2)中,视图矩阵与投影矩阵相乘得到“视图投影矩阵”记为\large M_{vp} 。

由公式(1)和公式(2)得

v_{world} = M_{vp}^{-1} * v_{clip}                                                                            (3)

P点的NDC坐标记为\large v_{ndc},透视除法的过程是用裁剪空间坐标的4个分量分别除以w分量,即

v_{ndc} = v_{clip} / v_{clip}.w                                                                              (4)

由公式(3)和公式(4)得

v_{world} = M_{vp}^{-1} * v_{ndc} * v_{clip}.w                                                             (5)

在公式(5)中,M_{vp}^{-1}由相机参数获取,v_{ndc}是输入参数,v_{clip}.w是P点在裁剪空间中坐标的第四个分量w。

由前文可知v_{world}.w=1.0,结合公式(5)有

v_{view}.w = (M_{vp}^{-1}*v_{ndc}).w*v_{clip}.w=1.0                                      (6)

由公式(6)可得

v_{clip}.w=\frac{1.0}{(M_{vp}^{-1}*v_{ndc}).w}                                                                (7)

将公式(7)带入公式(5)可得

v_{world} = M_{vp}^{-1} * v_{ndc} * \frac{1.0}{(M_{vp}^{-1} * v_{ndc}).w}                                          (8)

使用公式(8)就可将NDC坐标转换为世界坐标。

3.实现

我们根据上述原理,在osg中实现以下功能:鼠标左键点击屏幕,从眼睛(相机)处向场景中发射一条射线并在场景中绘制出来(绘制射线从相机到远裁剪平面之间的线段)。

我们首先创建一个osgViewer::Viewer对象,并在窗口中创建视图。自定义函数createScene()创建一个简单场景并返回其根节点,然后将其加入到根节点root下。

而后我们创建一个自定义的事件处理类MyHandler并加入到osgViewer::Viewer对象的事件处理器列表中,用来处理我们的输入事件以实现交互。

最后进入主循环,直到程序退出。

int main()
{
	osgViewer::Viewer viewer;
	viewer.setUpViewInWindow(100, 100, 800, 600);
	osg::ref_ptr<osg::Group> root = new osg::Group;
	root->addChild(createScene());
	viewer.setSceneData(root);
	viewer.addEventHandler(new MyHandler(root));

	return viewer.run();
}

自定义函数createScene()创建一个由一个球体和一个立方体组成的简单场景并返回场景的根节点。

osg::ref_ptr<osg::Node> createScene()
{
	osg::ref_ptr<osg::Group> root = new osg::Group;
	osg::ref_ptr<osg::Geode> geode = new osg::Geode;
	root->addChild(geode);

	osg::ref_ptr<osg::Shape> sphere = new osg::Sphere(osg::Vec3(-5.f, 0.f, 0.f), 3.f);
	osg::ref_ptr<osg::ShapeDrawable> sphereDrawable = new osg::ShapeDrawable(sphere);
	sphereDrawable->setColor(osg::Vec4(1.f, 1.f, 1.f, 1.f));
	geode->addDrawable(sphereDrawable);

	osg::ref_ptr<osg::Shape> box = new osg::Box(osg::Vec3(5.f, 0.f, 0.f), 4.f);
	osg::ref_ptr<osg::ShapeDrawable> boxDrawable = new osg::ShapeDrawable(box);
	boxDrawable->setColor(osg::Vec4(0.f, 0.f, 1.f, 1.f));
	geode->addDrawable(boxDrawable);

	return root;
}

自定义类MyHandler 继承自osgGA::GUIEventHandler。定义osg::ref_ptr<osg::Group>类型的成员变量 _root作为绘制射线子场景的根节点,同时定义一系列成员变量对射线进行管理。

class MyHandler : public osgGA::GUIEventHandler
{
public:
	MyHandler(osg::ref_ptr<osg::Group> parent)
		: _parent(parent)
	{
		_root = new osg::Group;
		if (_parent.valid())
		{
			_parent->addChild(_root);
		}
	}

private:
	osg::ref_ptr<osg::Geode> _rayNode;
	osg::observer_ptr<osg::Group> _parent;
	osg::ref_ptr<osg::Vec3Array> _rayVertices;
	osg::ref_ptr<osg::Group> _root;
};

通过重写虚函数handle()实现事件处理。在本例中,用户按下Ctrl+鼠标左键组合键,就可以在三维空间中画出一条线段,这条线段起源于eye坐标,终止于鼠标点击位置的远裁剪平面。

在handle()函数中,取出相机的视图矩阵viewMatrix和投影矩阵projMatrix,由viewMatrix可以获取相机的位置eye,观察点center和上方向up。

根据公式(8),我们需要获取视图和投影矩阵乘积的逆矩阵invvpMatrix ,然后与NDC坐标相乘得到对应的世界坐标。使用osgGA::GUIEventAdapter::getXnormalized()和getYnormalized()方法可以获取鼠标当前位置的NDC坐标的x和y分量,我们取1.0作为NDC坐标的z分量,在相机视椎体frustum中,远裁剪平面对应的NDC坐标z分量为1.0,因此变换后的世界坐标位于远裁剪平面上。

透视投影视椎体frustum

下面代码中矩阵的运算顺序与公式(8)中推导的有所不同,原因是为了与GLSL中的矩阵对应,osg中默认的向量是列主序存储的,因此要将公式推导中的矩阵和向量做转置处理,即代码中的形式。 

使用相机位置eye和远裁剪平面上的点worldFar绘制射线,本例子中添加了一个自定义成员函数createOrUpdateRay()完成此工作,如果场景中还不存在射线(线段)节点,则创建一个节点,否则用最新的起始点和终止点更新它。

class MyHandler : public osgGA::GUIEventHandler
{
public:

    ...	

    virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa, osg::Object*, osg::NodeVisitor*) override
	{
		if ((ea.getEventType() & osgGA::GUIEventAdapter::EventType::PUSH) &&
			(ea.getModKeyMask() & osgGA::GUIEventAdapter::ModKeyMask::MODKEY_CTRL) && 
			(ea.getButton() & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON))
		{
			do
			{
				auto view = aa.asView();
				if (!view)
				{
					break;
				}
				auto camera = view->getCamera();
				if (!camera)
				{
					break;
				}

				const auto& viewMatrix = camera->getViewMatrix();
				const auto& projMatrix = camera->getProjectionMatrix();
				auto vpMatrix = viewMatrix * projMatrix;
				auto invvpMatrix = osg::Matrixd::inverse(vpMatrix);

				osg::Vec3d eye, center, up;
				viewMatrix.getLookAt(eye, center, up);

				osg::Vec4d ndcFar(ea.getXnormalized(), ea.getYnormalized(), 1.f, 1.f);
				auto worldFar = invvpMatrix.preMult(ndcFar);
				worldFar /= worldFar.w();

				createOrUpdateRay(eye, worldFar);
			} while (0);
		}

		return false;
	}

private:

	void createOrUpdateRay(const osg::Vec3d& start, const osg::Vec3d& end)
	{
		if (!_rayNode && _root)
		{
			//create new ray node
			_rayNode = new osg::Geode;
			_root->addChild(_rayNode);
			osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
			_rayNode->addDrawable(geom);
			geom->setUseVertexBufferObjects(true);
			_rayVertices = new osg::Vec3Array(2);
			geom->setVertexArray(_rayVertices);
			osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array(2);
			colors->at(0) = osg::Vec4(1.f, 0.f, 0.f, 1.f);
			colors->at(1) = osg::Vec4(1.f, 1.f, 0.f, 1.f);
			geom->setColorArray(colors, osg::Array::Binding::BIND_PER_VERTEX);
			geom->addPrimitiveSet(new osg::DrawArrays(GL_LINES, 0, _rayVertices->size()));
			geom->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
		}

		if (_rayVertices)
		{
			_rayVertices->at(0) = start;
			_rayVertices->at(1) = end;
			_rayVertices->dirty();
		}
	}

...
}

至此,主要代码就完成了。程序运行的效果如下:

按住ctrl点击窗口,则会从相机(眼睛)位置到远裁剪平面发射一条射线并绘制,此时我们是看不到这条射线的,想象一下,一条从眼睛发出的射线,经过投影后变为一个没有面积的“点”,在光栅化的过程中,这样的线段不形成像素,因此不可见。只需拖动鼠标移动一下视点位置就可以看到射线了。

 文末,贴上全部代码。另外我已将此例子的CMake工程代码提交至Gitee,传送门

完整代码

#include <osg/ShapeDrawable>
#include <osgViewer/Viewer>

osg::ref_ptr<osg::Node> createScene()
{
	osg::ref_ptr<osg::Group> root = new osg::Group;
	osg::ref_ptr<osg::Geode> geode = new osg::Geode;
	root->addChild(geode);

	osg::ref_ptr<osg::Shape> sphere = new osg::Sphere(osg::Vec3(-5.f, 0.f, 0.f), 3.f);
	osg::ref_ptr<osg::ShapeDrawable> sphereDrawable = new osg::ShapeDrawable(sphere);
	sphereDrawable->setColor(osg::Vec4(1.f, 1.f, 1.f, 1.f));
	geode->addDrawable(sphereDrawable);

	osg::ref_ptr<osg::Shape> box = new osg::Box(osg::Vec3(5.f, 0.f, 0.f), 4.f);
	osg::ref_ptr<osg::ShapeDrawable> boxDrawable = new osg::ShapeDrawable(box);
	boxDrawable->setColor(osg::Vec4(0.f, 0.f, 1.f, 1.f));
	geode->addDrawable(boxDrawable);

	return root;
}

class MyHandler : public osgGA::GUIEventHandler
{
public:
	MyHandler(osg::ref_ptr<osg::Group> parent)
		: _parent(parent)
	{
		_root = new osg::Group;
		if (_parent.valid())
		{
			_parent->addChild(_root);
		}
	}

	virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa, osg::Object*, osg::NodeVisitor*) override
	{
		if ((ea.getEventType() & osgGA::GUIEventAdapter::EventType::PUSH) &&
			(ea.getModKeyMask() & osgGA::GUIEventAdapter::ModKeyMask::MODKEY_CTRL) && 
			(ea.getButton() & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON))
		{
			do
			{
				auto view = aa.asView();
				if (!view)
				{
					break;
				}
				auto camera = view->getCamera();
				if (!camera)
				{
					break;
				}

				const auto& viewMatrix = camera->getViewMatrix();
				const auto& projMatrix = camera->getProjectionMatrix();
				auto vpMatrix = viewMatrix * projMatrix;
				auto invvpMatrix = osg::Matrixd::inverse(vpMatrix);

				osg::Vec3d eye, center, up;
				viewMatrix.getLookAt(eye, center, up);

				osg::Vec4d ndcFar(ea.getXnormalized(), ea.getYnormalized(), 1.f, 1.f);
				auto worldFar = invvpMatrix.preMult(ndcFar);
				worldFar /= worldFar.w();

				createOrUpdateRay(eye, osg::Vec3d(worldFar.x(), worldFar.y(), worldFar.z()));
			} while (0);
		}

		return false;
	}

private:
	void createOrUpdateRay(const osg::Vec3d& start, const osg::Vec3d& end)
	{
		if (!_rayNode && _root)
		{
			//create new ray node
			_rayNode = new osg::Geode;
			_root->addChild(_rayNode);
			osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
			_rayNode->addDrawable(geom);
			geom->setUseVertexBufferObjects(true);
			_rayVertices = new osg::Vec3Array(2);
			geom->setVertexArray(_rayVertices);
			osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array(2);
			colors->at(0) = osg::Vec4(1.f, 0.f, 0.f, 1.f);
			colors->at(1) = osg::Vec4(1.f, 1.f, 0.f, 1.f);
			geom->setColorArray(colors, osg::Array::Binding::BIND_PER_VERTEX);
			geom->addPrimitiveSet(new osg::DrawArrays(GL_LINES, 0, _rayVertices->size()));
			geom->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
		}

		if (_rayVertices)
		{
			_rayVertices->at(0) = start;
			_rayVertices->at(1) = end;
			_rayVertices->dirty();
		}
	}

private:
	osg::ref_ptr<osg::Geode> _rayNode;
	osg::observer_ptr<osg::Group> _parent;
	osg::ref_ptr<osg::Vec3Array> _rayVertices;
	osg::ref_ptr<osg::Group> _root;
};

int main()
{
	osgViewer::Viewer viewer;
	viewer.setUpViewInWindow(100, 100, 800, 600);
	osg::ref_ptr<osg::Group> root = new osg::Group;
	root->addChild(createScene());
	viewer.setSceneData(root);
	viewer.addEventHandler(new MyHandler(root));

	return viewer.run();
}

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值