テラByteの時代にキロByte

shader又はdemosceneに関係する事

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を同居させる

pygameOpenGLをすると全画面を独占される。それだと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()   

caption

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を使わせて貰えって話もあるけど、そういうハックも面白かったりするので困ったものです。
後日の為に情報を残しておきます。 ハフマン符号化

darkcrowcorvus.hatenablog.jp

garakuta-toolbox.hatenablog.com

リアルタイムコンパイルの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を使う

pythonmidiを使う為のコード。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のエディターでリアルタイムコンパイル

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

つぶやき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の中に色々とスキルがありますよ。