這篇GPGPU 概念1: 數組= 紋理 - 文檔文章提出的數組與紋理相等讓人打開新的眼界與思維,本文在這文基礎上,嘗試把這部分思想拿來用在VBO粒子系統上.
在前面的文章中,我們把CPU的數據傳到GPU后,然后就直接從楨緩沖到顯示屏幕上了,那么還能不能把從GPU的數據拿回來放入CPU,然后進行處理。例如最基本的GPGPU編程中,把數組放入GPU運算后返回CPU。以及圖片用GPU來加速處理。
和以前把數據從CPU通過相關GLSL,Cg着色器語言傳入數據到GPU並進行處理不同,多了個返回數據到CPU中,因為GPU擅長的是並行處理,所以我們一般把一系列數據處理從CPU中交給GPU。在前面的頂點拾取中,獲取地形高度中,我們都把一系列數據當做紋理,然后從CPU傳入GPU中處理。現在關鍵問題是,如果把GPU數據送回到CPU中。
在前面這篇文章中(WebGL 利用FBO完成立方體貼圖),我們看到前面,可以先在FBO里,把當前的內存數據經過GPU處理后輸出到應用程序楨緩沖關聯的紋理中,然后此紋理拿來做后面球所需要的立方體貼圖。在這過程中,我們可以知道通過FBO實現,數據從CPU處理傳入GPU,在着色器中進行處理后,然后我們可以在CPU中得到對應FBO中紋理里的數據。
下面我們完成一個簡單實例,通過把一個數組,傳入GPU中,在GPU中進行處理,然后返回到CPU中並顯示出來。在這我們只介紹,數據一對一的處理,就是傳入多少數據,返回多少數據。
結合上面所說,大致過程如下,第一步創建FBO,並關聯一個紋理(存放經過GPU處理的數據),然后再創建一個紋理,里面用來存放我們需要處理的數據,因此要求,這個紋理和FBO中關聯的紋理大小一樣,方便處理。第二步,我們在創建的FBO中,來完成一個輸出渲染,因為上步,要求相同大小的紋理,所以這個渲染窗口我們需要特殊處理下,在這窗口,最大保證窗口大小與紋理大小一樣,紋理剛好占用整個窗口大小。這樣才能保證輸出到FBO中的紋理和傳入的紋理索引一一對應。這樣FBO中的紋理就存放的是原始數據經過GPU處理后的數據。
讓我們來完成整個過程,首先,我們把轉入的數據整理成我們需要的數組列表。請看代碼如下:

1 def __init__(this,*args): 2 this.imageFormat = GL_FLOAT 3 if len(args) == 1 : 4 if isinstance(args[0],Image): 5 image = args[0] 6 this.width = image.size[0] 7 this.height = image.size[1] 8 this.data = image.im 9 this.imageFormat = GL_UNSIGNED_BYTE 10 if isinstance(args[0],list): 11 listf = args[0] 12 this.width = len(listf) if len(listf) > 0 else 1 13 this.height = 1 14 this.data = listf 15 if len(args) == 3: 16 this.width = int(args[0]) 17 this.height = int(args[1]) 18 this.data = args[2] 19 this.imagedata = [] 20 for i in range(this.height): 21 for j in range(this.width): 22 index = i * this.width + j 23 f4 = this.data[index] if index < len(this.data) else float(j) / float(this.width) 24 r,g,b,a = 0.0,0.0,0.0,0.0 25 if isinstance(f4,tuple): 26 if len(f4) == 2: 27 r,g = f4 28 elif len(f4) == 3: 29 r,g,b = f4 30 else: 31 r,g,b,a = f4 32 else: 33 r = f4 34 this.imagedata.append(r) 35 this.imagedata.append(g) 36 this.imagedata.append(b) 37 this.imagedata.append(a) 38 #this.imagedata = ny.array(this.imagedata,dtype=ny.float) 39 #print this.imagedata
這段代碼里,會列舉幾種情況,一種是直接傳入的Image信息,還有是傳入的自定義數組,還有定義了格式化長與寬的數組。然后結合傳入數組的信息,可以是單個數據,也可以是如Image里保存的如r,g,b,a這樣的四元組,整理后,得到我們需要的紋理的長與寬,紋理所保存的數據。
然后如上面的第一步中所說,創建一個FBO和與之關聯的紋理,還有一個保存需要處理的數據的紋理。具體代碼如下:

1 def createFBO(this): 2 this.fbo = glGenFramebuffers(1) 3 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 4 #create texture for fbotext,GPU->CPU 5 this.fbotext = glGenTextures(1) 6 glBindTexture(GL_TEXTURE_2D,this.fbotext) 7 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, this.width, this.height,0,GL_RGBA,GL_FLOAT,None)#GL_FLOAT 8 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) 9 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) 10 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 11 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 12 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) 13 #relation fbo and fbotext(FBO and Texture2D) 14 glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,this.fbotext,0) #glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,this.fbotext,0) 15 glBindFramebuffer(GL_FRAMEBUFFER,0) 16 #save data to texture.CPU->GPU 17 this.fbodata = glGenTextures(1) 18 glBindTexture(GL_TEXTURE_2D,this.fbodata) 19 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) 20 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) 21 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 22 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 23 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) 24 print this.imageFormat 25 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, this.width, this.height, 0, GL_RGBA,this.imageFormat, this.imagedata) 26
就如上面所說,創建FBO,二個紋理,其中紋理需要注意的是glTexImage2D中的第三個參數,這里我們選擇的是GL_RGBA32F,這樣可以在紋理中,對應的r,g,b,a通道保證存放完整的32位浮點數據,而不必要限制在0.0-1.0。下面這些參數按理說可以不設置,但是如果你傳入的是圖片,為了保證處理后的圖片顯示正常,還是有必要設置的。其中glTexImage2D的參數對應詳細說明,如果大家有興趣,可以自己搜索。
下面就如上面第二步所說,創建輸出渲染,窗口需要與紋理大小一一對應,主要代碼如下:

1 def renderFBO(this,shader): 2 glDisable(GL_DEPTH_TEST) 3 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 4 glPushAttrib(GL_VIEWPORT_BIT)#| GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 5 glDrawBuffer(GL_COLOR_ATTACHMENT0) 6 glMatrixMode(GL_PROJECTION) 7 glLoadIdentity() 8 gluOrtho2D(0.0,this.width,0.0,this.height) 9 glMatrixMode(GL_MODELVIEW) 10 glLoadIdentity() 11 glViewport(0,0,this.width,this.height) 12 glActiveTexture(GL_TEXTURE0) 13 glBindTexture(GL_TEXTURE_2D, this.fbodata) 14 glUseProgram(shader) 15 glUniform1i(shader.tex0, 0) 16 glUniform1f(shader.xl, this.width) 17 glUniform1f(shader.yl, this.height) 18 glBegin(GL_QUADS) 19 glVertex2f(0.0, 0.0) 20 glVertex2f(this.width, 0.0) 21 glVertex2f(this.width, this.height) 22 glVertex2f(0.0, this.height) 23 glEnd() 24 glUseProgram(0) 25 glBindFramebuffer(GL_FRAMEBUFFER,0)
嗯,我發現需要把對應的着色器代碼一起貼出來,然后再好說明。請看對應的着色器代碼如下:

1 gpgpu_v = """ 2 //#version 330 compatibility 3 #version 130 4 out vec4 pos; 5 void main() { 6 pos = vec4(gl_Vertex); 7 //The following coding must be in fragment shader 8 //vec2 xy = v.xy; 9 //vec2 uv = vec2(xy/vec2(xw,yw)).xy; 10 //o_color = texture2D(tex0, uv); 11 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 12 }""" 13 14 gpgpu_f = """ 15 //#version 330 compatibility 16 #version 130 17 in vec4 pos; 18 uniform sampler2D tex0; 19 uniform float xw; 20 uniform float yw; 21 void main() { 22 vec2 xy = pos.xy; 23 vec2 uv = vec2(xy/vec2(xw,yw)).xy; 24 vec4 o_color = texture2D(tex0, uv);// vec4(uv.x,uv.y, 0, 1 );// 25 o_color = o_color + vec4(0.2); 26 gl_FragColor = o_color; 27 }"""
這二段代碼就是主要的過程了,首先告訴OpenGL,我們用的是應用程序楨緩沖(glBindFramebuffer),是不輸出到屏幕中的。首先就是把輸出窗口和紋理大小一一對應,保證紋理剛好顯示在一個滿屏幕上,因此,在這我們用的投影矩陣是正投影矩陣,和透視投影矩陣的區別,這里就不詳細說明,簡單來說,透視投影會帶給人如人眼中看到的遠處的特殊顯示比較少的效果,而正投影就是遠處和近處大小顯示不改變,他們主要就是對三維點xyz中的z值采用不同的處理造成的。而視圖矩陣,和模型矩陣一樣,他們不需要改變點的位置,直接采用單元矩陣就可。結合着色器中相應代碼,簡單來說,傳入的長與寬,是用了得到相應頂點的索引值,這里相應代碼在上文中的獲取地形的高度一樣的道理,這里不需要x,y二分量分別加0.5,是因為頂點一個是0-x,一個是(-x/2)-x/2.根據我們平常根據索引取數組里的值一樣,只是多考慮成二維的情況。特意說下,相關過程據需要放入片斷着色器中,因為我們是頂點和像素一一對應,只有在片斷中,才是針對像素的處理。
下面我們來驗證我們二個處理,看下能不能滿足我們。一個是圖片,我們把圖片里的r,g,b,a分量分別加上0.2,這樣看起來應該顯示更亮一些。第二個我們是傳入一個數組,然后按上面我們進行處理后,輸出到控制台里面。
第一個,傳入數據與相應改動如下(在上文與前面的基礎上): 在gpgpubasic中,新增一個方法,主要是輸出原始圖片,與經過GPU在各分量上加上0.2處理后的圖像。代碼增加如下:

1 def render(this,shader): 2 glMatrixMode(GL_PROJECTION) 3 glLoadIdentity() 4 gluOrtho2D(0.0,this.width * 2,0.0,this.height * 2) 5 glMatrixMode(GL_MODELVIEW) 6 glLoadIdentity() 7 glViewport(0,0,this.width,this.height) 8 9 glColor4f(1.0, 1.0, 1.0, 0.5) 10 glActiveTexture(GL_TEXTURE0) 11 glEnable(GL_TEXTURE_2D) 12 glBindTexture(GL_TEXTURE_2D, this.fbodata) 13 glBegin(GL_QUADS) 14 glTexCoord2f(0.0, 0.0) 15 glVertex2f(0.0, 0.0) 16 glTexCoord2f(1.0, 0.0) 17 glVertex2f(this.width, 0.0) 18 glTexCoord2f(1.0, 1.0) 19 glVertex2f(this.width, this.height) 20 glTexCoord2f(0.0, 1.0) 21 glVertex2f(0.0, this.height) 22 glEnd() 23 24 glTranslatef(this.width + 10, 0.0, 0.0) 25 glColor4f(1.0, 1.0, 1.0, 0.5) 26 glActiveTexture(GL_TEXTURE0) 27 glEnable(GL_TEXTURE_2D) 28 glBindTexture(GL_TEXTURE_2D, this.fbotext) 29 glBegin(GL_QUADS) 30 glTexCoord2f(0.0, 0.0) 31 glVertex2f(0.0, 0.0) 32 glTexCoord2f(1.0, 0.0) 33 glVertex2f(this.width, 0.0) 34 glTexCoord2f(1.0, 1.0) 35 glVertex2f(this.width, this.height) 36 glTexCoord2f(0.0, 1.0) 37 glVertex2f(0.0, this.height) 38 glEnd()
效果如下:
顯示效果如我們所想,整體上變亮了。
第二個例子,我們傳入一個數組,然后在GPU對每個數組加1。相應代碼個性如下:

1 part = particle.gpgpubasic([11,12,13,14,15]) 2 def InitGL(width,height): 3 glClearColor(0.1,0.1,0.5,0.1) 4 glClearDepth(1.0) 5 glEnable(GL_DEPTH_TEST) 6 glShadeModel(GL_SMOOTH) 7 #glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) 8 camera.move(0.0,1.3,0.0) 9 camera.setthree(True) 10 camera.length = 3 11 global shaderall 12 shaderall = shaderProg.allshader() 13 global colorMap 14 global hightMap 15 colorMap = loadtexture.Texture.loadmap("ground2.bmp") 16 #hight map for cpu to gpu 17 hightMap = loadtexture.Texture.loadmap("hight.gif") 18 #create terrain use cpu 19 hightimage = loadtexture.Texture.loadterrain("hight.gif") 20 image = open("ground2.bmp").convert("RGBA") 21 plane.setHeight(hightimage) 22 #global part 23 #part = particle.gpgpubasic(image) 24 part.createFBO() 25 pingpong.createFBO() 26 part.renderFBO(shaderall.gpgpuProgram)
因為是混合在上文的代碼中的,所以有一些多余代碼,主要就是創建一個[112,128,132,141,152]的數組,然后把上文中相關傳入圖片的相關代碼隱藏掉。原來着色器中增加vec4(0.2)改成vec4(1.0),並且在上文中的renderFBO添加如下代碼:

1 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 2 glReadBuffer(GL_COLOR_ATTACHMENT0) 3 data = glReadPixels(0, 0, this.width, this.height,GL_RGBA,GL_FLOAT) 4 print "fbo data:",type(data),len(data),data[0],data[1]#,data[2] 5 glPopAttrib() 6 glBindFramebuffer(GL_FRAMEBUFFER,0) 7 #close fbo 8 glBindTexture(GL_TEXTURE_2D, 0) 9 glEnable(GL_DEPTH_TEST)
顯示效果:
根據前面的代碼,可以看到,返回的數據長度還是5,前二個分量由112,128變成113,129了。
有了上面這些,我們已經知道如何把CPU的數據經過GPU處理后返回了,下面介紹一個在這基礎之上的乒乓技術,是一個用來把渲染輸出轉換成為下一次運算的輸入的技術,常見的如粒子系統里的運算如果放在GPU中,那么必需用乒乓技術來實現,因為GPU中數據是前次更新加上現在的結果。當然CPU中運算就簡單了,直接每楨改變內存中的數據,然后更新VBO,VAO啥的就行了。
實現原理很簡單,用FBO關聯二個紋理,在第一渲染時,用第一個紋理數據經GPU處理后放入FBO關聯的第二個紋理中,然后下一楨,把第二個紋理中的數據經過GPU處理放入第一個紋理。代碼如下:

1 class gpgpupingpong(object): 2 def __init__(this,*args): 3 this.imageFormat = GL_FLOAT 4 this.index = 0 5 if len(args) == 1 : 6 if isinstance(args[0],Image): 7 image = args[0] 8 this.width = image.size[0] 9 this.height = image.size[1] 10 this.data = image.im 11 this.imageFormat = GL_UNSIGNED_BYTE 12 if isinstance(args[0],list): 13 listf = args[0] 14 this.width = len(listf) if len(listf) > 0 else 1 15 this.height = 1 16 this.data = listf 17 if len(args) == 3: 18 this.width = int(args[0]) 19 this.height = int(args[1]) 20 this.data = args[2] 21 this.imagedata = [] 22 for i in range(this.height): 23 for j in range(this.width): 24 index = i * this.width + j 25 f4 = this.data[index] if index < len(this.data) else float(j) / float(this.width) 26 r,g,b,a = 0.0,0.0,0.0,0.0 27 if isinstance(f4,tuple): 28 if len(f4) == 2: 29 r,g = f4 30 elif len(f4) == 3: 31 r,g,b = f4 32 else: 33 r,g,b,a = f4 34 else: 35 r = f4 36 this.imagedata.append(r) 37 this.imagedata.append(g) 38 this.imagedata.append(b) 39 this.imagedata.append(a) 40 def createFBO(this): 41 this.fbo = glGenFramebuffers(1) 42 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 43 #create texture for fbotext,GPU->CPU 44 this.fbotext = glGenTextures(1) 45 glBindTexture(GL_TEXTURE_2D,this.fbotext) 46 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, this.width, this.height,0,GL_RGBA,GL_FLOAT,None)#GL_FLOAT 47 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) 48 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) 49 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 50 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 51 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) 52 #relation fbo and fbotext(FBO and Texture2D) 53 glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,this.fbotext,0) #glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,this.fbotext,0) 54 55 #save data to texture.CPU->GPU 56 this.fbodata = glGenTextures(1) 57 glBindTexture(GL_TEXTURE_2D,this.fbodata) 58 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, this.width, this.height, 0, GL_RGBA,GL_FLOAT,this.imagedata) 59 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) 60 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) 61 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 62 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 63 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) 64 glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT1,GL_TEXTURE_2D,this.fbodata,0) 65 66 glBindFramebuffer(GL_FRAMEBUFFER,0) 67 this.pingpong = [[GL_COLOR_ATTACHMENT0,this.fbodata],[GL_COLOR_ATTACHMENT1,this.fbotext]] 68 #cpu to gpu,and gpu compute result in fbo and fbotext.(cpu -(fbodata)> gpu 69 #-(fbotext)> cpu) 70 def renderFBO(this,shader): 71 ind = this.index % 2 72 this.index = this.index + 1 73 print "index",ind 74 pp = this.pingpong[ind] 75 glDisable(GL_DEPTH_TEST) 76 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 77 glDrawBuffer(pp[0]) 78 glPushAttrib(GL_VIEWPORT_BIT) 79 glMatrixMode(GL_PROJECTION) 80 glLoadIdentity() 81 gluOrtho2D(0.0,this.width,0.0,this.height) 82 glMatrixMode(GL_MODELVIEW) 83 glLoadIdentity() 84 glViewport(0,0,this.width,this.height) 85 glActiveTexture(GL_TEXTURE0) 86 glBindTexture(GL_TEXTURE_2D, pp[1]) 87 glUseProgram(shader) 88 glUniform1i(shader.tex0, 0) 89 glUniform1f(shader.xl, this.width) 90 glUniform1f(shader.yl, this.height) 91 glBegin(GL_QUADS) 92 glVertex2f(0.0, 0.0) 93 glVertex2f(this.width, 0.0) 94 glVertex2f(this.width, this.height) 95 glVertex2f(0.0, this.height) 96 glEnd() 97 glUseProgram(0) 98 glBindFramebuffer(GL_FRAMEBUFFER,0) 99 if ind == 0 : 100 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 101 glReadBuffer(GL_COLOR_ATTACHMENT0_EXT) 102 data = glReadPixels(0, 0, this.width, this.height,GL_RGBA,GL_FLOAT) 103 print "fbo 0:",len(data),data[0]#,data[1],data[2] 104 glBindFramebuffer(GL_FRAMEBUFFER,0) 105 else : 106 glBindFramebuffer(GL_FRAMEBUFFER,this.fbo) 107 glReadBuffer(GL_COLOR_ATTACHMENT1_EXT) 108 data = glReadPixels(0, 0, this.width, this.height,GL_RGBA,GL_FLOAT) 109 print "fbo 1:",len(data),data[0]#,data[1],data[2] 110 glBindFramebuffer(GL_FRAMEBUFFER,0) 111 112 glPopAttrib() 113 #close fbo 114 glBindTexture(GL_TEXTURE_2D, 0) 115 glEnable(GL_DEPTH_TEST)
整個過程和上面的差不多,但是需要注意的,在glBindFramebuffer后渲染前,需要指定輸出到的FBO對應的綁定點,就是glDrawBuffer(pp[0])。
我們可以看下效果。傳入pingpong = particle.gpgpupingpong([10,20,30]),然后每楨每頂點在GPU中加一。效果哪下(跑的有點快,只拿到加到200后的數據):
主要實踐就到這個,因為上文中,頂點的着色器中處理不復雜,傳入的頂點也不多,大家可能認為作用不大。但是大家如果想下,一張1000*1000像素的紋理,簡單來算GPU只要能並行處理100個點,那也比CPU要高100倍。
今天這個特殊日子,祝大家馬年吉祥,萬事如意。