OpenGL可视化入门:体绘制(VRT)

3 篇文章 0 订阅

1.初始化

使用OpenGL绘制图像一般分为两步:初始化阶段和渲染阶段
在初始化阶段对要用到的资源交给OpenGL,在渲染阶段告诉OpenGL如何去使用它们。

1.1 设置几何体

几何体是由顶点构成的,将顶点数据交给OpenGL的方式有很多种,本文使用一种常见的方式。及VAO(EBO, VBO)的方式。
EBO ( Element Buffer Object ) 里面存的是顶点的绘制顺序,如果想绘制三角形,就需要将构成三角形的三个顶点放在一起。(注意:逆时针存放为正面,顺时针为反面)
VBO ( Vertex Buffer Object ) 里面存的是顶点的信息,最基本的就是位置信息Position了。也可以存顶点的颜色、方向方向、纹理坐标等任意需要的信息。可以使用一个VBO存放一种顶点信息 ( Attribute )
VAO ( Vertex Array Buffer Object ) 由一个 EBO 和 数个 VBO 构成,即顶点的绘制顺序和顶点的各种属性。可以理解为一个 VAO 就代表了一个几何体

注意:VAO、VBO、EBO的搭配方式和使用方式是多种多样的,这里使用一种便于理解的方式进行说明。

这里使用OpenGL 4.5进行代码演示

//顶点数据 
const float vertices[] = {
        -1.0,-1.0,0.0,
        1.0,-1.0,0.0,
        1.0,1.0,0.0,
        -1.0,1.0,0.0,
};

const UInt indices[] = {
    0,1,2,
    0,2,3
};
//创建 VBO EBO VAO
glCreateBuffers(1, &m_iVBO);
glNamedBufferData(m_iVBO, sizeof(vertices), vertices, GL_STATIC_DRAW);

glCreateBuffers(1, &m_iEBO);
glNamedBufferData(m_iEBO, sizeof(indices), indices, GL_STATIC_DRAW);

glCreateVertexArrays(1, &m_iVAO);

//设置VAO的EBO
glVertexArrayElementBuffer(m_iVAO, m_iEBO);

//将VBO绑定到VAO的第一个绑定点(vaoBindingIndex )上
UInt vaoBindingIndex = 0;
glVertexArrayVertexBuffer(m_iVAO, vaoBindingIndex, m_iVBO, 0, 3 * sizeof(float));

//将VAO的绑定点0上的VBO设置为VAO的第一个属性
UInt vaoAttributeSlot = 0;
glVertexArrayAttribBinding(m_iVAO, vaoAttributeSlot, vaoBindingIndex);
//设置属性的格式(每个顶点Position 为3个float变量)
glVertexArrayAttribFormat(m_iVAO, vaoAttributeSlot, 3, GL_FLOAT, GL_FALSE, 0);
//启用该顶点属性
glEnableVertexArrayAttrib(m_iVAO, vaoAttributeSlot);

1.2 设置着色器

着色器是GPU运行的小程序。顶点着色器用于处理每一个顶点,片元着色器用于处理每一个片元 ( fragment )。片元是光栅化的结果。可能会有多个片元落在同一个像素上。

着色器的设置有什么特别的,可以参考 LearnOpenGL-着色器.,重要的是如何编写体绘制的着色器,这部分在后面进行介绍。

1.3 设置纹理

体绘制处理的体数据是三维的,可以使用一张三维纹理存放它。

//创建三维纹理
glCreateTextures(GL_TEXTURE_3D, 1, &m_iVolume);
//分配三维纹理的内存空间
glTextureStorage3D(m_iVolume, 1, GL_R32F, imageSize[0], imageSize[1], imageSize[2]);
//设置三维纹理的数据
glTextureSubImage3D(m_iVolume,
        0,
        0, 0, 0,
        imageSize[0], imageSize[1], imageSize[2],
        GL_RED, GL_FLOAT,
        imageData);

需要注意的是 glTextureStorage3D 的参数GL_R32F是内部格式,即要求OpenGL以怎样的方式使用这张纹理。而 glTextureSubImage3D 中的 GL_RED 和 GL_FLOAT,是外部格式,描述的是传给OpenGL的数据的格式,也可以说是让OpenGL如何解释传给它的imageData。如果内部格式和外部格式不同,OpenGL会进行转化,对于体数据来说,这可能会很费时

1.4 设置帧缓存

如果需要将图像绘制到某个指定的内存中,而不是屏幕上,则需要创建自己的帧缓存,并在绘制之前绑定它。
FBO ( Frame Buffer Object ) 并不直接存放数据,画面中的数据是附件的形式存在的。一个帧缓存可以包含多个附件,因为一帧的内容可能不仅包括我们肉眼看到的图像,如果开启深度测试,帧缓存中还会存放片元在相机中的深度,用来进行深度比较。
因此创建帧缓存时,除了创建 FBO,还需要创建真实存放数据的对象,它可以是一个纹理,也可以是渲染缓存对象 RBO ( Render Buffer Object ) 。
RBO ( Render Buffer Object ) 可以和 FBO 的附件进行绑定

这里使用OpenGL 4.5进行代码演示

m_iTargetSize[0] = 512;
m_iTargetSize[1] = 512;

//render to texture
//glCreateTextures(GL_TEXTURE_2D, 1, &m_iRenderTarget);
//glTextureStorage2D(m_iRenderTarget, 1, GL_R32F, m_iTargetSize[0], m_iTargetSize[1]);
    
//render to buffer
glCreateRenderbuffers(2, m_iRBOs);
glNamedRenderbufferStorage(m_iRBOs[0], GL_R32F, m_iTargetSize[0], m_iTargetSize[1]);
glNamedRenderbufferStorage(m_iRBOs[1], GL_DEPTH_COMPONENT, m_iTargetSize[0], m_iTargetSize[1]);
glCreateFramebuffers(1, &m_iFBO);
glNamedFramebufferRenderbuffer(m_iFBO, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_iRBOs[0]);
glNamedFramebufferRenderbuffer(m_iFBO, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_iRBOs[1]);  

用纹理做帧缓存附件,可以将一个Pass ( 渲染流程 )的结果传给下一个Pass使用。用RBO做附件,速度更快,一般用来读取渲染结果到内存。

2.渲染

渲染部分就是直接调用函数了。

2.1 渲染到屏幕

void VRT::Render()
{
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(m_iShader);
    glBindTextureUnit(0, m_iVolume);
	glBindVertexArray(m_iVAO);
	glDrawElements(GL_TRIANGLES, sizeof(indices), GL_UNSIGNED_INT, 0);
}

2.2 渲染到内存

void VRT::RenderToTarget(float* _pData, UInt _x, UInt _y)
{
    if (_x > m_iTargetSize[0] && _y > m_iTargetSize[1])
    {
        _pData = nullptr;
        return;
    }
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_iFBO);
    Render();
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

    glBindFramebuffer(GL_READ_FRAMEBUFFER, m_iFBO);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glReadPixels(0, 0, m_iTargetSize[0], m_iTargetSize[1], GL_RED, GL_FLOAT, _pData);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
}

3.体绘制算法

这里使用一个极简的着色器来展示体绘制原理,这个程序只能从一个固定的方向观察体数据。

3.1着色器程序

我们需要绘制一个铺满平面的四边形,顶点着色器里只是将输入的顶点直接输出。四边形的顶点范围是-1到1,直接需要转换到0到1变成纹理坐标,就是每个点在体数据中对应的位置。

#version 460 core                                         
layout(location = 0) in vec3 aPos;                        
out vec2 texCoord;
void main()   
{
	texCoord = vec2(aPos.x + 1.0, aPos.y + 1.0) * 0.5;
	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

经过OpenGL图元装配和光栅化之后,可以得到一个铺满平面的四边形,这样屏幕的四个角刚好对应四边形的四个角,而屏幕正好对应这体数据的一个面。以这个面上的点作为起始点进行光线步进 ( Ray Marching )

#version 460 core      
#define RAY_STEP 10               
uniform sampler3D volume;                           
in vec2 texCoord;                                  
out vec4 FragColor;                                  
                                                              
void main()
{
    vec3 StartPos = vec3(texCoord.xy, 0.0);//优化点 1
    vec3 Step = vec3(0.0, 0.0, 0.1); 
    vec3 CurrentPos = StartPos; 
    float res = 0.0;
    for (int i = 0; i < 10; i++)
    {
        float value = texture(volume, CurrentPos).r;//优化点 2
        res += value;//优化点 3
        CurrentPos += Step;
    }
    FragColor = vec4(res);
}

StartPos 是光线步进的起始点,Step是光线步进的步长。
沿着z方向步进十次,每步进一次,在当前位置采样一次体数据,将这十次采样的结果叠加,显示在屏幕上,这便是一个最简单的体绘制了。

3.2 进阶

这个体绘制Demo在以下两个方面进行改进

  1. 光线步进的起始点以及方向
    从不同的方向观察体数据

  2. 使用 Transfer Function 对采样得到的值进行映射
    可以突出显示感兴趣区域,剔除无关数据

  3. 采样结果的叠加方式
    直接影响体绘制效果,稍作修改直接变成Surface Shaded Display, Min/Max Intensity Projection

4.代码链接

你可以在这里找到该案例的程序

这有一个进阶版的体绘制Demo

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值