CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探
2016-08-13
由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了。CSharpGL源码中包含10多个独立的Demo,更适合入门参考。
为了尽可能提升渲染效率,CSharpGL是面向Shader的,因此稍有难度。
一图抵千言
您可以在(http://files.cnblogs.com/files/bitzhuwei/VolumeRendering01.rar)下载此demo,或者到(https://github.com/bitzhuwei/CSharpGL)下载完整源码。
此demo来源于
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(); |
初始化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 }
遇到的问题
在legacy OpenGL里完全没有问题的渲染方式,换成modern OpenGL就出现问题了。
Volume rendering是需要开启blend的,这样才能画出半透明的效果。但是在modern OpenGL下,开启blend时,各个顶点的渲染顺序不同就会改变渲染出的结果。(legacy OpenGL则没有出现这个问题)
所以下一步需要对VBO里的顶点进行排序,使远离camera的顶点先被渲染。