基于深度纹理的复杂透明对象稳定绘制方法(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顺序的方法。