テラByteの時代にキロByte

shader又はdemosceneに関係する事

pyOpenGLをace.jsのエディターでリアルタイムコンパイル

pythonOpenGLを使いだして何年か経つ。コンパイル時間が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については、記事を書きました。そちらも参考にしてください。

qiita.com