How to convert hydra-synth to MSL
hydra-synthというビデオシンセは独自の記法を使ってGLSLを生成することできます。
osc().out(o0)
というコードは以下のようにGLSLに展開されます。
precision highp float;
uniform float frequency1; // 60
uniform float sync2; // 0.1
uniform float offset3; // 0
uniform float time;
uniform vec2 resolution;
varying vec2 uv;
vec4 osc(vec2 _st, float freq, float sync, float offset){
vec2 st = _st;
float r = sin((st.x-offset/freq+time*sync)*freq)*0.5 + 0.5;
float g = sin((st.x+time*sync)*freq)*0.5 + 0.5;
float b = sin((st.x+offset/freq+time*sync)*freq)*0.5 + 0.5;
return vec4(r, g, b, 1.0);
}
void main () {
vec4 c = vec4(1, 0, 0, 1);
vec2 st = gl_FragCoord.xy/resolution.xy;
gl_FragColor = osc(st, frequency1, sync2, offset3);
}
特徴としてはフィードバック処理を簡単に記述することができます。
osc(10).diff(o0).modulatePixelate(o0,0.2).rotate(0.002).out(o0)
GLSLを書くよりも簡易な記述によって、GLSLによる映像をつくることができます。GLSLに変換している部分を少しいじることにより、MSLとしても書き出すことが可能です。今回はその話となります。
https://github.com/ojack/hydra-synth/blob/master/src/GeneratorFactory.js
にGLSLに変換している処理が書かれています。(変換自体も興味深いですが、今回は割愛します)
osc().out(o0)
out()を呼ぶタイミングで
Generator.prototype.out = function (_output) {
// console.log('UNIFORMS', this.uniforms, output)
var output = _output || this.defaultOutput
output.renderPasses(this.glsl(output))
}
GLSLに変換しています。
この引数_outputは
https://github.com/ojack/hydra-synth/blob/master/src/output.js
に定義してありますが、browserで動くようにつくられており、MSLに変換する場合には都合が悪く、以下のようにNode.jsで動くように変更します。
var Output = function(uid) {
global["o"+uid] = {
index:uid,
uniforms:{},
getTexture:function() {},
renderPasses:function(glsl) {
require("fs").writeFileSync("./assets/s"+this.index+".metal",glsl[0].frag);
require("fs").writeFileSync("./assets/u"+this.index+".json",stringifyWithFunctions(glsl[0].uniforms));
}
};
}
Generator.prototype.outでrenderPassesが呼ばれたときに.metalとuniformをJSONとしてファイルに書き出します。
空のgetTexture関数がいるのはGeneratorFactory.jsのformatArguments()で
typedArg.value.getTextureによる分岐があり、その対策のためです。
hydra-synthの関数は、GLSLに書かれている関数に対応しており
https://github.com/ojack/hydra-synth/blob/master/src/composable-glsl-functions.js
に定義してあります。
GLSLではグローバルにuniformの値がありますが
uniform float time;
uniform vec2 resolution;
MSLの場合は関数内で初期化するため
fragment float4 fragmentShader(VertInOut inFrag[[stage_in]],constant FragmentShaderArguments &args[[buffer(0)]]) {
float time = args.time[0];
float2 resolution = args.resolution[0];
timeとresolutionを使っている関数は引数を追加し
vec4 osc(vec2 _st, float freq, float sync, float offset, float time) {
inputも引数の値の設定をする必要があります。
{
name: 'time',
type: 'float',
default: 'time'
}
次にGeneratorFactory.jsの不要な部分を削っていき、GLSLのテンプレートの部分をMSLに向けに書き換えていきます。
https://github.com/mizt/MSL-Hydra-Synth/blob/master/src/MSLGeneratorFactory.js
GLSLとMSLで組み込み関数が違っている部分があり、 modとatanを置き換えています。
#define mod fmod
#define atan 6.28318530718-atan2
composable-glsl-functions.jsの_rgbToHsvの部分で
http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
のhsvへの変換処理が使われていますが、ゼロ除算対策の以下の部分がMSLだとおかしな結果となります。
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
正しい表示となるようにコードを修正します。
return vec3(abs(q.z+((d==0.0)?1.0:((q.w-q.y)/(6.0*d)))), (q.x==0.0)?3.402823466e+38:(d/q.x), q.x);
https://hydra-editor-v1.glitch.me/?sketch_id=example_6&code=JTJGJTJGJTIwYnklMjBEJUMzJUE5Ym9yYSUyMEZhbGxlaXJvcyUyMEdvbnphbGVzJTBBJTJGJTJGJTIwaHR0cHMlM0ElMkYlMkZ3d3cuZ29uemFsZXNkZWJvcmEuY29tJTJGJTBBJTBBb3NjKDUpLmFkZChub2lzZSg1JTJDJTIwMikpLmNvbG9yKDAlMkMlMjAwJTJDJTIwMykuY29sb3JhbWEoMC40KS5vdXQoKSUwQSUwQSUwQSUwQQ==
またhydra-synthの記述では変数に関数を渡すことができます。
.modulate(o0, () => mouse.x * 0.0003)
Node.jsで書き出したJSONのuniformの値は文字列化された関数になっているので
"amount_17": "(() => mouse.x * 0.0003)",
JavaScriptCore.frameworkを使いシェーダの値に関数の結果を渡せるようにします。
JSContext *context = [JSContext new];
[context evaluateScript:[NSString stringWithFormat:@"mouse={x:%f,y:%f};",mouseX,mouseY]];
[[context evaluateScript:[NSString stringWithFormat:@"(%@)();",@"(() => mouse.x * 0.0003)"] toDouble];
https://github.com/mizt/MSL-Hydra-Synth-Player/blob/master/libs/HydraMetalLayer.h
とりあえずこれで書き出した.metalを.metallibにビルドして読み込めばMetalでhydra-synthが動かせます。
この記事が気に入ったらサポートをしてみませんか?