pygameでsound shaderを使い音を鳴らす
普段使っているsound shaderをpygameに組み込んでみた。
transform feedbackを使っているのでグラフィック部分は要らないのでOpenGLのcontextは短く書ける。
compute shaderでやる方法もあるので、そっちも試したい。
とりあえず、コードだけ。
from os import environ environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' import pygame from OpenGL.GL import * from OpenGL.WGL import * from ctypes import * import pyaudio import numpy user32 = windll.user32 width,height = 340,240 pygame.init() screen_display = pygame.display display = screen_display.set_mode((width,height)) # Get OpenGL context hWnd = user32.CreateWindowExA(0,0xC018,0,0,0,0,0,0,0,0,0,0) hDC = user32.GetDC(hWnd) pfd = PIXELFORMATDESCRIPTOR(0,1,0,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) # audio chunk = 4096 rate = 48000 channels = 2 samples = (c_float * chunk * channels)() chunkData=numpy.frombuffer(samples, dtype=numpy.float32) vbo = glGenBuffers(1) glBindBuffer(GL_ARRAY_BUFFER, vbo) glBufferData(GL_ARRAY_BUFFER, sizeof(samples), None, GL_STATIC_DRAW) glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo) program = glCreateProgram() shader = glCreateShader(GL_VERTEX_SHADER) glsl=''' #version 300 es precision highp float; out vec2 gain; uniform float iFrameCount; const float iChunk = {0:.1f}; const float iSampleRate = {1:.1f}; '''.format(chunk ,rate)+''' vec2 mainSound( int samp, float time ); void main(){ float time = (iChunk * iFrameCount + float(gl_VertexID)) / iSampleRate; gain = clamp(mainSound(int(iSampleRate),time), -1.0, 1.0); } ''' glsl += ''' vec2 mainSound(int samp,float time){ return 0.5*vec2(sin(3.14*440.*time)); } ''' glShaderSource(shader, glsl) glCompileShader(shader) if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE: raise RuntimeError(glGetShaderInfoLog(shader).decode()) glAttachShader(program, shader) glDeleteShader(shader) outs = cast((c_char_p*1)(b"gain"), POINTER(POINTER(c_char))) glTransformFeedbackVaryings(program, 1, outs, GL_INTERLEAVED_ATTRIBS) glLinkProgram(program) glUseProgram(program) p = pyaudio.PyAudio() stream = p.open( format = pyaudio.paFloat32, channels = channels, rate = rate, frames_per_buffer = chunk, output=True, input=False ) stream.start_stream() tick = 0 running = True while running: glUniform1f(glGetUniformLocation(program, "iFrameCount"), tick) glEnable(GL_RASTERIZER_DISCARD) glBeginTransformFeedback(GL_POINTS) glDrawArrays(GL_POINTS, 0, chunk) glEndTransformFeedback() glDisable(GL_RASTERIZER_DISCARD) glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(samples), byref(samples)) data=numpy.frombuffer(samples, dtype=numpy.float32) stream.write(data.tobytes()) tick += 1 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False stream.stop_stream() stream.close() # OpenGL finsh wglMakeCurrent(0, 0) wglDeleteContext(hGLrc) user32.ReleaseDC(hWnd, hDC) user32.PostQuitMessage(0) user32.DestroyWindow(hWnd) pygame.quit()
pygameでOpenGLとその他のimageを同居させる
pygameでOpenGLをすると全画面を独占される。それだとglutでやる事と変わらない。pygameの特性を活かしきれない。そこでOpenGLをimageとして使う方法をやってみた。imageをつくるのにpycairoというライブラリーも使ってる。細かい説明も厄介なのでコードだけ。
from os import environ environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' import pygame from OpenGL.GL import * from OpenGL.WGL import * import cairo import numpy import math from ctypes import * user32 = windll.user32 width, height = 512, 512 pygame.init() pygame.display.set_mode((width, height)) window = pygame.display.get_surface() background = pygame.Surface((width, height)) background.fill((20,20,20)) # Get OpenGL context SM_CXSCREEN, SM_CYSCREEN = 0,1 WS_OVERLAPPEDWINDOW = 0xcf0000 PFD_SUPPORT_OPENGL = 32 PFD_DOUBLEBUFFER = 1 xsc, ysc =user32.GetSystemMetrics(SM_CXSCREEN),user32.GetSystemMetrics(SM_CYSCREEN) hwnd = user32.CreateWindowExA(0,0xC018,0,WS_OVERLAPPEDWINDOW,0,0,xsc,ysc,0,0,0,0) hdc = user32.GetDC(hwnd) user32.SetForegroundWindow(hwnd) pfd = PIXELFORMATDESCRIPTOR(0,1,PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER,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) wglc = wglCreateContext(hdc) wglMakeCurrent(hdc, wglc) # OpenGL init program = glCreateProgram() shader = glCreateShader(GL_FRAGMENT_SHADER) glsl = ''' #version 430 out vec4 fragColor; uniform vec2 resolution; uniform float time; void main() { vec2 uv = gl_FragCoord.xy/resolution; vec3 col = 0.5 + 0.5*cos(time+uv.xyx+vec3(0,2,4)); fragColor = vec4(col,1); } ''' glShaderSource(shader, glsl) glCompileShader(shader) if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE: raise RuntimeError(glGetShaderInfoLog(shader).decode()) glAttachShader(program, shader) glLinkProgram(program) glUseProgram(program) pixels = numpy.zeros((width, height,3), numpy.uint8) gl_width = width-50 gl_height = height-200 glViewport(0, 0, gl_width, gl_height) glUniform2f(glGetUniformLocation(program, "resolution"), gl_width, gl_height) cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 120, 120) def cairo_draw(surface): ctx = cairo.Context(surface) ctx.set_source_rgb(0.8, 0.5, 0.1) ctx.rectangle(10, 10, 110, 110) ctx.fill() fpsClock = pygame.time.Clock() running = True while running: time = pygame.time.get_ticks()/1000 window.blit(background, (0, 0)) glUniform1f(glGetUniformLocation(program, "time"), time) glRects(1, 1, -1, -1) glReadPixels(0, 0, pixels.shape[0],pixels.shape[1], GL_RGB, GL_UNSIGNED_BYTE, pixels) image = pygame.image.frombuffer( pixels.tobytes(), pixels.shape[:2], 'RGB') window.blit(image, (25,100), pygame.Rect(0, 0, gl_width, gl_height)) cairo_draw(cairo_surface) image = pygame.image.frombuffer( numpy.array(cairo_surface.get_data())[..., ::-1].tobytes(), (cairo_surface.get_width(),cairo_surface.get_height()), 'ARGB') x= (width-cairo_surface.get_width())/2 + 150*math.sin(time) y= (height-cairo_surface.get_height())/2 + 150*math.cos(time) window.blit(image, (x,y)) pygame.display.flip() for event in pygame.event.get(): if event.type == pygame.QUIT: running = False fpsClock.tick(30) # OpenGL finish wglMakeCurrent(0, 0) wglDeleteContext(wglc) user32.ReleaseDC(hwnd, hdc) user32.PostQuitMessage(0) user32.DestroyWindow(hwnd) pygame.quit()
JSの圧縮
JSの圧縮ツールにJsExeというのがある。ちょっとダウンロードしようとしたが出来なかった。
JsExeは持っていたが、どうせならばで作ってみた。
参考にした記事は
yomotsu.net
正月早々に、俺は何でJSでバイナリなんて弄っているんだって、ぼやきながら、ずぶずぶにハマっていた。で、出来てみれば、大したことのない行数で済んでしまうんだな。こんな感じ。python3で書いてます。
from PIL import Image import numpy import base64 from io import BytesIO def js2img(js): js = numpy.array(list(js.encode()), dtype='uint8').reshape((1, len(js))) img = Image.fromarray(js) buffer = BytesIO() img.save(buffer, "png") return buffer.getvalue() def asciiHtml(js, filename='asciiDemo.html'): f = open(filename, 'w') f.write( "<canvas id=c><img src='data:image/png;base64," + base64.b64encode(js2img(js)).decode() + "' onload=C=c.getContext('2d');for($=_='';C.drawImage(this,-$,0),X=C.getImageData(0,0,1,1).data[0],$++<this.width;_+=String.fromCharCode(X));(1,eval)(_)>" ) f.close() def binaryHtml(js, filename='binaryDemo.html'): f = open(filename, 'bw') f.write( js2img(js) + b"<canvas id=c><img src=# onload=C=c.getContext('2d');for($=_='';C.drawImage(this,-$,0),X=C.getImageData(0,0,1,1).data[0],$++<this.width;_+=String.fromCharCode(X));(1,eval)(_)>" ) f.close() def stdHtml(js, filename='stdDemo.html'): f = open(filename, 'w') f.write( "<body><script>" + js + "</script></body>" ) f.close() js = ''' var canvas = document.createElement("canvas"); canvas.style.position = "fixed"; canvas.style.cursor = "none"; canvas.style.left = canvas.style.top = 0; canvas.width = window.innerWidth; canvas.height = window.innerHeight; document.body.appendChild(canvas); var gl = canvas.getContext("webgl2") || canvas.getContext("experimental-webgl2"); var compileShader = function(prog, src, type){ var sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh); gl.attachShader(prog, sh); gl.deleteShader(sh); }; vs = `#version 300 es void main() { gl_Position = vec4(ivec2(gl_VertexID&1,gl_VertexID>>1)*2-1,0,1); } ` fs = `#version 300 es precision mediump float; uniform vec2 resolution; uniform float time; out vec4 O; #define R(p,a,t) mix(a*dot(p,a),p,cos(t))+sin(t)*cross(p,a) #define H(h) (cos((h)*6.3+vec3(0,23,21))*.5+.5) void main(){ vec3 p,c=vec3(0), d=normalize(vec3((gl_FragCoord.xy-.5*resolution.xy)/resolution.y,1)); float i=0.,s,e,g=0.,t=time; for(;i++<99.;){ p=g*d;; p.z-=3.; p=R(p,vec3(.577),t*.3); s=3.; for(int i=0;i++<8;p*=e) p=vec3(1,3.+sin(t)*.3,2)-abs(p-vec3(1,2,1.5+sin(t)*.2)), s*=e=9./clamp(dot(p,p),.8,9.); g+=e=abs(p.y/s-.001)+1e-3; c+=mix(vec3(1),H(length(p*.2+.5)),.6)*.0015/i/e; } c*=c; O=vec4(c,1); } ` var p = gl.createProgram(); compileShader(p, vs, gl.VERTEX_SHADER); compileShader(p, fs, gl.FRAGMENT_SHADER); gl.linkProgram(p); gl.useProgram(p); gl.uniform2f(gl.getUniformLocation(p, "resolution"), canvas.width, canvas.height); var zero = Date.now(); (function () { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.uniform1f(gl.getUniformLocation(p, "time"), (Date.now() - zero) * 0.001); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimationFrame(arguments.callee); })(); ''' stdHtml(js) asciiHtml(js) binaryHtml(js)
これで出来たbinaryDemo.html
が圧縮の対象。
stdDemo.html 1,986 バイト asciiDemo.html 2,005 バイト binaryDemo.html 1,530 バイト
こんな感じの圧縮状況。
binaryDemo.html はバイナリなので、通常では見れない。localhostを立てて見ます。完全にdemoparty仕様です。
python3を使ってlocalhostを立ててみるには
python -m http.server
これでOK。
http://localhost:8000/
にアクセスしてください。
asciiDemo.html はbase64を使った奴。サーバー上じゃなくても見れます。しかしJSのコードがbase64に変換されるので、まあ読めなくなりますね。
stdDemo.html は普通のパターンな奴。
ちょっと気がかりな点でperformance.now()
を使うとバイナリhtmlが表示出来なかった。他にも落とし穴的な関数があるかもしれない。
簡単なトリミング
JSの空白とか改行をトリミングする関数。shader用に使っていたのを、ちょっと弄ったので、まだ実績が無い。とりあえず使えそうなので載せておく。想定外なエラーが出る可能性有りです。
import re def trim(src): src = re.compile(r'/\*.*?\*/', re.DOTALL).sub("", src) src = re.sub(r"//.*", "", src) src = re.sub(r"\t", " ", src) src = re.sub(r" +", " ", src) src = re.sub(r" *\n *", "\n", src) src = re.sub(r"\n+", "\n", src) src = re.sub(r"^\n", "", src) ##### line = src.split("\n") for i in range(len(line)): s = line[i] if re.search("#", s) != None: line[i] = "\n" + line[i] + "\n" else: s = re.sub(r" *\+ *" ,"+", s) s = re.sub(r" *\- *" ,"-", s) s = re.sub(r" *\* *" ,"*", s) s = re.sub(r" */ *" ,"/", s) s = re.sub(r" *= *" ,"=", s) s = re.sub(r" *< *" ,"<", s) s = re.sub(r" *> *" ,">", s) s = re.sub(r" *& *" ,"&", s) s = re.sub(r" *\| *" ,"|", s) s = re.sub(r" *\( *" ,"(", s) s = re.sub(r" *\) *" ,")", s) s = re.sub(r" *\[ *" ,"[", s) s = re.sub(r" *\] *" ,"]", s) s = re.sub(r" *{ *" ,"{", s) s = re.sub(r" *} *" ,"}", s) s = re.sub(r" *; *" ,";", s) s = re.sub(r" *, *" ,",", s) line[i] = s src = "".join(line) src = re.sub(r"\n+","\n", src) return src
あとがき
JsExeはpngのバイナリを壊して更にminifyしてます。そこまではやってません。8bitグレースケールのpngを使っているので、大体良い線にはいっていると思います。
今年はコードを隠してみようかなってって考えてます。今まではコードを見て見て感が強くて、良いトリックを思いつくと、それで満足しちゃうところあって絵がおろそかになってました。そんな事もあって年の頭に、面倒な事をやってました。
追記
以前にJsExeを使って圧縮したファイルのサイズをみたところ、半分くらいに圧縮されているようでした。まだまだ奥がありそうです。pngの圧縮も出来るようなので、その辺りの技術を使っているのかもしれません。いづれ手を出すかも。まあ素直にJsExeを使わせて貰えって話もあるけど、そういうハックも面白かったりするので困ったものです。
後日の為に情報を残しておきます。
ハフマン符号化
リアルタイムコンパイルのsound shader editor
考えてみたら、リアルタイムコンパイルのsound shader editorって無いですね。使いたい人がいるかもで、pythonのソースを載せる事にしました。ファイルの入出力は付いていません。ライブコーディングみたいな感じで使えます。ちょこちょこと音が変わるので面白いです。midiも使えます。m0~m15まで用意しました。0~1を割り当ててます。mix(440.,880.,mo0)
みたいな感じで使うのがいいかと。リアルタイムコンパイルは外せます。OpenGLなのでコンパイルが速いので、ストレスはあまりないと思いますが、チャンクとチャンクの間の書き込み時間にコンパイルをさせているので、重いshaderだと音が遅れることが有るかも。
GUIにflexxを使っています。エディターにCodeMirror.jsです。Ace.jsはflexxと相性が悪いみたいなのでCodeMirror.jsにしました。ただリンクを辿れるところにGLSLのシンタックスハイライトが無かったので使えませんでした。glsl.js
を自力で書くか、探してローカルに置けば使えます。
FFTのグラフは、良く解らなかったので、適当にそれっぽく使っているだけです。解る人がいたら教えてください。
shadertoyのmusic shaderをコピペで音が出ます。
お勧めは
https://www.shadertoy.com/user/athibaul
https://www.shadertoy.com/user/nabr
この2人です。
from OpenGL.GL import * from OpenGL.WGL import * from ctypes import * import numpy import time import rtmidi2 import threading import pyaudio import re from flexx import flx import os kernel32 = windll.kernel32 user32 = windll.user32 winmm = windll.winmm class Tick(): def __init__(self, chunk, rate): self.n = 0 self.chunk = chunk self.rate = rate self.startN = 0 self.endN = 600 def clucN(self, sec): return sec * self.rate / self.chunk def clucTime(self, n): return n * self.chunk / self.rate def startTime(self, sec): self.startN = self.clucN(sec) self.n = max(self.startN, self.n) def endTime(self, sec): self.endN = self.clucN(sec) self.n = min(self.endN, self.n) def reset(self): self.n = self.startN def time(self): return self.clucTime(self.n) def tick(self): self.n += 1 if self.endN < self.n: self.n = self.startN return self.n class ShaderSound: def __init__(self, **kwargs): self.chunk = 4096 self.rate = 48000 self.channels = 2 self.shader = """vec2 mainSound(int samp, float time) { return vec2( sin(6.2831*440.0*time)*exp(-3.0*time) ); } """ self.__dict__.update(kwargs) self.tick = Tick(self.chunk, self.rate) self.success = -1 self.compileLog='' self.chunkData=[] self.active = False self.flag = True self.volume = 1.0 self.midi = [0] * 16 self.time=0 self.startTime=0 self.endTime=180 self.head = """ #version 430 out vec2 gain; uniform float iFrameCount; uniform float m0; uniform float m1; uniform float m2; uniform float m3; uniform float m4; uniform float m5; uniform float m6; uniform float m7; uniform float m8; uniform float m9; uniform float m10; uniform float m11; uniform float m12; uniform float m13; uniform float m14; uniform float m15; const float iChunk = {0:.1f}; const float iSampleRate = {1:.1f}; """.format(self.chunk ,self.rate) + """ vec2 mainSound( int samp, float time ); void main(){ float time = (iChunk * iFrameCount + float(gl_VertexID)) / iSampleRate; gain = clamp(mainSound(int(iSampleRate),time), -1.0, 1.0); } """ def compileProgram(self,src): shader = glCreateShader(GL_VERTEX_SHADER) glShaderSource(shader, self.head + 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) outs = cast((c_char_p*1)(b"gain"), POINTER(POINTER(c_char))) glTransformFeedbackVaryings(program, 1, outs, GL_INTERLEAVED_ATTRIBS) glLinkProgram(program) return program def glDraw(self): hWnd = user32.CreateWindowExA(0,0xC018,0,0,0,0,0,0,0,0,0,0) hDC = user32.GetDC(hWnd) pfd = PIXELFORMATDESCRIPTOR(0,1,0,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) self.samples = (c_float * self.chunk * 2)() vbo = glGenBuffers(1) glBindBuffer(GL_ARRAY_BUFFER, vbo) glBufferData(GL_ARRAY_BUFFER, sizeof(self.samples), None, GL_STATIC_DRAW) glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, vbo) p = pyaudio.PyAudio() stream = p.open( format = pyaudio.paFloat32, channels = 2, rate = self.rate, frames_per_buffer = self.chunk, output=True, input=False ) stream.start_stream() self.active = True pMain = self.compileProgram("vec2 mainSound(int samp,float time){return vec2(0);}") while self.active: time.sleep(0.01) if self.flag is False: p=self.compileProgram(self.shader) self.success = p if p>-1: tmp=pMain pMain=p glDeleteProgram(tmp) self.flag = True glUseProgram(pMain) glUniform1f(glGetUniformLocation(pMain, "iFrameCount"), self.tick.tick()) for i in range(16): glUniform1f(glGetUniformLocation(pMain, "m{0}".format(i)), self.midi[i]); glEnable(GL_RASTERIZER_DISCARD) glBeginTransformFeedback(GL_POINTS) glDrawArrays(GL_POINTS, 0, self.chunk) glEndTransformFeedback() glDisable(GL_RASTERIZER_DISCARD) glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(self.samples), byref(self.samples)) data=numpy.frombuffer(self.samples, dtype=numpy.float32) self.chunkData=data stream.write((data*self.volume).tobytes()) stream.stop_stream() stream.close() 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 getShader(self): return self.shader def getCompileLog(self): while self.flag is False: time.sleep(0.01) return self.compileLog def getTime(self): return self.tick.time() def getChunkData(self): return self.chunkData def reTime(self): self.tick.reset() def setTimeBand(self,start,end): self.tick.startTime(start) self.tick.endTime(end) def getTimeBand(self): return dict( start_time = self.startTime, end_time = self.endTime ) def setMidiValue(self, idx, value): if idx>-1 and idx<16: self.midi[idx]=value def setVolume(self, volume): self.volume=volume # web editorは CodeMirrorを使ってます。 https://codemirror.net/ base_url = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/' flx.assets.associate_asset(__name__, base_url + '5.21.0/codemirror.min.css') flx.assets.associate_asset(__name__, base_url + '5.21.0/codemirror.min.js') flx.assets.associate_asset(__name__, base_url + '5.21.0/theme/midnight.css') flx.assets.associate_asset(__name__, base_url + '5.21.0/theme/rubyblue.css') flx.assets.associate_asset(__name__, base_url + '5.21.0/theme/erlang-dark.css') flx.assets.associate_asset(__name__, base_url + '5.21.0/theme/cobalt.css') # ローカルにシンタックスハイライトのスクリプトを書いておく場合。 #with open(os.getcwd() + '/glsl.js') as f:glsl = f.read() #flx.assets.associate_asset(__name__, 'glsl.js', glsl) class Editor(flx.Widget): def init(self): global window options = dict( #mode='glsl', theme='cobalt', autofocus=True, styleActiveLine=True, matchBrackets=True, tabSize=2, lineWrapping=True, lineNumbers=True, ) self.cm = window.CodeMirror(self.node, options) self.cm.on("change", self.change); @flx.reaction('size') def __on_size(self, *events): self.cm.refresh() @flx.action def setValue(self, value): self.cm.setValue(value) @flx.emitter def change(self): self.emit('clear',{}) for s in self.cm.getValue().split('\n'): self.emit('value',dict(line=s)) return {} class WavePlot(flx.CanvasWidget): def init(self): super().init() self.ctx = self.node.getContext('2d') self.tick() def tick(self): global window self.emit('waveUpdate', {}) window.setTimeout(self.tick, 100) @flx.action def wave(self, data): w, h = self.size size = len(data) pitch = w/size rate = h/2 self.ctx.clearRect(0, 0, w, h) self.ctx.beginPath() self.ctx.strokeStyle = '#f80' self.ctx.lineWidth = 1 self.ctx.lineCap = 'round' self.ctx.moveTo(0,(data[0]+1)*h/2*.7) for i in range(size-1): self.ctx.lineTo(pitch*(i+1),(data[i+1]+1)*h/2*.7) self.ctx.stroke() self.ctx.closePath() class FftPlot(flx.CanvasWidget): def init(self): super().init() self.ctx = self.node.getContext('2d') self.tick() def tick(self): global window self.emit('fftUpdate', {}) window.setTimeout(self.tick, 100) @flx.action def wave(self, data): w, h = self.size size = len(data) pitch = w/size rate = h/2 self.ctx.clearRect(0, 0, w, h) self.ctx.beginPath() self.ctx.strokeStyle = '#f80' self.ctx.lineWidth = 1 self.ctx.lineCap = 'round' self.ctx.moveTo(0,(data[0]+1)*h/2) for i in range(size-1): self.ctx.lineTo(pitch*(i+1),(data[i+1]+1)*h/2) self.ctx.stroke() self.ctx.closePath() class Page(flx.Widget): CSS = """ body { background: rgb(15,20,30); font-size: 14px; } .CodeMirror { font-family: Monaco, 'Andale Mono', 'Lucida Console', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace; font-size: 9pt; width: 100%; height: 100%; } """ def init(self): with flx.VBox(flex=1): with flx.HBox(flex=1): self.editor=Editor(flex=1) with flx.VBox(flex=0, maxsize=220): flx.Widget(flex=1,minsize=10) self.plot = WavePlot(flex=1,minsize=100) self.fft_plot = FftPlot(flex=1,minsize=100) flx.Label(flex=0,text='Volume', style='color:whitesmoke') self.volume = flx.Slider(flex=0,min=0,max=1,value=1) with flx.HBox(flex=0): flx.Label(text='start time', style='color:whitesmoke') flx.Widget(flex=1) self.startTime = flx.LineEdit(maxsize=50,title='start time:', text='0',) with flx.HBox(flex=0): flx.Label(text='end time', style='color:whitesmoke') flx.Widget(flex=1) self.endTime = flx.LineEdit(maxsize=50,title='end time:', text='180') with flx.HBox(flex=0): flx.Widget(flex=1) self.timereset_button = flx.Button(flex=0,text='TimeReset') with flx.HBox(flex=0): self.auto = flx.CheckBox(checked=True,text='Auto Compile', style='color:whitesmoke') flx.Widget(flex=1) self.compile_button = flx.Button(text='_') with flx.HBox(flex=0): flx.Widget(minsize=15) self.labelTime = flx.Label(flex=0,style='color:orange') flx.Widget(flex=1) with flx.HBox(flex=0): flx.Widget(flex=0,minsize=10) self.labelDebag = flx.Label(flex=1,text="", style='border:1px solid red; color:whitesmoke') flx.Widget(flex=0,minsize=10) flx.Widget(flex=1) self.tick() def tick(self): global window self.emit('cmd', dict(cmd = 'self.timeUpate()')) window.setTimeout(self.tick, 100) @flx.action def timeUpate(self, s): self.labelTime.set_text("Time : {0:.1f}sec".format(s)) @flx.action def setValue(self, value): self.editor.setValue(value) @flx.action def waveUpdate(self, d): self.plot.wave(d) @flx.action def fftUpdate(self, d): self.fft_plot.wave(d) @flx.action def debag(self, s): self.labelDebag.set_text(s) @flx.reaction('timereset_button.pointer_click') def _6(self, *events): self.emit('cmd', dict(cmd = 'mzk.reTime()')) @flx.reaction('startTime.user_done') def _7(self, *events): if self.startTime.text.isdigit(): if int(self.startTime.text)>=int(self.endTime.text): self.startTime.set_text(str(int(self.endTime.text)-1)) else: self.startTime.set_text(str(int(self.endTime.text)-1)) self.emit('cmd', dict(cmd = 'mzk.setTimeBand({0},{1})'.format(self.startTime.text, self.endTime.text))) self.emit('cmd', dict(cmd = 'mzk.reTime()')) @flx.reaction('endTime.user_done') def _8(self, *events): if self.endTime.text.isdigit(): if int(self.startTime.text)>= int(self.endTime.text): self.endTime.set_text(str(int(self.startTime.text)+1)) else: self.endTime.set_text(str(int(self.startTime.text)+1)) self.emit('cmd', dict(cmd = 'mzk.setTimeBand({0},{1})'.format(self.startTime.text, self.endTime.text))) self.emit('cmd', dict(cmd = 'mzk.reTime()')) @flx.reaction('auto.pointer_click' ) def _9(self, *events): if not self.auto.checked: self.compile_button.set_text('Compile') else: self.compile_button.set_text('_') @flx.reaction('compile_button.pointer_click' ) def _10(self, *events): if not self.auto.checked: self.emit('cmd', dict(cmd = "self.compile()")) @flx.reaction('editor.change') def _11(self, *events): if self.auto.checked: self.emit('cmd', dict(cmd = "self.compile()")) @flx.reaction('!editor.value') def _12(self, *events): for ev in events: self.emit('value', dict(line = ev['line'])) @flx.reaction('!plot.waveUpdate') def _13(self, *events): ev = events[-1] self.emit('cmd', dict(cmd = "self.waveUpdate()")) @flx.reaction('!fft_plot.fftUpdate') def _14(self, *events): ev = events[-1] self.emit('cmd', dict(cmd = "self.fftUpdate()")) @flx.reaction('volume.user_done') def _15(self, *events): self.emit('cmd', dict(cmd = "mzk.setVolume("+self.volume.value+")")) @flx.reaction('!editor.clear') def _16(self, *events): self.emit('cmd', dict(cmd = "self.valueClear()")) class App(flx.PyComponent): def init(self): global mzk self.widget = Page() self.setValue() self.value='' # midi def callback(message, time_stamp): a,idx,value =message # KORG nano KONTROL2 の場合 idx=(idx//16)*8+(idx%16) value=value/127 mzk.setMidiValue(idx, value) self.midi_in = rtmidi2.MidiIn() self.midi_in.callback = callback if len(rtmidi2.get_in_ports())>0: self.midi_in.open_port(0) @flx.reaction('!widget.cmd') def _cmd(self, *events): global mzk for ev in events: eval(ev['cmd']) def timeUpate(self): global mzk self.widget.timeUpate(mzk.getTime()) def waveUpdate(self): global mzk d=mzk.getChunkData() w=(d[::2]+d[1::2])/2 self.widget.waveUpdate(w) def fftUpdate(self): # http://abcz.wp.xdomain.jp/2020/05/18/numpy-fft/ # fftが良くわからないから、それっぽくしただけ。 global mzk d=mzk.getChunkData() w=(d[::2]+d[1::2])/2 w=numpy.abs(numpy.fft.rfft(w)) self.widget.fftUpdate(w) @flx.reaction('!widget.value') def s14(self, *events): for ev in events: self.value += ev['line'] + '\n' def setValue(self): global mzk self.widget.setValue(mzk.getShader()) def valueClear(self): self.value='' def compile(self): if len(self.value)==0: return mzk.compile(self.value) self.value='' self.widget.debag(mzk.getCompileLog()) if __name__ == '__main__': src = """vec2 mainSound(int samp, float time) { time=mod(time,2.); return vec2( sin(6.2831*440.0*time)*exp(-3.0*time) ); } """ mzk=ShaderSound(shader=src) #mzk=ShaderSound() mzk.run() flx.launch( App, title='MzkShaderEdtor', size=(800, 500), pos=(500, 10) ) flx.run() mzk.stop()
pythonでmidiを使う
pythonでmidiを使う為のコード。rtmidi2でコールバックを使う方法。
import rtmidi2 import time from ctypes import * def callback(message, time_stamp): print(message, time_stamp) midi_in = rtmidi2.MidiIn() midi_in.callback = callback if len(rtmidi2.get_in_ports())>0: midi_in.open_port(0) VK_ESCAPE = 0x1B # エスケープキーで終了 while True: time.sleep(0.01) if(windll.user32.GetAsyncKeyState(VK_ESCAPE)):break
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については、記事を書きました。そちらも参考にしてください。
つぶやきGLSLで俺的raymarching
やり方と言うのは色々あるので俺的raymarchingって事にさせてもらいました。
久々につぶやきGLSLをやったら、スタイルを忘れてた。なので纏める事にしました。
2通りの手法を使ってました。一つはレイが衝突したらループを抜けて色を付ける方法。もう一つは、ループを最後までやって衝突しても加算で色を付けていく方法。
解りづらいので改行を入れて書きます。カウントは改行無しの文字数とします。これらは単純に球を出すだけのshader。twiglのgeekest(300es)で絵がでます。
レイが衝突したらループを抜けて色を付ける方法。(100chars)
float i,g,d=1.; for(vec3 p;++i<99.&&d>.001;g+=d=length(p)-1.) p=vec3((FC.xy-.5*r)/r.y*g,g-3.); o+=3./i;
ループを最後までやって衝突しても加算で色を付けていく方法(99chars)
vec3 p; for(float i,g,e;++i<99.;e<.001?o+=.2/i:o) p=vec3((FC.xy-.5*r)/r.y*g,g-3.), g+=e=length(p)-1.;
最近の奴は後述のループを最後までやって衝突しても加算で色を付けていく方法を使っていました。GPU負荷は微妙に高い気もするし、要らない
処理な気もするが、こちら一本になってました。1char短いのが本当の理由では無くてループを最後まですることによるメリットのせいであります。最後まですることでボリュームレンダリングみたいな事ができます。ボケもどきやグローぽい事が出来ます。
色付けの方法ついて。
本来ならo=vec4(0);
もしくはo-=o;
と初期化をしないといけないのですが省略しています。shadertoyで、これをやると速攻コメントが飛んでくるので注意。
o=vec4(0);
が前提なのでo+=.2/i:
これで色付け(モノトーン)はOKです。
超基本の.2/i
。これはループ回数の逆数を使うことでAOになるというfakeスキルです。なので厳密ではありませんが立体感がでます。数字は画面を見ながら調整です。これだけの事ですけど、SDFを駆使する方向の事なら充分に遊べます。
ボケもどきやグローぽい事なのですが、複雑なSDFを利用しての方法になります。説明しきれないのでヒント的な事だけにします。
e<.001?o+=.2/i:o
この部分のe<.001
の数字を大きくしてボケ、グローみたい事が出来るシチュエーションがあったり、三項式を外してビルトイン関数とg
,e
,p
等の変数を駆使して色々したりします。これは追々、整理しておきます。とりあえず、つぶやきGLSLの中に色々とスキルがありますよ。