GPU的并行计算能力特别适用于基于Raycasting的体绘制方法: GPU为每一条光线开辟一个线程,可以高效地完成绘制。然而每一条光线需要穿过整个数据场,数据量小时可以将整个数据场载入到纹理中保存在GPU内存空间。当数据量很大时,GPU无法容纳整个原始数据,解决办法之一是将原始数据分成若干个块,然后对每个块的子数据分别绘制,最后再将各个块的绘制结果组合起来。
Raycasting分块绘制需要确定各个块绘制的先后顺序,这是由于光线沿着某个射出的方向会按照顺序穿过某些分块,因此绘制的过程中需要按照光线的前进方向确定哪些块先绘制,哪些块后绘制。假设某条光线依次穿过了A分块和B分块,则先绘制A,再绘制B。这个过程中需要将完成A的绘制时的中间结果保存,然后绘制B时要利用这个中间结果。因此分多个块绘制时需要在绘制后一个块时访问前一个块的绘制结果。其实前一个块绘制的结果已经保存在了帧缓冲区当中,但是片段着色器不能访问帧缓冲区,这就需要将帧缓冲区中的内容作为纹理传给片段着色器。如果没有保存中间结果,而只是简单地将各个子块独立地绘制出来,不利用光线到达该块之前时所经过的那些块的绘制结果,是不符合光线投射的原理的。从而会导致绘制结果不连续,如下图:
该图是将整个体数据沿着z轴分为两部分,成为两个独立的体数据场,然后利用这两个子块数据分别绘制两个立方体,可以看到同时穿过了两个子块的那些光线处,绘制结果是不正确的。
基于FBO的方法是将每个块先按照顺序绘制在用户申请的帧缓冲区中,绘制后续的块时能够访问到上一步的绘制结果。实现方法如下:
由于绘制后面的块会重新计算光线对应的片段的颜色值,每次绘制时需要使用源颜色值替换目标颜色值,因此混合模式应该设为glBlendFunc(GL_ONE, GL_ZERO);同时需要关闭深度测试,以防止近处的立方体遮挡远处的立方体。
glDisable(GL_DEPTH_TEST); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ZERO); glEnable(GL_CULL_FACE); glCullFace(GL_BACK);
//FBO and render buffer object ID GLuint fboID, rbID; //offscreen render texture ID GLuint renderTextureID; void InitFBO() { //generate and bind fbo ID glGenFramebuffersEXT(1, &fboID); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboID); //generate and bind render buffer ID glGenRenderbuffersEXT(1, &rbID); glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, rbID); //set the render buffer storage glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT32, WIDTH, HEIGHT); //generate the offscreen texture glActiveTexture(GL_TEXTURE1); glGenTextures(1, &renderTextureID); glBindTexture(GL_TEXTURE_2D, renderTextureID); //set texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, WIDTH, HEIGHT, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); //bind the renderTextureID as colour attachment of FBO glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, renderTextureID, 0); //set the render buffer as the depth attachment of FBO glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT,GL_RENDERBUFFER_EXT, rbID); //check for frame buffer completeness status GLuint status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); if(status==GL_FRAMEBUFFER_COMPLETE_EXT) { printf("FBO setup succeeded."); } else { printf("Error in FBO setup."); } //unbind the texture and FBO glBindTexture(GL_TEXTURE_2D, 0); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); }绘制时,先将体数据绘制到帧缓存,再绘制到屏幕:
void OnRender() { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glEnable(GL_BLEND); int draw_order=glm::dot(camPos,glm::vec3(0.0,0.0,1.0))>0?1:2; glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboID); glViewport(0,0,WIDTH, HEIGHT); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); if(draw_order==1) { updataSecondSubTexture();//先绘制第二个体数据子块 glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, renderTextureID); updataFirstSubTexture();//再绘制第一个体数据子块 } else { updataFirstSubTexture();//先绘制第一个体数据子块 glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, renderTextureID); updataSecondSubTexture(); } glViewport (0, 0, (GLsizei) window_w, (GLsizei) window_h); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); drawQuad(); glDisable(GL_BLEND); //swap front and back buffers to show the rendered result glutSwapBuffers(); }上述代码中draw_order是一个根据摄像机的位置而变化的一个取值为1或2 的数,其值用于标记绘制的顺序。将第一次绘制的结果作为纹理,保存在GL_TEXTURE1中,传递给第二次绘制的片段处理器。
片段处理器中的部分代码为:
void dvr_raycasting() { init(); if((draw_order==1 && subImage==1) || (draw_order==2 && subImage==2)) { vec2 fragCoord; fragCoord.x=gl_FragCoord.x/FBOSize.x; fragCoord.y=gl_FragCoord.y/FBOSize.y; vFragColor=texture(frameBuffer, fragCoord).rgba; }; // other parts of ray casting }体绘制的结果最后保存在GL_TEXTURE1中,并且利用该纹理绘制了一个四边形覆盖在整个视口上。上述代码有两处修改视口大小的操作,第一个是进行体绘制之前调用的glViewport(0,0,WIDTH, HEIGHT);其目的是使绘制的视口大小与FBO的大小一致。第二个是绘制四边形之前调用的glViewport (0, 0, (GLsizei) window_w, (GLsizei) window_h);其目的是使绘制的视口恢复到窗口大小。如果将WIDTH和HEIGHT设置成固定的一个不太大的数,程序运行时将窗口放到全屏大小,也不会增加体绘制时的GPU线程数,只是相当于将一个WIDTH和HEIGHT大小的纹理绘制到了窗口上,因此放大窗口不会降低体绘制的性能。这样可以部分解决有些时候直接在屏幕上全屏绘制会因为显存不足而崩溃的问题。最后绘制的效果如下:
总结一下,使用FBO可以解决两个问题:
1, 图像太大导致GPU显存不够用的问题:分块绘制到FBO中
2, 屏幕尺寸或者分辨率太高导致GPU显存不够用或者交互速度低的问题:先绘制到一个视口较小的FBO,再映射到屏幕
关于frame buffer的使用可以阅读http://blog.csdn.net/wl_soft50/article/details/7916955