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的頂點先被渲染。
文章列表