基于深度纹理的复杂透明对象稳定绘制方法(OSG实现)

基于深度纹理的复杂透明对象稳定绘制方法(OSG实现)

多个透明物体的绘制问题,可以参见《OpenGL编程指南(第9版)》的11.4节。本文主要讨论单个透明物体绘制中的问题与解决方法。



一、问题分析

透明对象绘制时,当视点处于不同位置时,由于绘制顺序的原因,往往会出现不同的绘制效果,有些效果甚至会令人产生绘制错误的感觉。在此称之为透明对象绘制的不稳定。
绘制透明对象时,第一个需要选择的策略就是:是否写入Z缓存。
(1)不写入Z缓存
如果保持不写入Z缓存,那么由于表达对象的所有表面都会得到绘制,那么一般不会出现“绘制结果错误”的那种现象。但还是存在2个主要问题。
第一个问题是绘制效果仍然是位置相关的,即当从不同角度观察时,效果并不一致。如图所示,可以看出有些部分背景透过更多,有些部分更少。可以看出下半部分面绘制2遍,自然看起来透明程度低。
在这里插入图片描述
而如果物体的截面呈现下图所示形状,那么绘制结果显然更加多样。如视点处于位置A时,物体所呈现出来的透明程度是一样的。而当视点处于位置B时,一侧的透明度会与另一侧的透明度显著不同。但是需要强调的是,这种效果并非“绘制结果错误”,相反这个结果到底是好是坏本身就是见仁见智的问题,因为也可以认为这样的不同透明度可以凸显对象形状,更为合理。
在这里插入图片描述
第二个问题是,有时希望透过表面的网格线增加形状的立体感,如下图所示。此时如果不写入Z缓存,将导致所有网格线被绘制,显得过于凌乱。显然,此时的网格线达不到增加立体感的效果。
在这里插入图片描述
(2)写入Z缓存
Z缓存是为了消隐,但是透明物体启用Z缓存存在的一个突出问题是绘制顺序产生的。
如果是简单形状的闭合形状,如球体等。在启用Z缓存的基础上,启动背面裁剪CULL_FACE,那么通过控制几何图元的时针方向,完全可以确保只有朝向视点的面得到绘制,即透明对象是稳定的、一致的,与视点位置无关,且无论在哪个角度观察,透明效果一致。
而对于复杂一些的形状或者不闭合的形状,启动背面裁剪、控制几何图元时针方向,仍然存在较大问题。
在这里插入图片描述
如图所示,为一较为复杂的几何形状,我们希望在任何情况下都得到类似于上图的稳定效果:只显示朝向视点的一侧。
然而,由于形状本身的复杂,随着视点改变,却会出现类似下图的情况。可以明显看出,出现了2个条状区,与周边颜色明显不同。
在这里插入图片描述
这种现象,其原因可以通过下图来解释。图中是一个不封闭的圆锥形状,采用三角形扇来绘制,其顺序是ABCA。当视点在E处时,AB段的三角形都已经绘制了,那么在绘制BC段三角形的时候,虽然其Z值小于AB段,但由于是透明的,部分区域绘制2次。而当绘制到CA段三角形扇的时候,由于Z缓冲已经有值,所以不会被重复绘制。即如图中斜线区域将会明显区别于其他部分,这与上图中2个条带是相同原因。
在这里插入图片描述
以上现象在视点和几何体位置改变时,尤其是交互操作时会频繁发生,给人的印象就是绘制错了,这就是前述“绘制结果错误”问题。
概括起来就是:由于透明特性导致Z缓存并不能成功实现透明对象自身的消隐,相反却依赖于绘制顺序而导致显示结果的不稳定,甚至给观察者以“绘制错误”的感受。
关于上述问题的解决,在博文《基于osgEarth的卫星瞬时覆盖绘制方法》中已经做了阐述,一个是根据视点和物体的位置关系动态调整三角形扇(或其他primitiveSet)的绘制顺序,二是在Fragment Shader中根据片元对应的几何坐标和视点位置,实时计算是否被本身遮挡。这两种方法的局限性在于需要专门编写处理代码,且只能处理较为简单的几何形状,对于相对复杂的形状,无论是动态调整绘制顺序,还是实时遮挡计算,都很难解决。


二、基于深度纹理的透明对象稳定绘制方法与实现

(1)原理
因此,提出了一种基于深度纹理的透明对象稳定绘制方法。
①设置一个单独的camera,根据当前viewport,构造深度纹理,并作为camera的绘制目标;
②绘制前,以当前场景的camera设置camera参数,进行一次渲染(设置Polygon Offset),得到深度纹理,这样深度纹理中存储要处理的透明对象的深度值;
③定义Fragment Shader,进行深度比较,深度值大于纹理中对应值,discard。

(2)核心代码
基于OSG实现,由Geode派生,将组成Geometry加入后,即支持稳定绘制,通用性较好。
类定义如下:

class TransparentGeomRenderWithDepthTexture : public osg::Geode{
public:
	TransparentGeomRenderWithDepthTexture(std::vector<osg::ref_ptr<osg::Geometry>>& geometrys, osg::Vec2i viewportSize);
	virtual void traverse(osg::NodeVisitor& nv);
	virtual void cull(osgUtil::CullVisitor& cv);
public:
	class DrawCallbackForGeometrys;
	class CameraCullCallback;
	static const char fragmentShaderSource_noBaseTexture[];
private:
	void	init();
	void	setGeometrys(std::vector<osg::ref_ptr<osg::Geometry>>& geometrys);
	std::vector<osg::ref_ptr<osg::Geometry>>			_geometrys;
	std::vector<osg::ref_ptr<DrawCallbackForGeometrys>>	_geometrysCallBack;	
	osg::ref_ptr<osg::Camera>      	_camera ;
	osg::ref_ptr<osg::Texture2D>    _textureDepth ;
	osg::ref_ptr<osg::StateSet>     	_statesetForProgram;
	osg::ref_ptr<osg::Program>      _program;
	osg::ref_ptr<osg::Shader>		_fragmentShader;
	unsigned int                  _depthTextureUnit;
	osg::Vec2i                    _textureSize;};

成员变量主要包括:_camera,用于绘制深度纹理的相机;_textureDepth ,深度纹理,作为渲染目标;_statesetForProgram ,为用于绘制的shader提供的stateset;_geometrys,是构成透明物体的几何体。另外还有2个回调类,后续介绍,其他的不再解释。
首先是初始化函数,如下。分别完成了_textureDepth、_camera和_statesetForProgram的初始化。

void	TransparentGeomRenderWithDepthTexture::init(){
	_textureDepth = new osg::Texture2D;
	_textureDepth->setInternalFormat(GL_DEPTH_COMPONENT);
	_textureDepth->setTextureSize(_textureSize.x(), _textureSize.y());
	_textureDepth->setShadowTextureMode(osg::Texture2D::LUMINANCE);
	_textureDepth->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR);
	_textureDepth->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR);
	_textureDepth->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::CLAMP_TO_BORDER);
	_textureDepth->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::CLAMP_TO_BORDER);
	_textureDepth->setBorderColor(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
	_textureDepth->setShadowComparison(true);
	_textureDepth->setShadowAmbient(0.0); // The r value if the test fails
	_textureDepth->setShadowCompareFunc(osg::Texture::LEQUAL);

	_camera = new osg::Camera;
	_camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT);
	_camera->setCullCallback(new CameraCullCallback(this));
	_camera->setClearMask(GL_DEPTH_BUFFER_BIT);
	_camera->setClearColor(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
	_camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR);
	_camera->setRenderOrder(osg::Camera::PRE_RENDER);
	_camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
	osg::ref_ptr<osg::PolygonOffset> polygon_offset = new osg::PolygonOffset;
	polygon_offset->setFactor(10);
	polygon_offset->setUnits(10);
	_camera->getOrCreateStateSet()->setAttribute(polygon_offset.get(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
	_camera->getOrCreateStateSet()->setMode(GL_POLYGON_OFFSET_FILL, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
	_camera->setViewport(0, 0, _textureSize.x(), _textureSize.y());
	_camera->attach(osg::Camera::DEPTH_BUFFER, _textureDepth.get());

	_statesetForProgram = new osg::StateSet;
	_statesetForProgram->setTextureAttributeAndModes(_depthTextureUnit, _textureDepth.get(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
	_program = new osg::Program;
	_fragmentShader = new osg::Shader(osg::Shader::FRAGMENT, fragmentShaderSource_noBaseTexture);
	_program->addShader(_fragmentShader);
	osg::Uniform* depthTextureSampler = new osg::Uniform("depthTextureSampler", (int)_depthTextureUnit);
	_statesetForProgram->addUniform(depthTextureSampler);
	osg::Uniform* invWidth = new osg::Uniform("invWidth", (float)1.0 / _textureSize.x());
	_statesetForProgram->addUniform(invWidth);
	osg::Uniform* invHeight = new osg::Uniform("invHeight", (float)1.0 / _textureSize.y());
	_statesetForProgram->addUniform(invHeight);
	_statesetForProgram->setAttribute(_program.get());}

在_textureDepth部分,最后3行设置了纹理采样时进行比较以及比较的方法、比较失败的结果,这决定了shader中的纹理采样方式。
在_camera部分,设置了渲染顺序为osg::Camera::PRE_RENDER,这会令其在场景绘制前渲染当前几何体;其他的还包括PolygonOffset设置、attach等。此外,还需要设置一个回调CameraCullCallback(this),后续再描述。
在_statesetForProgram部分,主要是shader及uniform变量的设置。
初始化之后,要如何才能让这些其作用呢?需要重载traverse(osg::NodeVisitor& nv)函数,在该函数中,当参数的类型为osg::NodeVisitor::CULL_VISITOR时,进行处理,其他情况直接调用geode。CULL_VISITOR的处理代码如下

void TransparentGeomRenderWithDepthTexture::cull(osgUtil::CullVisitor& cv){
	osgUtil::RenderStage* orig_rs = cv.getRenderStage();
	osg::Viewport* vp = orig_rs->getCamera()->getViewport();
	if (_textureSize.x() != vp->width() ||_textureSize.y() != vp->height())	{
		_textureSize = osg::Vec2i(vp->width(), vp->height()) ;
		init();	}

	for (std::vector<osg::ref_ptr<DrawCallbackForGeometrys>>::iterator it = _geometrysCallBack.begin();
		it != _geometrysCallBack.end(); ++it)
		(*it)->_cameraMaster = cv.getCurrentCamera();

	cv.pushStateSet(_statesetForProgram.get());
	osg::Geode::traverse(cv);
	cv.popStateSet();

	_camera->setViewMatrix(cv.getCurrentCamera()->getViewMatrix());
	_camera->setProjectionMatrix(cv.getCurrentCamera()->getProjectionMatrix());
	_camera->accept(cv);}

上述代码中
第一步判断场景的viewport是否改变,如果改变需要重新初始化,这只有当窗口大小改变时才会发生;
第二步设置了DrawCallbackForGeometrys的一个参数;
第三步利用_statesetForProgram遍历场景构造状态树,即使用了shader;
第四步是通过_camera->accept(cv)来实现利用_camera渲染到深度纹理,之前需要设置_camera的变换矩阵。
Shader的代码如下

const char TransparentGeomRenderWithDepthTexture::fragmentShaderSource_noBaseTexture[] =
"uniform sampler2DShadow depthTextureSampler; \n"
"uniform float invWidth;\n"  // 1.0/width 
"uniform float invHeight;\n" // 1.0/height 
"\n"
"void main(void) \n"
"{ \n"
"    vec3 r0 = vec3(gl_FragCoord.x*invWidth,\n"
"                   gl_FragCoord.y*invHeight,\n"
"                   gl_FragCoord.z);\n"
" if( shadow2D( depthTextureSampler, r0 ).r == 0.0 )\n"
"	discard ;\n"
" gl_FragColor = gl_Color ;\n"
"}\n";

由于前述的纹理设置(setShadow…),因此此处纹理对象类型为sampler2DShadow,纹理采样函数使用shadow2D。如果不进行shadow设置,采用sampler2D类型和texture2D采样函数也可以,但需要在shader中自行进行比较。
下面阐述涉及到的2个回调类。

class TransparentGeomRenderWithDepthTexture::CameraCullCallback : public osg::NodeCallback{
public:
	CameraCullCallback(TransparentGeomRenderWithDepthTexture* tg) :
		_transparentGeomRender(tg) {};
	virtual void operator()(osg::Node*, osg::NodeVisitor* nv)	{
		osgUtil::CullVisitor* cv = nv->asCullVisitor();
		if (_transparentGeomRender)
			_transparentGeomRender->osg::Geode::traverse(*nv);	}
protected:
	TransparentGeomRenderWithDepthTexture* _transparentGeomRender;};

以上是camera的cullcallback,支持调用camera的accept时,进行本类的Geode::traverse,实现单独一遍绘制。

void TransparentGeomRenderWithDepthTexture::cull(osgUtil::CullVisitor& cv){
class TransparentGeomRenderWithDepthTexture::DrawCallbackForGeometrys :public osg::Drawable::DrawCallback{
public:
	DrawCallbackForGeometrys(){
		_refProjectionMatrix = new osg::RefMatrixd();	}
	virtual void drawImplementation(osg::RenderInfo & ri, const osg::Drawable* drawable) const{
		ri.getState()->applyModelViewMatrix(_cameraMaster->getViewMatrix());
		refProjectionMatrix->set(_cameraMaster->getProjectionMatrix().ptr());
		ri.getState()->applyProjectionMatrix(refProjectionMatrix.get());
		drawable->drawImplementation(ri);}
	osg::Camera* _cameraMaster;
	osg::ref_ptr<osg::RefMatrixd> _refProjectionMatrix;};

以上类是一个osg::Drawable::DrawCallback类型的回调类,这个也是本方法的关键之一。
如果不使用此回调,直接进行绘制,在静态情况下,绘制完全正常,也只是绘制未被自身遮挡的部分。
但有一个致命的问题,当进行视图操作,如快速放缩场景、转动地球,视点与目标位置关系发生变化时,显示极其错误,出现大块的缺口。当目标处于场景中央时,持续放大场景稳定出现错误,持续缩小场景错误并不出现。
这个现象的原因与OSG的机制有关。上述的cull函数在frame中构造状态树的过程中被调用,但是场景的绘制是用的另外一个独立的线程。于是就会出现生成深度缓存时所用的变换矩阵(cull函数最后3行)与实际绘制所用变换矩阵不同,生成的深度纹理数据不正确,导致最终显示的错误。
而对于每个geometry,设置上述callback,在绘制前再实际得到当前的变换矩阵值,保证2次绘制所用变换矩阵的一致。对于所有geometry,需要setUseDisplayList(false),否则上述回调不会每次被调用,同样出现错误。


三、绘制结果、局限性与源码下载地址

上述类源代码下载地址为:传送门
类的使用方法如下

osg::ref_ptr<osg::Geometry> geometry = generateGeometry();
std::vector<osg::ref_ptr<osg::Geometry>> geoms;
geoms.push_back(geometry);
osg::Vec2i viewportSize = osg::Vec2i();
osg::ref_ptr<TransparentGeomRenderWithDepthTexture>	_detectShow3DbyDepthTexture = new TransparentGeomRenderWithDepthTexture(geoms, viewportSize);

当然也可以加入多个geometry。
下图为使用本方法后绘制的结果,原本的2个条带不再出现,绘制稳定,无错绘感。
在这里插入图片描述
本方法通用性强、使用简便,适于各种复杂形状。缺点有2:一是每个对象需要使用深度纹理、camera、stateset,占用了较大资源,显然不适于场景中有大量透明对象的场景;二是需要禁止显示列表,性能会有所降低。
对于存在大量透明对象的场景,可能有时还不得不采用实时计算遮挡关系的shader或修改primitive顺序的方法。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值