文章目录
- 一、场景的观察与变换
- 二、图形设备接口GraphicsContext(载体)
- 1.图形设备接口结构
- 2.窗口、像素缓存Pixel Buffer、G-Buffer(GC可以设置成其中之一)
一、场景的观察与变换
计算机图形学旨在生成三维物体的二维图像,因此主要工作之一就是在三维空间中进行想象并创建模型,适当地变换它们的位置和姿态,最终将他们转换为屏幕上的像素。
一般假设观察者用一台相机来观察,观察的结果除了显示在屏幕上,也可以保存到其他地方(如一副纹理图片中)
1.OpenGL中的变换
三维世界与屏幕像素的变换包括多种类型的操作,用相机拍照类比,大概有如下几个步骤:
- 模型视点变换(Mode-View Matrix)
调整要拍摄的物体的位置、姿态,称为模型变换;
调整相机使其对准景物的过程,改变相机位置或者拍摄角度,称为视点变换;其中可以将该变换的行为看作模型变换的逆操作(相机矩阵是根据改变视点位置创建,但是实际的级联效果是对物体做反向模型变换)。
实用库函数gluLookAt()是一个快速的视点变换函数,它使用三个参数:相机/视点位置eye,参考点位置center,视点的上方向up来表达相机的位置和拍摄角度。
经过模型视点变换之后,我们可认为场景从世界坐标系WCS转换到了相机坐标系VCS。
VCS,分别用n,u,v表示坐标基向量,按照下面规则创建viewMatrix:
n使从center指向eye的向量,相当于世界坐标系的Z轴;eye一般都是在z轴方向上。
u是向上的分量up和n的叉乘,为X轴;
v是n和u的叉乘,为Y轴。
- 投影变换(视景体)
立方体视景体通常使用glOrtho函数创建,成为正交投影;
棱台形视景体,通常用glPerspective或glFrustum,成为透视投影,近大远小。
投影变换之后,转换到投影坐标系CCS(Clipping Coordinate System),视景体之外的对象都会被裁剪掉。
再通过裁切变换,即视景体裁切以及使用glClipPlane函数实现的结果,得到的向量通常已经变换到标准化设备坐标系NDCS。
NDCS下所有坐标都被映射到了-1到1之间。
需要注意的是,near是指视点到近平面的距离,必须为正数,在near后面到far中间的点才渲染。
- 视口变换
将投影变换得到的结果反映到指定的屏幕窗口上。如果照片的大小与底片不相符,那么将可能产生成像的放大或者缩小,以及拉伸变形效果。通常用glViewPort指定视口变换的结果区域。
此时变换到窗口坐标系DCS(Device)/WCS(Window)。
X/Y方向取值在(0,0)~(width,height);Z取值在[0,1],也就是OpenGL中深度缓存值的含义。
2.相机节点osg::Camera :osg::Tranform
- OpenGL中模型视点矩阵的概念,在OSG表达为模型变换矩阵和视点变换矩阵两者的乘积,还辅助构建观察的投影矩阵和窗口矩阵,并合并为MVPW矩阵,实现三维场景向二维平面的映射。
- 模型变换矩阵:
对于一个OSG场景中,通过对某一对象子节点树中多个Transform节点的级联,可以自由变换这个对象各个组成部分在场景中的实时位置和姿态(即模型变换的概念),更准确地说,是将某个在局部对象坐标系OCS中定义的对象(一般是组成部分),通过一个或者多个具有父子关系的Transform节点变换到世界坐标系中。
Vwcs = Vocs*Mmodel;
可以参考osg::MatrixTransform::computeLocalToWorldMatrix()。
-
视点变换矩阵:
Vvcs= Vwcs*Mview
-
MVPW矩阵:
Vdcs = Vocs*Mmodel*Mview*Mprojection*Mwindow
-
可以看出camera和其他空间变换节点并没有本质区别,这里的相机观察矩阵也可以视为普通的变换矩阵:
-
更多接口
// 设置相机的渲染目标RenderTargetImplementation ,可以选择:
//FRAME_BUFFER_OBJECT
//PIXEL_BUFFER_RTT
//PIXEL_BUFFER
//FRAME_BUFFER
//SEPARATE_WINDOW
void setRenderTargetImplementation (RenderTargetImplementation impl);
// 将相机与一个纹理相关联,如果该相机用于RTT实现,那么它所观察的子场景将渲染到这个纹理对象中。
void attach (BufferComponent buffer, osg::Texture *texture, unsigned int level=0, unsigned int face=0, bool mipMapGeneration=false, unsigned int multisampleSamples=0, unsigned int multisampleColorSamples=0);
// 同上
void attach (BufferComponent buffer, osg::Image *image, unsigned int multisampleSamples=0, unsigned int multisampleColorSamples=0);
//BufferComponent :
DEPTH_BUFFER
STENCIL_BUFFER
PACKED_DEPTH_STENCIL_BUFFER
COLOR_BUFFER
COLOR_BUFFER0
COLOR_BUFFER1 。。。。
// 设置相机剔除掩码,某个节点使用setNodeMask,相机使用该方法,在场景遍历时,访问器会将相机剔除掩码和节点剔除掩码按位&运算,如果不为0,才显示节点。
void setCullMask (osg::Node::NodeMask nm);
// 动态修改相机远近裁剪面
// 参考:https://www.cnblogs.com/mazhenyu/p/7054087.html
void setClampProjectionMatrixCallback (ClampProjectionMatrixCallback *cpmc);
- 并且Camera类保存了大量相机设置参数,例如视口、投影矩阵、背景颜色等,则在场景的渲染过程中被取出和加以使用。
- 使用Camera节点和别的节点一样,例如可以将一个节点子树通过addChild传递给它。如果有一些未作设置的重要参数,默认继承上一个父节点相机的属性,并且没有设置Trackball或其他漫游器的话,系统会自动按照“囊括场景到视野中”的原则重新设置摄像机的观察矩阵,那么视点就会改变!
// 一下为继承父相机时使用camera
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setClearColor(osg::Vec4(1.0,1.0,1.0,1.0));
camera->setViewport(100,100,800,600);
camera->addChild(subgraph);
实例:创建一个相机节点,使用自定义view+透视投影——ABSOLUTE_RF
设置矩阵的具体方法建议参考这里
int main
{
osg::Node* cow = osgDB::readNodeFile("cow.osg");
osg::Node* cow1 = osgDB::readNodeFile("cow.osg");
// 创建一个相机
osg:ref_ptr<osg::Camera> camera = new osg::Camera;
camera ->setClearColor(osg::Vec4(1,1,1,1));
camera ->setViewport(0,0,800,600);//屏幕左下角,宽800,高600的视口
camera ->setReferenceFrame(osg:Transfrom::ABSOULUTE_RF);
osg::Vec3d up(0.0,1.0,0.0);
osg::Vec3d viewDirection(0.0,0.0,6.0);
osg::Vec3d center = cow->getBound().center();
osg::Vec3d eye = center+viewDirection;
// z轴方向为center指向eye,即此时视点在沿着z轴正方向上(参考osg的z轴,此时视点相当于在正上方,从上往下看)
camera->setViewMatrixAsLookAt(eye,center,up);
// 设置一个变换节点当作模型矩阵,会因为绝对下的相机矩阵和osg变换结果有所不同,因为视点不同了
osg::PosiotionAttitudeTransform* pat = new osg::PosiotionAttitudeTransform;
pat->setPosition(osg::Vec3(0,0,-4));// 此时是牛远离我们,沿着z轴负方向移动,即原理视点
pat->addChilde(cow);
camera->addChild(pat);
double r = cow->getBound().radius();
// 此时近平面和视点的距离为6-5.999999
camera->setProjectionMatrixAsFrustum(-0.8*r,0.8*r,-0.6*r,0.6*r,5.999999,9999);
osg::Group* g = new osg::Group;
g->addChild(camera);
g->addChild(cow1);
osgViewer::Viewer viewer;
viewer.setSceneData(g);
return viewer.run();
}
- 多个Camera节点允许共享同一颗节点子树。两台相机拍摄结果可以显示在不同窗口,也可以在同一个窗口中进行叠加(参考后续视景器部分)。
与普通的场景节点最大的不同在于,它是场景裁减和渲染过程的一个重要载体(普通场景节点只是一个用于保存场景数据机构的辅助工具)。
一个相机中的所有节点都会被渲染到这个相机对用的图形设备Graphics Context中。相机的设置参数也决定了它所采用的场景裁剪方法以及子节点是否会被剔除。
实例:鸟瞰图(正交投影,ABSOLUTE_RF)
osg::Camera* createBirdsEye(const osg::BoundingSphere& bs)
{
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
double viewDistance = 2.0 * bs.radius();
double znear = viewDistance - bs.radius();
double zfar = viewDistance + bs.radius();
float top = bs.radius();
float right = bs.radius();
camera->setProjectionMatrixAsOrtho(-right, right, -top, top, znear, zfar);
osg::Vec3d upDirection(0.0, 1.0, 0.0);
osg::Vec3d viewDirection(0.0, 0.0, 1.0);
osg::Vec3d center = bs.center();
osg::Vec3d eyePoint = center + viewDirection * viewDistance;
camera->setViewMatrixAsLookAt(eyePoint, center, upDirection);
return camera.release();
}
int main()
{
osg::Node* model = osgDB::readNodeFile("cow.osg");
osg::Camera* camera = createBirdsEye(model->getBound());
camera->addChild(model);
osgViewer::Viewer viewer;
viewer.setSceneData(camera);
return viewer.run();
}
二、图形设备接口GraphicsContext(载体)
osg::Camera::setGraphicsContext就是设置相机对应的图形设备对象,是任意图形子系统的抽象层接口,提供了统一的图形设备操作函数,用于实现渲染结果与底层设备的交互,与平台无关。
1.图形设备接口结构
- GraphicsContext主要工作是提供场景渲染结果的载体,这个载体可以是显示缓存,进而绘制到一个图形窗口中,也可以是其他特殊的缓存对象(例如作为像素缓存设备PBuffer),从而实现复杂的渲染和图像多次曝光等功能。
- 不能用new创建,需要使用
createContext()
,自动根据当前用户环境和特性参数traits,构建一个平台相关的图形设备对象:
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits);
osg::GraphicsContext::Traits特性参数
x,y,width heizht,// 窗回的坐标、高度及宽度,默认值都为 0
windowDecoration(fa1se) // 是否支持窗口扩展的功能,Win32中style
supportsResize(true),// 是否支持窗回缩放。
red(8),// 红色位散,默认8位
bluc(8),// 蓝色位数,默认8位
green(8),// 绿色位数,默认8位
alpha(0), //透明度,默认没有 alpha通道,为 RGB 格式
depth(24),//颜色的深度(16,24.32),默认使用 24 位
stencil(0),//模板默认天
sampleBuffer(0),//采样缓存,默认无
samples(0),//采样倍数(抗锯齿的倍数),默认无
pbuffer(false),
quadBufferStereo(flse),//立体四缓存,主要在高端显卡上有,如 OUDRO 显卡上
doublcBuffer(false), //是否支持双缓存,默认不支持
target(0),//目标
format(0),//格式
level(0)//嵌套的层数,默认无,
facc(0),
mipMapGeneration(tse),//是否支持生成 Mipmap,默认不支持
vsync(tue),//是否支持同步,默认同步模式
useMultiThreadedOpenGLEngine(fnlse),//是否采用多线程,默认不支持
useCursor(tue),//是否使用鼠标的指针,默认使用
sharedContext(0),//共享上下文
setlnheritedWindowPixelForst(false)//是否继承 Window中的位格:
包含了一系列可能影响图形设备创建的属性参数。
创建一个图形设备
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
traits->x=0;
traits->y=0;
traits->windowDecoration = true;
traits->pbuffer= false;
traits->doubleBuffer = true;
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits);
2.窗口、像素缓存Pixel Buffer、G-Buffer(GC可以设置成其中之一)
窗口osgViewer::GraphicsWindow :osg::GraphcisContext (补
系统设备接口WindowingSystemInterface(实景器中中全局唯一)
通过WindowingSystemInterface类得到系统窗口接口,该系统接口主要是为了关联窗口系统。
int main()
{
// 需要通过编译一个osg类实例,来连带编译osg的系统、上下文等
osg::ref_pt<osgViewer::Viewer> viewer = new osgViewer::Viewer;
// WindowingSystemInterface *是在osg中全局唯一,是系统设备接口
osg::GraphicsContext:: WindowingSystemInterface *ws=osg::GraphicsContext::getWindowingSystemInterface();
// 图形环境/上下文特性
osg::GraphicsContext::Traits traits;
unsigned int height,width;// 系统分辨率
//
if(ws)
{
ws->getScreenResolution(traits,width,height);
//ws->setScreenResolution(traits,width,height)可以改变系统设备设置。
// 边框
std::count<<traits.windowDecoration<<std:endl;
}
return 0;
}
窗口显示
其中,如果在环境变量中添加OSG_WINDOW,则会在自动创建上下文时,创建窗口而不是全屏。
方法一:只需要设置示例中的图形环境特性osg::GraphicsContext::Traits的x,y(屏幕左上角坐标),width,height(窗口宽高)即可。
方法二:osg::Viewer::Windows
osg::Viewer::Windows ws;
viewer->getWindows(ws);
if(!ws.empty())
{
osgViewer::Viewer::Windows::iterator it = ws.begin();
for(;it!=ws.end();it++)
{
(*it)->setWindowRectangle(50,50,800,600);
(*it)->setWindowDecoration(false);
(*it)->SetCursor(osgViewer::GraphicsWindow::WaitCursor);
}
}
像素缓存Pixel Buffer——渲染到纹理RTT方法之一
主要用于实现离屏渲染,以及渲染到纹理图片(又称纹理烘焙RTT)。
PBuffer机制将本来渲染到显示缓存的场景数据换向输出到一处用户缓存中,进而可以将渲染数据绑定到纹理图片,甚至直接取出进行处理。
RTT也可以通过使用着色器进行逐顶点或逐像素的数学运算,通过glReadPixels等函数将渲染到纹理结果重新取出,并加以储存和重新运算的过程,属于通用GPU计算的范畴,离不开拷贝。
- 像素缓存PBuffer实际上可以理解为一个建立在已有窗口上的一个虚拟窗口设备,同样需要创建一个窗口句柄,并分配设备环境,关联渲染环境,该PBuffer就可以和窗口一样操作了。
- 当将前文的Traits::pbuffer参数设置为真时,系统会根据当前系统平台加载相应的PBuffer设备,
RTT主要两个作用:离屏渲染之后的后期处理;实现多种不同场景的融合显示。
PBuffer只是实现RTT手段之一,还有直接复制帧缓存中的像素、使用像素缓设备(就是PBuffer)、以及使用帧缓存对象。
-
由此可见PBuffer省却了复制的过程,而是直接将子场景渲染到与之绑定的纹理(将纹理绑定到camera即可)中。但是需要自己的渲染环境RC,要根据一个实际窗口的设备环境DC才能创建,且对多个PB切换和管理十分困难。
-
用帧缓存FBO可以解决以上问题。保存了OpenGL渲染管线最终得到的像素数据,可以输出到窗口系统(显示缓存);也可以输出到一个虚拟窗口设备,进而绑定到纹理对象(PBuffer概念);也可以定义一种不参与显示的FBO,当FBO与一个纹理对象绑定,它实现就是RTT。
-
FBO支持多达16个绑定通道,例如可以绑定结果的多个颜色缓存值、深度缓存值以及模板缓存值到纹理或者自定义空间上;易于管理,切换迅速。还具有平台无关性(PBuffer与平台有关!)。
FBO是什么
osg::Camera提供FBO\PBuffer、读取纹理三种RTT方式接口
// 设置相机的渲染目标RenderTargetImplementation ,可以选择:
//FRAME_BUFFER_OBJECT
//PIXEL_BUFFER_RTT
//PIXEL_BUFFER
//FRAME_BUFFER
//SEPARATE_WINDOW
void setRenderTargetImplementation (RenderTargetImplementation impl);
// 将相机与一个纹理相关联,如果该相机用于RTT实现,那么它所观察的子场景将渲染到这个纹理对象中。
void attach (BufferComponent buffer, osg::Texture *texture, unsigned int level=0, unsigned int face=0, bool mipMapGeneration=false, unsigned int multisampleSamples=0, unsigned int multisampleColorSamples=0);
// 同上
void attach (BufferComponent buffer, osg::Image *image, unsigned int multisampleSamples=0, unsigned int multisampleColorSamples=0);
//BufferComponent :
DEPTH_BUFFER
STENCIL_BUFFER
PACKED_DEPTH_STENCIL_BUFFER
COLOR_BUFFER
COLOR_BUFFER0
COLOR_BUFFER1 。。。。
- 其中BufferComponent决定了何种缓存数据将被渲染到关联的纹理/图像中,包括深度缓存、模板缓存和多个颜色缓存通道。
- 颜色缓存通道就是OpenGL渲染管线输出的最终像素结果,使用片元着色器可以同时输出不止一个颜色缓存通道的数据,被称为多重渲染目标MRT。
实例:使用FBO将子场景渲染到纹理,再映射到主场景的一个方片(ABSOLUTE_RF)
osg::Texture* createRttTexture( int texWidth, int texHeight )
{
osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
texture->setInternalFormat( GL_RGBA );
texture->setFilter( osg::Texture::MIN_FILTER, osg::Texture::LINEAR );
texture->setFilter( osg::Texture::MAG_FILTER, osg::Texture::LINEAR );
texture->setTextureSize( texWidth, texHeight );
return texture.release();
}
osg::Camera* createRttCamera(int texWid,int texHeigh,const osg::BoundingSphere& bs)
{
osg::ref_ptr<osg::Camera> rttCamera = new osg::Camera;
rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 不受主相机影响的,如果是RELATIVE_RF,不应用下面方式创建VP矩阵,而是参考本文关于RELATIVE_RF的案例
rttCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
rttCamera->setViewport(0,0,texWid,texHeigh);
rttCamera->setRenderOrder(osg::Camera::PRE_RENDER);
rttCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT);
double viewDistance = 2.0*bs.radius();
double znear = viewDistance -bs.radius();
double zfar = viewDistance+bs.radius();
float top = 0.6*znear;
float right = 0.8*znear;
rttCamera->setProjectionMatrixAsFrustum(-right,right,-top,top,znear,zfar);
osg::Vec3d up(0.0,0.0,1.0);
osg::Vec3d viewDirection(0.0,-1.0,.0);
osg::Vec3d center = bs.center();
osg::Vec3d eye = center+viewDirection*viewDistance;
rttCamera->setViewMatrixAsLookAt(eye ,center ,up);
return rttCamera.release();
}
int main()
{
osg::Node* axes= osgDB::readNodeFile("axes.osg");
osg::Node* model = osgDB::readNodeFile("cow.osg");
// 不收光线影响,不然太暗看不清
model->getOrCreateStateSet()->setMode(GL_LIGHTING,osg::StateAttribute::OFF);
// 创建rttCamera(如果attach纹理,这该节点下面的节点将指定缓存数据,渲染到纹理上)
// 其实是先渲染到图形设备GC,再通过PBuffer或者FBO渲染到纹理上
int texWidth=512,texHeight=512;//可以用这个值直接设置,也可以按照下面用系统分辨率,主要是纹理size和相机viewport最好一样,好算纹理坐标
unsigned int screenWidth, screenHeight;
osg::GraphicsContext::WindowingSystemInterface * wsInterface = osg::GraphicsContext::getWindowingSystemInterface();
if (!wsInterface)
{
return -1;
}
wsInterface->getScreenResolution(osg::GraphicsContext::ScreenIdentifier(0), screenWidth, screenHeight);
texWidth = screenWidth;
texHeight = screenHeight;
osg::Camera* rttCamera = createRttCamera(texWidth,texHeight,model->getBound());
// 创建纹理
osg::Texture* rttTex = createRttTexture(texWidth,texHeight);
// 创建一个用于映射纹理的方面。
osg::ref_ptr<osg::Geode> quad = new osg::Geode;
quad->addDrawable(osg::createTexturedQuadGeometry(osg::Vec3(0,0,0),osg::Vec3(1,0,0),osg::Vec3(0,0,1)));
// 附加纹理和附加渲染内容
rttCamera->addChild(model );
rttCamera->attach(osg::Camera::COLOR_BUFFER,rttTex );// 将像素渲染到纹理上
// 通过将纹理设置给drawable quad,间接将纹理渲染到场景中
quad->getOrCreateStateSet()->setTextureAttributeAndModes(0,rttTex);
osg::ref_ptr<osg::Group> root = new osg::Group;
root->addChild(quad.get());
root->addChild(rttCamera );
root->addChild(model);
root->addChild(axes);
// 不通过视景器加到场景中,永远都是数据,建议参看《最长的一帧》
osgViewer::Viewer viewer;
viewer.setSceneData(root.get());
viewer.run();
}
延迟渲染Deferred Rendering
-
前向渲染是一种最直接的光照计算渲染路径,我们在VS中对每个物体的顶点做一系列的变换(主要是将顶点位置和法线变换到裁剪空间),然后在FS中对每个像素进行光照计算,然后换算到framebuffer中,即正常的OpenGL渲染管线。
-
延迟渲染(Deferred rendering)或延迟着色(Deferred shading)是一种不同于传统前向渲染(Forward rendering)的技术,它的出现是为了解决前向渲染在多动态光源场景下效率过低的问题,所谓延迟渲染的意思就是将光照计算推迟到必要的stage来计算,延迟渲染一般会有好几个rendering stages:
- 主要用于例如,因为深度测试在片段着色器之后,而被遮挡的光源还是会通过FS进行计算,延迟渲染就可以减少这些不必要的光照计算开销。
-
先创一个geometry pass,利用管线的depth test,把看不见的片元先剔除掉,再利用MRT(片元着色器同时输出不止一个颜色缓存通道的数据,每个纹理需要相同的bit-depth)将片元的Normal,Diffuse color,Position等纹理写入到G-Buffer中(而不是传给FS进行光照计算)。
-
延迟渲染第二个阶段叫做Lighting Pass,我们会依次遍历G buffer中的每个像素,从不同的贴图中读取每个像素对应的属性值然后进行光照计算(直接采样G-Buffer中纹理)。由于在创建G buffer的时候,经过深度测试除了离相机最近的像素其他的全部被抛弃了,所以现在每个像素对应只会进行一次光照计算。
即将前向渲染中几何计算(pos,normal矩阵变换等)和光照计算分离来,也就是将渲染管线分为两个阶段(一般要两个pass,其中FS中并不是计算各种光,而是只输出我们需要的数据到G-Buffer,light pass阶段再用这些数据进行有选择的光照计算)。
- 如何逐像素遍历G-Buffer
- 渲染一个屏幕空间quad。
平行光时。
但是由于光源会衰减,参考点光源,只对有限区域有一定影响,所以我们希望很多像素能不受影响和某些光源无关,即当一个光源对一个像素的影响足够小的时候基于性能考虑我们可以直接忽略这个光源的影响。因此参考第二种方法:
- 对于点光源,我们可以计算围绕光源的球体的尺寸(对于聚光源我们则计算光锥体的尺寸),在球体之外我们则会忽略这个光源的影响,不进行光源的光照计算。
VS阶段只需要将position数据变换到裁剪空间而不用干别的。FS阶段只在受光源影响的的相关像素上执行并进行光照计算。
G-Buffer(Geometry Buffer)
其实就是一个FBO(一系列attachment的集合,通过camera->attach(各种BufferComponent ,texture/image/gl_renderbuffer)),而osg中我们可知一个图形设备GC,可以是窗口,也可以是P-Buffer像素设备,当然也可以是FBO,或者G-Buffer,只不过每个图形设备GC中设置renderTargetImplementation和attach设置不同。
即G buffer是一组按照我们设计格式存储的一组2D贴图,保存着顶点每个属性的数据,normal、depth、alpha、specular等等。
其中通过opengl设置FBO的源码可知BufferComponent ,在一个FBO中,一个纹理对应一个。
- 参考,是因为在MRT中,我们是通过
void glDrawBuffers( GLsizei n, const GLenum *bufs);
,定义了缓冲区数组,片段着色器数据的输出将写入其中用bufs指定的每个缓冲区中,即启用纹理的写入。
- G-Buffer一般需要3个color和一个depth 。
OpenGL——Defer Rendering实例:用于加深理解
实例:OpenGL创建G-Buffer+将G-Buffer的纹理复制到屏幕中不同象限+MRT着色器(不是真的执行延迟渲染)
class GBuffer
{
public:
enum GBUFFER_TEXTURE_TYPE {
GBUFFER_TEXTURE_TYPE_POSITION,
GBUFFER_TEXTURE_TYPE_DIFFUSE,
GBUFFER_TEXTURE_TYPE_NORMAL,
GBUFFER_TEXTURE_TYPE_TEXCOORD,
GBUFFER_NUM_TEXTURES
};
GBuffer();
~GBuffer();
bool Init(unsigned int WindowWidth, unsigned int WindowHeight);
void BindForWriting();
void BindForReading();
void SetReadBuffer(GBUFFER_TEXTURE_TYPE TextureType);
private:
GLuint m_fbo;
GLuint m_textures[GBUFFER_NUM_TEXTURES];
GLuint m_depthTexture;
};
// 创建G-Buffer,并绑定了4个GL_COLOR_ATTACHMENT0格式的附件,一个深度纹理附件(因为深度纹理是在初始化时明确的,所以需要不同格式)
bool GBuffer::Init(unsigned int WindowWidth, unsigned int WindowHeight)
{
// Create the FBO
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
// Create the gbuffer textures
glGenTextures(ARRAY_SIZE_IN_ELEMENTS(m_textures), m_textures);
glGenTextures(1, &m_depthTexture);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_textures) ; i++) {
glBindTexture(GL_TEXTURE_2D, m_textures[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, WindowWidth, WindowHeight, 0, GL_RGB, GL_FLOAT, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, m_textures[i], 0);
}
// depth
glBindTexture(GL_TEXTURE_2D, m_depthTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT,
NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_depthTexture, 0);
// 启用四个纹理的写入,深度纹理是在初始化的时候明确进行的(通过绑定帧缓冲区和glClear),不是在着色器运行过程中读取出来的像素。
// 这个数组具有一定程度的灵活性,因为如果我们将 GL_COLOR_ATTACHMENT6 作为第一个索引,那么当 FS 向第一个输出变量写入时,就会写入连接到 GL_COLOR_ATTACHMENT6 的纹理。
GLenum DrawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
glDrawBuffers(ARRAY_SIZE_IN_ELEMENTS(DrawBuffers), DrawBuffers);
// 确认完整性
GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (Status != GL_FRAMEBUFFER_COMPLETE) {
printf("FB error, status: 0x%x\n", Status);
return false;
}
// restore default FBO
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
return true;
}
void GBuffer::BindForWriting()
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
}
void GBuffer::BindForReading()
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo);
}
void GBuffer::SetReadBuffer(GBUFFER_TEXTURE_TYPE TextureType)
{
glReadBuffer(GL_COLOR_ATTACHMENT0 + TextureType);
}
virtual void RenderSceneCB()
{
CalcFPS();
m_scale += 0.05f;
m_pGameCamera->OnRender();
DSGeometryPass();
DSLightPass();
RenderFPS();
glutSwapBuffers();
}
void DSGeometryPass()
{
m_DSGeomPassTech.Enable();
// 绑定自定义FBO(G-Buffer)到GL_DRAW_FRAMEBUFFER,设置了GB缓冲对象的写入!
m_gbuffer.BindForWriting();
// 作用于当前FBO,即我们自定义G-Buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Pipeline p;
p.Scale(0.1f, 0.1f, 0.1f);
p.Rotate(0.0f, m_scale, 0.0f);
p.WorldPos(-0.8f, -1.0f, 12.0f);
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
m_DSGeomPassTech.SetWVP(p.GetWVPTrans());
m_DSGeomPassTech.SetWorldMatrix(p.GetWorldTrans());
m_mesh.Render();
}
void DSLightPass()
{
// 将源 FBO 绑定到 GL_READ_FRAMEBUFFER,将目标 FBO 绑定到 GL_DRAW_FRAMEBUFFER
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_gbuffer.BindForReading();
GLsizei HalfWidth = (GLsizei)(WINDOW_WIDTH / 2.0f);
GLsizei HalfHeight = (GLsizei)(WINDOW_HEIGHT / 2.0f);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_POSITION);
// 从一个 FBO 复制到另一个 FBO 的方法,这里是默认帧缓冲区被绑定到GL_DRAW_FRAMEBUFFER,所以是将G-Buffer复制到默认帧缓冲区中。
// 在 GL_COLOR_BUFFER_BIT 的情况下,GL_LINEAR 是唯一有效的选项。
// 下面是将四个纹理分别渲染到不同的象限中。
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
0, 0, HalfWidth, HalfHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_DIFFUSE);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
0, HalfHeight, HalfWidth, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_NORMAL);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
HalfWidth, HalfHeight, WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
m_gbuffer.SetReadBuffer(GBuffer::GBUFFER_TEXTURE_TYPE_TEXCOORD);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
HalfWidth, 0, WINDOW_WIDTH, HalfHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
{
#version 330
layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;
uniform mat4 gWVP;
uniform mat4 gWorld;
out vec2 TexCoord0;
out vec3 Normal0;
out vec3 WorldPos0;
void main()
{
gl_Position = gWVP * vec4(Position, 1.0);
TexCoord0 = TexCoord;
Normal0 = (gWorld * vec4(Normal, 0.0)).xyz;
WorldPos0 = (gWorld * vec4(Position, 1.0)).xyz;
}
}
{
#version 330
in vec2 TexCoord0;
in vec3 Normal0;
in vec3 WorldPos0;
layout (location = 0) out vec3 WorldPosOut;
layout (location = 1) out vec3 DiffuseOut;
layout (location = 2) out vec3 NormalOut;
layout (location = 3) out vec3 TexCoordOut;
uniform sampler2D gColorMap;
void main()
{
WorldPosOut = WorldPos0;
DiffuseOut = texture(gColorMap, TexCoord0).xyz;
NormalOut = normalize(Normal0);
TexCoordOut = vec3(TexCoord0, 0.0);
}
}
实例:OpenGL中实现Defer Rendering(点光源+定向光)
- 点光源衰弱系数公式:
参考 - 点光源中计算指定光源的边界框大小的公式:
virtual void RenderSceneCB()
{
CalcFPS();
m_scale += 0.05f;
m_pGameCamera->OnRender();
DSGeometryPass();
BeginLightPasses();
DSPointLightsPass();
DSDirectionalLightPass();
RenderFPS();
glutSwapBuffers();
}
void DSGeometryPass()
{
m_DSGeomPassTech.Enable();
// 启动写入自定义的G-Buffer
m_gbuffer.BindForWriting();
// Only the geometry pass updates the depth buffer
// 再light pass中每个屏幕像素只有一个纹素texel,没有任何内容需要写入深度缓冲区,因为在这里进行深度测试之后,在light pass已经没有竞争对手了
// 纹理是一个数组的概念,其中每一个数据(RGB颜色以及Alpha值,或系统及用户定义类型)称为一个纹素texel。
// 下面流程是允许写入,作用于自定义FBO/G-Buffer,启动深度测试(将会剔除掉看到不到顶点)
glDepthMask(GL_TRUE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
// 盒子
Pipeline p;
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
p.Rotate(0.0f, m_scale, 0.0f);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_boxPositions) ; i++) {
p.WorldPos(m_boxPositions[i]);
m_DSGeomPassTech.SetWVP(p.GetWVPTrans());
m_DSGeomPassTech.SetWorldMatrix(p.GetWorldTrans());
m_box.Render();
}
// When we get here the depth buffer is already populated and the stencil pass
// depends on it, but it does not write to it.
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);
}
void BeginLightPasses()
{
//在前向渲染中,我们将所有光源的结果累积到 FS 中,但现在每次 FS (下面两个)调用只处理一个光源。我们需要一种将光源累积在一起的方法。
//混合是一个简单的函数,它接收源颜色(FS 的输出)和目标颜色(来自帧缓冲),并对它们进行计算。
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE);
// 启动写入默认帧缓冲区,加上上面的设置,之后的光照计算的颜色结果都会按照上面规则叠加到默认帧缓冲区。
m_gbuffer.BindForReading();
glClear(GL_COLOR_BUFFER_BIT);
}
// 光照传递,其实就是计算出我们想要的施加光照效果的像素点会被渲染(还是渲染管线)
void DSPointLightsPass()
{
m_DSPointLightPassTech.Enable();
m_DSPointLightPassTech.SetEyeWorldPos(m_pGameCamera->GetPos());
Pipeline p;
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
// 光球,每个光源中心为光球圆心
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_pointLight); i++) {
m_DSPointLightPassTech.SetPointLight(m_pointLight[i]);
p.WorldPos(m_pointLight[i].Position);
float BSphereScale = CalcPointLightBSphere(m_pointLight[i]);
p.Scale(BSphereScale, BSphereScale, BSphereScale);
m_DSPointLightPassTech.SetWVP(p.GetWVPTrans());
m_bsphere.Render();
}
}
// 计算指定光源的边界框大小,是根据之前给出的公式直接实现的。
float CalcPointLightBSphere(const PointLight& Light)
{
float MaxChannel = fmax(fmax(Light.Color.x, Light.Color.y), Light.Color.z);
float ret = (-Light.Attenuation.Linear + sqrtf(Light.Attenuation.Linear * Light.Attenuation.Linear -
4 * Light.Attenuation.Exp * (Light.Attenuation.Exp - 256 * MaxChannel * Light.DiffuseIntensity)))
/
(2 * Light.Attenuation.Exp);
return ret;
}
// 定向光(只支持一种这类光源)只需要一个全屏四边形来覆盖所有像素,模型标准化设备坐标为(-1,-1)到(1,1),等视口变换之后
// 得到一个从 (0,0) 到 (SCREEN_WIDTH,SCREEN_HEIGHT) 的四边形。
void DSDirectionalLightPass()
{
m_DSDirLightPassTech.Enable();
m_DSDirLightPassTech.SetEyeWorldPos(m_pGameCamera->GetPos());
Matrix4f WVP;
WVP.InitIdentity();
m_DSDirLightPassTech.SetWVP(WVP);
m_quad.Render();
}
/// VS着色器
{
#version 330
layout (location = 0) in vec3 Position;
uniform mat4 gWVP;
void main()
{
gl_Position = gWVP * vec4(Position, 1.0);
}
}
// 纹理大小都是屏幕大小,我们需要根据像素在屏幕上的位置从G缓冲区中采样。
// gl_FragCoord,这正是我们所需要的。它是一个 4D 向量,包含当前像素的屏幕空间坐标(XY 分量)、像素深度(Z 分量)和 1/W (W 分量)。
// 除以屏幕尺寸,就能得到一个介于 0 和 1 之间的值,该值可用作纹理坐标。
vec2 CalcTexCoord()
{
return gl_FragCoord.xy / gScreenSize;
}
/// 点光源FS着色器
// 下面是对G-Buffer中的几个纹理进行采样
void main()
{
vec2 TexCoord = CalcTexCoord();
vec3 WorldPos = texture(gPositionMap, TexCoord).xyz;// MVP之后坐标
vec3 Color = texture(gColorMap, TexCoord).xyz;// M之后
vec3 Normal = texture(gNormalMap, TexCoord).xyz;
Normal = normalize(Normal);
FragColor = vec4(Color, 1.0) * CalcDirectionalLight(WorldPos, Normal);
}
/// 定向光FS着色器
void main()
{
vec2 TexCoord = CalcTexCoord();
vec3 WorldPos = texture(gPositionMap, TexCoord).xyz;
vec3 Color = texture(gColorMap, TexCoord).xyz;
vec3 Normal = texture(gNormalMap, TexCoord).xyz;
Normal = normalize(Normal);
FragColor = vec4(Color, 1.0) * CalcPointLight(WorldPos, Normal);
}
// 修改一下G-Buffer的初始化,因为屏幕像素和G缓冲区像素之间是1对1映射关系(每个纹理大小和屏幕大小一样),所以改成GL_NEAREST。
// 这样可以避免在像素之间进行不必要的插值,以免产生细微的扭曲。
bool GBuffer::Init(unsigned int WindowWidth, unsigned int WindowHeight)
{
...
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_textures) ; i++) {
...
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
...
}
...
}
// 修改一下绑定G-Buffer的读取方式,
void GBuffer::BindForReading()
{
// 不需要绑定到GL_READ_FRAMEBUFFER,是因为不需要再讲G-Buffer复制到默认帧缓存,而且通过绑定纹理,之后进行采样实现渲染。
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_textures); i++) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, m_textures[GBUFFER_TEXTURE_TYPE_POSITION + i]);
}
}
实例:优化上面OpenGL中Defer Rendering
我们目前的延迟着色实现存在一些问题。第一个问题你可能会注意到,当摄像机进入光量时,光线会消失。原因是我们只渲染了边界球的正面,所以一旦进入就会被剔除。如果我们禁用背面剔除,那么由于混合的原因,我们在球体外会获得更多的光线(因为我们会同时渲染两个面),而在球体内(只渲染背面时)则只有一半的光线。
第二个问题是,边界球并没有真正约束光线,有时边界球外的物体也会被照亮。
在前向渲染时候,球体光栅化之后,计算完光源,然后完成渲染管线才在屏幕上显示。
延迟渲染中,球体是在光栅化之前,计算光源之前,就投影变换到屏幕上了,那么光源计算的实际上是按照屏幕像素来算的,每一个在屏幕空间被球覆盖的像素都参与了计算,即使离得很远。
模板缓冲跟颜色缓冲(color buffer)和深度缓冲(depth buffer)是同等存在的buffer,共享同一个分辨率(颜色缓冲中的每一个像素同时也在模板缓冲中)。
模板缓冲可以用来按照一定版式限制一些像素的pixel着色器的执行,来实现一些想要的结果。
模板缓冲区与模板测试(stencil test)是相关联的。
- 模板测试(stencil test)与深度测试类似的方式,模版测试也可用于在执行像素着色器之前丢弃像素。它通过将模板缓冲区中当前像素位置的值与参考值进行比较来工作。有下面几种可用的比较模式:
Always pass
Always fail
Less/greater than
Less/greater than or equal
Equal
Not equal
- 根据模板测试和深度测试的结果,我们可以对存储的模板值定义一个模板操作。模板操作有以下几种:
保持模板值不变
将模板值置0
递增/递减模板值
模板值按位取反
- 我们可以针对下面不同的情形配置不同的模板操作:
模板测试失败
深度测试失败
深度测试成功
- 另外还可以为每个多边形的两个面配置不同的模板测试和模板操作。
- 最后关于模板缓冲区的一点说明:模板缓冲不是一个单独的缓冲区,实际上通常是深度缓冲的一部分,深度/模板缓冲可以为每个像素用24或32位数据存深度,用8位数据存模板值。
参考:
class GBuffer
{
public:
enum GBUFFER_TEXTURE_TYPE {
GBUFFER_TEXTURE_TYPE_POSITION,
GBUFFER_TEXTURE_TYPE_DIFFUSE,
GBUFFER_TEXTURE_TYPE_NORMAL,
GBUFFER_NUM_TEXTURES
};
GBuffer();
~GBuffer();
bool Init(unsigned int WindowWidth, unsigned int WindowHeight);
void StartFrame();
void BindForGeomPass();
void BindForStencilPass();
void BindForLightPass();
void BindForFinalPass();
private:
GLuint m_fbo;
GLuint m_textures[GBUFFER_NUM_TEXTURES];
GLuint m_depthTexture;
GLuint m_finalTexture;
};
bool GBuffer::Init(unsigned int WindowWidth, unsigned int WindowHeight)
{
...
glGenTextures(1, &m_finalTexture);
...
// depth
//这就为每个像素中的模版值留出了一个完整字节。这个深度缓冲区被附加到 GL_DEPTH_STENCIL_ATTACHMENT
glBindTexture(GL_TEXTURE_2D, m_depthTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH32F_STENCIL8, WindowWidth, WindowHeight, 0, GL_DEPTH_STENCIL,
GL_FLOAT_32_UNSIGNED_INT_24_8_REV, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, m_depthTexture, 0);
// final纹理
glBindTexture(GL_TEXTURE_2D, m_finalTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, WindowWidth, WindowHeight, 0, GL_RGB, GL_FLOAT, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT4, GL_TEXTURE_2D, m_finalTexture, 0);
...
}
// 每帧开始时,我们需要将GBuffer的final纹理的色彩清除。
void GBuffer::StartFrame()
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
glDrawBuffer(GL_COLOR_ATTACHMENT4);
glClear(GL_COLOR_BUFFER_BIT);
}
void GBuffer::BindForFinalPass()
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo);
glReadBuffer(GL_COLOR_ATTACHMENT4);
}
//以前,G 缓冲区中的 FBO 是静态的(就其配置而言),并且是事先设置好的,因此我们只需在几何图形传递开始时将其绑定以供写入。
//现在我们不断更改 FBO,因此每次都需要为属性配置绘制缓冲区glDrawBuffers。
void GBuffer::BindForGeomPass()
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
GLenum DrawBuffers[] = { GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1,
GL_COLOR_ATTACHMENT2 };
glDrawBuffers(ARRAY_SIZE_IN_ELEMENTS(DrawBuffers), DrawBuffers);
}
// 因为FS是空的,默认输出黑色,而我们不希望在模板pass阶段输出任何颜色。在这里禁用绘制缓冲区。
void GBuffer::BindForStencilPass()
{
// must disable the draw buffers
glDrawBuffer(GL_NONE);
}
// 当传递点光源时,我们绑定了GBuffer之前传递的各种纹理,并把绘制缓冲区设置为final 纹理,之后我们把这个复制给默认帧缓冲的color_bit
void GBuffer::BindForLightPass()
{
glDrawBuffer(GL_COLOR_ATTACHMENT4);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_textures); i++) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, m_textures[GBUFFER_TEXTURE_TYPE_POSITION + i]);
}
}
void GBuffer::BindForFinalPass()
{
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_fbo);
glReadBuffer(GL_COLOR_ATTACHMENT4);
}
virtual void RenderSceneCB()
{
CalcFPS();
m_scale += 0.05f;
m_pGameCamera->OnRender();
// 后续讲解,现在G-Buffer需要获得新帧的开始
m_gbuffer.StartFrame();
DSGeometryPass();
// We need stencil to be enabled in the stencil pass to get the stencil buffer
// updated and we also need it in the light pass because we render the light
// only if the stencil passes(这就靠对于每个光源,都进行一次模板传递,标记相关像素,再根据模板值进行一次光源传递).
glEnable(GL_STENCIL_TEST);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_pointLight); i++) {
DSStencilPass(i);
DSPointLightPass(i);
}
// The directional light does not need a stencil test because its volume
// is unlimited and the final pass simply copies the texture.
glDisable(GL_STENCIL_TEST);
DSDirectionalLightPass();
DSFinalPass();
RenderFPS();
glutSwapBuffers();
}
// 几何传递到GBuffer
void DSGeometryPass()
{
m_DSGeomPassTech.Enable();
// 其实就是原来来的GBuffer::BindForWriting()
m_gbuffer.BindForGeomPass();
// Only the geometry pass updates the depth buffer
glDepthMask(GL_TRUE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
Pipeline p;
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
p.Rotate(0.0f, m_scale, 0.0f);
for (unsigned int i = 0 ; i < ARRAY_SIZE_IN_ELEMENTS(m_boxPositions) ; i++) {
p.WorldPos(m_boxPositions[i]);
m_DSGeomPassTech.SetWVP(p.GetWVPTrans());
m_DSGeomPassTech.SetWorldMatrix(p.GetWorldTrans());
m_box.Render();
}
// When we get here the depth buffer is already populated and the stencil pass
// depends on it, but it does not write to it.
glDepthMask(GL_FALSE);
}
// 模板传递中完全不需要FS内容,只有模板缓冲区会被更新
void GBuffer::BindForStencilPass()
{
// must disable the draw buffers
glDrawBuffer(GL_NONE);
}
// 模板传递-GBuffer
void DSStencilPass(unsigned int PointLightIndex)
{
m_nullTech.Enable();
// Disable color/depth write and enable stencil
m_gbuffer.BindForStencilPass();
glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glClear(GL_STENCIL_BUFFER_BIT);
// We need the stencil test to be enabled but we want it
// to succeed always. Only the depth test matters.
// 因为模板一般是深度的一部分
glStencilFunc(GL_ALWAYS, 0, 0);
// 背面深度测试失败时,模板值递增;正面深度测试失败时,模板递减。这样保证只有像素上物体在球体内部,模板值大于0。
glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);
Pipeline p;
p.WorldPos(m_pointLight[PointLightIndex].Position);
float BBoxScale = CalcPointLightBSphere(m_pointLight[PointLightIndex].Color,
m_pointLight[PointLightIndex].DiffuseIntensity);
p.Scale(BBoxScale, BBoxScale, BBoxScale);
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
m_nullTech.SetWVP(p.GetWVPTrans());
m_bsphere.Render();
}
void DSPointLightPass(unsigned int PointLightIndex)
{
// 这个渲染到GBuffer的final纹理中,即GL_COLOR_ATTACHMENT4
m_gbuffer.BindForLightPass();
m_DSPointLightPassTech.Enable();
m_DSPointLightPassTech.SetEyeWorldPos(m_pGameCamera->GetPos());
// 只有模板值不为0通过
glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
// 禁用深度测试(因为不需要,禁用性能提高)
glDisable(GL_DEPTH_TEST);
// 之后的混合写入按照下面规则
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE);
glEnable(GL_CULL_FACE);
// 启用正面剔除是为了防止相机在光源球内部,我看到的应该是球体背面,则导致需要相机离开光源球才看到光源。
glCullFace(GL_FRONT);
Pipeline p;
p.WorldPos(m_pointLight[PointLightIndex].Position);
float BBoxScale = CalcPointLightBSphere(m_pointLight[PointLightIndex].Color,
m_pointLight[PointLightIndex].DiffuseIntensity);
p.Scale(BBoxScale, BBoxScale, BBoxScale);
p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
p.SetPerspectiveProj(m_persProjInfo);
m_DSPointLightPassTech.SetWVP(p.GetWVPTrans());
m_DSPointLightPassTech.SetPointLight(m_pointLight[PointLightIndex]);
m_bsphere.Render();
glCullFace(GL_BACK);
glDisable(GL_BLEND);
}
// 定向光和之前一模一样
// 这个最终纹理传递建议参考GBuffer结构
// G 缓冲区内的色彩缓冲区(final纹理)混合到屏幕中
void DSFinalPass()
{
// 将GBuffer中final纹理的color复制到默认帧缓存,而4号颜色附件(final纹理)就是DSPointLightPass中被写入的纹理
m_gbuffer.BindForFinalPass();
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
在DSFinalPass中,我们之所以不是像之前那样子直接堆GBuffer中各种纹理采样,混合渲染到屏幕上,而是在GBuffer中加一个中间颜色缓冲区(fnal 纹理),是因为我们的GBuffer将属性缓冲区与深度/模版缓冲区合并为一个目标。
- 在不使用模板优化延迟渲染的时候,我们传递光照之前已经glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);+绑定纹理,光照pass部分其实更多是按照光体积计算哪些像素需要走渲染管线(且已经关闭了深度测试并未使用),然后通过混合(ADD),渲染到默认帧缓冲上。
- 而在这里使用模板优化延迟渲染的点光源的之前,通过向GBuffer中的深度/模板组合附件按照规则写入了模板值,然后在点光源pass阶段除了按照光体积计算哪些像素需要走渲染管线,还通过开启深度测试/模板测试(GBuffer中的缓冲区),计算哪些点需要走渲染管线。所以如果像之前那样在光照pass阶段之前就切换到默认FBO(只使用纹理采样渲染),在点光源pass阶段是没办法访问GBuffer中的深度/模板缓冲区的。同理,GBuffer必须有自己的深度/模板缓冲区,因为我们渲染到GBuffer时,也是没办法访问默认FBO的深度缓冲区的。
OSG——FBO、Defer Rendering等实例
实例:使用FBO将子场景渲染到纹理(采样相机),在渲染到为显示窗口的另一个相机(显示相机(RELATIVE_RF))+全屏显示(ABSOLUTE_RF)
- 这里重点是要规格化坐标系,位置【-1,1】,纹理坐标【0,1】。
- 需要两个摄像机,一个是采样摄像机,一个是最终显示的摄像机。
#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osg/ShapeDrawable>
#include <osg/Shape>
#include <osg/Material>
#include <osg/Texture2D>
osg::ref_ptr<osg::Texture2D> createRttTexture(int texWidth, int texHeight)
{
osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
texture->setInternalFormat(GL_RGBA);
texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
texture->setTextureSize(texWidth, texHeight);
return texture;
}
osg::ref_ptr<osg::Geode> createTexturePanelGeode()
{
// 坐标
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(-1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 1.0f, 0.0f));
vertices->push_back(osg::Vec3(-1.0f, 1.0f, 0.0f));
// 纹理坐标
osg::ref_ptr<osg::Vec2Array> texCoord = new osg::Vec2Array;
texCoord->push_back(osg::Vec2(0.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 1.0));
texCoord->push_back(osg::Vec2(0.0, 1.0));
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices);
geom->setTexCoordArray(0, texCoord);
geom->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(geom);
osg::ref_ptr<osg::StateSet> set1 = geode->getOrCreateStateSet();
//设置不受光照影响
set1->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
return geode;
}
osg::Camera* createCamera(int x, int y, int w, int h)
{
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
traits->x = x;//左上角,且根据电脑屏幕来的!
traits->y = y;
traits->width = w;
traits->height = h;
traits->doubleBuffer = true;
//osg::DisplaySetting* ds = osg::DisplaySetting::instance();
osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setGraphicsContext(gc.get());
//设置视口,用于最后变换标准化设备坐标,x和y是相对与Traits中定义的窗口的左下角开始的偏移。
camera->setViewport(new osg::Viewport(0, 0, traits->width, traits->height));
return camera.release();
}
int main()
{
osg::ref_ptr<osgViewer::Viewer> viewer = new osgViewer::Viewer;
//各个pass的(参考上文Defer Redering中的pass概念,这里是封装成两个相机了,一个采样相机,一个渲染相机)
//在采样相机中可以设置节点进行深度测试和关闭深度测试
//也可以在最后多加一个模拟不进行深度测试的相机(特殊用途)
osg::ref_ptr<osg::Group> passRoot = new osg::Group();
//场景根
osg::ref_ptr<osg::Group> sceneRoot = new osg::Group();
osg::ref_ptr<osg::Node> node = osgDB::readNodeFile("D:\\BaiduNetdiskDownload\\vs2019 64位 3rdParty osg365 oe32\\OpenSceneGraph-Data\\cow.osg");
sceneRoot->addChild(node);
//获取系统分辨率/或者直接设置固定值都行,只要纹理size和相机viewport一致即可,这样纹理可以用GL_NEAREST,效率高
unsigned int screenWidth, screenHeight;
osg::GraphicsContext::WindowingSystemInterface* wsInterface = osg::GraphicsContext::getWindowingSystemInterface();
if (!wsInterface)
{
return -1;
}
wsInterface->getScreenResolution(osg::GraphicsContext::ScreenIdentifier(0), screenWidth, screenHeight);
int texWidth = screenWidth;
int texHeight = screenHeight;
// 纹理
osg::ref_ptr<osg::Texture2D> tex = createRttTexture(texWidth, texHeight);
//绑定pass1采样摄像机
osg::ref_ptr<osg::Camera> sampleCamera = new osg::Camera;
{
sampleCamera->addChild(sceneRoot);
sampleCamera->setClearColor(osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f));
sampleCamera->setClearMask(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
sampleCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); //这句话使内容不渲染到屏幕上
sampleCamera->attach(osg::Camera::COLOR_BUFFER, tex); //关联采样贴图
sampleCamera->setViewport(0, 0, screenWidth, screenHeight);//摄像机关联视口
}
// 创建片面并绑定到pass2显示相机中,使用的是pass1相机中的纹理。
osg::ref_ptr<osg::Geode> panelGeode = createTexturePanelGeode();
osg::ref_ptr<osg::StateSet> ss = panelGeode->getOrCreateStateSet();
ss->setTextureAttributeAndModes(0, tex);
//final 1 camera
osg::ref_ptr<osg::Camera> finalCamera = new osg::Camera;
{
finalCamera->setClearColor(osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));
finalCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
finalCamera->addChild(panelGeode);
finalCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);// 全屏显示
// 这里之所以全屏是因为该相机的MVP不受父节点(viewer影响),使用默认的,而片面是相当于标准化设备坐标了,而且在[-1,1],参考createTexturePanelGeode中的创建
}
// final 2 camera,用于多加一层hud相机,用于模拟no depTest
osg::ref_ptr<osg::Camera> NoDepTestCamera = new osg::Camera;/*createCamera(100, 100, 400, 300);*/
{
// 应该保持和采样相机一样的vp和viewport一样!
osg::ref_ptr<osg::Node> axes = osgDB::readNodeFile("D:\\BaiduNetdiskDownload\\vs2019 64位 3rdParty osg365 oe32\\OpenSceneGraph-Data\\axes.osgt");
NoDepTestCamera->addChild(axes);
NoDepTestCamera->setClearMask(GL_DEPTH_BUFFER_BIT);//透明背景
//NoDepTestCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);// 需要配合viewer->addSlave(NoDepTestCamera,false)使用
}
passRoot->addChild(sampleCamera); //将摄像机加入场景
passRoot->addChild(finalCamera);
passRoot->addChild(NoDepTestCamera);
viewer->setSceneData(passRoot);
//viewer->addSlave(NoDepTestCamera,false);// 配合NoDepTestCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF)使用,需要自行设置vp矩阵和viewport
viewer->run();
return 0;
}
实例:使用FBO之着色器
- 通过着色器将颜色缓冲区每个像素都变色,不叠加:
// 下面这个两个是通过着色器将颜色缓冲区每个像素都变色,不叠加
static const char* vertSource =
{
"#version 330\n"
"in vec4 osg_Vertex;\n"
"uniform mat4 osg_ModelViewProjectionMatrix;\n"
"void main(){\n"
"gl_Position = osg_ModelViewProjectionMatrix*osg_Vertex;\n"
"}\n"
}
static const char* fragSource =
{
"#version 330\n"
"void main(){\n"
"gl_FragColor = vec4(0.5,0.0,0.0,1.0);\n"
"}\n"
}
- 使用:
osg::ref_ptr<osg::Geode> createTexturePanelGeode()
{
// 坐标
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(-1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 1.0f, 0.0f));
vertices->push_back(osg::Vec3(-1.0f, 1.0f, 0.0f));
// 纹理坐标
osg::ref_ptr<osg::Vec2Array> texCoord = new osg::Vec2Array;
texCoord->push_back(osg::Vec2(0.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 1.0));
texCoord->push_back(osg::Vec2(0.0, 1.0));
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices);
geom->setTexCoordArray(0, texCoord);
geom->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(geom);
osg::ref_ptr<osg::StateSet> set1 = geode->getOrCreateStateSet();
//设置不受光照影响
set1->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
return geode;
}
int main()
{
......
// 创建片面并绑定到pass2显示相机中,使用的是pass1相机中的纹理。
osg::ref_ptr<osg::Geode> panelGeode = createTexturePanelGeode();
osg::ref_ptr<osg::StateSet> ss = panelGeode->getOrCreateStateSet();
ss->setTextureAttributeAndModes(0, tex);
osg::ref_ptr<osg::Shader> vs1 = new osg::Shader(osg::Shader::VERTEX, vertSource );
osg::ref_ptr<osg::Shader> ps1 = new osg::Shader(osg::Shader::FRAGMENT, fragSource );
osg::ref_ptr<osg::Program> program1 = new osg::Program;
program1->addShader(vs1);
program1->addShader(ps1);
ss->setAttribute(program1, osg::StateAttribute::ON);
...
viewer->realize();//初始化
viewer.getCamera()->getGraphicsContext()->getState()->resetVertexAttributeAlias(false);
viewer->getCamera()->getGraphicsContext()->getState()->setUseModelViewAndProjectionUniforms(true);
viewer->getCamera()->getGraphicsContext()->getState()->setUseVertexAttributeAliasing(true);
......
}
- 基于上面着色器,如果通过对场景节点设置着色器,相当于是对渲染到纹理/FBO上的物体叠加效果:
int main()
{
......
osg::ref_ptr<osg::StateSet> statset_SceneRoot = sceneRoot->getOrCreateStateSet();
statset_SceneRoot->setAttribute(program1, osg::StateAttribute::ON);
....
viewer->realize();//初始化
// 如果是按照下面方式使用着色器(偷懒),第一句必须有,因为只是场景节点使用着色器,但是片面没使用自定义着色器,导致一片黑。
viewer.getCamera()->getGraphicsContext()->getState()->resetVertexAttributeAlias(false);
viewer->getCamera()->getGraphicsContext()->getState()->setUseModelViewAndProjectionUniforms(true);
// 保证a卡和n卡location一致,但是改变了默认着色器!使用默认着色器会导致一片黑。
viewer->getCamera()->getGraphicsContext()->getState()->setUseVertexAttributeAliasing(true);
......
}
实例:通过shader计算纹理坐标并采样(下个实例更好且逻辑清晰)
- 在顶点着色器中,通过每一个顶点计算出纹理坐标([-x,x]对应[0,1]),并且片面使用着色器进行纹理采样。
- 此时应该是对纹理的片面设置着色器。
- GLSL常用内置函数
static const char* vertSource =
{
"#version 330\n"
"in vec4 osg_Vertex;\n"
"out vec2 texCoord;\n"
"uniform mat4 osg_ModelViewProjectionMatrix;\n"
"void main(){\n"
"vec2 coord = sign(osg_Vertex.xy);\n"
"texCoord.x = coord.x * 0.5 + 0.5;\n"
"texCoord.y = coord.y * 0.5 + 0.5;\n"
"gl_Position = osg_ModelViewProjectionMatrix*osg_Vertex;\n"
"}\n"
};
static const char* fragSource =
{
"#version 330\n"
"in vec2 texCoord;\n"
"uniform sampler2D tex1;\n"
"layout (location=0) out vec4 FragColor;\n"
"void main(){\n"
"FragColor= texture2D(tex1,texCoord);\n"
"}\n"
};
int main()
{
......
// 创建片面并绑定到pass2显示相机中,使用的是pass1相机中的纹理。
osg::ref_ptr<osg::Geode> panelGeode = createTexturePanelGeode();
osg::ref_ptr<osg::StateSet> ss = panelGeode->getOrCreateStateSet();
ss->setTextureAttributeAndModes(0, tex);// 将纹理绑定在纹理卡槽0
osg::ref_ptr<osg::Shader> vs1 = new osg::Shader(osg::Shader::VERTEX, vertSource );
osg::ref_ptr<osg::Shader> ps1 = new osg::Shader(osg::Shader::FRAGMENT, fragSource );
osg::ref_ptr<osg::Program> program1 = new osg::Program;
program1->addShader(vs1);
program1->addShader(ps1);
// 将着色器中的采样器绑定到纹理卡槽0
osg::ref_ptr<osg::Uniform> tex1 = new osg::Uniform(“tex1”, 0);
ss->addUniform(tex1);
ss->setAttribute(program1, osg::StateAttribute::ON);
...
viewer->realize();//初始化
viewer.getCamera()->getGraphicsContext()->getState()->resetVertexAttributeAlias(false);
viewer->getCamera()->getGraphicsContext()->getState()->setUseModelViewAndProjectionUniforms(true);
viewer->getCamera()->getGraphicsContext()->getState()->setUseVertexAttributeAliasing(true);
......
}
实例:向着色器传入纹理坐标(两种方法,注意着色器使用方法)
- 下面方法1为混用的,但是最好不要混用,除非你明确知道Aliasing和OSG与OpenGL内置变量映射之后对应location。
- 但是由此可以更清晰知道着色器的用法,即关于osg中resetVertexAttributeAlias是将OSG内置变量和OpenGL内置变量做了个别名和索引的映射;而setUseVertexAttributeAliasing是将例如setTexCoordArray都改成了setVertexAttribArray,且有固定序号,参考
// 方法1:
// 着色器
static const char* vertSource =
{
"#version 330\n"
"in vec4 osg_Vertex;\n"
"layout (location=1) in vec2 texC;\n"
"out vec2 texCoord;\n"
"uniform mat4 osg_ModelViewProjectionMatrix;\n"
"void main(){\n"
"texCoord = texC;\n"
"gl_Position = osg_ModelViewProjectionMatrix*osg_Vertex;\n"
"}\n"
};
static const char* fragSource =
{
"#version 330\n"
"in vec2 texCoord;\n"
"uniform sampler2D tex1;\n"
"layout (location=0) out vec4 FragColor;\n"
"void main(){\n"
"FragColor= texture2D(tex1,texCoord);\n"
"}\n"
};
osg::ref_ptr<osg::Geode> createTexturePanelGeode()
{
// 坐标
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(-1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 1.0f, 0.0f));
vertices->push_back(osg::Vec3(-1.0f, 1.0f, 0.0f));
//纹理
osg::ref_ptr<osg::Vec2Array> texCoord = new osg::Vec2Array;
texCoord->push_back(osg::Vec2(0.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 1.0));
texCoord->push_back(osg::Vec2(0.0, 1.0));
//
geom->setVertexArray(vertices);// location=0
geom->setVertexAttribArray(1, texCoord, osg::Array::BIND_PER_VERTEX);// location=1
}
int main()
{
....
// 必须注释掉第一句和第二句是因为无论要把location=1设置给纹理坐标,这两句都是将location=1设置给不同的值了
// 但是location=0是通过setVertexArray已经设置了。
//viewer.getCamera()->getGraphicsContext()->getState()->resetVertexAttributeAlias(false);
viewer->getCamera()->getGraphicsContext()->getState()->setUseModelViewAndProjectionUniforms(true);
// 保证a卡和n卡location一致,但是改变了默认着色器!使用默认着色器会导致一片黑。
//viewer->getCamera()->getGraphicsContext()->getState()->setUseVertexAttributeAliasing(true);
....
}
- 方法2,都是用setVertexAttribArray或者使用osg封装好的例如setVertexArray和setTexCoordArray,重点是着色器的使用。
- 下面展示的是都是用osg封装好的设置时,着色器使用
// 着色器
static const char* vertSource =
{
"#version 330\n"
"in vec4 osg_Vertex;\n"
"in vec4 osg_MultiTexCoord0;\n"
"out vec2 texCoord;\n"
"uniform mat4 osg_ModelViewProjectionMatrix;\n"
"void main(){\n"
"texCoord = osg_MultiTexCoord0.st;\n"
"gl_Position = osg_ModelViewProjectionMatrix*osg_Vertex;\n"
"}\n"
};
static const char* fragSource =
{
"#version 330\n"
"in vec2 texCoord;\n"
"uniform sampler2D tex1;\n"
"layout (location=0) out vec4 FragColor;\n"
"void main(){\n"
"FragColor= texture2D(tex1,texCoord);\n"
"}\n"
};
osg::ref_ptr<osg::Geode> createTexturePanelGeode()
{
// 坐标
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(-1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 1.0f, 0.0f));
vertices->push_back(osg::Vec3(-1.0f, 1.0f, 0.0f));
// 纹理坐标
osg::ref_ptr<osg::Vec2Array> texCoord = new osg::Vec2Array;
texCoord->push_back(osg::Vec2(0.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 1.0));
texCoord->push_back(osg::Vec2(0.0, 1.0));
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices);
geom->setTexCoordArray(0, texCoord);//至少支持8个纹理坐标。
geom->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(geom);
osg::ref_ptr<osg::StateSet> set1 = geode->getOrCreateStateSet();
//设置不受光照影响
set1->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
return geode;
}
int main()
{
......
osg::ref_ptr<osg::StateSet> statset_SceneRoot = sceneRoot->getOrCreateStateSet();
statset_SceneRoot->setAttribute(program1, osg::StateAttribute::ON);
....
viewer->realize();//初始化
viewer.getCamera()->getGraphicsContext()->getState()->resetVertexAttributeAlias(false);
viewer->getCamera()->getGraphicsContext()->getState()->setUseModelViewAndProjectionUniforms(true);
viewer->getCamera()->getGraphicsContext()->getState()->setUseVertexAttributeAliasing(true);
......
}
实例:使用FBO之深度图
- 之前通过osg::Camera进行RTT时采样的是颜色bit,附加颜色附件。
- 现在想要获得深度缓冲区的值,则需要添加深度附件。
// 着色器
static const char* vertSource =
{
"#version 330\n"
"in vec4 osg_Vertex;\n"
"in vec4 osg_MultiTexCoord0;\n"
"out vec2 texCoord;\n"
"uniform mat4 osg_ModelViewProjectionMatrix;\n"
"void main(){\n"
"texCoord = osg_MultiTexCoord0.st;\n"
"gl_Position = osg_ModelViewProjectionMatrix*osg_Vertex;\n"
"}\n"
};
static const char* fragSource =
{
"#version 330\n"
"in vec2 texCoord;\n"
"uniform sampler2D tex1;\n"
"layout (location=0) out vec4 FragColor;\n"
"void main(){\n"
"FragColor= texture2D(tex1,texCoord);\n"
"}\n"
};
osg::ref_ptr<osg::Geode> createTexturePanelGeode()
{
// 坐标
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
vertices->push_back(osg::Vec3(-1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, -1.0f, 0.0f));
vertices->push_back(osg::Vec3(1.0f, 1.0f, 0.0f));
vertices->push_back(osg::Vec3(-1.0f, 1.0f, 0.0f));
// 纹理坐标
osg::ref_ptr<osg::Vec2Array> texCoord = new osg::Vec2Array;
texCoord->push_back(osg::Vec2(0.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 0.0));
texCoord->push_back(osg::Vec2(1.0, 1.0));
texCoord->push_back(osg::Vec2(0.0, 1.0));
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices);
geom->setTexCoordArray(0, texCoord);//至少支持8个纹理坐标。
geom->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4));
osg::ref_ptr<osg::Geode> geode = new osg::Geode;
geode->addDrawable(geom);
osg::ref_ptr<osg::StateSet> set1 = geode->getOrCreateStateSet();
//设置不受光照影响
set1->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
return geode;
}
osg::ref_ptr<osg::Texture> createDepthTexture( int texWidth, int texHeight )
{
osg::ref_ptr<osg::Texture2D> texture = new osg::Texture2D;
texture ->setInternalFormat(GL_DEPTH_COMPONENT24);
texture ->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::NEAREST);
texture ->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::NEAREST);
texture ->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE);
texture ->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);
texture ->setSourceFormat(GL_DEPTH_COMPONENT);
texture ->setSourceType(GL_FLOAT);
texture->setTextureSize( texWidth, texHeight );
return texture;
}
int main()
{
osg::ref_ptrosgViewer::Viewer viewer = new osgViewer::Viewer;
//各个pass根
osg::ref_ptrosg::Group passRoot = new osg::Group();
//场景根
osg::ref_ptrosg::Group sceneRoot = new osg::Group();
osg::ref_ptrosg::Node node = osgDB::readNodeFile("cow.osg");
sceneRoot->addChild(node);
//获取系统分辨率/或者直接设置固定值都行,只要纹理size和相机viewport一致即可,这样纹理可以用GL_NEAREST,效率高
unsigned int screenWidth, screenHeight;
osg::GraphicsContext::WindowingSystemInterface * wsInterface = osg::GraphicsContext::getWindowingSystemInterface();
if (!wsInterface)
{
return -1;
}
wsInterface->getScreenResolution(osg::GraphicsContext::ScreenIdentifier(0), screenWidth, screenHeight);
int texWidth = screenWidth;
int texHeight = screenHeight;
// 深度附件/纹理
osg::ref_ptr<osg::Texture2D> texDepth = createDepthTexture(texWidth, texHeight);
// 绑定pass1采样摄像机
osg::ref_ptr<osg::Camera> sampleCamera = new osg::Camera;
{
sampleCamera->addChild(sceneRoot);
sampleCamera->setClearColor(osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));
sampleCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); // 这句话使内容不渲染到屏幕上
sampleCamera->attach(osg::Camera::DEPTH_BUFFER, texDepth ); // 获得深度缓冲的值
sampleCamera->setViewport(0, 0, screenWidth, screenHeight);// 摄像机关联视口
}
// 创建片面并绑定到pass2显示相机中,使用的是pass1相机中的纹理。
osg::ref_ptr<osg::Geode> panelGeode = createTexturePanelGeode();
//final
osg::ref_ptr<osg::Camera> finalCamera = new osg::Camera;
{
finalCamera->addChild(panelGeode);
finalCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);// 全屏显示
// 这里之所以全屏是因为该相机的MVP不受父节点(viewer影响),使用默认的,而片面是相当于标准化设备坐标了,而且在[-1,1]
}
// 通过shader处理场景(把深度纹理传递给final相机),这里与直接在片面上设置着色器不同,而是相机节点下所有drawable都执行
osg::ref_ptr<osg::StateSet> ss = finalCamera ->getOrCreateStateSet();
ss->setTextureAttributeAndModes(0, texDepth );// 纹理卡槽
osg::ref_ptr<osg::Shader> vs1 = new osg::Shader(osg::Shader::VERTEX, vertSource );
osg::ref_ptr<osg::Shader> ps1 = new osg::Shader(osg::Shader::FRAGMENT, fragSource );
osg::ref_ptr<osg::Program> program1 = new osg::Program;
program1->addShader(vs1);
program1->addShader(ps1);
// 采样器
osg::ref_ptr<osg::Uniform> tex1 = new osg::Uniform("tex1", 0);
ss->addUniform(tex1);
ss->setAttribute(program1, osg::StateAttribute::ON);
passRoot->addChild(sampleCamera); //将摄像机加入场景
passRoot->addChild(finalCamera);
viewer->setSceneData(passRoot);
// 自定义着色器某些必要操作
viewer->realize();//初始化
viewer.getCamera()->getGraphicsContext()->getState()->resetVertexAttributeAlias(false);
viewer->getCamera()->getGraphicsContext()->getState()->setUseModelViewAndProjectionUniforms(true);
viewer->getCamera()->getGraphicsContext()->getState()->setUseVertexAttributeAliasing(true);
viewer->run();
return 0;
}
实例:使用FBO之深度图与颜色缓冲的混合
- 在上面例子们的基础下,我们可以知道,pass1相机/采样相机就是渲染不同缓冲区/通道到不同类型纹理/附件上(RTT),可以将这些纹理传递给pass1相机/显示相机,并且可以继续往后传…不过需要注意往Group添加相机节点的时候需要按照传递顺序添加。
- 在显示相机的着色器中进行对纹理+光照等数据的混合计算,相当于是Defer Redering的概念了,当然这里没有进行筛选像素渲染(例如点光源),而是对片面每一个像素都通过显示相机pass阶段了。
- 这里并不使用光照例子,而是使用深度纹理图+颜色纹理图混合采样(简单叠加)作为例子。
- edl效果其实也是基于这种概念的混合计算。
下面代码是基于上个例子的修改:
// 着色器
static const char* vertSource =
{
"#version 330\n"
"in vec4 osg_Vertex;\n"
"in vec4 osg_MultiTexCoord0;\n"
"out vec2 texCoord;\n"
"uniform mat4 osg_ModelViewProjectionMatrix;\n"
"void main(){\n"
"texCoord = osg_MultiTexCoord0.st;\n"
"gl_Position = osg_ModelViewProjectionMatrix*osg_Vertex;\n"
"}\n"
};
static const char* fragSource =
{
"#version 330\n"
"in vec2 texCoord;\n"
"uniform sampler2D texColor;\n"
"uniform sampler2D texDepth;\n"
"layout (location=0) out vec4 FragColor;\n"
"void main(){\n"
"FragColor= texture2D(texColor,texCoord) + texture2D(texDepth,texCoord);\n"
"}\n"
};
int main()
{
......
// 深度附件/纹理
osg::ref_ptr<osg::Texture2D> textureColor = createRttTexture(texWidth, texHeight);
osg::ref_ptr<osg::Texture2D> textureDepth = createDepthTexture(texWidth, texHeight);
// 绑定pass1采样摄像机
osg::ref_ptr<osg::Camera> sampleCamera = new osg::Camera;
{
sampleCamera->addChild(sceneRoot);
sampleCamera->setClearColor(osg::Vec4(0.0f, 0.0f, 0.0f, 1.0f));
sampleCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); // 这句话使内容不渲染到屏幕上
sampleCamera->attach(osg::Camera::COLOR_BUFFER0, textureColor ); //
sampleCamera->attach(osg::Camera::DEPTH_BUFFER, textureDepth ); // 获得深度缓冲的值
sampleCamera->setViewport(0, 0, screenWidth, screenHeight);// 摄像机关联视口
}
// 创建片面并绑定到pass2显示相机中,使用的是pass1相机中的纹理。
osg::ref_ptr<osg::Geode> panelGeode = createTexturePanelGeode();
//final
osg::ref_ptr<osg::Camera> finalCamera = new osg::Camera;
{
finalCamera->addChild(panelGeode);
finalCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);// 全屏显示
// 这里之所以全屏是因为该相机的MVP不受父节点(viewer影响),使用默认的,而片面是相当于标准化设备坐标了,而且在[-1,1]
}
// 通过shader处理场景(把深度纹理传递给final相机),这里与直接在片面上设置着色器不同,而是相机节点下所有drawable都执行
osg::ref_ptr<osg::StateSet> ss = finalCamera ->getOrCreateStateSet();
ss->setTextureAttributeAndModes(0, textureDepth );// 纹理卡槽
ss->setTextureAttributeAndModes(1, textureColor );// 纹理卡槽
osg::ref_ptr<osg::Shader> vs1 = new osg::Shader(osg::Shader::VERTEX, vertSource );
osg::ref_ptr<osg::Shader> ps1 = new osg::Shader(osg::Shader::FRAGMENT, fragSource );
osg::ref_ptr<osg::Program> program1 = new osg::Program;
program1->addShader(vs1);
program1->addShader(ps1);
// 采样器
osg::ref_ptr<osg::Uniform> texColor= new osg::Uniform("texColor", 0);
ss->addUniform(texColor);
osg::ref_ptr<osg::Uniform> texDepth= new osg::Uniform("texDepth", 0);
ss->addUniform(texDepth);
ss->setAttribute(program1, osg::StateAttribute::ON);
......
}
实例:使用FBO之多pass(补
- 一个场景节点,传递给第一个相机(sampleCamera0,采样相机,渲染到FBO)获得tex0,然后再将tex0绑定到第二个相机(pass1Camera,pass相机,渲染到FBO)下的片面节点以渲染(使用自定义着色器),第二个相机添加颜色附件tex1,将颜色缓冲区输入到tex1;
- 此时可以继续将tex1绑定到第三个相机节点(pass2Camera ,显示相机,渲染窗口)下的片面节点,并通过着色器,将此时背景的黑色(clearColor)变成红色。