• CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探


    CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探

     

    2016-08-13

    由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了。CSharpGL源码中包含10多个独立的Demo,更适合入门参考。

    为了尽可能提升渲染效率,CSharpGL是面向Shader的,因此稍有难度。

    +BIT祝威+悄悄在此留下版了个权的信息说:

    一图抵千言

    您可以在(http://files.cnblogs.com/files/bitzhuwei/VolumeRendering01.rar)下载此demo,或者到(https://github.com/bitzhuwei/CSharpGL)下载完整源码。

    此demo来源于

    +BIT祝威+悄悄在此留下版了个权的信息说:

    3D纹理

    比较常见的可能是2D纹理。用GL.TexImage2D(GL.GL_TEXTURE_2D,…);来设定2D纹理的数据。

     1             // generate texture.
     2             {
     3                 //  Lock the image bits (so that we can pass them to OGL).
     4                 BitmapData bitmapData = targetImage.LockBits(new Rectangle(0, 0, targetImage.Width, targetImage.Height),
     5                     ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
     6                 //GL.ActiveTexture(GL.GL_TEXTURE0);
     7                 GL.GenTextures(1, texture);
     8                 GL.BindTexture(GL.GL_TEXTURE_2D, texture[0]);
     9                 GL.TexImage2D(GL.GL_TEXTURE_2D, 0, (int)GL.GL_RGBA,
    10                     targetImage.Width, targetImage.Height, 0, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE,
    11                     bitmapData.Scan0);
    12                 //  Unlock the image.
    13                 targetImage.UnlockBits(bitmapData);
    14                 /* We require 1 byte alignment when uploading texture data */
    15                 //GL.PixelStorei(GL.GL_UNPACK_ALIGNMENT, 1);
    16                 /* Clamping to edges is important to prevent artifacts when scaling */
    17                 GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, (int)GL.GL_CLAMP_TO_EDGE);
    18                 GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, (int)GL.GL_CLAMP_TO_EDGE);
    19                 /* Linear filtering usually looks best for text */
    20                 GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, (int)GL.GL_LINEAR);
    21                 GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, (int)GL.GL_LINEAR);
    22             }
     

    类似地可以用GL.TexImage3D(GL.GL_TEXTURE_3D来设置一个3D纹理。

     1             GL.GenTextures(1, m_nTexId);
     2 
     3             GL.BindTexture(GL.GL_TEXTURE_3D, m_nTexId[0]);
     4             GL.TexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, (int)GL.GL_REPLACE);
     5             GL.TexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_S, (int)GL.GL_CLAMP_TO_BORDER);
     6             GL.TexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_T, (int)GL.GL_CLAMP_TO_BORDER);
     7             GL.TexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_WRAP_R, (int)GL.GL_CLAMP_TO_BORDER);
     8             GL.TexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MAG_FILTER, (int)GL.GL_LINEAR);
     9             GL.TexParameteri(GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MIN_FILTER, (int)GL.GL_LINEAR);
    10 
    11             //uint target, int level, int internalformat, int width, int height, int depth, int border, uint format, uint type, IntPtr pixels)
    12 
    13             GL.TexImage3D(GL.GL_TEXTURE_3D, 0, (int)GL.GL_RGBA, m_uImageWidth, m_uImageHeight, m_uImageCount, 0,
    14                 GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pRGBABuffer.Header);
    15             GL.BindTexture(GL.GL_TEXTURE_3D, 0);
     

    1D纹理是若干个点排成一排的一个线段。2D纹理是若干个1D纹理那样的线段排成的一个矩形。3D纹理是若干个2D纹理排成的一个长方体。如果理解了2D纹理,就可以推论到3D纹理上了。

    Legacy OpenGL如何调用3D纹理渲染体数据?

    OpenGL是不管什么体数据、volume rendering之类的,它只知道你设定了一个3D纹理,然后使用了这个纹理。

     
     1             GL.Clear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
     2 
     3             GL.Enable(GL.GL_ALPHA_TEST);
     4             GL.AlphaFunc(GL.GL_GREATER, alphaThreshold);
     5 
     6             GL.Enable(GL.GL_BLEND);
     7             GL.BlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
     8 
     9             GL.MatrixMode(GL.GL_TEXTURE);
    10             GL.LoadIdentity();
    11 
    12             GL.Enable(GL.GL_TEXTURE_3D);
    13             GL.BindTexture(GL.GL_TEXTURE_3D, m_pRawDataProc.GetTexture3D());
    14             for (float fIndx = -1; fIndx <= 1; fIndx += 0.01f)
    15             {
    16                 GL.Begin(GL.GL_QUADS);
    17 
    18                 GL.TexCoord3f(0.0f, 0.0f, ((float)fIndx + 1.0f) / 2.0f);
    19                 GL.Vertex3f(-dOrthoSize, -dOrthoSize, fIndx);
    20 
    21                 GL.TexCoord3f(1.0f, 0.0f, ((float)fIndx + 1.0f) / 2.0f);
    22                 GL.Vertex3f(dOrthoSize, -dOrthoSize, fIndx);
    23 
    24                 GL.TexCoord3f(1.0f, 1.0f, ((float)fIndx + 1.0f) / 2.0f);
    25                 GL.Vertex3f(dOrthoSize, dOrthoSize, fIndx);
    26 
    27                 GL.TexCoord3f(0.0f, 1.0f, ((float)fIndx + 1.0f) / 2.0f);
    28                 GL.Vertex3f(-dOrthoSize, dOrthoSize, fIndx);
    29 
    30                 GL.End();
    31             }
    32             GL.BindTexture(GL.GL_TEXTURE_3D, 0);

    Modern OpenGL如何调用3D纹理渲染体数据?

    Modern OpenGL渲染一个最简单的三角形都是很繁琐的(好处是执行效率高)。这里正好整理一下这个过程,以后我打算做个GUI的向导,让计算机自动生成那些模式化的代码,既避免低级错误,又加快开发效率,还利于新手学习。

    首先写出shader

    为什么要先写shader?

    因为shader虽小,五脏俱全,渲染一个模型所需的各路英雄都在里面露脸了。敲定了shader,之后就可以据此来逐步完成其他零散的部分。

    最基本的2个shader

    下面是用3D纹理渲染的vertex shader:

     1 #version 150 core
     2 
     3 in vec3 in_Position;
     4 in vec3 in_uv;
     5 out vec3 pass_uv;
     6 
     7 uniform mat4 MVP;
     8 
     9 void main(void) 
    10 {
    11     gl_Position = MVP * vec4(in_Position, 1.0);
    12 
    13     pass_uv = in_uv;
    14 }
     

    下面是用3D纹理渲染的fragment shader:

     
     1 #version 150 core
     2 
     3 out vec4 out_Color;
     4 in vec3 pass_uv;
     5 
     6 uniform sampler3D tex;
     7 
     8 void main(void) 
     9 {
    10     vec4 color = texture(tex, pass_uv);
    11     out_Color = color;
    12 }

    分析shader

    shader敲定后,我们要从这里找到这样一些信息:

    顶点属性

    顶点属性都在vertex shader里。

    这个例子中,有in_Position和in_uv两个属性。所以后面会有2个VBO。

    其他

    这个例子里还有一个' uniform sampler3D tex',所以后面会有1个3D纹理。

    总的来说,shader说的是如何渲染数据,它包含了数据和处理过程(即算法),所以在逻辑上是完整的。我们先写出shader,就可以以此为指导方针,创建VBO、纹理了。

    然后初始化shader

    这是比较固定的一个过程。在初始化过程中这个要靠前,因为其他部分是依赖它的。

     
     1         ShaderProgram InitializeShader()
     2         {    
     3             var vertexShaderSource = ManifestResourceLoader.LoadTextFile(@"VolumeRendering.DemoVolumeRendering01.vert");
     4             var fragmentShaderSource = ManifestResourceLoader.LoadTextFile(@"VolumeRendering.DemoVolumeRendering01.frag");
     5 
     6             var shaderProgram = new ShaderProgram();
     7             shaderProgram.Create(vertexShaderSource, fragmentShaderSource);
     8 
     9             shaderProgram.AssertValid();
    10 
    11             return shaderProgram;
    12         }

    然后初始化各个VBO

    我们基于下面这几条规律,设计初始化VBO的过程。

    VBO所需数据在CPU内存中指定,在初始化VBO时上传到GPU内存,此后CPU内存中的数据不再需要。

    OpenGL提供的设置VBO的指令glBufferData(GLenum target,GLsizeiptr size,const GLvoid * data,GLenum usage);和void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);没有任何业务逻辑上的含义,很容易出错,且难以调试。

    我习惯的使用VBO的方式是这样的:

    一个VBO只存放模型的一个顶点属性(例如只存放位置或只存放颜色)。这样能尽可能缩短一个VBO的长度,利于处理大量数据。

    一个VBO里只有一种基本的图形对象(例如只有三角形或只有六面体)。这个图形对象用一个struct描述。在CPU内存中设置模型数据时,不用void*而是用具体的struct*类型来赋值。例如:

     1                 //创建位置VBO,并绑定到shader里的in_Position
     2                 VR01PositionBuffer positionBuffer = new VR01PositionBuffer(strin_Position);
     3                 //在CPU内存中申请VBO需要的内存空间(非托管数组)
     4                 positionBuffer.Alloc(zFrameCount);
     5                 //获取非托管数组的首地址,并转换为struct QuadPosition*类型
     6                 QuadPosition* array = (QuadPosition*)positionBuffer.FirstElement();
     7                 //设定VBO里的数值
     8                 for (int i = 0; i < zFrameCount; i++)
     9                 {
    10                     array[i] = new QuadPosition(
    11                         new vec3(-xLength, -yLength, (float)i / (float)zFrameCount - 0.5f),
    12                         new vec3(xLength, -yLength, (float)i / (float)zFrameCount - 0.5f),
    13                         new vec3(xLength, yLength, (float)i / (float)zFrameCount - 0.5f),
    14                         new vec3(-xLength, yLength, (float)i / (float)zFrameCount - 0.5f)
    15                         );
    16                 }
    17                 //上传VBO数据到GPU内存,并获取renderer(用于渲染)。此时VR01PositionBuffer positionBuffer已经不再需要。
    18                 this.positionBufferRenderer = positionBuffer.GetRenderer();
    19                 //释放CPU内存(刚刚申请的非托管数组)
    20                 positionBuffer.Dispose();
    +BIT祝威+悄悄在此留下版了个权的信息说:

    初始化VAO

    初始化VAO实际上就是把渲染过程执行一遍。

     
     1         public void Create(RenderEventArgs e, Shaders.ShaderProgram shaderProgram)
     2         {
     3             uint[] buffers = new uint[1];
     4             GL.GenVertexArrays(1, buffers);
     5 
     6             this.ID = buffers[0];
     7 
     8             this.Bind();
     9             foreach (var item in this.bufferRenderers)
    10             {
    11                 item.Render(e, shaderProgram);
    12             }
    13             this.Unbind();
    14         }
    +BIT祝威+悄悄在此留下版了个权的信息说:

    遇到的问题

    在legacy OpenGL里完全没有问题的渲染方式,换成modern OpenGL就出现问题了。

    Volume rendering是需要开启blend的,这样才能画出半透明的效果。但是在modern OpenGL下,开启blend时,各个顶点的渲染顺序不同就会改变渲染出的结果。(legacy OpenGL则没有出现这个问题)

    所以下一步需要对VBO里的顶点进行排序,使远离camera的顶点先被渲染。

  • 相关阅读:
    关于sizeof,对空指针sizeof(*p)可以吗?
    Mysql之锁(一)
    Mysql之事务
    Mysql之Explain关键字及常见的优化手段
    Mysql查询优化器之关于子查询的优化
    Mysql查询优化器之关于JOIN的优化
    Mysql查询优化器之基本优化
    Mysql之B+树索引实战
    Mysql之索引
    Mysql之InnoDB行格式、数据页结构
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/CSharpGL-7-volume-rendering-first-try.html
Copyright © 2020-2023  润新知