GLSL(OpenGL Shading Language)を使って画像処理する練習。
今回は色々な切り抜き(Cropping)と敷き詰め(Tiling)。
最終的にreact nativeで使いたいのでgl-reactを前提として書きます。
ひし形切り抜き DiamondCrop
1 2 3 4 5 6 7 8 9 10 |
precision highp float; varying vec2 uv; uniform sampler2D t; void main() { gl_FragColor = mix( texture2D(t, uv), vec4(0.0), step(0.5, abs(uv.x - 0.5) + abs(uv.y - 0.5)) ); } |
座標中心を(0.5, 0.5)
から(0.0, 0.0)
としてx, y
の絶対値を加算、0.5
を超えるかどうかで書き分ける。
if文(もしくは三項演算子)を使わずstep
で0.0
と1.0
に分けてmix
している。
ifは処理の負荷がかかるというのをよく見かけるからそのせいかな。
円形切り抜き CircleCrop
1 2 3 4 5 6 7 |
void main() { gl_FragColor = mix( texture2D(t, uv), vec4(0.0), step(0.5, distance(uv, vec2(0.5, 0.5))) ); } |
mix
の条件を変えただけ。
単純に中心からの距離をstep
で0, 1
に分けているだけです。
三角形切り抜き TriCrop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform vec2 coords[3]; float cross2d (vec2 p1, vec2 p2) { return p1.x * p2.y - p2.x * p1.y; } bool check (vec2 p1, vec2 p2, vec2 p3) { return cross2d(p1 - p3, p2 - p3) < 0.0; } void main() { bool b1 = check(uv, coords[0], coords[1]); bool b2 = check(uv, coords[1], coords[2]); bool b3 = check(uv, coords[2], coords[0]); gl_FragColor = ((b1 == b2) && (b2 == b3)) ? texture2D(t, uv): vec4(0.0); } |
点同士のベクトルを順に外積をとって向き(符号)が同じであれば中にあるというのがわかりやすかったので、GLSLで書いてみます。
三角形の座標3つを渡して計算しようとしましたが、2次元は一般的な外積ではないのでcross2d
と符号をbool
で取る関数を用意しました。
if文を避ける方法は思いつかなかった。
coords = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]]
とするとこうなる。
多角形切り抜き PolygonCrop
多角形の内外判定はCrossing Number Algorithm(点と周囲との交差判定)とWinding Number Algorithm(点と周囲とのベクトル間角度の合計判定)が一般的のようです。
Winding Number AlgorithmがGLSL実装しやすそうだったので書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform int length; uniform vec2 coords[10]; float sign (vec2 p1, vec2 p2) { return p1.x * p2.y - p2.x * p1.y < 0.0 ? -1.0 : 1.0; } void main() { float sum = 0.0; int len = length; //coords.length(); for (int i = 0; i < len; i++) { int ii = i==len-1 ? 0 : i+1; vec2 _v1 = normalize(coords[i] - uv); vec2 _v2 = normalize(coords[ii] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); } gl_FragColor = mix(vec4(0.0), texture2D(t, uv), min(abs(sum), 1.0)); } |
配列のサイズを不定にできなかったので最大10にして実際のサイズも渡す。
acos
で角度を計算しますが正負は得られないため外積の符号を使います。
ところでこのコードは多くの環境で動きません(gl-react-nativeでのみ動いた)。
配列のサイズが合わないことや配列のインデックスに変数を使えないことが原因。
1 2 |
'[]' : Index expression must be constant Failed to execute 'uniform2fv' on 'WebGLRenderingContext': No function was found that matched the signature provided. |
配列で未定義の操作が起きうるとダメ見たいです。
そもそも配列も基本的にあまり使わないほうがよいとか。
配列サイズを固定して平書きすれば動きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform int length; uniform vec2 coords[6]; float sign (vec2 p1, vec2 p2) { return p1.x * p2.y - p2.x * p1.y < 0.0 ? -1.0 : 1.0; } void main() { float sum = 0.0; vec2 _v1 = normalize(coords[0] - uv); vec2 _v2 = normalize(coords[1] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); _v1 = normalize(coords[1] - uv); _v2 = normalize(coords[2] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); _v1 = normalize(coords[2] - uv); _v2 = normalize(coords[3] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); _v1 = normalize(coords[3] - uv); _v2 = normalize(coords[4] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); _v1 = normalize(coords[4] - uv); _v2 = normalize(coords[5] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); _v1 = normalize(coords[5] - uv); _v2 = normalize(coords[0] - uv); sum += sign(_v1, _v2) * acos(dot(_v1, _v2)); gl_FragColor = mix(vec4(0.0), texture2D(t, uv), min(abs(sum), 1.0)); } |
これで6つの座標に基づいた切り抜きが出来ます。うーん。
1 2 3 4 5 6 7 8 |
coords={[ [0.0, 0.0], [0.0, 1.0], [0.5, 0.8], [1.0, 1.0], [1.0, 0.0], [0.5, 0.2] ]} |
こんなcoords
をわたすとこうなります。
敷き詰め Tiling
1 2 3 4 5 6 7 8 9 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float tile; uniform vec2 center; void main() { vec2 p = (uv - center) * tile + center; gl_FragColor = texture2D(t, vec2(mod(p.x, 1.0), mod(p.y, 1.0))); } |
tile
で1列に敷き詰めたい数を指定して元画像の大きさを変えています。
center
はどこを中心に大きさを変えるかですが、敷き詰めの原点でもあります。
重要なのはmod
で[0~tile]
になった座標ではみでた部分を再描画することです。
交互に敷き詰め TilingAlt
レンガのように継ぎ目がずれる敷き詰め方を考えます。
1 2 3 4 5 6 7 8 9 10 11 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float tile; uniform vec2 center; void main() { vec2 p = (uv - center) * tile + center; float _x = mod(p.x + step(1.0, mod(p.y, 2.0)) * 0.5, 1.0); float _y = mod(p.y, 1.0); gl_FragColor = texture2D(t, vec2(_x, _y)); } |
変わったのはx
座標の値にy
座標の値を使うところです。
これで1段ごとに0.5
ズレるようになっています。
もちろんx, y
の処理を逆にすれば縦に半分ズレます。
間隔をあけて敷き詰め TilingStep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
precision highp float; varying vec2 uv; uniform sampler2D t; uniform float tile; uniform float stepx; uniform float stepy; uniform vec2 center; void main() { vec2 p = (uv - center) * tile + center; float x = mod(p.x, 1.0 + stepx); float y = mod(p.y, 1.0 + stepy); if(x>1.0||y>1.0)discard; gl_FragColor = texture2D(t, vec2(x, y)); } |
mod
で描画しない分まで含めた座標にして範囲外は無視しています。
範囲外はこれまでのように三項演算子でvec4(0.0)
にしてもよかったんですが、描画しないならdiscard
の方が正しい気がしたので使っています。どっちがいいんだろう。
tile=3.0, stepx=0.5, stepy=0.1
とするとこんな感じ。
組み合わせ
単純なシェーディングですがこれらを組み合わせによっては面白い表現もできそう。
例えば多角形切り抜きしてずらして敷き詰め、円状に切り抜くとこうなります。
所感
これまでなんどかGLSLやHLSLに触れてきましたが、環境構築が面倒、環境によって動作が異なる、包括的なドキュメントがないなど理由を付けて敬遠してました。
改めて書いてみるとやっぱり面倒ではありますが、今のところは面白みの方が強い。
多角形の切り抜きで特に思ったことですが、シェーダーは目的に合わせてワンオフで作るべきでライブラリ的に便利に使えるものではなさそう。
何でもできそうでいて色々と制限があるけど工夫すればなんとかできる。
とりあえずそこら中にある理解不能なサンプルを目指して遊んでみたい。