pyOpenGLをace.jsのエディターでリアルタイムコンパイル
pythonでOpenGLを使いだして何年か経つ。コンパイル時間がWebGLより断然速いし、FPSも出て素晴らしいのだが、これを使うための環境が酷過ぎる。いわゆるGUIが酷い。twiglのような快適に使えないのである。リアルタイムコンパイルを味わうとチマチマとshaderを書いていられないのです。以前eelを使いpyopenglからhtmlのimgタグにjpegを送るエディターを作ったけどfpsがイマイチ。WebGLの方が速いんじゃないって感じ。だけど遂に出来た。twiglと同じace.jsでリアルタイムコンパイル。映像は別ウィンドウを使うから転送ロスはなし。
GUIライブラリにflexxを使った。ほぼ日本語情報無し。英語も本家のマニュアルくらいしか無い。eelとpywebviewを触った経験を元に何とか出来ました。flexxってGUIライブラリは、かなり良さそうなのに普及されてないのは、もったいないと思う。たぶん入れ口が難解なせいかもしれない。実際解ってみるとコアで使う部分は少ない。色々と出来過ぎちゃうのが敗因では。コードの記述量も少なくて済むし良いと思うな。flexxを使うにはFirefoxが必要みたいです。
shaderは自由に書ける。でも、書け過ぎちゃうので共有が大変な代物でもある。なので、shadertoyとかGLSLsandboxのフォーマットで、みんな書いている。今回のエディターでは、twiglのgeeker(MRT)のフォーマットを使っている。これにfloat textureのバックバッファを4枚用意した。かなりの事が出来る予感はある。このフォーマットの良いところはshaderが一枚で書ける事。何枚か有ると管理が大変すぎエディターを作るのも面倒だ。後、#define o o0
#define b b0
を使うとgeeker(300es)になる。
ということで、このpyOpenGL with ace.jsのリアルタイムコンパイルエディターを共有します。これで一山越えたのもあるし、ここから先は、オレオレスタイルの為、共有しようとしたところで共感もらえない所に行くと思うので、ここで放出しておきます。
from flexx import flx from OpenGL.GL import * from OpenGL.WGL import * from ctypes import * from ctypes.wintypes import * import numpy import threading import time kernel32 = windll.kernel32 user32 = windll.user32 winmm = windll.winmm class GLui: def __init__(self, **kwargs): self.size=(640,480) self.pos=(0,0) self.__dict__.update(kwargs) self.success = -1 self.compileLog='' self.active = False self.flag = True self.shaderHeader = """ #version 430 uniform vec2 r; uniform float t; uniform sampler2D b0; uniform sampler2D b1; uniform sampler2D b2; uniform sampler2D b3; layout (location = 0) out vec4 o0; layout (location = 1) out vec4 o1; layout (location = 2) out vec4 o2; layout (location = 3) out vec4 o3; #define o o0 #define b b0 #define FC gl_FragCoord """ self.shader = "void main(){o-=o;}" def compileProgram(self,src): shader = glCreateShader(GL_FRAGMENT_SHADER) glShaderSource(shader, src) glCompileShader(shader) if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE: self.compileLog=glGetShaderInfoLog(shader).decode() glDeleteShader(shader) return -1 else: self.compileLog='Success' program = glCreateProgram() glAttachShader(program, shader) glDeleteShader(shader) glLinkProgram(program) return program def glDraw(self): def createFramebuffer(widrh,height,mrt): frameBuffer = glGenFramebuffers(2) textures =glGenTextures(mrt * 2) for j in range(2): glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer[j]) attach = [] for i in range(mrt): glActiveTexture(GL_TEXTURE0 + i + j * mrt) glBindTexture(GL_TEXTURE_2D, textures[i + j * mrt]) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, None) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, textures[i + j * mrt], 0) attach.append(GL_COLOR_ATTACHMENT0 + i) glDrawBuffers(mrt, numpy.array(attach, numpy.uint32)) glBindFramebuffer(GL_FRAMEBUFFER, 0) return frameBuffer WS_OVERLAPPEDWINDOW = 0xcf0000 WS_VISIBLE = 0x10000000 hWnd = user32.CreateWindowExA(0,0xC018,0,WS_OVERLAPPEDWINDOW|WS_VISIBLE, self.pos[0], self.pos[1], self.size[0], self.size[1],0,0,0,0) hdc = user32.GetDC(hWnd) user32.SetForegroundWindow(hWnd) pfd = PIXELFORMATDESCRIPTOR(0,1,33,32,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0) SetPixelFormat(hdc, ChoosePixelFormat(hdc, pfd), pfd) hGLrc = wglCreateContext(hdc) wglMakeCurrent(hdc, hGLrc) mrt=4 width, height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) frameBuffer = createFramebuffer(width, height,mrt) sSub ="#version 430\nuniform vec2 r;uniform sampler2D b;out vec4 o;void main(){o=texelFetch(b,ivec2(gl_FragCoord.xy),0);}" pSub=self.compileProgram(sSub) glUseProgram(pSub) glUniform2f(glGetUniformLocation(pSub, "r"), self.size[0], self.size[1]) pMain=self.compileProgram(self.shaderHeader + self.shader) glUseProgram(pMain) glUniform2f(glGetUniformLocation(pMain, "r"), self.size[0], self.size[1]) # GL loop msg = MSG() lpmsg = pointer(msg) cnt, s0 = 0, 0 self.zero = winmm.timeGetTime() id = 0 self.active = True while self.active: while user32.PeekMessageA(lpmsg, 0, 0, 0, 1): if (msg.message == 161 and msg.wParam == 20): self.active=False user32.DispatchMessageA(lpmsg) if self.flag is False: p=self.compileProgram(self.shaderHeader + self.shader) self.success = p if p>-1: tmp=pMain pMain=p glDeleteProgram(tmp) self.flag = True #time.sleep(0.01) #if(user32.GetAsyncKeyState(27)):break rect = RECT() user32.GetClientRect.restype = ctypes.c_bool user32.GetClientRect.argtypes = (ctypes.c_long, ctypes.POINTER(RECT)) user32.GetClientRect(hWnd, rect) _width, _height=rect.right-rect.left, rect.bottom-rect.top if width!=_width or height!=_height: width, height=_width,_height frameBuffer = createFramebuffer(width, height,mrt) glViewport(0, 0, width, height) t = (winmm.timeGetTime() - self.zero)*0.001 glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer[1-id]) glUseProgram(pMain) glUniform2f(glGetUniformLocation(pMain, "r"), width, height) glUniform1f(glGetUniformLocation(pMain, "t"), t) for i in range(mrt): glUniform1i(glGetUniformLocation(pMain, "b{0}".format(i)), id*mrt+i); glRects(1, 1, -1, -1) glBindFramebuffer(GL_FRAMEBUFFER, 0) glUseProgram(pSub) glUniform2f(glGetUniformLocation(pSub, "r"), width, height) glUniform1i(glGetUniformLocation(pSub, "b"), (1-id)*mrt); glRects(1, 1, -1, -1) id = 1-id SwapBuffers(hdc) wglMakeCurrent(0, 0) wglDeleteContext(hGLrc) user32.ReleaseDC(hWnd, hdc) user32.PostQuitMessage(0) user32.DestroyWindow(hWnd) def run(self): t = threading.Thread(target=self.glDraw) t.start() def stop(self): self.active=False def compile(self,src): self.shader=src self.flag = False def getSuccess(self): return self.success def getCompileLog(self): while self.flag is False: time.sleep(0.01) return self.compileLog base_url = 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/' flx.assets.associate_asset(__name__, base_url + 'ace.js') flx.assets.associate_asset(__name__, base_url + 'mode-glsl.js') flx.assets.associate_asset(__name__, base_url + 'theme-tomorrow_night_blue.js') class CodeEditor(flx.Widget): CSS = """ .flx-CodeEditor > .ace { width: 100%; height: 100%; } """ def init(self): global window self.ace = window.ace.edit(self.node, "editor") self.ace.navigateFileEnd() # otherwise all lines highlighted self.ace.setTheme("ace/theme/tomorrow_night_blue") self.ace.getSession().setMode("ace/mode/glsl") self.ace.setFontSize(16) self.ace.session.on("change", self.compile); @flx.action def setValue(self, src): self.ace.setValue(src) @flx.emitter def compile(self): for s in self.ace.getValue().split('\n'): self.emit('value',dict(line=s)) return {} class App(flx.PyComponent): def init(self): self.widget = CodeEditor() self.value = '' global shader self.widget.setValue(shader) @flx.reaction('!widget.value') def _foo(self, *events): for ev in events: self.value += ev['line'] + '\n' @flx.reaction('widget.compile') def _foo2(self, *events): global gxf gfx.compile(self.value) self.value = '' print(gfx.getCompileLog()) if __name__ == '__main__': shader = """#define R(p,a,r)mix(a*dot(p,a),p,cos(r))+sin(r)*cross(p,a) void main(){ vec3 rd=normalize(vec3((FC.xy*2.-r)/r.y,-2)); vec3 ro=vec3(0,0,-t); float g=0.,e; for(int i=0;i<99;i++) { vec3 p=rd*g+ro; p=fract(p)-.5; p=R(p,vec3(.557),t); g+=e=.6*length(p-clamp(p,-.2,.2)); e<.001?o1+=.4/i:o1; } o0=textureLod(b1,FC.xy/r,0.); if(all(lessThan(abs(FC.xy/r-.5),vec2(.38)))) o0=vec4(.7,.5,.3,0)-o0; } """ gfx = GLui( size=(640,480), pos=(10,10) ) gfx.run() flx.launch( App, title='ShaderEdtor', size=(700, 500), pos=(650, 10) ) flx.run() gfx.stop()
version 430を採用しているので、若干version300esと違います。pyopengl部分は、ctypesを使っているので、ほぼほぼC言語と同じになってます。コンパイルエラーは標準出力にしてあります。
Flexxについては、記事を書きました。そちらも参考にしてください。